# feature() — single-export sugar

The `q.table()` + `defineTable()` pair has been co-equal with Drizzle since day one. It works, but it means two exports and two concepts for something most feature files treat as one unit: a table with a security contract.

`feature()` collapses both into a single default export.

## Before

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

export const contacts = q.table("contacts", {
  id:             q.id(),
  organizationId: q.text().required().index(),
  address:        q.text().required().index(),
  name:           q.text().optional(),
});

export default defineTable(contacts, {
  firewall: [{ field: 'organizationId', equals: 'ctx.activeOrgId' }],
  read: { access: { roles: ["member+"] } },
  create: { access: { roles: ["member+"] } },
  update: { access: { roles: ["member+"] } },
  delete: { access: { roles: ["admin+"] }, mode: "soft" },
  guards: { createable: ["address", "name"] },
});

export type Contact = typeof contacts.$infer;
```

## After

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

export default feature("contacts", {
  columns: {
    id:             q.id(),
    organizationId: q.text().required().index(),
    address:        q.text().required().index(),
    name:           q.text().optional(),
  },
  firewall: [{ field: 'organizationId', equals: 'ctx.activeOrgId' }],
  read: { access: { roles: ["member+"] } },
  create: { access: { roles: ["member+"] } },
  update: { access: { roles: ["member+"] } },
  delete: { access: { roles: ["admin+"] }, mode: "soft" },
  guards: { createable: ["address", "name"] },
});
```

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

## 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 reference the table by default import.** `import contacts from "./contacts"` gives you the same typed handle `q.table("contacts", ...)` would have returned, so `defineActions(contacts, { … })` still works.
- **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() },
  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

A feature directory holds tables, optional shared schemas, and the handler files that back any actions:

```
features/podcast/
├── episodes.ts                 ← defineTable + (optional) defineActions(episodes, {…})
├── shows.ts                    ← defineTable + (optional) defineActions(shows, {…})
├── episode-tags.ts             ← defineTable only (junction)
├── _feature.ts                 ← non-table-bound actions + shared zod schemas
├── handlers/
│   ├── episodes/{publish,revertToDraft}.ts
│   └── shows/…
└── lib/
    └── shared.ts               ← feature-local helpers, copied verbatim
```

Three things to know:

- **`handlers/` walks recursively**, keyed by relative path (`handlers/episodes/publish.ts`). The canonical layout is per-table subdirectories so action handlers stay grouped with the table they act on.
- **`lib/` rides along** — feature-local helper modules get copied verbatim into the generated output. Use this for shared utilities that both handlers and inline `execute:` bodies reference.
- **`_feature.ts` is the universal slot** for non-table-bound actions and shared schemas. Top-level decls in `_feature.ts` get prepended once to the merged actions module, so a `const sharedSchema = z.object({…})` in `_feature.ts` is referenceable from any sibling action's `input:` without re-declaring it.

For everything action-related — three modes, conflict rules, the `_feature.ts` shape — see [Actions](/compiler/definitions/actions).

## Handlers — one `q` namespace for everything

Custom action handlers 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 handler code stays inside one DSL:

```ts title="quickback/features/mail/handlers/inbound.ts"
import { q } from "../../../lib/q";
import mailboxes from "../mailboxes";
import messages from "../messages";
import threads from "../threads";

export const execute = async ({ 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,
    createdAt: new Date().toISOString(),
  });

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

The `q` in handlers 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 a handler — 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.
