The four surfaces that build the audit trail: append-only event logs wired into every signup, encryption at rest with rotation, RBAC join tables, and a transparent data-handling summary you can read end to end.
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.
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'payload_hash the verifier can recomputeBEFORE UPDATE OR DELETE trigger raises an exception for any mutation attempt on the ledger β not just an app-layer convention.audit_logs_append_onlyaudit_logs_block_migration() raises 'audit_logs is append-only'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.db/auditLogs.js hashPayload()recordEvent() call β across users and cronsThe 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 β authenticated encryption with a 12-byte IV and 16-byte auth tag
encrypted_payload BYTEA in the format iv β tag β cdata
lib/audit-crypto.js β encryptPayload() / decryptPayload()
audit_logs row carries key_version SMALLINT (default 1) β the key that encrypted it
AUDIT_ENCRYPTION_KEY_VERSION, re-encrypt the affected rows in a backfill pass
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
payload_hash from the JSONB snapshot to confirm integrity
Three tables split the catalog from the assignments. Every assignment writes its own audit row at the DB layer.
id, name, description, created_atcreateRole() call emits an audit_logs row inside the same transactionid, name, description, created_atauditLogs.recordEvent() path as rolesuser_roles joinsuser_roles_append_only BEFORE UPDATE/DELETE trigger raises an exception on any mutationuser_roles_audit_assignment() writes { user_id, role_id, granted_by, granted_at } to audit_logs on every INSERTShort, honest summary β every bullet maps to a code path you can read.
lib/audit-crypto.js).
roles, permissions) are mutable by design β changes are audit-logged, not blocked.
audit_logs_append_only, user_roles_append_only).
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.
We'll send the SOC 2 readiness summary within one business day.
Want to try Parlo now? βTwo on-ramps: a qualification-led early-access pilot, or a self-serve sandbox you can spin up in two days.