# The `app` template
The starting shape for a full-stack Noxid application. `noxid new <dir>
--template app` copies this project; it compiles, tests, and deploys with
zero edits and no service running.
It is secure by default. There is no anonymous principal anywhere in it: a
request with no valid session cookie is refused at `server/middleware/`
before any handler runs, so the first thing the page does is sign you in.
```sh
pnpm install --frozen-lockfile # your install; `noxid new` runs none
noxid test --gate # scenarios + invariants + API contract
noxid build . --out-dir dist # client, server, manifests
noxid adapt . --adapter node --out-dir dist # a runnable Node deployment
```
`noxid new` never runs a package manager. It vendors the vetted plugin files
this project imports — `plugins/drizzle-orm/adapter.js`, its `VETTING.md` and
`data-scopes.test.mjs`, `plugins/postgres/VETTING.md`, and the two
`tools/*.mjs` modules the adapter imports — byte-identical, and records their
content hashes plus the reviewed package versions in `.noxid-plugins.json`.
`package.json` pins `drizzle-orm` and `postgres` at exactly those versions, and
`noxid build` refuses (`PLUGIN_VENDOR_DRIFT`, `NPM_IMPORT_UNVETTED`) if a
vendored file was edited or the lockfile resolves a different release. The
SQLite driver is Node's builtin `node:sqlite`: WO-38 admits no SQLite registry
package, native addon, or install script, so there is nothing to pin but the
Node version in `engines`.
## What is here, and why
| `src/routes/+page.nox` | A typed form whose submit awaits a remote action and consumes `Result<Note, RemoteError>` exhaustively |
| `src/routes/+page.nox` (`resource Board`) | A `live` resource: the compiler publishes an invalidate-only notice after any validated mutation, and every lifecycle case is rendered |
| `src/types/notes.nox` | The types shared by the page and the endpoint — one definition, both sides |
| `server/api/notes/[boardId].get.nox` | One typed endpoint with `params`, `result`, `capabilities`, `timeout`, and `limit`, plus a `scenario` that makes a real request through the shipped handler. The declaration is the contract |
| `server/middleware/session.ts` | Global server middleware: it runs on every request with no registration, resolves one principal, mints and expires the session cookie, and refuses everything else that arrives without one |
| `src/routes/+page.nox` (`signIn`/`signOut`) | The session doors: a display name in, a signed cookie back, the board behind it. A **starter placeholder** — replace it with a real identity provider |
| `server/utils/schema.ts` | One `scopedTable` and one `unscopedTable` over real `drizzle-orm` tables. Every reachable table states its row policy or the build refuses |
| `plugins/drizzle-orm/adapter.js` | The vendored, vetted adapter: the only sanctioned path from server code to a database, and the enforcer of the scope `schema.ts` declares |
| `.noxid-plugins.json` | The vendoring ledger: source commit, one content hash per vetted file, and the package versions those files were reviewed against |
| `server/db/migrations/0001-create-notes.sql` | A forward-only, checksummed migration made by `noxid db new` |
| `server/host.ts` | The implementations, keyed by exact semantic id (`endpoint:LoadBoard@1`, `action:AppPage.signIn`, `action:AppPage.signOut`, `action:AppPage.createNote`). The read and the write both bind the scoped `notes` table to the runtime principal through the adapter |
| `api-contract.json` | The committed API baseline. `noxid test --gate` fails a breaking change unless `version:` increments |
## Where the compiler will stop you
- Add a field to `NoteBoard` and forget the host: the result fails validation
on the way out, so the endpoint refuses instead of lying.
- Remove a `#match` arm on `board`: resource lifecycles are exhaustive.
- Change the endpoint's result type without bumping `version:`:
`API_CONTRACT_BREAKING_CHANGE`.
- Delete the `CreateNote`, `SignInOpensTheBoard`, or `RefusesWriteWithoutSession`
scenario: `APP_NOTE_CREATE`, `APP_SIGN_IN`, or `APP_SESSION_REQUIRED` has no
executable coverage and `noxid test --gate` fails.
- Delete the endpoint scenario's `given`: the scenario then declares no host
for the endpoint, so its authority is undeclared and the shipped handler
refuses the real request with `ENDPOINT_CAPABILITY_DENIED`.
- Query `notes` without binding `owner_id` to the runtime principal:
`DATA_SCOPE_VIOLATION` at the adapter, before the statement is prepared.
- Edit `plugins/drizzle-orm/adapter.js`: `PLUGIN_VENDOR_DRIFT`.
- Add a table to `schema.ts` without a policy wrapper:
`DATA_POLICY_UNDECLARED`.
## The session
The project has exactly one way in, and it is deliberately small.
1. The page shows a sign-in form whenever `signedInAs` is empty, and the board
only once it is not.
2. Submitting the form awaits `action:AppPage.signIn`. `server/middleware/session.ts`
validates the display name, signs a `noxid_session` cookie with
`SESSION_SECRET`, and puts the principal on the shared middleware context;
`Set-Cookie` belongs to middleware, so the host action only reports the
principal the runtime bound. Signing in `invalidates [Board]`, so the live
resource re-acquires under the new principal.
3. Every other endpoint and remote action needs that cookie. Without it the
request never reaches a handler:
```sh
$ curl -s -o /dev/null -w '%{http_code}\n' localhost:3000/api/notes/welcome
403
$ curl -s localhost:3000/api/notes/welcome | jq -r .error.code
SESSION_PRINCIPAL_REQUIRED
```
On an endpoint the middleware answers directly, so the JSON body carries
`SESSION_PRINCIPAL_REQUIRED` and a message that says what to do. On a remote
action the compiler-owned runtime reserves direct middleware responses to
SSR routes, so the refusal is the platform's own `BOUNDARY_MIDDLEWARE_DENIED`
(also 403) and the reason travels in the `x-noxid-refusal` header. Both are
closed doors; neither reaches your host.
4. `action:AppPage.signOut` expires the cookie. There is no server-side session
record to delete, because the signed cookie *is* the session.
**`signIn` is a starter placeholder.** It signs a cookie for whatever display
name the page typed — that demonstrates the boundary, it does not authenticate
anybody. Before this project meets a real user, replace the `signIn` branch in
`server/middleware/session.ts` and `action:AppPage.signIn` in `server/host.ts`
with the identity provider you actually run (OIDC, WorkOS, a magic link). Keep
the shape around it: resolve one principal in middleware, refuse when there is
none, and never let a handler mint an identity for itself.
`SESSION_SECRET` is declared under `[server] secrets` and must be at least 16
characters. With no signing key nobody can be verified, so the project refuses
every scoped request rather than falling open.
## The database
SQLite through Node's builtin `node:sqlite`; no service, no native addon.
```sh
DATABASE_URL=sqlite://./app.db noxid db migrate
node --test plugins/drizzle-orm/data-scopes.test.mjs # the scope boundary, proved
```
The same migrations apply to `postgres://` and `mysql://`; on PostgreSQL
`noxid db migrate` additionally forces row-level security on every scoped
table. Query only through `database(context)` — never a driver, and never a
raw Drizzle predicate: the adapter accepts its own `eq` so a scoped query
cannot be pointed at another principal.