Security
Built like the bank we pay through.
Postgres row-level security on every table. Per-merchant Vault for secrets. Append-only audit log. Standard Webhooks signing. SOC 2 Type I attestation in progress (target Q3 2026).
Multi-tenancy
RLS or it didn't happen.
Every tenant-scoped table in our Postgres schema enforces row-level security. The app.current_merchant_ids() helper resolves the user's memberships from merchant_users, and every SELECT / INSERT / UPDATE policy filters on it. Cross-tenant data exposure is structurally impossible — even if our application code has a bug, the database refuses the query.
We carry an automated RLS smoke test in CI that creates two test merchants and asserts merchant A's session can't read merchant B's rows. Build doesn't pass without it.
Secret management
Per-merchant Vault, never in the app DB.
- Yes Mercury API keys
- Yes WordPress plugin HMAC shared secrets
- Yes Shopify Admin API access tokens
- Yes Outbound webhook signing secrets
- Yes Affiliate Tax IDs (TINs)
- Yes Mercury webhook validation secrets
- Yes Affiliate payout method details (PayPal email, Stripe Connect ID, Wise recipient)
Each is a row in vault.secrets with a per-merchant Vault key. The application reads them via security-definer Postgres functions (vault_read_secret) that only the service-role key can execute. We never log secret values, never return them to the browser, and never include them in error reports.
Audit log
Append-only, every state transition.
audit_log is an append-only table that captures every meaningful event: affiliate approvals/suspensions, commission state transitions, payout sends, tax form submissions, Mercury connections, custom domain changes, GDPR requests, even branding edits. It writes via service-role only — there's no INSERT/UPDATE/DELETE policy that allows merchants to tamper with their own history.
A daily retention cron (purge_audit_log_older_than(p_days)) trims anything older than your configured retention (default 365 days). Enterprise plans get configurable retention SLAs.
Outbound + inbound signing
Standard Webhooks v1 in both directions.
Outbound: every webhook we send is HMAC-SHA256 signed per the Standard Webhooks v1.0.2 spec. Receivers can drop in svix-libs for instant verification. Inbound: the WordPress plugin signs every order/refund/coupon webhook with the shared HMAC secret. Mercury webhooks are validated against per-merchant Mercury signing secrets stored in Vault.
PII handling
Customer data hashed at the source.
- Yes Customer emails, phones, IPs hashed with a per-merchant random salt at the WP / Shopify layer before reaching us
- Yes Same merchant + same email always hashes consistently (so we can match across orders); cross-merchant collisions are statistically impossible
- Yes GDPR data export + deletion endpoints in Settings → Privacy
- Yes Affiliate erasure: clears PII, drops Vault secrets, deletes auth user, preserves financial history
Compliance
Roadmap.
- Yes SOC 2 Type I attestation — in progress with Drata, target Q3 2026
- Yes GDPR data subject request flow shipped
- Yes EU SCC + UK IDTA for international transfers (DPA available)
- Yes ISO 27001 — under evaluation
- Yes Vulnerability disclosure: team@cobalz.com (subject:
[Security])
Read the DPA. Sign for Enterprise.
We counter-sign Enterprise DPAs on a per-customer basis.