Build, test, and choose the next guide
View as MarkdownThe tutorial now spans schema, generated records, access rules, an atomic workflow, a typed client, a custom dashboard, a route-based report, and scoped operator access. This page validates those pieces as one application. You will check migrations and builds, exercise the live API’s success and failure paths, confirm transaction and row-scope invariants, and inspect the browser at two viewports before choosing the guide for the next feature.
Run a release-style validation of the task app. Check migrations and builds, exercise the generated and app-owned APIs including their failure paths, confirm the dashboard and report in the browser, and summarize anything that is not ready.Start with artifacts that can be checked offline
Section titled “Start with artifacts that can be checked offline”The database schema, shared contracts, API handlers, and frontend imports form one dependency chain. Check them in that order:
pnpm --filter ./packages/api db:checkpnpm --filter ./packages/shared typecheckpnpm --filter ./packages/api typecheckpnpm builddb:check validates migration metadata. It does not apply pending migrations.
The production build compiles the browser-safe shared package before the API and
frontend consume it. A failure in packages/shared often means a wire contract
or exported type changed; a frontend failure can reveal a typed client that no
longer matches that contract.
Run the focused API tests from the workflow and report pages:
pnpm --filter ./packages/api exec vitest run \ app/complete-task.test.ts \ modules/reports/project-progress.test.tsThe complete-task suite should cover a successful transition, a repeated
completion conflict, a missing or invisible task, and a forced event-insert
failure. The last case proves rollback: the task remains open when the history
row cannot be inserted. The report suite should parse the full result with
gridDatasetSchema and fix today to a known Temporal.PlainDate when it
asserts overdue totals.
import { Temporal } from "@sapporta/shared/temporal";import { gridDatasetSchema } from "@sapporta/shared/grid-dataset";
const dataset = projectProgressDataset({ projects: [{ id: 1, name: "Website relaunch" }], tasks: [ { project_id: 1, status: "open", due_date: Temporal.PlainDate.from("2026-07-09"), }, { project_id: 1, status: "completed", due_date: Temporal.PlainDate.from("2026-07-10"), }, ], today: Temporal.PlainDate.from("2026-07-10"),});
gridDatasetSchema.parse(dataset);expect(dataset.nodes[0]?.columns).toMatchObject({ total: 2, completed: 1, overdue: 1, completion: 0.5,});The tutorial route and tests use the documented 2026-07-10 as-of date. A
continuing application can expose that date as report input or derive it from
Temporal.Now.plainDateISO(). All parsing, comparison, arithmetic, and
formatting for dates and timestamps continues to use Temporal.
Discover the server before the smoke test
Section titled “Discover the server before the smoke test”Start the built app or the development server. Then query the mounted surface:
pnpm devpnpm exec sapporta tables listpnpm exec sapporta endpoints listpnpm exec sapporta endpoints show "GET /api/tables/tasks"pnpm exec sapporta endpoints show "PUT /api/tables/tasks/{id}"pnpm exec sapporta endpoints show "POST /api/tasks/{id}/complete"pnpm exec sapporta endpoints show "GET /api/reports/project-progress"This step catches two deployment mistakes that TypeScript cannot: a TsRestApi
sub-app that was not mounted with route(), and contract emitters that were not
composed with extend(). Direct calls come after discovery and use the mounted
/api paths.
Create one open task through the generated API. The request contains domain fields only; the server derives workspace ownership:
pnpm exec sapporta rows create tasks --values '{ "project_id": 1, "title": "Publish release notes", "status": "open", "priority": "medium", "due_date": "2026-08-01"}'pnpm exec sapporta rows list tasks \ --where '{"status":{"eq":"open"}}' \ --output jsonA representative list response retains the generated envelope:
{ "data": [ { "id": 7, "project_id": 1, "title": "Publish release notes", "status": "open", "priority": "medium", "due_date": "2026-08-01" } ], "meta": { "total": 1, "page": 1, "limit": 50, "pages": 1 }}Use PUT /api/tables/tasks/7 for another ordinary row edit. Do not substitute
PATCH. Use the app-owned endpoint for completion because that transition also
creates immutable history:
pnpm exec sapporta api post /api/tasks/7/complete --body '{}'pnpm exec sapporta api post /api/tasks/7/complete --body '{}'pnpm exec sapporta rows list task_events \ --where '{"task_id":{"eq":7}}' \ --output jsonpnpm exec sapporta api get /api/reports/project-progressThe first completion returns 200 with the completed task and event. The second
returns the declared conflict body, and the event query still returns one
completion event:
{ "error": "Task is already completed", "code": "TASK_ALREADY_COMPLETED"}The report footer should agree with the generated task and event reads. Its exact total depends on the smoke-test rows created and completed in this pass. The generated list, history query, and report are three views of the same state, not independent fixtures.
Prove the negative boundaries
Section titled “Prove the negative boundaries”Run the same read checks with a Workspace A token after seeding one visibly named task in Workspace B. The generated list and report must omit Workspace B. Call complete-task with that hidden task ID and with an unused ID. Both results must follow the same declared missing-row behavior.
Authentication, feature abilities, and row visibility are separate assertions:
- no session or token receives the documented authentication error;
- a signed-in user without the workflow ability receives the declared authorization error;
- an authorized Workspace A caller receives only Workspace A rows; and
- clients never submit
workspace_id, owner, role, or scope columns.
The forced transaction failure belongs in an automated test because it needs a
controlled database error. The SQLite transaction callback remains synchronous,
uses insertValuesSync() for the history write, and contains no awaited
external effect.
Walk the browser states
Section titled “Walk the browser states”Sign in and open each generated table once, then exercise the custom routes:
- Open
/tables/projects, create or edit a project, and follow its Tasks child relationship. - Open
/tables/tasks, filter and sort the worklist, and inspect one task’s Task history. - Open
/projects/progress, complete an open task, and observe the updated project total and action state. - Open
/reports/project-progress, run the full report, select a project filter, reload the URL, and follow project and status links back to generated records.
Check a narrow viewport and a desktop viewport. Loading, empty, ready, expected
error, and refreshed states should remain explicit. Browser Network requests for
the custom routes should use /api/tasks/:id/complete and
/api/reports/project-progress; the React routes themselves do not include
/api.
The finished app has three workspace-scoped tables, generated CRUD surfaces, an atomic multi-table transition, a shared contract and typed client, a protected workflow screen, a scoped report, and a revocable operating path. The important constraints remain visible in the final checks: server-authored scope, row-secured base reads, synchronous SQLite transactions, Temporal values, declared error bodies, explicit route composition, and frontend-owned report links.
Choose the next guide by the boundary that needs to change:
- Model data for tables, relationships, validation, semantic values, and migrations.
- Generated surfaces for ordinary record screens and table-aware Grid workflows.
- Security for abilities, workspaces, ownership, and row visibility.
- App-owned features for business transitions, transactions, uploads, external effects, and custom response shapes.
- Reports for scoped datasets, filters, formatting, and drill-through links.
- Discovery and operations for CLI, agents, OpenAPI, configuration, builds, and deployment.
The guide index explains those workflows. The reference index is the lookup path for exact contracts, types, routes, and options.