Nest Dashboard: Next.js 15, better-auth Organisations, Engine JWT, Billing
nestr intermediate 5 min read
ELI5
The Nest is the control room. You sign in, belong to an organisation, and the room fetches a short-lived pass to talk to the engine on your behalf. Some screens show live readings from the engine; the quarantine, diff and provenance screens are still mock-ups with labelled sample data.
Technical Deep Dive
Source: nestr-tools/web package.json:11-34, src/app/ (route tree), src/api/client.ts:1-50, src/lib/auth.ts:49,170-224, src/lib/billing/features.ts:30-55, src/lib/toll/config.ts:1-90, src/hooks/useNestrAPI.ts:23, src/hooks/useCacheStatus.ts:12, playwright.config.ts:10,37 @6f42cca; nestr-tools docs/decisions/2026-10-22-early-access-design-partner-seat.md (countersigned 2026-10-23) @b8f0585; read 2026-10-25.
Stack: Next.js 15 App Router, React 18, better-auth 1.5 with pg, TanStack Query 5, axios, Vitest; deployed on Vercel. The previous revision’s Vite dev server and “BetterAuth proxy in front of an external IdP” no longer describe it: better-auth is the identity provider here.
Routes
| Group | Pages |
|---|---|
(auth) | login, signup, verify-email, forgot-password, reset-password, accept-invitation/[id] |
(app) | dashboard, pellets, cache, metrics, graph, explain, namespaces, quarantine (+ [id]), chat, billing, settings/organization, admin/payments |
api | auth/[...all] (better-auth), billing/checkout, billing/entitlements |
Calls
flowchart LR UI["page or hook"] --> TQ["TanStack Query"] TQ --> AX["axios client.ts"] AX -- "getAuthToken from better-auth jwt plugin" --> TOK["EdDSA JWT, 15 min, aud nestr-engine"] AX -- "Bearer token, NEXT_PUBLIC_API_URL" --> ENG["engine REST"] AX -- "401" --> LOGIN["redirect to /login"] BILL["billing routes"] -- "toll.checkout, toll.entitlements, sandbox only" --> TOLL["null0-toll"] CHAT["chat page"] -- "WebSocket" --> OLLY["Olly service"]client.tsinjectsAuthorization: BearerfromgetAuthToken(), redirects to/loginon 401, and retries retryable errors with a linear backoff.- Polling: pellet lists every 30 s (
useNestrAPI.ts:23), cache stats every 10 s (useCacheStatus.ts:12). The only WebSocket client is the Olly chat. - Organisations: better-auth
organization()with owner/admin/member roles, 48-hour invitations, email verification required for invitees, deletion disabled because the organisation is the billing subject (auth.ts:189-224).
Real data versus preview
flowchart TD REAL["Reads engine or registry"] --> R1["pellets, cache, metrics, usage and security record"] REAL --> R2["namespaces: registry-wide read view, not per org"] REAL --> R3["explain: refusal catalogue"] FIX["Sample data, labelled preview"] --> F1["quarantine list and detail: seedQuarantinedPellets"] FIX --> F2["diff and provenance tabs inside quarantine"] FIX --> F3["quarantine actions are not sent or saved"]The countersigned design-partner decision records the fixtures by path (quarantine/page.tsx:4,19, ActionModal.tsx:76-77, QuarantineDetail.tsx:178-197) and requires the landing to call them previews.
Key Terms
- getAuthToken → client helper that obtains the engine JWT from better-auth.
- NEVER_GATED → features that must not depend on billing (verification, quarantine view, dashboard, refusal explanations, security-record totals).
- GATED_FEATURES →
invites,seats,support,usage_history; neednestr.team.
Q&A
Q: How does the dashboard authenticate its calls to the engine?
A: The axios client asks better-auth’s jwt plugin for a short-lived EdDSA token (getAuthToken) and sends it as Authorization: Bearer. The engine verifies it against JWKS_URL with issuer and audience checks (nestr-006).
Q: Which dashboard screens show real data, and which show fixtures?
A: Pellets, cache, metrics, usage, explain and a registry-wide namespaces view read live data. Quarantine, and the diff and provenance tabs inside it, render seedQuarantinedPellets, and quarantine actions are neither sent nor saved.
Q: Where does the dashboard talk to TOLL?
A: In its own server routes api/billing/checkout and api/billing/entitlements, with config that refuses any TOLL_ENV other than sandbox (toll/config.ts:76,87).
Examples
A 401 bounce loop after an engine restart usually means the engine cannot reach JWKS_URL or its ENGINE_JWT_ISSUER does not match web’s BETTER_AUTH_URL exactly (a trailing slash or path changes the issuer). Check the engine log for the JWT validator warning before touching the web app.
neighbors on the map
- LORE Route Structure & Data Model implementing a new LORE page or route
- Choco Factory Architecture Map onboarding to a new choco service repo
- Engine Auth: better-auth JWT via JWKS, Rate Limiter, Quota Middleware wiring web or another client to the engine with a JWT