# Security Overview

Quickback's security is not one mechanism. It is four different questions, each
answered by a different pillar, each enforced at a different point in the request.
This page is the map; every mechanism has its own page.

## The four questions

| The question | Pillar | Where it runs |
|---|---|---|
| **Which rows** may this caller ever see? | [Firewall](/define/firewall) | A `WHERE` clause added to every query |
| **Who** may perform this operation? | [Access](/define/access) | Middleware, before the handler |
| **Which columns** come back on a read? | [Views](/define/views) | A read projection |
| **Which fields** may be written? | [Guards](/define/guards) | Write validation, before the DB |
| What is **redacted** in the response? | [Masking](/define/masking) | A response transform |
| How **often** may they ask? | [Rate limits](/define/rate-limit) | Edge, before routing |

Views and Guards are documented under **Reads** and **Writes** rather than under
Security, because that is where you are when you need them: a view is a shape you
return, a guard is a rule about a write. They are security mechanisms either way.

## Firewall — which rows

The firewall is tenant isolation. It rewrites every query so a caller can only ever
address rows inside their own scope. It is not a permission check that can be granted
away — it is the boundary of the addressable data set.

Isolation columns are **auto-detected from the schema**:

- `organizationId` — plus five other accepted spellings, see below — scoped to `ctx.activeOrgId`
- `teamId` — scoped to `ctx.activeTeamId`
- `ownerId` — scoped to `ctx.userId`

### The owner-column rule

This is the rule that is most often stated backwards, so state it once and link it:

> `ownerId` is auto-detected as the owner column: the compiler firewalls reads with
> `WHERE ownerId = ctx.userId` and auto-stamps the column on insert.
>
> `userId` is **not** auto-detected on feature tables. It is a Better Auth convention
> on Better Auth's own tables (`session`, `account`, `member`, `passkey`), and on a
> feature table it usually means *a user this row references* — a member, a contact, an
> invitee — rather than *the row's owner*.
>
> If `userId` is the only candidate column on the table, the compiler raises an
> **error**. Alongside a detected isolation column it raises a **warning**.
>
> Two escapes: rename the column to `ownerId`, or declare
> `firewall: { owner: { column: "userId" } }` explicitly.

### Six spellings of the organization column

Organization auto-detection accepts six column names, American and British, in this
precedence order. The first one present in the schema source wins:

```text
organizationId
organisationId
orgId
organization
organisation
org
```

## Access — who

Access is the role check. It runs as middleware, before your handler, per operation —
`read`, `create`, `update`, `delete`, and each action independently.

Two things it is **not**:

- It is not row filtering. A caller who passes the access check still only sees the
  rows the firewall admits.
- It is not a substitute for the firewall. A scoped-role gate is an access decision;
  the row-scope-column boundary is a firewall decision. Mixing them produces policies
  that read as security but enforce nothing.

Beyond plain role lists, Access carries record conditions, context substitution,
relationship-based roles, and the `SESSION` / `SCOPED` / `AUTHENTICATED` pseudo-role
tiers. See [Access](/define/access).

## Columns and fields

**Reads** — [Views](/define/views) are named column-level projections with their own
access control. A view is how you return a narrower shape to a broader audience.

**Writes** — [Guards](/define/guards) declare which fields can be set on create
(`createable`), modified on update (`updatable`), changed only through an action
(`protected`), or never changed after creation (`immutable`). A protected field is
omitted from the generated request schemas entirely, so a client cannot even send it.

**Redaction** — [Masking](/define/masking) redacts values in the response based on the
caller's role. Use it for data the caller may legitimately address but should not read
in full.

**State changes** — `protected: { status: ["publish", "cancel", "refund"] }` removes the
column from the PATCH schema and names the only doors to it. Each name is an action:
either a [transition](/define/transitions) the compiler generates from
`transitions: { publish: { field: "status", from: "draft", to: "live", access: {...} } }`,
or an [action file](/define/actions) you write when the change needs real logic — a
`refund` that calls Stripe, then sets the column. Both carry their own `access` rule and
appear in OpenAPI; the transition additionally gets row guards, stamps, and an `undo`
inverse for free. Declare the transition when the change is state plus stamps; write the
action when it is genuinely imperative. Either beats leaving the column freely writable.

## Advanced authorization

When roles are not enough — when access depends on a *relationship* between the caller
and the row — the model extends:

- [Permissions](/define/permissions) — named capabilities instead of raw role strings
- [Scopes](/define/scopes) — sessionless scope-token principals
- [Arrows](/define/arrows) — relationship traversal
- [FGA](/define/fga) — fine-grained authorization tuples
- [Diagnostics](/define/diagnostics) — what the compiler decided, and why

## Encryption

[Encryption](/define/encryption) and the [sealed protocol](/define/sealed-protocol)
cover data at rest. They are orthogonal to the pillars above: encryption governs what
the stored bytes are, the pillars govern who reaches them.

## Where this is enforced

Everything on this page is a **compile-time** decision. The pillars are declared in your
feature files, checked for coherence when you run `quickback compile`, and emitted as
generated middleware and query builders. There is no runtime policy engine to
misconfigure and no rules to keep in sync by hand — if a policy is incoherent, the
compile fails.
