Askr
Guides

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.

  1. Import the published @askrjs/askr entrypoint.
  2. Keep build an authenticated full-stack app configuration next to the component, route, or server composition root that owns it.
  3. 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.