# Bundles

## Overview

Client bootstrap screens want many small lists in one request. Hand-written
"bundle" endpoints all repeat the same 150 lines: a `Promise.all` of selects
shaped byte-identically to the CRUD list endpoints they replace, a
`sections: string[]` subset filter, a copy-pasted SHA-1 ETag +
`ifNoneMatch → notModified` short-circuit, a `Server-Timing` header, and
hand-re-derived per-slice masking. `defineBundle` generates all of it.

```typescript
// features/events/actions/getOverviewBundle.ts
import { defineBundle, list, custom } from "../.quickback/define-bundle";

export default defineBundle({
  path: "/event/:eventId/overview-bundle",
  description: "Console Overview bootstrap",
  access: { roles: ["member", "admin"] },
  etag: true,                          // default true
  serverTiming: "overview",            // optional
  sections: {
    guests: list("guests", {
      columns: ["id", "status", "modifiedAt", "createdAt", "displayName"],
      where: { eventId: "$ctx.event.id" },
      orderBy: [{ createdAt: "desc" }],
      limit: 500,
    }),
    tickets: list("supportTickets", { where: { eventId: "$ctx.event.id" }, orderBy: [{ modifiedAt: "desc" }], limit: 100 }),
    counts: custom(async ({ db, ctx }) => ({ open: 0 })),   // computed slice — author owns masking
  },
});
```

## Semantics

- **Generated POST action** with input `{ sections?: string[], ifNoneMatch?: string }`,
  mounted through the standard standalone pipeline — auth, access, org/team
  preconditions, rate limits, OpenAPI, MCP tools, security contracts all
  apply.
- **`list()` sections run through the scoped db** — the org firewall and
  soft-delete filter apply by construction — and the target table's REAL
  mask function is applied (the same read projection the CRUD list endpoint
  runs), so masked columns are safe to include. Every list slice lands as
  `{ data: rows }` (the per-resource cache-seed contract).
- **`sections` filter**: omitted/empty = all. An un-wanted list section
  returns `{ data: [] }` (drop-in compatible with existing clients);
  un-wanted `custom` sections are absent.
- **All sections run in one `Promise.all`.**
- **ETag**: SHA-1 hex over `{ scopeSeed: <path params>, userId, payload }`.
  An `ifNoneMatch` match returns `{ success: true, etag, notModified: true }`
  with **no sections**. The digest is also exported as `sha1Hex` from
  `lib/etag` so remaining hand-written reads can delete their copies.
- **`custom()` sections** receive the full action context and are embedded
  verbatim under the section name — the author owns masking there.

## Compile-time validation (fail-closed)

- Unknown section tables error, listing candidates.
- `columns` / `where` / `orderBy` must name declared columns
  (compiler-injected audit columns like `createdAt`/`modifiedAt` are allowed).
- `where` values are literals or the strict substitution allowlist
  (`$ctx.userId`, `$ctx.activeOrgId`, `$ctx.<scope>.id`) — always
  parameterized. Anything richer belongs in a `custom()` section.
- Section names may not shadow the response envelope
  (`success`, `etag`, `notModified`, `sections`).
- **A bundle must never widen read access**: its `access.roles` must be a
  subset of every list section table's `read.access` roles — a violation is
  a compile error, not a warning.
