Every query scoped by the server

The browser never talks to the database directly — it asks a server proxy that stamps your account id on every write and filters every read by it.

What it is

A single POST endpoint that accepts an action, a table, data and filters. It runs against an explicit allowlist of tables and actions, applies role gates per table, ignores any org_id the client sends, stamps the authenticated caller's org id onto outgoing rows, and appends the org filter last so it cannot be overridden by anything the caller passed.

Also called: api/db · org_id stamping · database proxy · row level scoping

See it
Every query scoped by the server
Area
Platform & Admin
Group
Tenancy & isolation
System
Multi-Tenancy & Identity
Solves
3 named problems
01The table must be in ALLOWED_TABLES and the action in ALLOWED_ACTIONS, otherwise the request is refused outright.
02Read gates: some tables are admin-only, some exclude the field 'worker' role, and anything not explicitly readable is refused.
03Write gates: some tables are forbidden entirely (use the dedicated endpoint), some are admin-only config, deletes default to admin with a small named exception list.
An annotated request/response pair: the JSON the client sent (with a spoofed org_id struck through) and the SQL-shaped filter chain the server actually ran. Source: src/app/api/db/route.ts. Sample data — no customer information appears here.
How it works
  1. 1The table must be in ALLOWED_TABLES and the action in ALLOWED_ACTIONS, otherwise the request is refused outright.
  2. 2Read gates: some tables are admin-only, some exclude the field 'worker' role, and anything not explicitly readable is refused.
  3. 3Write gates: some tables are forbidden entirely (use the dedicated endpoint), some are admin-only config, deletes default to admin with a small named exception list.
  4. 4stampOrg() overwrites any client-supplied org_id with the caller's; stampSelf() does the same for per-user tables; stampAuthor() records who created a version or took a photo.
  5. 5On select/update/delete, .eq('org_id', caller's org) is applied after every user filter so it always narrows and never widens.
  6. 6Selects are further narrowed by per-rep lead visibility, and updates and deletes mirror that same narrowing.
  7. 7Every write is audited with a before-snapshot and the affected row count.
Why we built it

The hard rule in this codebase is that the browser must never hold a database key — every query goes through this proxy. The comments say why each guard exists rather than describing what it does. On stamping: 'Any client-supplied org_id is ignored — the authenticated user's id always wins, so there's no way to write into another org's namespace.' On ordering: org scoping is 'added LAST so it can't be overridden.' On the write-side lead scope: read-scope alone was not enough, because org-scoped tables permit filterless updates, 'so a restricted rep could POST a filterless update and overwrite EVERY lead in the org.' Even the error text is a control: raw database errors 'echo the data' on constraint violations, so production clients get a generic message and the real error goes to the server log.

The problem
  • A client-supplied account id could otherwise write rows into another company's namespace.
  • A filterless update on a shared table could rewrite every row in the account.
  • Database error text can leak schema details and row values to the browser.
Sound familiar?
What you get
Reads and writes are scoped identically, so hiding a record also protects it from being overwritten.
Table and role permissions are declared in one readable place rather than scattered across endpoints.
Every change is logged with what the row looked like before it changed.
What's inside

See it on your own jobs

Twenty minutes, your numbers, no slide deck. We’ll build one of your real buildings in front of you and send you the estimate link at the end — yours to keep either way.

or keep browsing features →