01 β€” Append-only audit_logs

Every signup writes an immutable audit row

Both qualification paths β€” early-access and self-serve β€” emit an audit row inside the same database transaction. UPDATEs and DELETEs on the ledger fail at the DB layer.

Wired into both signup paths
auditLogs.recordEvent runs on the same transactional client as the parent INSERT so an audit row never exists without its parent write β€” or vice versa.
  • /api/pilot β†’ db/pilots.js writes audit_logs row with 'insert' + 'pilots'
  • /api/pilot-signups β†’ db/pilotSignups.js writes audit_logs row with 'insert' + 'pilot_signups'
  • Both insert the canonical JSONB payload and a SHA-256 payload_hash the verifier can recompute
Database-enforced append-only
A BEFORE UPDATE OR DELETE trigger raises an exception for any mutation attempt on the ledger β€” not just an app-layer convention.
  • Trigger name: audit_logs_append_only
  • Function: audit_logs_block_migration() raises 'audit_logs is append-only'
  • Applies statement-level β€” even bulk DELETEs across the table are blocked
Hash-chained payload
Every row carries payload_hash β€” a SHA-256 over the canonicalized JSONB snapshot. The same hash is recomputable from a SELECT, so verifier scripts can prove integrity offline.
  • Canonical form: sorted keys, no whitespace (stable across inserts)
  • Helper: db/auditLogs.js hashPayload()
  • Same routine runs for every recordEvent() call β€” across users and crons

02 β€” Encryption at rest with rotation

Ciphertext on disk, key version on every row

The audit ledger is encrypted with AES-256-GCM. Every row records the key version that encrypted it, so rotation is a backfill pass β€” not a re-key.

AES-256-GCM at rest
  • Algorithm: aes-256-gcm β€” authenticated encryption with a 12-byte IV and 16-byte auth tag
  • Stored as encrypted_payload BYTEA in the format iv β€– tag β€– cdata
  • Helper module: lib/audit-crypto.js β€” encryptPayload() / decryptPayload()
Rotation by key version
  • Every audit_logs row carries key_version SMALLINT (default 1) β€” the key that encrypted it
  • Rotation: bump AUDIT_ENCRYPTION_KEY_VERSION, re-encrypt the affected rows in a backfill pass
  • DB-side safety-net trigger audit_logs_backfill_encrypted_payload() rejects any insert that skips the encrypted column
  • audit_logs_encrypted_required CHECK (NOT VALID) blocks the unencrypted path at insert time
  • Decryption is symmetric and round-trip verifiable β€” recompute the payload_hash from the JSONB snapshot to confirm integrity

03 β€” RBAC with per-action audit rows

Role catalog is mutable. Role assignment is not.

Three tables split the catalog from the assignments. Every assignment writes its own audit row at the DB layer.

roles
Role catalog
Mutable by design β€” every create writes an audit row
Schema: id, name, description, created_at
Mutability is allowed β€” changes are logged, not blocked
Every createRole() call emits an audit_logs row inside the same transaction
permissions
Permission catalog
Same mutability + per-action audit row pattern
Schema: id, name, description, created_at
Logged via the same auditLogs.recordEvent() path as roles
Lookup tables for downstream user_roles joins
user_roles
Role assignments
Append-only at the DB layer + per-action audit row
user_roles_append_only BEFORE UPDATE/DELETE trigger raises an exception on any mutation
user_roles_audit_assignment() writes { user_id, role_id, granted_by, granted_at } to audit_logs on every INSERT
This is the key per-action audit trail the SOC 2 evidence pack references

04 β€” Data-handling summary

What we collect, what we store, what we keep

Short, honest summary β€” every bullet maps to a code path you can read.

  • No PII retained from sales-call transcripts unless explicitly enabled.
  • Audit-ledger rows are encrypted at rest β€” plaintext is never persisted (lib/audit-crypto.js).
  • RBAC catalog rows (roles, permissions) are mutable by design β€” changes are audit-logged, not blocked.
  • Append-only ledger + RBAC join table β€” DB-layer triggers block UPDATEs and DELETEs (audit_logs_append_only, user_roles_append_only).

05 β€” Request the controls summary

Send me a SOC 2 readiness summary

Tell us what your procurement team needs in the brief β€” we will reply within one business day with the matching controls matrix and the supporting policy references.

Request received.

We'll send the SOC 2 readiness summary within one business day.

Want to try Parlo now? β†’
Ready when you are

Skip the summary β€” put Parlo on a real call.

Two on-ramps: a qualification-led early-access pilot, or a self-serve sandbox you can spin up in two days.