Recipes
Short answers to common tasks. Each recipe is a complete example: the code on the page is type-checked, and, except for the React form, runs in CI against Postgres, with the values in its // => comments checked.
Each recipe shows its schemas, tables and starting rows in a folded block. Run each one in an empty database of its own: they create their own tables, and some use the same table names with other columns.
Reading
| Recipe | In short |
|---|---|
| Pagination | limit and offset with a total, or keyset pagination with a tuple cursor. |
| Top N per parent | A preload with a per-parent limit, or a window function. |
| Filters from a query string | Validate ?q=…&page=… with a schemaless changeset, then add clauses conditionally. |
| Full-text search | A generated tsvector column, and fragment for @@ and ts_rank. |
Writing
| Recipe | In short |
|---|---|
| Upserts | onConflict on insert and insertAll. |
| Find or create | Insert with onConflict: "nothing", then read the row that won. |
| Optimistic locking | optimisticLock("lockVersion"), and a conflict reported on the form. |
| Soft deletes | A deletedAt field and a reusable scope. |
| Auditing | A batch that writes a row and its audit entry together. |
Modelling
| Recipe | In short |
|---|---|
| Enums | t.enum for text and integer columns, with a check constraint. |
| Money | A custom type that stores cents in a numeric column. |
| Multi-tenancy with database schemas | A database schema per tenant, chosen per call or per repo. |
Forms and messages
| Recipe | In short |
|---|---|
| Showing form errors (React) | A server action that returns errorMessages(), and fields that show them. |
| Translating error messages | traverseErrors with a message catalogue and plural rules. |
| Errors as HTTP responses | 422 for an invalid changeset, 404, 409 and 500 for the rest, typed on the client with Json and ErrorMessages. |

