Accounts and mintable API tokens on Cloudflare D1 - #15
Open
codellyson wants to merge 9 commits into
Open
Conversation
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.
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
@opennextjs/cloudflareso the D1/R2 bindings are reachable.next devkeeps working (bindings injected via miniflare);pnpm preview/pnpm deploybuild the real Worker.apiKeyplugin, 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.requireAuthguards the flows/agent/share routes, accepting either a session cookie (browser canvas) orAuthorization: Bearer <token>(MCP/agent).middleware.tsoptimistically redirects unauthenticated visitors to/login./login,/signup, and/accountroutes (no inline gate cramping the canvas). The rail links to/account, where tokens are minted (plaintext shown once), listed, and revoked.@vercel/blob(which can't run on Workers) to an R2 bucket.mcp/server.mjssendsAuthorization: Bearer $JUSTAPI_TOKENand returns a clear "mint a token" message on 401.Verification
/accountmint-once UI, canvas load, and SSE-with-cookie verified in the browser.tsc --noEmitclean;next buildpasses (all 13 routes + middleware).Deploy notes
Deploy needs real Cloudflare resources:
wrangler d1 create justapi(pastedatabase_idintowrangler.jsonc, replacing the placeholder),wrangler r2 bucket create justapi-shares,wrangler secret put BETTER_AUTH_SECRET/BETTER_AUTH_URL, thenpnpm db:migrateandpnpm deploy. Documented in the README's new "Deploy (Cloudflare)" section.Follow-ups (out of scope here)