Quickback Docs

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).
  • 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.

On this page