Auto-deploy to Fly + single-user auth#23
Conversation
Design for an upstream PR that adds Docker + Fly deployment to boop-agent, coupled with Convex Auth single-user gating to close the admin-route auth gap that has blocked deployment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
17-task plan implementing the design at docs/superpowers/specs/2026-04-27-auto-deploy-pr-design.md. Each task is self-contained for fresh-subagent execution. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves tsx and jose into dependencies (from devDependencies), adds test and deploy scripts to package.json, and regenerates package-lock.json. npm test exits 0 with 0 tests collected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…webhook Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…vex codegen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Server callers now use internalQuery/internalMutation twins (no requireUser) instead of the public+auth versions, fixing runtime "unauthenticated" errors when the Express server calls Convex with a deploy key rather than a JWT. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…bug UI Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…UDE_CODE_OAUTH_TOKEN Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…terns) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per CONTRIBUTING.md: every PR adds an Unreleased entry, breaking changes get [BREAKING] markers, and migrations are written as a Claude skill that /upgrade-boop can run. This change introduces six [BREAKING] entries (admin endpoints require JWT, Convex public functions require auth, webhook HMAC verification, WS upgrade token, schema additions, new env vars) all addressed by /setup-deploy-auth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR adds turnkey Fly.io deployment (Dockerfile,
Confidence Score: 2/5Not safe to merge — the unresolved P0 in server/convex-client.ts means the deployed server cannot reach any internal Convex function, breaking webhooks and all background loops. A P0 (no admin auth on ConvexHttpClient) from the prior review remains unfixed; the changeset does not touch convex-client.ts. All 51 files of auth/deploy infrastructure are correct in isolation, but they will not function together until that root fix lands alongside the missing Fly deploy-key secret. server/convex-client.ts (P0, not in this diff — needs .setAdminAuth()); scripts/deploy.ts (P1 — Convex deploy key missing from flySecrets) Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
subgraph Internet
User([User Browser])
Sendblue([Sendblue])
end
subgraph Fly_Machine["Fly.io Machine (port 3456)"]
Static["express.static /debug/dist\n(login page, no auth)"]
Webhook["POST /sendblue/webhook\n(no JWT required)"]
AdminGate["requireAdmin middleware\n(JWT via Authorization header)"]
API["Protected API routes\n/chat /consolidate /agents /composio /ws"]
end
subgraph Convex["Convex Backend (.convex.site)"]
JWKS["/.well-known/jwks.json"]
Internal["internal.* functions\n(requires deploy key)"]
end
User -->|GET /| Static
User -->|POST login| ConvexAuth["@convex-dev/auth Password Provider"]
ConvexAuth -->|JWT| User
User -->|Bearer JWT| AdminGate
AdminGate -->|verify JWT| JWKS
AdminGate -->|pass| API
API -->|internal.*| Internal
Sendblue -->|POST + HMAC sig| Webhook
Webhook -->|verifyHmac| HMACCheck{HMAC valid?}
HMACCheck -->|yes| Internal
HMACCheck -->|no| Reject401["401 Unauthorized"]
Reviews (3): Last reviewed commit: "fix(server): correct relative path to de..." | Re-trigger Greptile |
| // load before the SPA can render the login form, so they're served | ||
| // BEFORE requireAdmin gates the API surface. | ||
| if (process.env.NODE_ENV === "production") { | ||
| const here = path.dirname(fileURLToPath(import.meta.url)); | ||
| const debugDist = path.resolve(here, "../../debug/dist"); | ||
| app.use(express.static(debugDist)); | ||
| app.get("/debug/*", (_req, res) => { | ||
| res.sendFile(path.join(debugDist, "index.html")); | ||
| }); | ||
| } | ||
|
|
||
| // AUTH GATE: every route below requires a valid Convex Auth JWT, except | ||
| // the explicit allowlist inside requireAdmin() (/sendblue/webhook + /health). | ||
| app.use(requireAdmin()); |
There was a problem hiding this comment.
SPA catch-all intercepts API 404s in production
app.get("/debug/*", ...) is registered before app.use(requireAdmin()) and before the API routes (/sendblue, /composio, etc.). In production any request for an undefined path will hit express.static, fail to find the file, then fall through to this handler and return index.html with a 200 — masking the real 404.
Move the SPA catch-all to the very end of the route list (after all API routes and the auth gate) so it only fires for genuine client-side navigation paths.
There was a problem hiding this comment.
▎ The catch-all is scoped to /debug/*, not *. None of the API routes (/sendblue,
▎ /composio, /agents, /chat, /consolidate, /health) live under /debug/, so undefined
▎ API paths don't reach this handler — they fall through to requireAdmin (which 401s
▎ on missing token) or to Express's default 404. The static mount is intentionally
▎ before requireAdmin because the SPA login form needs to load before the user has a
▎ token to gate other routes with. Could you point at a specific path that gets
▎ masked? I don't see one.
@convex-dev/auth signs tokens with CONVEX_SITE_URL as issuer and serves /.well-known/jwks.json on the .convex.site host. The middleware was using CONVEX_URL (.convex.cloud) for both, so every authenticated request 401'd in production. Addresses Greptile P1 raroque#1 on PR raroque#23. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The WebSocket upgrade handler was calling createRemoteJWKSet inside the event handler, building a fresh JWKS instance (with empty cache) for every connection. A momentary JWKS endpoint blip would reject all concurrent WS upgrades. Construct one verifier at server startup via defaultVerifier() and share it between requireAdmin() and the upgrade handler — single source of truth for JWT verification, single JWKS cache. Addresses Greptile P1 raroque#2 on PR raroque#23. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
server/auth.ts needs CONVEX_SITE_URL at startup to fetch the Convex JWKS. The deploy script wasn't pushing it, so production servers would throw on boot. Read it from .env.local; fall back to deriving from CONVEX_URL by swapping .convex.cloud → .convex.site if absent. Also document it in .env.example. Addresses Greptile P1 raroque#3 on PR raroque#23. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bootstrap is an internalAction, so the countUsers query and createAccount call don't share a transaction. A second concurrent invocation could see zero users and race into createAccount, producing a thrown error. Catch the "already exists" error from @convex-dev/auth's createAccountFromCredentials and treat it as the idempotent path. Addresses Greptile P2 raroque#5 on PR raroque#23. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tsx runs server/index.ts directly from /app/server in the Docker image, so import.meta.url resolves to /app/server/index.ts. The previous "../../debug/dist" walked up two levels to / and pointed at /debug/dist (which doesn't exist), making every /debug/* request 500 in prod. One level up is correct: /app/server → /app/debug/dist. Addresses Greptile P1 on PR raroque#23. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Description
This PR adds a turnkey production deploy path (Fly.io) and the auth layer that makes the deploy safe to host on a public URL.
It's a single coupled PR by design — a deploy-only PR ships a knowingly-vulnerable URL, and an auth-only PR has no real consumer until deploy ships. The two halves answer the same question ("how do I host this without anyone hitting the admin endpoints?") and only make sense together. Happy to split further if you'd rather see them separately.
What's in here
Deploy infrastructure
Dockerfile— multi-stagenode:22-slimbuild (Debian for SSH-friendliness, runs server withtsxto matchnpm start).dockerignorefly.toml— single machine, always-on (min_machines_running = 1,auto_stop_machines = false) because the in-process loops require single-replica.github/workflows/deploy.yml— test →convex deploy→ bootstrap →fly deploy→ smokescripts/deploy.ts— interactivenpm run deploymirroring yoursetup.tspatterns. Helpers (banner,runCapture, etc.) are duplicated fromsetup.tsfor now; happy to follow up with a dedupe PR if you want.docs/deploying.md+ a one-line link from the READMEAuth layer (Convex Auth, single-user password provider)
convex/auth.config.ts,convex/auth.ts—convexAuth({ providers: [Password] })+ arequireUserhelperconvex/users.ts— idempotentbootstrapaction (CI-triggered) andsetPasswordaction (rotation), both usingcreateAccountfrom@convex-dev/auth/serverinternalQuery/internalMutation/internalAction(server-only) orquery/mutationwithawait requireUser(ctx)(browser-callable)agents.listInternal,messages.recentInternal, etc.) for the 10 functions called from BOTH server and browser — server uses the internal twin (deploy-key path, no JWT), browser uses the public version with auth checkExpress edge
server/auth.ts—verifyHmac(timing-safe) andrequireAdmin(JWT verification via Convex's JWKS endpoint usingjose)server/index.ts— globalrequireAdminmiddleware with allowlist for/healthand/sendblue/webhook; static debug UI served fromdebug/distin production; WebSocket/wsupgrade auth via?token=<jwt>query paramserver/sendblue.ts— HMAC signature verification +from_numberwhitelist on the inbound webhook (preserves all existing dedup/broadcast/handler logic underneath)Debug UI
<ConvexAuthProvider>debug/src/auth.tsx— single-password login form usinguseAuthActions().signIn("password", ...)debug/src/api-client.ts—useApiClient()hook that attaches the JWT to outboundfetchcallsfetchcall sites inConsolidationPanel+ComposioSectionmigrated to use the authed wrapperTests (15 new unit tests, no new test framework — uses Node 22's built-in
node:test)verifyHmac: 5 casesrequireAdmin: 6 casesCONTRIBUTING.md compliance
CHANGELOG.md— six[BREAKING]entries under Unreleased + a list of additions, all referencing the migration skill below.claude/skills/setup-deploy-auth/SKILL.md— one-shot migration that/upgrade-boopwill offer to run when a fork pulls this in: installs deps, pushes the schema additions, generates and stores the admin password, bootstraps the admin user, reconciles.env.localagainst.env.example. Idempotent.What I verified
tsc --noEmitclean for new code (37 baseline errors all stem fromconvex/_generatednot being on disk locally — codegen runs in CI/Docker)npm test— 15/15 passnpm run build:debug— clean Vite builddocker build— stages 1–2 succeed; stage 3 needsCONVEX_DEPLOY_KEY(CI provides it)What I couldn't verify (no accounts)
npm run deployscript in full (only typechecked — needs Fly + Convex deploy key + gh CLI to exercise)@convex-dev/authv0.0.x docs; verified at compile time but not at runtimeIf you (or another contributor with full env) can run the runtime smoke test, I'd appreciate it. The most likely failure modes I'd watch for:
createAccountargument shape if the installed@convex-dev/authversion differs from what I targeted?token=upgrade pathnpx convex run users:bootstrapflag handling under the deploy-key pathOut of scope (explicit)
Not in this PR (separate concerns, can be follow-ups):
CLAUDE_CODE_OAUTH_TOKEN(1-year manual rotation documented indocs/deploying.md)setup.tsanddeploy.tshelpers into a sharedscripts/lib/(commit ready inrefactor/cli-helpersbranch on my fork; happy to open as PR 2 if you want)Design doc
Full spec lives at
docs/superpowers/specs/2026-04-27-auto-deploy-pr-design.mdif you want the long-form rationale on the auth perimeters and trade-offs.