CRUMB a card from devarno-cloud

Engine Auth: better-auth JWT via JWKS, Rate Limiter, Quota Middleware

nestr intermediate 5 min read

ELI5

The web app is the passport office: it signs a 15-minute pass naming you, your organisation and your plan. The engine checks the signature against the office’s public stamp list and checks that the pass was meant for the engine. The turnstile behind it makes fast callers wait their turn rather than turning them away.

Technical Deep Dive

Source: nestr-tools/engine internal/auth/jwt.go:21-108, internal/auth/middleware.go:9-60, internal/server/rest.go:25-48,100-105,140-144,282-295,529-536, internal/server/usage.go:80-110 @e9180e4; nestr-tools/web src/lib/auth.ts:49,170-200 @6f42cca; nestr-tools docs/decisions/2026-10-12-phase-12-launch-scope.md §6 (closed) @b8f0585; read 2026-10-25.

Token path

sequenceDiagram
participant B as Browser
participant W as web, better-auth
participant E as engine
participant K as JWKS endpoint on web
B->>W: session cookie
W->>W: jwt plugin signs EdDSA, exp 15m
W-->>B: JWT with sub, email, org, plan, plan_source
B->>E: Authorization Bearer JWT
E->>K: fetch and cache keys from JWKS_URL
E->>E: verify EdDSA, iss, aud nestr-engine, exp, 30s leeway
E-->>B: 200, or 401 unauthorized envelope
  • Issuer default https://app.nestr.tools, compared exactly after trimming a trailing slash (ENGINE_JWT_ISSUER overrides); audience default nestr-engine (ENGINE_JWT_AUDIENCE). Only EdDSA is accepted, and exp is required (jwt.go:43-86).
  • The web plugin issues one token with two audiences, the registry’s and the engine’s (auth.ts:170-177).
  • org, plan (community or early_access), plan_source and plan_confirmed_at are set by web’s definePayload after a live membership check. The engine trusts them only because the signature verified, uses them only for Community caps and security-record history, and treats a missing or unknown plan as Community. Verification never reads them (nestr-024).
  • Auth is better-auth in web/, a recorded deviation from the estate’s airlock/hatch convention, deferred to Phase 13 (launch decision §6).

Off switch

NewJWTValidator needs JWKS_URL; if it is unset the engine logs JWT validator not available (auth disabled) and mounts /api with no auth middleware (rest.go:100-105,140-144). Capped routes then see no claims and meter as Community. Whether production sets JWKS_URL: UNVERIFIED (Railway env not read).

WebSocket

/ws reads ?token= first, then the Authorization header, and validates with the same validator; failure is 401 before upgrade. Frames are not re-validated after the upgrade.

Rate limiter, as written

flowchart LR
R["request"] --> K["key = r.RemoteAddr, ip and port"]
K --> L["rate.Limiter 10 per s, burst 20, created lazily"]
L --> W["limiter.Wait(ctx): blocks until a token is free"]
W -- "ctx cancelled or deadline" --> E429["429 Rate limit exceeded"]
W -- "token granted" --> H["handler"]

Two consequences of the code (rest.go:282-295): Wait delays callers instead of rejecting them, so 429 appears only when the request context ends first; and the key is RemoteAddr, which includes the client port, so a new connection gets a fresh bucket. Behind Railway’s proxy the peer is the proxy. Limiters are never evicted.

Key Terms

  • JWKS_URL → the key-set endpoint the engine trusts; unset means auth is off.
  • plan_source → billing, fallback or no_org; an I-7 marker of how sure web was about the plan.
  • limiter.Wait → token-bucket wait, not an immediate allow/deny.

Q&A

Q: Who issues the tokens the engine accepts, and which claims does the engine enforce? A: The web app’s better-auth jwt plugin, Ed25519-signed, 15-minute expiry. The engine enforces signature against JWKS_URL, method EdDSA, issuer (default https://app.nestr.tools), audience (default nestr-engine) and expiry with 30 seconds of leeway.

Q: What happens when JWKS_URL is unset? A: The validator is not created and the /api subrouter is mounted without auth; the engine only logs a warning. Every capped call then meters as an anonymous Community identity.

Q: Does the rate limiter reject with 429 at 10 requests per second? A: Not directly. It calls limiter.Wait, which queues the request until a token is available; a 429 is returned only if the request’s context ends while waiting. And because buckets are keyed by IP and port, the limit is per connection rather than per client.

Examples

A script sends 50 requests in a second over one keep-alive connection. About 20 pass at once (burst), the rest are delayed to roughly 10 per second; none get 429 unless the client times out. Opening a new connection per request would give each its own fresh bucket.

neighbors on the map