# feature() — the canonical form

`feature()` is **the** way to write a Quickback feature: one file, one default export, the table and its security contract together. It expands at compile time into `q.table()` + `defineTable()`, which stays available for the cases that need it — see [When to use which](#when-to-use-which).

## Quick reference

One object. Everything a table exposes is a key in it, and every key is optional
except `columns`.

{/* doc-compile: skip — key map, not a config: every value is an empty placeholder. The compiled forms are in the fences below and on each key's own page. */}
```ts
feature("name", {
  columns:     {},  // q.* column helpers — the Drizzle schema

  // Security — the five pillars
  firewall:    {},  // which ROWS (auto-derived from organizationId / ownerId / teamId)
  read:        {},  // who may read + `views` — named column projections
  create:      {},  // who may write — `action: "send"` maps the URL to an action file
  update:      {},
  delete:      {},  // + `mode: "soft" | "hard"`
  guards:      {},  // which FIELDS a caller may set
  masking:     {},  // which VALUES come back redacted
  rateLimit:   {},  // per-operation caps

  // Endpoints beyond CRUD — declared here, not written as action files
  transitions: {},  // guarded state changes → POST /:id/{name}
  triggers:    {},  // before/after hooks on every write
  owns:        {},  // composition boundary → atomic parent+children writes
  realtime:    {},  // broadcast insert/update/delete
  embeddings:  {},  // vectors generated on write

  // Placement + presentation
  path:        "",  // route base (defaults to the feature name)
  namespace:   {},  // hoisted route prefix + admission gate
  displayColumn: "", defaultSort: {}, inputHints: {}, layouts: {}, references: {},
});
```

| You want | Key | Page |
|---|---|---|
| Tenant isolation, role gates, field and value control | `firewall`, `read`/`create`/`update`/`delete`, `guards`, `masking` | [Security](/define/security) |
| A narrower shape for a broader audience | `read.views` | [Views](/define/views) |
| `publish` / `approve` / `checkIn` — a state change | `transitions` | [Transitions](/define/transitions) |
| A live CRUD URL that needs custom `execute` | `create.action` / `update.action` / `delete.action` | [Actions](/define/actions#replace-a-crud-verb) |
| A side-effect or data rule on every write | `triggers` | [Triggers](/define/triggers) |
| Parent plus its children written atomically | `owns` | [Changesets](/define/changesets) |
| Clients updated live | `realtime` | [Realtime](/platform/realtime/using-realtime) |
| Semantic search over the table | `embeddings` | [Embeddings](/platform/vector/using-embeddings) |
| The CMS to render it well | `displayColumn`, `defaultSort`, `inputHints`, `layouts` | [CMS](/ui/admin/table-views) |

The rest of this page is the form itself; [One object, not just
CRUD](#one-object-not-just-crud) shows the non-CRUD keys filled in.

## The form

```ts title="quickback/features/contacts.ts"
import { feature, q } from "@quickback/compiler";

export default feature("contacts", {
  columns: {
    id:             q.id(),
    organizationId: q.scope("organization"),   // firewall auto-derives from this
    address:        q.text().required().index(),
    name:           q.text().optional(),
    ...q.audit(),
    ...q.softDelete(),
  },
  // "+" role expansion requires auth.roleHierarchy in quickback.config.ts
  read: { access: { roles: ["member+"] } },
  create: { access: { roles: ["member+"] } },
  update: { access: { roles: ["member+"] } },
  delete: { access: { roles: ["admin+"] }, mode: "soft" },
  guards: { createable: ["address", "name"] },
});
```

Type inference comes with it — `typeof import("./contacts").default.$infer` gives you the `Contact` row type.

## One object, not just CRUD

The example above declares security and CRUD because that is the floor. The same
config object carries every other per-table capability — a live feed, a state
machine, a narrower projection, a write hook. They are keys next to `read:` and
`guards:`, not separate files or services, and each generates a real endpoint on
the same firewall → access → guards → masking pipeline.

{/* doc-compile: skip — shape sketch: `columns` is elided so the keys stay readable side by side, and `member+`/`admin+` need `auth.roleHierarchy` in quickback.config.ts. Each key's own page carries the compiled version. */}
```ts
export default feature("episodes", {
  columns: { /* … */ },
  firewall: [{ field: "organizationId", equals: "ctx.activeOrgId" }],
  read: {
    access: { roles: ["member+"] },
    views: { public: { fields: ["id", "title"], access: { roles: ["member+"] } } },
  },
  guards:      { updatable: ["title"], protected: { isPublished: ["publish"] } },
  masking:     { submitterEmail: { type: "email", show: { roles: ["admin"] } } },
  transitions: { publish: { field: "isPublished", from: false, to: true, access: { roles: ["admin+"] } } },
  realtime:    { enabled: true, onInsert: true, onUpdate: true },
  embeddings:  { fields: ["title", "summary"] },
});
```

| Key | Gives you | Reference |
|---|---|---|
| `read.views` | Named column projections, each with its own route and access rule | [Views](/define/views) |
| `transitions` | Guarded state changes generated as real `POST /:id/{name}` actions — guards, stamps, `undo`, cascades | [Transitions](/define/transitions) |
| `triggers` | `before`/`after` hooks on write, lowered to a real SQL trigger or an application handler | [Triggers](/define/triggers) |
| `realtime` | Per-table broadcasts on insert/update/delete, audience-scoped | [Realtime](/platform/realtime/using-realtime) |
| `owns` | The composition boundary that turns on atomic parent-plus-children writes | [Changesets](/define/changesets) |
| `embeddings` | Vector embeddings generated on insert/update | [Embeddings](/platform/vector/using-embeddings) |
| `masking` / `rateLimit` / `namespace` | Field redaction, per-operation caps, a hoisted route prefix + gate | [Masking](/define/masking), [Rate limit](/define/rate-limit), [Areas](/define/areas) |
| `displayColumn` / `defaultSort` / `inputHints` / `layouts` / `references` | How the CMS renders the table — no runtime effect | [CMS](/ui/admin/table-views) |

Reach for these before writing an action file: an action is code you maintain,
these are declarations the compiler owns. See the full option list in
[Definitions Overview](/define#other-resource-options) and the "before you write
one" routing table at the top of [Actions](/define/actions).

## How it works

`feature(name, config)` is a **compile-time sugar**, not a runtime abstraction. When the compiler sees `export default feature(...)`, it rewrites the source to the canonical two-export form before any parsing runs. Every downstream layer — dialect pass, audit injection, security contracts, generated routes, OpenAPI, MCP tools, RLS emission — runs on the same Drizzle source it always did. Zero runtime cost; zero semantic difference from `q.table()` + `defineTable()`.

That means:

- **Both forms remain supported indefinitely.** Mix them freely — some files `feature(...)`, some files `q.table() + defineTable()`, some files Drizzle `sqliteTable() + defineTable()`. The compiler dispatches per file.
- **Action files get the table from the generated helper.** Each action lives in its own file under `actions/` and imports both `defineAction` and the feature's table from `../.quickback/define-action` — the helper re-exports the table typed with the generated schema, so `record` and the audit columns are visible without casts.
- **Diagnostics are identical.** Missing firewall → same warning. Protected-field wiring → same check. Nothing about `feature()` weakens the secure-by-default posture.

## Aliases

`q` is canonical (matches Zod's `z.`, Superstruct's `s.`). `qb` is exported as an alias if you'd rather self-document the namespace:

```ts
import { feature, qb } from "@quickback/compiler";

export default feature("contacts", {
  columns: { id: qb.id(), name: qb.text().required(), ...qb.audit(), ...qb.softDelete() },
  firewall: [{ field: 'organizationId', equals: 'ctx.activeOrgId' }],
  // …
});
```

Same object, two names — pick one.

## When to use which

| Pattern | When |
|---|---|
| `feature(name, {...})` | Default. One table, one security contract, one file. |
| `q.table()` + `defineTable()` | Multiple tables per file (rare), or you want to reference the table identifier before the default export. |
| `sqliteTable()` / `pgTable()` + `defineTable()` | Interop with existing Drizzle code / migrations. |
| `q.table(...)` alone (no `defineTable`) | Internal join/lookup/pivot tables with no public API. Compiler warns so you confirm the opt-out is intentional. |

## Feature directory layout

Feature directories normally sit flat under `features/`. To group several features under one
authz concept (a shared route prefix + shared relationships/roles/scopes), put them inside an
**area** folder carrying `_area.ts` — see [Feature areas](/define/areas).
Reserved sub-directories (`actions/`, `lib/`, `pages/`) are never child features, and a
subdirectory of a NON-area feature containing `defineTable(...)`/`defineAction(...)` fails the
compile (nested features load only under an area).

A feature directory holds tables, one file per action, and optional shared helpers:

```
features/podcast/
├── podcast.ts                  ← feature() — the primary table
├── shows.ts                    ← feature() (sibling table)
├── episode-tags.ts             ← q.table only (junction, no routes)
├── actions/
│   ├── publish.ts              ← defineAction, binds to the primary table
│   ├── report.ts               ← defineAction, standalone (has `path:`)
│   └── shows/
│       └── archive.ts          ← defineAction, binds to shows.ts
└── lib/
    └── shared.ts               ← feature-local helpers, copied verbatim
```

Two things to know:

- **`actions/` walks recursively**, keyed by relative path. A flat `actions/<name>.ts` binds to the feature's primary table; `actions/<table>/<name>.ts` binds to the sibling table file `<table>.ts`. An action with a `path:` is standalone and binds to nothing.
- **`lib/` rides along** — feature-local helper modules get copied verbatim into the generated output. Put shared Zod schemas and helpers here and import them from each action file.

For everything action-related — record vs standalone, conflict rules, the generated `defineAction` helper — see [Actions](/define/actions).

## Composition vs reference — `owns`

A schema-level FK (`references:` / `.references()`) says "this row points at
that row." It does **not** say who owns whom. `.references()` may only target
another **feature table in the same database** — never Better Auth `users` /
`user` / `organization` / `member` (those live in `AUTH_DB`). When a set of child rows has no
lifecycle of its own — junction memberships, normalized phones/emails, line
items — declare the composition with `owns` on the root table:

```ts
export default feature("projects", {
  columns: {
    id:             q.id(),
    name:           q.text().required(),
    organizationId: q.scope("organization"),
    ...q.audit(),
    ...q.softDelete(),
  },
  owns: {
    tasks:   { table: "tasks",          fk: "projectId", inherit: ["organizationId"] },
    members: { table: "projectMembers", fk: "projectId", refs: { personId: "people" }, inherit: ["organizationId"] },
  },
});
```

That single fact generates the whole aggregate write path — one atomic
changeset request over the root plus its owned relations, every op paying the
child's own firewall/access/guards. Keep plain `references` for pointers at
independent entities (those go in the relation's `refs`, existence-checked
per-tenant). Contract v2 only. Full reference:
[changesets](/define/changesets) — including the per-op
`routes: false` knob for widening a child's access for changeset use without
mounting its raw route.

## Action `execute` — one `q` namespace for everything

Action bodies need to run queries against the database. Quickback's compiled projects expose the Drizzle query-builder helpers on the same `q` namespace you use for schema authoring, so `execute` stays inside one DSL:

```ts title="quickback/features/mail/actions/inbound.ts"
import { z } from "zod";
import { defineAction } from "../.quickback/define-action";
import { q } from "../../../lib/q";
import { mailboxes } from "../mailboxes";
import { messages } from "../messages";
import { threads } from "../threads";

export default defineAction({
  description: "Ingest an inbound email into the right thread.",
  path: "/mail/inbound",
  input: z.object({ mailboxAddress: z.string(), parsed: z.any() }),
  access: { roles: ["owner"] },
  unsafe: { reason: "Inbound webhook resolves the mailbox before a tenant is known" },
  async execute({ unsafeDb, input }) {
    // Find the mailbox this email was addressed to
    const mailbox = await unsafeDb.query.mailboxes.findFirst({
      where: q.eq(mailboxes.address, input.mailboxAddress),
    });
    if (!mailbox) return { error: "unknown mailbox" };

    // Look up the thread by in-reply-to + organization scope
    const thread = input.parsed.inReplyTo
      ? await unsafeDb.query.threads.findFirst({
          where: q.and(
            q.eq(threads.organizationId, mailbox.organizationId),
            q.eq(threads.lastMessageId, input.parsed.inReplyTo),
          ),
        })
      : null;

    // Insert the new message
    await unsafeDb.insert(messages).values({
      id: crypto.randomUUID(),
      threadId: thread?.id,
      organizationId: mailbox.organizationId,
      subject: input.parsed.subject,
    });

    return { ok: true };
  },
});
```

The `q` in action bodies is a runtime re-export of Drizzle's query builder — `q.eq`, `q.and`, `q.or`, `q.not`, `q.ne`, `q.gt`, `q.gte`, `q.lt`, `q.lte`, `q.like`, `q.ilike`, `q.inArray`, `q.notInArray`, `q.isNull`, `q.isNotNull`, `q.between`, `q.desc`, `q.asc`, `q.sql`. Pure re-export, zero wrapper cost.

**The compiler generates `src/lib/q.ts`** in every project so the runtime `q` is always available. You never need to `import { eq } from "drizzle-orm"` in an action — though you can if you want (Quickback doesn't hide drizzle, it just offers a consistent surface that matches the schema DSL).

## Why one namespace

Quickback is an opinionated wrapper with opinionated security. One DSL for schema (`q.table`, `q.text`, `q.id`), one DSL for queries (`q.eq`, `q.and`, `q.gte`), one opt-in for routes (`feature()` / `defineTable`). You never need to learn the names of the underlying tools to write a feature — the same way Next.js users don't need to know they're using webpack, or Astro users don't need to know they're using Vite.
