feat(dev): ENABLE_DEV_AUTH — run the stack without Clerk for local dev#145
feat(dev): ENABLE_DEV_AUTH — run the stack without Clerk for local dev#145DIodide wants to merge 3 commits into
Conversation
…cal dev Adds an opt-in, prod-safe flag that bypasses Clerk end-to-end so the app can be run loginless (e.g. to capture screenshots) without a Clerk project. Off by default and prod-safe BY CONSTRUCTION — when the flag is unset every layer behaves byte-for-byte as before: - Convex: a `getIdentity(ctx)` helper (convex/authDev.ts) that returns a fixed dev identity when the deployment env `ENABLE_DEV_AUTH=true`, else delegates to `ctx.auth.getUserIdentity()`. All 72 call sites across 17 files now call it. The 199-test suite verifies the delegate path; a new unit test covers both. - FastAPI: `verify_token` / `verify_token_optional` short-circuit to a fixed dev user when `settings.enable_dev_auth`. Covered by new pytest cases. - Frontend: a `@/lib/auth` wrapper re-exports Clerk's useAuth/useUser/useClerk/ useReverification, swapping to dev stubs only when `VITE_ENABLE_DEV_AUTH=true` (a build-time constant, so it's a pure re-export otherwise). 25 call sites import from the wrapper; `__root.tsx` skips ClerkProvider in dev and the sign-in/up routes don't mount Clerk components. The three layers share `DEV_USER_ID = "dev-user"`. Documented in all three .env.example files. Also adds scripts/dev-screenshots.py (Playwright) to capture product shots once the stack runs with dev auth. NOTE: the off-path is fully test-verified; the in-browser dev path (incl. the Convex client auth handshake) needs functional verification on a running stack.
| * Off by default, and `DEV_AUTH` is a build-time constant, so the exported hooks | ||
| * below resolve to the REAL Clerk hooks in every normal build — this module is a | ||
| * pure pass-through unless a developer explicitly opts in. Import the auth hooks | ||
| * from here (`@/lib/auth`) instead of `@clerk/tanstack-react-start` so the | ||
| * bypass reaches every call site. | ||
| */ | ||
| export const DEV_AUTH = env.VITE_ENABLE_DEV_AUTH === "true"; |
There was a problem hiding this comment.
DEV_AUTH is not actually a build-time constant — dev stubs ship in every production bundle.
The comment claims DEV_AUTH is a build-time constant so the stubs are tree-shaken in production. This is not the case. Vite's static substitution only fires when the literal token sequence import.meta.env.VITE_* appears in source. Here DEV_AUTH is read via:
// env.ts
runtimeEnv: { ...import.meta.env, ... } // spread — Vite does not enumerate keys through a spreadfollowed by createEnv(...) from @t3-oss/env-core — a runtime function call whose return value Vite cannot inline. So env.VITE_ENABLE_DEV_AUTH is a runtime property read, not a static token Vite replaces.
As a result:
- The
devUseAuth / devUseUser / devUseClerk / devUseReverificationstubs are retained in every production bundle. - The
useAuth = DEV_AUTH ? devUseAuth : clerkUseAuthternaries are evaluated at runtime, not eliminated at build time. - A single
VITE_ENABLE_DEV_AUTH=trueat build time activates the full auth bypass (isSignedIn: truealways,getTokenreturns"dev-auth") in production.
Fix: Read the env var directly in this file so Vite can inline it:
// Replace:
import { env } from "../env";
export const DEV_AUTH = env.VITE_ENABLE_DEV_AUTH === "true";
// With (Vite statically replaces import.meta.env.VITE_* at build time):
export const DEV_AUTH = import.meta.env.VITE_ENABLE_DEV_AUTH === "true";With direct access, Vite inlines false for production builds (where the var is unset), allowing the bundler to dead-code-eliminate all four dev stubs.
See:
Harness/apps/web/src/lib/auth.ts
Lines 13 to 21 in 3e86a49
| const devUseUser = (() => ({ | ||
| isLoaded: true, | ||
| isSignedIn: true, | ||
| user: { | ||
| id: DEV_USER_ID, | ||
| fullName: "Dev User", | ||
| firstName: "Dev", | ||
| lastName: "User", | ||
| primaryEmailAddress: { emailAddress: "dev@localhost" }, | ||
| imageUrl: "", | ||
| }, | ||
| })) as unknown as typeof clerkUseUser; |
There was a problem hiding this comment.
devUseUser stub is missing Clerk User instance methods — crashes in princeton-connect-row.tsx under dev auth.
princeton-connect-row.tsx was updated by this PR to import useUser from @/lib/auth, so in VITE_ENABLE_DEV_AUTH=true mode it receives the devUseUser stub. The component calls three methods on the returned user object that the stub does not implement:
user.createEmailAddress({ email })— line 41 — called via the reverification wrapper when the user clicks "Send Code"user.reload()— lines 56 and 91 — called after email creation and after verificationuser.emailAddresses— lines 58 and 83 — iterated to find the newly created address
The as unknown as typeof clerkUseUser cast silences TypeScript, so the gap isn't caught at compile time. At runtime, clicking the Princeton connect button throws TypeError: user.createEmailAddress is not a function.
Fix: Add no-op stubs for the missing methods in devUseUser. For example:
user: {
id: DEV_USER_ID,
fullName: "Dev User",
firstName: "Dev",
lastName: "User",
primaryEmailAddress: { emailAddress: "dev@localhost" },
imageUrl: "",
emailAddresses: [],
createEmailAddress: async () => { throw new Error("Not supported in dev mode"); },
reload: async () => {},
},(The Princeton email flow is inherently non-functional without real Clerk, so throwing on createEmailAddress with a clear message is more developer-friendly than a silent TypeError.)
…ied live) Stood the stack up against a real Convex dev deployment with ENABLE_DEV_AUTH and fixed two gaps that only surface at runtime: - start.ts registered Clerk's requestMiddleware globally, which throws "no secret key provided" on every request in loginless mode → skip it in dev-auth. - The dev identity handed ConvexProviderWithClerk / the SSR http client a fake "dev-auth" token, which Convex rejects as a malformed JWT (InvalidAuthHeader). Return null instead — unauthenticated calls resolve to the dev user via the backend getIdentity bypass. With these, the app renders fully loginless (workspaces, chats, the agent transcript, share dialog, command palette) — confirmed by screenshots.
| // useReverification wraps an action that may need step-up auth; in dev, pass through. | ||
| const devUseReverification = (<T>(fn: T) => | ||
| fn) as unknown as typeof clerkUseReverification; | ||
|
|
There was a problem hiding this comment.
apps/web/src/components/sandbox-result.tsx was not migrated to import from this module. It still imports useAuth directly from @clerk/clerk-react:
Harness/apps/web/src/components/sandbox-result.tsx
Lines 1 to 3 in e179f3c
In dev mode ClerkProvider is not mounted (see __root.tsx fetchClerkAuth), so calling Clerk's useAuth() directly will throw a React context error if GitHubAuthRequiredError renders — e.g., when a sandbox run returns error_code === "github_auth_required". SandboxResult is rendered in both the basic chat and workspaces routes via ChatMessages → message-blocks.tsx.
The fix is a one-line import swap in sandbox-result.tsx:
| import { useAuth } from "@/lib/auth"; |
| return {"sub": DEV_USER_ID} | ||
| if not request.headers.get("authorization", "").startswith("Bearer "): |
There was a problem hiding this comment.
verify_token_optional's documented contract is to return None for anonymous callers (no bearer token) — the /follow endpoint relies on this sentinel:
# packages/fastapi/app/routes/chat.py
if user is None and not token:
raise HTTPException(status_code=403, detail="Not authorized")Because this early return fires before the Authorization header is inspected, user is never None when enable_dev_auth=True — even for a request with no bearer token at all. Two consequences:
- The share-token access path in
/followbecomes unreachable and untestable with dev auth on. - Any request (with or without a valid share token) is silently granted the full
dev-useridentity.
Since the intent in dev mode is to simulate a logged-in user, not to bypass access control entirely, consider returning None when no Authorization header is present even with dev auth on — so that the share-token flow remains exercisable locally:
if not request.headers.get("authorization", "").startswith("Bearer "):
return None
if settings.enable_dev_auth:
return {"sub": DEV_USER_ID}The Files/Terminal/Git/Agents sandbox panels imported useAuth from @clerk/clerk-react directly (a different package than the rest of the app's @clerk/tanstack-react-start), so the earlier wrapper swap missed them. Opening the panel in loginless dev mode crashed with "useAuth can only be used within <ClerkProvider>". Point all 8 at @/lib/auth like every other hook call site. Found by driving a real agent session with dev-auth and opening the Agents panel.
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. |
What
An opt-in, prod-safe flag that bypasses Clerk end-to-end so the app can run loginless locally (e.g. to capture screenshots) without a Clerk project. Off by default.
Prod-safe by construction
When the flag is unset, every layer behaves byte-for-byte as before — the changes are pure pass-throughs:
getIdentity(ctx)helper (convex/authDev.ts); all 72ctx.auth.getUserIdentity()call sites (17 files) now call it=== ctx.auth.getUserIdentity()verify_token/verify_token_optionalshort-circuit to a dev user whensettings.enable_dev_auth@/lib/authre-exports Clerk'suseAuth/useUser/useClerk/useReverification; 25 sites import from it;__root.tsxskipsClerkProviderin devAll three layers share
DEV_USER_ID = "dev-user". Turn it on withVITE_ENABLE_DEV_AUTH=true(web),ENABLE_DEV_AUTH=true(FastAPI), andnpx convex env set ENABLE_DEV_AUTH true(Convex). Documented in all three.env.example.Verification
withIdentity, which exercises the delegate path through all 72 swapped sites) + a new unit test covering both branches ofgetIdentity.tscclean; biome clean.Honest caveat
The off-path is fully test-verified. The in-browser dev path can't be exercised here (no running stack), and there's one known subtlety: the fake token won't complete the Convex client auth handshake, so
app.tsxgates the settings fetch onconvexReady || DEV_AUTH. This needs a quick functional check on a running stack — ifConvexProviderWithClerkrejects the"dev-auth"token outright, switch the devgetTokento returnnull. Flagging so it's reviewed before relying on it.Also
scripts/dev-screenshots.py(Playwright) captures product shots once the stack runs with dev auth — the intended path to refresh the README's screenshots.🤖 Generated with Claude Code