Guides
Auth
Native Robodev Auth on each project host: email/password now, Google when configured, and defineApi auth.
Each deployed project host has Robodev Auth. End users sign up and log in on that host. This is not the Starbase dashboard or CLI login, and it does not reuse control-plane JWTs. Dashboard Google sign-in, password reset, and Profile on /login and /profile are control-plane only and do not change project-host Auth users. Organization members configure providers and see signed-up users on the project Auth page (/projects/:id/auth).
Access tokens are Bearer JWTs (15 minutes, typ project_access). Refresh tokens last 30 days and rotate on use. Store both and send Authorization: Bearer <accessToken>. There are no auth cookies. Sign-out revokes the refresh token.
Reserved routes
These Tiny /api/user/* paths are reserved on every project host. A file at api/user.ts or api/user/** is not registered.
- POST /api/user/auth/login — body { email, password } → { accessToken, refreshToken }. 401 invalid-credentials.
- POST /api/user/auth/register — body { email, password, name? }, password min 12 → 201 tokens. 409 identity-already-exists.
- POST /api/user/auth/refresh — body { refreshToken } → new token pair. Reuse → 401 identity-not-found.
- GET /api/user/auth/magic-link?email= — 200 generic StatusResponse. Optional redirect_uri (same rules as Google: the project's own https hosts, loopback http, or a custom scheme). Email links to {app}?type=magic&code=.
- GET /api/user/auth/magic-link/callback?code= — 200 tokens, or 400 nonce-invalid.
- POST /api/user/auth/forgot-password — body { email } → 200 generic StatusResponse. Email includes the code and {projectPublicUrl}?type=forgot-password&code=.
- POST /api/user/auth/forgot-password/callback — body { code, password } min 12 → 200 Password updated. No tokens.
- GET /api/user/auth/google?redirect_uri= — 302 to Google. After success the app redirect gets query accessToken and refreshToken.
- GET /api/user/me — Bearer required → { id, name, email, picture }. picture is the Google profile picture URL, refreshed on every Google sign-in; null for password and magic-link users.
- PUT /api/user/me — Bearer body { name?, email?, currentPassword? } → 200 updated profile. Name-only updates need nothing else.
- Changing the email when the user has a password needs currentPassword: missing → 400 current-password-required, wrong → 403 invalid-credentials. On success the email changes at once and auth.email-changed goes to the old address.
- Changing the email when the user has no password (Google or magic-link only) → 202 { id, name, email, pendingEmail }. The email does not change yet; auth.email-change mails {app}?type=email-change&code= to the new address (valid 15 minutes; a newer request replaces it). A name in the same request applies at once.
- POST /api/user/auth/email-change/callback — body { code }, no Bearer → 200 { id, name, email } with the new email, and auth.email-changed goes to the old address. 400 nonce-invalid, 409 identity-already-exists. Works even when email/password sign-in is off. Apps handle ?type=email-change&code= by POSTing the code here.
- GET /api/user/auth/apple/callback — 503 not_implemented.
Errors use top-level code and message (invalid-credentials, identity-already-exists, identity-not-found, nonce-invalid, invalid-request). Users live in the tenant schema robodev_auth, not in your app tables. Register welcome, magic-link, forgot-password, and email-change mail go through the project's SMTP and appear on the Email page.
defineApi auth
- false (default) — public. Existing APIs stay public.
- "required" — missing or invalid Bearer → 401, handler not called. ctx.user is the project user { id, email, name, picture }, read from the access token: a new Google picture shows up after the next refresh (at most 15 minutes).
- "optional" — invalid token becomes user: null; the handler still runs.
- custom verifier — async ({ headers }) => AuthUser | null. Null → 401.
@robodev-ai/client
Browser helper. No React hooks in this package. Tokens are stored under robodev:{url}:accessToken and robodev:{url}:refreshToken. api.get("/api/...") parses JSON. api.fetch refreshes once on 401 and retries. Nested api.boards.list.get() is a path builder, not a typed SDK.
signUp and signIn.email return { accessToken, refreshToken }. getUser wraps flat GET /api/user/me as { user }. consumeTokenFromUrl reads query accessToken and refreshToken after Google redirect, then strips them with replaceState.
On by default per project (Robodev mode). On the dashboard Auth page, you can switch to Off or Custom (your Google client id and secret). Platform env is GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and PUBLIC_URL — those keys are still required for Robodev Google. Authorized redirect URI is always {PUBLIC_URL}/internal/auth/google/callback. Start Google from GET /api/user/auth/google?redirect_uri=. After success the browser returns to redirect_uri with query accessToken and refreshToken. redirect_uri must be https on one of the project's own hosts (the project id host, the slug host, or a verified custom domain), http on localhost / 127.0.0.1 / [::1], or a custom app scheme such as junaki-zime://auth/callback (not javascript: or data:). Any other https origin is rejected with invalid_redirect_uri, because tokens travel on that URL. robodev dev uses the same FE contract but talks to the platform identity broker unless the project .env has both GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET. Dashboard Robodev / Custom / Off is unchanged.
- Auth off → reserved /api/user/** return 404 auth_disabled.
- Email/password off → login, register, magic-link, and forgot-password return 403 email_password_disabled.
- Allow signups off → register 403 signup_disabled; existing users can still log in. New Google emails redirect with error=signup_disabled.
- OAuth off → /api/user/auth/google 503 google_oauth_disabled.
- OAuth on but keys missing → 503 google_oauth_unconfigured.
Custom auth
A header verifier that returns the same AuthUser shape is supported. Cookie or redirect flows you build yourself are out of scope for this ship.
Hooks
Default-export defineAuthHook from hooks/auth.ts to run app code after Auth outcomes. Only that file is registered. Other hooks/**/*.ts files may be imported by it. Missing hooks/auth.ts is fine — Auth is unchanged. A present file that does not default-export defineAuthHook fails deploy with 400.
Hooks run after the Auth mutation commits (user row exists; tokens issued or the email/nonce path finished). By default they are fire-and-forget: they cannot delay, reshape, or fail the Auth HTTP response. Hook exceptions, timeouts, and job enqueue errors are logged and do not change HTTP success. Timeout uses the same clamp as APIs and jobs (default 10s, max 30s). Fast DB work belongs in the hook; slow work should ctx.jobs.enqueue. The first client request after register may run before onUserCreated finishes, unless blocking: true.
Blocking hooks
Set blocking: true when the app's first request after sign-up needs the hook's writes (for example a profile or workspace row). Then onUserCreated and onUserSignedIn are awaited before Auth responds: POST /api/user/auth/register and /login return tokens, and the magic-link callback and Google callback redirect, only after the handlers finish or hit timeoutMs. This applies to hosted Starbase, robodev dev, and the MCP consent page's sign-in and sign-up.
- Only batches containing user.created or user.signed_in block. The whole batch is awaited in order inside one timeoutMs budget, so a returning Google user whose name changed also waits for onUserUpdated.
- Every other event (signed_out, password_updated, deleted, magic_link_requested, password_reset_requested, failed, and profile updates) stays fire-and-forget.
- A throw or a timeout is logged (hosted: Auth hook failed for <projectId>: <message|handler_timeout>; robodev dev: Auth hook failed: …) and Auth still returns its normal 200/201/302. Hooks still cannot change or reject the response.
- Response latency grows by the hook's run time, up to timeoutMs. Keep blocking hooks fast and set a small timeoutMs; after a timeout the handler may keep running in the background.
- blocking must be true or false. Any other value fails deploy with 400 invalid_auth_hook. blocking needs @robodev-ai/sdk 0.19.0 or later; older SDKs drop it and the hook stays fire-and-forget.
- onUserCreated (user.created) — password register insert; Google first-time insert.
- onUserSignedIn (user.signed_in) — password login; register (after created); magic-link callback; Google success (new or existing).
- onUserSignedOut (user.signed_out) — POST /refresh with revoke: true when the refresh token matched a row.
- onUserUpdated (user.updated) — PUT /api/user/me when name and/or email changed; POST /api/user/auth/email-change/callback when the new email is confirmed; Google existing user when name, google_subject, or picture changed.
- onUserPasswordUpdated (user.password_updated) — forgot-password callback after the hash update (no tokens).
- onUserDeleted (user.deleted) — dashboard delete of an Auth user after the tenant row is deleted.
- onMagicLinkRequested / onPasswordResetRequested — existing user, nonce created, send attempted (SMTP is best-effort).
- onAuthFailed (auth.failed) — invalid_credentials, identity_already_exists, signup_disabled, nonce_invalid, refresh_rejected, google_oauth_failed.
Hook context has db, email, env, push, jobs, and event. There is no HTTP request/response and no storage. ctx.event.user is a snapshot { id, email, name, picture } when a robodev_auth.users row is known (including after delete). Payloads never include passwords, tokens, nonce codes, or Google subjects. GET /api/user/me, refresh rotation, unknown-email magic/forgot, Apple, Google start, and config/validation gates do not fire hooks. Register welcome email stays in reserved Auth. robodev dev fires the same events (user.deleted excepted).