Skip to content
Read the codeArchitecture, not a badge wall

How tenant data is isolated, encrypted and audited.

This page describes what the code does and names the file that does it. It is not a certification. The section at the end lists what we do not claim, and what you can ask for instead.

  • 12every tenant-scoped table
    Tables under forced row-level security

    packages/db/prisma/migrations/20260907000000_rls_roles_grants_policies

  • 7AES-256-GCM, per-credential data key
    Connectors, one credential vault

    apps/api/src/lib/crypto.ts

  • 30 / 90days: read / any
    Notification retention

    apps/api/src/jobs/notifications.ts

The architecture

Seven facts, each with a file path

Isolation, secrets, inbound webhooks, sessions, logging, the audit log and roles — what each one does today, and where to read it.

Stacked translucent layers with a single lit path passing through them.
FORCE RLS12 tables
  • Tenant isolation is enforced by Postgres, and the API refuses to boot without it

    Every tenant-scoped table has row-level security enabled and forced, with a tenant_isolation policy keyed on the request's tenant id, so an unscoped query returns zero rows rather than everything. The app connects as a role that cannot bypass those policies; the handful of pre-login lookups run through SECURITY DEFINER functions owned by a separate no-login role with a pinned search_path. At boot the API checks every one of those tables is forced and policied and refuses to start, naming the table, if any is not.

    Where this livespackages/db/prisma/migrations/20260907000000_rls_roles_grants_policies/migration.sql · apps/api/src/server.ts

  • Connection credentials are envelope-encrypted at rest

    Each connection's credentials are encrypted with AES-256-GCM under a random per-credential data key, and that key is wrapped by a master key held outside the database. Ciphertext, IV, authentication tag and wrapped key are stored as separate columns; decryption happens only in the process that is about to call the provider. No key is in the repository — CI generates a throwaway master key for each run.

    Where this livesapps/api/src/lib/crypto.ts · .github/workflows/ci.yml

  • Signed webhooks are verified; unsigned ones are only a pointer

    Where a provider signs its webhooks, the signature is checked against the exact raw bytes received before anything is parsed. Where a provider does not sign — some do not — the payload is treated as nothing more than "record X changed": the connector must re-fetch record X from the provider's API by id before a flow acts on it, so a forged body cannot inject data.

    Where this livespackages/connector-sdk/src/types.ts · apps/api/src/routes/webhooks.ts

  • Sessions live in Redis; every route has a rate ceiling

    Sessions are server-side in Redis with a 30-day lifetime that the cookie and the store share, in an httpOnly, same-site cookie marked secure in production. A global limit of 300 requests a minute per IP applies to everything, shared across API instances; sign-up is capped at 5 an hour and login at 10 a minute because each attempt costs a memory-hard password hash.

    Where this livesapps/api/src/plugins/session.ts · apps/api/src/routes/auth.ts

  • Logs redact secrets at every depth and never carry a payload

    The request logger redacts request and response bodies wholesale and every credential field name at four levels of nesting. Worker failure logs are an allow-list of six identifiers — tenant, flow, connection, run, parent run, step — never the job payload, which is the customer's order. Every request carries an id that is echoed as a reference on any server error, so "it failed a minute ago" is one grep.

    Where this livesapps/api/src/server.ts · apps/api/src/queue/observability.ts

  • Configuration changes are written to a tenant-scoped audit log

    Creating, updating or deleting a connection, editing a flow, installing a bundle, saving or confirming its settings, and inviting, accepting or changing a team member's role each write an entry with the actor, the target and the time. The log is itself under row-level security and is readable by owners, filtered by the object it concerns.

    Where this livesapps/api/src/routes/auditLog.ts

  • Two roles: owner and member

    Every user is an owner or a member. Owners manage the team, invite by a seven-day link, change roles and read the audit log; a tenant can never be left without an owner. There is no finer-grained permission model today, and this page does not pretend otherwise.

    Where this livesapps/api/src/routes/users.ts

Plainly

What we do not claim

A statement of what has not been done, so nothing above reads as more than it is.

Not claimed

  • No SOC 2 audit has been performed, and no ISO certification is held.
  • No availability figure is published. There is a health endpoint and an error reference on every failure; there is no measured SLA.
  • No third-party penetration test report exists.
  • Notifications are in-app only. There is no email or Slack delivery, so an alert is seen when someone opens the dashboard.

Instead

What you can ask for instead

  • A walkthrough of the isolation tests — the suite that proves an unscoped query returns zero rows, that the same query inside a tenant context finds its own rows, and that no list or detail route ever answers with another tenant's data.
  • The migration file that creates the roles, grants and policies, read together.
  • The connector SDK type that makes re-fetch-on-notify mandatory for unsigned webhooks.
  • A screen-share of a failed run, its step record, and the log line found by its reference id.

Ask for any of these on the contact page. We answer security questions in writing.

Questions

Frequently asked

What security reviewers ask first.

Where is data hosted?
Bindato runs as one API with a Postgres database and Redis. Hosting provider and region are part of each early-access agreement rather than a figure on this page — ask, and we will tell you exactly where your tenant's database runs.
Can Bindato staff read my connection credentials?
Credentials are stored only as AES-256-GCM ciphertext with a wrapped per-credential key. Decryption requires the master key held by the running API process, and the plaintext exists only in memory at the moment a provider is called.
What does a webhook from an unsigned provider actually do?
It tells the engine which record changed, and nothing else. The connector re-fetches that record from the provider's API using your stored credentials before any flow step runs on it.
How would I know a run failed?
The run shows as failed or partial with its cause, every step keeps its input, output and error, and a notification is raised within five minutes. In-app only — see the list above of what is not claimed.

Ask a security question.

Send the question your reviewer would ask. We reply in writing, with the file that answers it.