Build a full-stack app

OPTZ 0.4 adds optional sessions, SQLite, input validation, private files, server-sent events and request observation. Start with a working app, then adapt its data layer.

On this page

Create the application

From the repository root, with workspace dependencies installed:

bun framework/src/cli.ts create apps/my-app --template fullstack
cd apps/my-app
bun install
cp .env.example .env
bun run dev

Open the address printed by the dev server. The full-stack starter uses port 3007 by default. Register an account with a password of at least 12 characters, create a note, attach a file, then log out and back in. The forms also work with JavaScript disabled.

To explore the checked-in version instead, run those install/dev commands inside apps/fullstack. The starter's README lists endpoints, quotas and configuration.

What the starter includes

CapabilityImplementation
Public pagesStatic HTML; no database initialized by the landing page build
AccountsBun Argon2id password hashes with a bounded password-work budget
SessionsRandom tokens, hash-only SQLite records, expiry, identity rotation and logout revocation
Private notesCreate, list, toggle and delete; every query constrains the authenticated owner
AttachmentsPrivate files, bounded multipart input, per-user quotas and owner-checked downloads
Live updatesAuthenticated SSE note counts; streams stop after disconnect, expiry or revocation
FormsNative form posts; runtime validation and safe field errors for API clients
Request protectionsSame-origin mutation checks, response headers and bounded local limits

These modules are separate imports. A static site does not open a database, check a session, or start a stream just because the framework supports them.

Keep data access on the server

Use .server.ts / .server.tsx files or the app/lib/server/ directory for database, session and private storage code. The browser build rejects these modules even through a transitive shared import. Imports removed with a route's server-only loader stay on the server.

The starter centralizes its state and requireUser(request) in app/lib/app.server.ts. Check the session again at each protected loader, API route and mutation, then constrain access by owner in SQL. A hidden button or a protected layout alone is not authorization.

Read the application API guide for the exported helpers and their runtime boundaries.

Configure persistent state

The local .env points at a loopback origin and .data/. For deployment, configure the exact external HTTPS origin and writable paths outside your release directory:

PUBLIC_URL=https://your-app.example
DATABASE_PATH=/var/lib/your-app/app.sqlite
UPLOAD_DIR=/var/lib/your-app/uploads
MAX_USERS=100
MAX_STORAGE_BYTES=104857600

PUBLIC_URL determines CSRF checks and secure cookie behavior. The starter enables host-only Secure, HttpOnly, SameSite=Lax session cookies on HTTPS. MAX_USERS=0 closes public registration. The default limits are 100 accounts and 100 MiB of attachments.

Deploy the SQLite starter as its own Bun service with persistent storage. The framework's edge target does not make bun:sqlite or local file storage portable. Back up the database using SQLite's online backup API and back up uploads as well. Add numbered migrations; do not edit migrations that have already run.

Know the integration boundaries

The starter includes password login, not email verification, password recovery, OAuth, MFA, invites or account administration. Integrate a maintained identity provider when those are part of your product. The session store contract lets you replace persistence without changing cookie handling.

Local request limits and connection budgets are per process. Multiple replicas need shared sessions, rate limits and storage. SSE is a live response stream; after() is a response lifecycle hook. Neither provides a durable queue or guaranteed background delivery.

Verify your changes

bun run check
bun run build

In the repository, framework/test/fullstack.test.ts covers primitives and framework/test/fullstack-app.test.ts exercises two users, access denial and restart persistence. apps/fullstack/e2e.cjs walks registration, notes, uploads and logout with JavaScript disabled. See Contributing for the complete check workflow.