CRUMB a card from devarno-cloud

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

GroupPages
(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
apiauth/[...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.ts injects Authorization: Bearer from getAuthToken(), redirects to /login on 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; need nestr.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