Skip to content

Accounts and mintable API tokens on Cloudflare D1 - #15

Open
codellyson wants to merge 9 commits into
mainfrom
feat/accounts-d1-auth
Open

Accounts and mintable API tokens on Cloudflare D1#15
codellyson wants to merge 9 commits into
mainfrom
feat/accounts-d1-auth

Conversation

@codellyson

Copy link
Copy Markdown
Owner

Turns JustAPI into an account-gated product. Users sign up, sign in, and mint personal access tokens that authenticate the MCP bridge and agents against their account — the bridge endpoints (/api/flows, /api/agent/*, /api/share/*) were previously wide open.

What's in it

  • Runtime — the app now runs on Cloudflare Workers via @opennextjs/cloudflare so the D1/R2 bindings are reachable. next dev keeps working (bindings injected via miniflare); pnpm preview/pnpm deploy build the real Worker.
  • Auth (better-auth + D1) — email/password + sessions + the apiKey plugin, persisted to D1 with the Drizzle adapter. getAuth(env) builds better-auth per-request from the request's D1 binding (the binding isn't available as a module singleton on Workers); app/api/auth/[...all] mounts the handler.
  • GatingrequireAuth guards the flows/agent/share routes, accepting either a session cookie (browser canvas) or Authorization: Bearer <token> (MCP/agent). middleware.ts optimistically redirects unauthenticated visitors to /login.
  • Frontend — dedicated /login, /signup, and /account routes (no inline gate cramping the canvas). The rail links to /account, where tokens are minted (plaintext shown once), listed, and revoked.
  • Share → R2 — the share store moved off @vercel/blob (which can't run on Workers) to an R2 bucket.
  • MCPmcp/server.mjs sends Authorization: Bearer $JUSTAPI_TOKEN and returns a clear "mint a token" message on 401.

Verification

  • Signup → session → mint verified against D1; the four gate cases pass (no-auth 401, cookie 200, bearer 200, bogus bearer 401).
  • R2 share create/read round-trip verified; unauthenticated read → 401.
  • /account mint-once UI, canvas load, and SSE-with-cookie verified in the browser.
  • tsc --noEmit clean; next build passes (all 13 routes + middleware).

Deploy notes

Deploy needs real Cloudflare resources: wrangler d1 create justapi (paste database_id into wrangler.jsonc, replacing the placeholder), wrangler r2 bucket create justapi-shares, wrangler secret put BETTER_AUTH_SECRET / BETTER_AUTH_URL, then pnpm db:migrate and pnpm deploy. Documented in the README's new "Deploy (Cloudflare)" section.

Follow-ups (out of scope here)

  • Canvas/flow data still lives in browser localStorage — per-user flow sync (and the deployed-Workers durability of the agent bridge) is the next persistence phase.
  • Public/anonymous share viewing: share GET is currently account-gated; a public render path is a later addition.

Turn JustAPI into an account-gated app. Users sign up, sign in, and mint
personal access tokens that authenticate the MCP bridge and agents against
their account — replacing the previously wide-open bridge endpoints.

- Runtime: run on Cloudflare Workers via @opennextjs/cloudflare so D1/R2
  bindings are available; next dev keeps working via miniflare.
- Auth: better-auth (email/password + sessions + apiKey plugin) persisted to
  D1 with the Drizzle adapter. getAuth(env) builds it per-request from the
  request's D1 binding; app/api/auth/[...all] mounts the handler.
- Gating: requireAuth guards the flows/agent/share routes, accepting either a
  session cookie or an Authorization: Bearer <token>. middleware.ts redirects
  unauthenticated visitors to /login.
- Frontend: dedicated /login, /signup, and /account routes; the rail links to
  /account, where tokens are minted (plaintext shown once), listed, and revoked.
- Share store moved off @vercel/blob to R2 (Vercel Blob can't run on Workers).
- MCP server sends Authorization: Bearer $JUSTAPI_TOKEN and reports a clear
  message on 401.

Canvas/flow data still lives client-side; per-user sync is a follow-up.
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
quick-rest Error Error Aug 1, 2026 11:08am

The canvas is now open to everyone: anonymous users work locally (localStorage),
and signing in unlocks the account-scoped features (agent bridge, sharing, token
minting, future remote sync).

- middleware.ts now guards only /account; / and the canvas are public.
- The agent-bridge SSE (useAgentSync) connects only when signed in, so anonymous
  sessions no longer hit the gated /api/agent/events in a 401 retry loop.
- The rail's account icon reflects state: "Sign in" (→ /login) when signed out,
  "Account" (→ /account) when signed in.

Bridge routes stay behind requireAuth; the change is purely which surfaces the
app forces you through.
AuthForm now holds its mode in state (seeded from initialMode) and flips between
sign-in and create-account in place — no navigation. /login and /signup both
still work as direct entry points; each just toggles to the other without a
route change.
/ is now a marketing landing page modeled on the justnotetaking site — nav,
hero with a live embedded canvas, a numbered feature trio, an editorial
statement, a closing CTA, and a footer, all on JustAPI's theme tokens (solid
accent, no gradients) with a light/dark toggle.

The canvas moves to /app (app/app/page.tsx). Redirects updated: the old
/canvas, /playground, /expand entry points and legacy /?s=ID share links now
land on /app (?s= carried across). Post-login also lands on /app, and the
account page's back-link points there.
The landing page's live-canvas iframe shares localStorage with the real app,
so left as-is a visitor could mutate their saved canvas by poking the preview.
Detect the framed context (isEmbedded) and, in that mode:

- persist reads the saved canvas but drops every write (no-op storage), so
  preview edits never touch real data;
- the agent-bridge SSE never connects;
- nodes can't be dragged, connected, selected, or deleted (pan/zoom stay).

Standalone /app is unchanged. Verified: standalone persists on a viewport
change; the embed leaves a localStorage sentinel untouched after the same change.
When embedded, render only the React Flow canvas — the rail, docked panes,
top control cluster, spec drawer, status bar, empty state, and tour are all
hidden. Standalone /app is unchanged.
The marketing embed now always renders a bundled demo (login → create → read,
with binding edges, captures, and asserts) instead of the visitor's saved
canvas — so first-time visitors see a real flow, not an empty board.

The embed's canvas storage ignores localStorage entirely (getItem → null) and
materializeFlow(DEMO_FLOW) builds the demo on mount. The demo spec declares no
environment, so materializing it touches only the (no-write) canvas store —
verified: nothing is written to localStorage. Standalone /app is unchanged.
The demo is materialized after React Flow's initial (empty-graph) fitView, and
the fitView prop doesn't re-fire on graph changes — so the tree sat un-framed
until the tidy button was clicked. Now, once the demo's nodes have mounted and
measured, run tidyGraph + fitView automatically (what the tidy button does), so
the flow fans out and centers itself on load.
Same root cause as the embed: React Flow's fitView prop only fires on the
initial mount, so when the active graph changes after that — an agent
materializes a flow, or you switch canvases — the view stayed put and the new
flow sat unframed until the tidy button was clicked.

On graph change, restore that graph's saved viewport, or fitView it if it has
none. No re-layout (unlike the embed's demo), so a hand-arranged board is never
reshuffled. Verified: pushing a flow over the agent bridge now reframes the
canvas onto it automatically.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant