Build an Authenticated Full-Stack App
Use build an authenticated full-stack app with the current published Askr packages.
@askrjs/askr0.0.59
How to use build an authenticated full-stack app
Resolve the session on the server, enforce permissions at both route and handler boundaries, and expose only the principal data needed for rendering.
- Import the published @askrjs/askr entrypoint.
- Keep build an authenticated full-stack app configuration next to the component, route, or server composition root that owns it.
- Handle pending, unavailable, cancellation, and failure states, then verify the production path.
import { createAuth, requirePermission, requireUser } from '@askrjs/auth';
const auth = createAuth({ sessions: sessionStore, principals: principalStore });
const canManageProjects = [requireUser(), requirePermission('projects:write')];
router.post('/projects', ...canManageProjects, createProject);Goal and architecture
This shape combines `@askrjs/server` for HTTP with `@askrjs/auth` for identity resolution and `@askrjs/askr`'s router for auth-aware client routes. `createAuth({ sessions, principals, jwt })` builds one `AuthResolver` your server middleware calls per request to produce an `AuthContext`; the same shared requirement factories — `requireUser()`, `requireRole()`, `requirePermission()` — gate both server routes and client route policies, so you're not maintaining two separate permission systems.
Implementation
On the server, attach `auth: requirePermission('reports:read')` to individual routes and read `ctx.auth.principal` in the handler; `createJwtValidator()` or a JWKS-backed validator plugs into `AuthOptions.jwt` if you're validating bearer tokens rather than cookies. On the client, `route(path, Component, { policy })` with `allow()`/`deny()`/`redirect()` decisions from your policy function keeps navigation from ever rendering a page the user shouldn't see, and `currentAuth()` gives components access to the resolved identity for the route currently rendering.
Failure states
A denied server route should return a real HTTP status through `deny(status)` — `unauthorized()` or `forbidden()` — not a 200 with an empty body; clients checking `result.ok` in `@askrjs/fetch` depend on that. On the client, an `AccessDenyDecision` should route through `redirect('/login')` for anonymous users versus a dedicated forbidden page for authenticated users lacking permission — conflating the two is the most common auth UX bug. Session expiry mid-session needs its own path: a stale `AuthContext` should trigger a re-resolve or redirect, not a raw 401 rendered as page content.
Verification
Test all three identity states explicitly: anonymous, authenticated without the required role, and authenticated with it — each should reach a different, correct outcome on both server routes and client-rendered pages. Confirm a token that fails `JwtValidator` verification is rejected with a clear error, not silently treated as anonymous. Finally verify that server-side auth decisions and client-side route policies agree; a route hidden in the nav but reachable directly by URL because only the client checked permissions is a real vulnerability, not just a UX gap.