Skip to main content

Security & Data Residency

Sourced from PROJECT-DOCS/DEVELOPER_PLATFORM.md (measured against production, 2026-08-05) and cross-checked against pkg/crypto/aesgcm.go, pkg/database/customer_memory_crypto.go, and database.Scoped(). Where a figure is a point-in-time production measurement rather than a structural fact, it's marked as such — it will drift.

Whose database is it

Ours — there is no bring-your-own-database option, and no code path that would allow one. A grep across the backend and migrations for any external-database concept (external_db, customer_db, a stored customer connection string) found nothing. Every customer's data lives in one shared Postgres 16 + pgvector instance, isolated by organization_id on every query.

Production, as measured 2026-08-05: 130 MB · 66 organizations · 124 live agents · 1,821 calls · 245 memory rows. (Point-in-time — will be stale by the time you read this; don't cite the exact counts, cite the architecture.)

Tenant isolation

Every tenant-scoped query goes through a helper (database.Scoped(c, db)) that panics in dev and fails closed in production if the organization filter is missing from the request context — a missing filter is treated as a cross-tenant leak waiting to happen, not a bug to fix at leisure. That design paid off directly: a 2026-08-04 sweep using this same reasoning found and fixed six real cross-tenant issues on the write side, including a body-supplied agent_id that could be written into another organization's row, a reseller code path that degenerated to WHERE id = ? with no org filter, and a call-completion webhook scoped so narrowly it had reached nobody for three months.

Encryption, layer by layer

Stated plainly because "we encrypt customer data" is true for some of it and not yet true for all of it, and the honest breakdown matters more than the summary claim:

LayerState
Disk at restAzure SSE, platform-managed key
OAuth tokens (HubSpot / Google / Salesforce), SSO, LTIAES-256-GCM, app-layer, our own key
Customer memory (summary, last-interaction, follow-up reason, key facts)AES-256-GCM, app-layer
API keysSHA-256 hash — not encrypted, unrecoverable by design
Call recordingsAzure Blob, SSE + short-lived SAS URLs
Call transcriptsDisk-layer encryption only — not application-layer

Call transcripts are the largest single body of Amendment 13 (Israeli privacy law) personal data this platform holds, and they are the one category not yet encrypted at the application layer. That's a real, current gap, not a historical one that's since been closed.

Separately: a backfill of the customer-memory encryption left some pre-existing rows in plaintext (all last written before 2026-06-28 as of the 2026-08-05 measurement) — the write path is correct and has been for a while, it's specifically old rows from before the encryption was added that are still catching up. A backfill job exists and is idempotent; running it against production is a deliberate, manually-triggered operation, not an automatic migration.

What this means if you're integrating

  • Don't put anything in a webhook payload_template, custom_headers, or agent metadata field that you wouldn't want stored at the same protection level as the category it belongs to above.
  • If your integration touches call transcripts via the API (GET /calls/:id includes transcript), treat that data with the same care you'd apply to any unencrypted-at-rest PII on your own side — VoiceMatrix's disk-level encryption protects against physical media loss, not against a database compromise.
  • API keys cannot be recovered if lost — only replaced. Build your integration assuming key rotation is a "generate new, update config, revoke old" flow, not a "retrieve the old one" flow.

Next

All pages