MeshLaw Security White Paper
2026-05-21 v0.1 · for legal and IT teams to review
This document summarizes MeshLaw’s data processing, isolation, encryption, and access-control policies. It is based on implementation facts (code, migrations, operational settings), and during verification or due diligence we present the relevant lines alongside.
1. System composition
| Component | Technology | Notes |
|---|---|---|
| Gateway | Moleculer (Node/Bun) :4098 | HTTP entry point. JWT verification + RLS context injection |
| DB | PostgreSQL 16 + pgvector | Single instance. RLS team_id isolation |
| AI backend | vLLM (self-hosted) | No external API egress — matter data does not leave for external LLM providers |
| Resend (domain verified) | Sign-up & password only, no matter content sent | |
| Mobile push | APNs + FCM | title/body + data only — no matter body |
Hosting: Vultr Seoul region — data resides in Korea. Operational SSH: key auth only (password disabled).
2. Tenant isolation
2.1 PostgreSQL Row-Level Security
Every domain table has FORCE ROW LEVEL SECURITY enabled:
- Direct isolation (team_id column): matters, clients, advisories, audit_log, notifications, llm_usage, team_token_quota, messages, device_tokens, stripe_webhook_events, invoice_receipts
- JOIN isolation (matter_id → matters.team_id): matter_events, invoices, timesheet_entries, writs, approvals, matter_refs, vault_files, matter_acl, deadlines
Policy: team_id = app_current_team_id() (USING + WITH CHECK). app_current_team_id() reads the transaction GUC app.team_id.
2.2 NOSUPERUSER role separation
The operational gateway connects to the DB with the meshlaw_app role (NOSUPERUSER · NOBYPASSRLS). Dev / migration tools are separated into a distinct superuser — so no BYPASSRLS user is involved in day-to-day operational traffic. Applied 2026-05-05.
2.3 Transaction GUC injection
sql.begin(async (tx) =>
await tx`select set_config('app.team_id', ${teamId}, true)`
await tx`select set_config('app.user_id', ${userId}, true)`
return fn(tx)
Because it is SET LOCAL, it is released automatically when the transaction ends. 50/50 domain actions apply the wrap. The remaining 7 are intentional raw cases such as the password_hash lookup in auth.signup/login — no data-leak path.
2.4 JWT → ctx.meta.user mapping
The gateway’s onBeforeCall verifies the JWT → injects ctx.meta.user = { id, team, role }. All domain routes are included in AUTH_REQUIRED_PREFIXES, so a missing token yields 401.
2.5 Regression protection
Tenant isolation is regression-tested before every deploy in e2e §7 (RLS):
- Unauthenticated call → 401
- Invalid JWT → 401
- A valid JWT reaches only its own team’s data → 200
3. Personal information protection (PII)
3.1 Resident registration number
Storage: pgp_sym_encrypt encryption (BYTEA rrn_encrypted) + last 4 digits (rrn_last4). Exposure: the client UI forces masking via maskRRN() → XXXXXX-XXX{last4}. On a format mismatch it returns an empty string (blocking raw exposure). 8 unit-test regressions.
3.2 Passwords
Stored as bcrypt hashes. The client shows real-time 4-rule visualization on input (8 chars / letters / numbers / special characters). HTTPS only — the iOS Release build does not set NSAllowsArbitraryLoads.
3.3 Token management
- JWT: short TTL (default 30 minutes), HS256
- Refresh: rotation + reuse detection. The plaintext is delivered to the user once; the DB stores only the bcrypt hash. On reuse detection the whole chain is revoked
3.4 Per-matter ACL
On top of the RLS team scope, an additional per-matter isolation (matter_acl). 4 roles (owner ⊃ editor ⊃ viewer). Adopted by messenger v0.1.6 — listing a matter’s channels requires viewer or above, writing requires editor or above.
4. Audit log
Actions recorded automatically in the audit_log table:
- Matter CRUD, ACL grant/revoke
- Approval approve/reject/request-changes (status_before/after, comment)
- Invoice issuance, payment receipts (delta_won, total)
- Usage → invoice/timesheet rollup
- Reference-document CRUD
Each row: actor_name, target_kind, target_id, http_status, matter_id, meta(JSONB), created_at. RLS enforced — you cannot query another tenant’s audit records.
5. Permission control
5.1 Approvals (matter approvals)
Server-side actor guard — computeApprovalChainAdvance(steps, status, currentUserId). Only the user matching the active step’s approver_id may act. On mismatch, 403 NOT_APPROVER. Same guard on desktop and mobile. 7 unit tests.
5.2 RBAC (system roles)
users.role_id → roles table (admin/partner/associate/staff). auth.invite assigns roles (only admins can issue).
6. Data processing
6.1 AI model calls
Self-hosted vLLM (Tailscale internal network) — no external API egress. LLM request bodies (matter context, documents) never leave the organization. Call tokens and cost are recorded in the llm_usage table (RLS team scope).
6.2 Files (Vault)
- Storage: server disk (
storage_directoryseparated per matter) - Metadata:
vault_filestable (file_name, mime, size_bytes, text_content) - Indexing: after text extraction, stored in
text_content→ ⌘K unified search (RLS enforced) - PDF multimodal: pages converted to JPEG with mupdf → vLLM vision encoder (no external egress)
6.3 Mail
Resend domain meshlaw.ai (DKIM/SPF/DMARC verified). Content: sign-up, invite, and password reset only — no matter content.
7. Infrastructure security
7.1 Transport
Gateway: HTTPS only (Caddy). iOS Release / Android Release both use the prod URL only (only Debug uses localhost).
7.2 Mobile push
- APNs JWT (.p8) — Apple Developer Team ID + Key ID
- FCM HTTP v1 — Firebase service account JSON
- Push bodies are metadata only (title/body + data.type routing hint) — no matter body
- Device tokens:
device_tokenstable (RLS team scope)
7.3 Operational access
- SSH access: key-based authentication (recommended for operations)
- DB: docker-compose binds to
127.0.0.1:54322as localhost bind only — not exposed to the external internet. Only the gateway container on the host can access it (network_mode: host) - Backup:
/var/backups/meshlaw-db-*.sql.gz(currently manual, automation planned) - Pre-backup is mandatory before applying migrations (operational discipline)
7.4 Stripe payments
Webhook signature verification + idempotency guard (stripe_webhook_events). Card data lives in the Stripe Vault — not stored by us (minimizing PCI scope).
8. Compliance posture
| Item | Status |
|---|---|
| Personal Information Protection Act (PIPA) | Standard processing · DPA provided separately |
| ISMS-P | Under review (separate notice at time of application) |
| KISA mark | Under review |
| GDPR | Out of scope (domestic only) |
| ZDR (Zero Data Retention) | Policy option — contractual addendum on Pro+ plans. Since only self-hosted vLLM is currently used, there is no external training exposure at all |
⚠️ ISMS-P / KISA mark are at the review stage. Use of a certification mark is only possible after it is obtained.
9. Incident response
9.1 Token leak
On refresh-reuse detection, the user’s entire chain is automatically revoked. /auth/logout or a password change → revoke all refresh tokens.
9.2 Data exposure
On discovery, immediate docker-compose rollback + DB backup restore.
9.3 User notification
Policy goal: notify of impact scope, recovery steps, and required user actions within 72 hours (PIPA §34 advisory level). There is currently no automation — when it occurs, the operator manually computes the impact scope and sends a bulk email.
10. Data transfer & deletion
- Export: users can export their own team’s data as JSON (planned for v0.2)
- Deletion: direct deletion via UI + account deletion by email request → 30-day grace → cascade
- Retention: indefinite by default. Statutory mandatory retention (e.g., 5 years under income tax law) is the user’s responsibility
11. Verification requests
If you would like fact verification or due-diligence cooperation for this white paper, request it via the contact page or your sales rep’s email — we provide code paths (line numbers) · migration SQL copies · penetration-test results (once performed).
A dedicated security mailbox (e.g., compliance@meshlaw.ai) will be announced separately once domain verification and operational readiness are complete.
Press ⌘P (Mac) / Ctrl+P (Win) and choose "Save as PDF". Or download the Markdown source.