Bundles
Single-round-trip bootstrap reads — declarative slices with firewall, masking, and ETag built in.
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.
// 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).sectionsfilter: omitted/empty = all. An un-wanted list section returns{ data: [] }(drop-in compatible with existing clients); un-wantedcustomsections are absent.- All sections run in one
Promise.all. - ETag: SHA-1 hex over
{ scopeSeed: <path params>, userId, payload }. AnifNoneMatchmatch returns{ success: true, etag, notModified: true }with no sections. The digest is also exported assha1Hexfromlib/etagso 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/orderBymust name declared columns (compiler-injected audit columns likecreatedAt/modifiedAtare allowed).wherevalues are literals or the strict substitution allowlist ($ctx.userId,$ctx.activeOrgId,$ctx.<scope>.id) — always parameterized. Anything richer belongs in acustom()section.- Section names may not shadow the response envelope
(
success,etag,notModified,sections). - A bundle must never widen read access: its
access.rolesmust be a subset of every list section table'sread.accessroles — a violation is a compile error, not a warning.