A small notes app that shows the bQuery modules working together at application scale: one TypeScript codebase, no framework besides bQuery, no bundler config.
bun install # from the repository root
bun examples/fullstack/server.tsOpen http://localhost:3000/ and sign in with [email protected] /
lovelace. Set PORT to change the port and SESSION_SECRET to keep
sessions across restarts.
| Concern | Where | bQuery APIs |
|---|---|---|
| Auth | server/app.ts |
session(), $regenerate() on sign-in, guard(), rateLimit() per account |
| CSRF | server/app.ts, client.ts |
csrf() bound to the session, csrfToken(), x-csrf-token header |
| Forms + server validation | shared/schema.ts |
one Standard Schema for createSchemaForm() and validate() |
| Router with loaders | server/app.ts |
routes with meta.loader, createSSRRouterContext() |
| SSR + hydration | shared/pages.ts, client.ts |
renderToString(), embedded state, hydrate({ onMismatch: 'repair' }) |
| Store persistence | client.ts |
createPersistedStore() keeps the unsent draft across reloads |
| i18n | shared/messages.ts |
defineMessages(), ICU plurals, negotiateLocale() from ?lang=, a cookie or Accept-Language |
| Optimistic updates | client.ts |
deleting a note updates the list first and rolls back on failure |
The schema's error messages are i18n keys, so the browser and the server report the same problems in the user's language.
fullstack/
├── server.ts # entry: bundles client.ts with Bun.build() and listens
├── server/
│ ├── app.ts # createApp(): the request pipeline (testable via app.handle())
│ └── notes-repo.ts # in-memory storage — swap for a database
├── client.ts # hydration, form, persisted draft
├── shared/
│ ├── schema.ts # Standard Schema definitions used on both sides
│ ├── messages.ts # en/de catalogs
│ └── pages.ts # page templates rendered by the server, hydrated by the client
└── styles.css
shared/schema.ts hand-rolls a minimal Standard Schema builder only to keep
the example dependency-free; in a real app write the schemas with Zod,
Valibot or ArkType and nothing else changes.
- Loop templates and hydration. The server output of
bq-forcontains the rendered items but not the loop template, soclient.tsputsnoteItemTemplateback into the list before callinghydrate(); the list is then re-rendered from client state. - Handlers inside
bq-for.bq-onhandlers see raw signals (socount.value++works), and loop variables are signals — the delete button callsremove(note.value.id), whilebq-textreadsnote.id. - Cookies.
secureCookiesis on whenNODE_ENV=production; then the session and CSRF cookies get the__Host-prefix and need HTTPS.
bun test tests/example-fullstack.test.tsdrives the pipeline in-process: redirects, locale negotiation, CSRF, rate limiting, loaders, schema validation and the client bundle build.bun run test:browserstarts this app next to the browser fixtures and runsbrowser/fullstack.pw.tsend to end in Chromium, Firefox and WebKit.