Quickback Docs

Owned Records

Owned child relations render inline on the aggregate root's record page — read via ?include=, written through the changeset engine, gated by the child's own access.

A table that declares an owns boundary gets its owned children rendered inline on the record page: each relation appears as a section below the record's fields, with a row table and Add / Edit / Delete controls. Line items live on the expense report, not in a sidebar collection.

When a relation renders

All three must hold — a relation missing any of them is skipped:

  1. The relation is declared in the root's owns.
  2. The relation name is in the root's read.include allowlist — reads ride GET /:id?include=<relation>, and a non-allowlisted include is a 400.
  3. The child table exists in the schema registry.
quickback/features/expenseReports/expenseReports.ts
export default feature("expenseReports", {
  // ...columns, firewall, guards, crud...
  owns: {
    lineItems: { table: "lineItems", fk: "reportId", inherit: ["organizationId"] },
  },
  read: {
    access: { roles: ["member+"] },
    include: ["lineItems"],   // without this, the CMS has no way to read the rows
  },
});

Writes go through the changeset engine

Every Add, Edit, and Delete is a single-op changeset PATCH on the rootContent-Type: application/vnd.quickback.changeset+json — never a raw child route. The generated aggregate engine enforces the child's own access, guards, body schema, and firewall on every op, and stamps the spine FK and inherit columns from the firewall-verified parent row.

Consequences in the UI:

  • The child's fk and inherit columns are excluded from the form — the engine stamps them, and a client-supplied value is rejected.
  • The Add / Edit / Delete buttons gate on the child table's create / update / delete access for the signed-in role, exactly like the child's own raw routes would.
  • Child deletes are soft (changeset v1 admits soft-delete children only).

routes: false children

The usual shape for a pure composition child — line items that only ever exist inside a report — is routes: false on every op plus cms: { internal: true }:

quickback/features/expenseReports/lineItems.ts
read:   { access: { roles: ["member+"] }, routes: false },
create: { access: { roles: ["member+"] }, routes: false },
update: { access: { roles: ["member+"] }, routes: false },
delete: { access: { roles: ["member+"] }, routes: false, mode: "soft" },

cms: { internal: true },   // no standalone sidebar entry

routes: false keeps the access declared (it is the changeset admission gate) but mounts no raw HTTP route. The CMS reads the registry's routesDisabled list and suppresses every raw-CRUD affordance for those ops — New button, row Edit/Delete, record-page Edit/Delete, and inline cell editing — so it never renders a button whose request can only 405. The owned-records section is unaffected: its writes are changeset ops, which routes: false deliberately leaves open.

Note the child's read access above is what gates the embedded rows: ?include= inherits the target's read access, firewall, and masking. A child readable only by admin+ renders an empty section for members.

Next Steps

On this page