Guides
Auth
Native Robodev Auth on each project host: email/password, Google and Apple 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. Project admins (and organization admins) configure providers on the project Auth page (/projects/:id/auth); other project members see the same settings read-only, plus the signed-up users.
Settings from agents and the CLI
Every setting on the Auth page is also available to agents and scripts: the get_auth_settings and update_auth_settings tools on the Robodev connector and robodev mcp, robodev auth-settings get | set, and GET / PATCH /v1/projects/:id/auth. Reading needs any project member; changing needs a project admin, and each change is recorded in the organization's audit log. All of them need a signed-in user; a deploy key is refused. Changes apply to the next sign-in or sign-up, with no worker restart or redeploy.
- Invite-only Google app: update_auth_settings with email_password_enabled: false and allow_signups: false, or robodev auth-settings set --email-password false --allow-signups false.
- The Google client secret and the Apple .p8 key are write-only: results show only whether each is set. The CLI reads them from --google-client-secret-file / --apple-private-key-file (a path, - for stdin, or no value for a hidden prompt) and refuses them as plain flag values.
- Settings: enabled, email_password_enabled, allow_signups, google_mode (off | robodev | custom), google_client_id, google_client_secret, apple_mode (off | custom), apple_team_id, apple_key_id, apple_services_id, apple_private_key, apple_bundle_ids (a list of iOS bundle IDs; replaces the whole list, [] clears). An empty ID clears it.
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/auth/apple?redirect_uri= — 302 to Sign in with Apple. Same redirect_uri rules and the same query accessToken and refreshToken on success.
- POST /api/user/auth/apple/native — body { identityToken, nonce, authorizationCode?, firstName?, lastName? } from a native iOS Sign in with Apple → { accessToken, refreshToken }. See Native iOS (#apple-native).
- DELETE /api/user/me — Bearer required → 200 { status: ok, message: Account deleted }. Deletes the signed-in user and revokes their Apple sign-in when one is stored. See Delete account (#delete-account).
- 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.
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 a Google or Apple redirect, then strips them with replaceState.
On by default per project (Google mode Robodev). On the dashboard Auth page, Google has its own Off / Robodev / Custom choice, independent of Apple. Custom uses 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. The dashboard Google mode (Off / Robodev / Custom) applies only to the deployed project.
- 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.
- Google Off → /api/user/auth/google 503 google_oauth_disabled.
- Google on but keys missing → 503 google_oauth_unconfigured.
Apple
Sign in with Apple always uses your project's own Apple keys, set on the project Auth page: one Apple Developer key (Team ID, Key ID and private key .p8) shared by both flows, plus a Services ID for web sign-in and/or iOS bundle IDs for native sign-in (see Native iOS below). There are no platform Apple keys, so Apple has its own Off / Custom choice, independent of Google. Web Apple is on when Apple is Custom, the key and Services ID are complete, and Robodev Auth is on. A project can keep Google on Robodev (platform Google keys) and set Apple to Custom with its own keys, or turn Google off and use Apple alone. Apple Off disables Apple and keeps the saved keys. The Apple sheet shows the Services ID description from Apple Developer (Sign in to <your app>). The key is stored encrypted and never shown again. Web Apple works on the deployed project only, not in robodev dev.
- In Apple Developer: create a Services ID (the client ID), turn on Sign in with Apple, add the domain of PUBLIC_URL and register <PUBLIC_URL>/internal/auth/apple/callback (the Apple redirect URI on the Auth page) as a Return URL. Its description is the name users see. Create a key with Sign in with Apple and note its Key ID and your Team ID.
- Start from GET /api/user/auth/apple?redirect_uri= (same redirect_uri rules as Google). Apple posts back to the Starbase apex, which redirects to redirect_uri with query accessToken and refreshToken, or error.
- Errors on redirect_uri: auth_disabled, apple_oauth_disabled, missing_code, signup_disabled, apple_link_unverified, apple_oauth_failed, or Apple's own error such as user_cancelled_authorize. The start route answers 503 apple_oauth_disabled (Apple Off) or apple_oauth_unconfigured (project Apple keys missing), and 400 invalid_redirect_uri; robodev dev answers 503 apple_oauth_unavailable_locally.
- Accounts match by Apple user id first. An Apple sign-in links to an existing account with the same email only when Apple verified the address, it is not a Hide My Email relay address, and the account has no unconfirmed password; otherwise it is apple_link_unverified. Relay addresses (@privaterelay.appleid.com) always create a separate user.
- Apple sends the user's name on the first authorization only. It fills an empty name and never overwrites one.
- Web sign-in does not store or revoke Apple tokens. Robodev uses the id_token once to identify the user. Native iOS sign-in can store a token for revocation (see below).
Apple: native iOS
An iOS app can sign in with Apple natively (AuthenticationServices, or expo-apple-authentication in Expo) and send Apple's identityToken to POST /api/user/auth/apple/native. It needs the shared Apple Developer key (Team ID, Key ID, .p8) plus at least one iOS bundle ID on the Auth page (up to 10; the Services ID is optional). The token's audience must be one of those bundle IDs. Native and web users are the same users: both match by Apple user id, so an App ID and a Services ID grouped under the same primary App ID sign in the same account. Android has no native Apple SDK; use the web flow with a custom-scheme redirect_uri there.
- In Apple Developer: enable Sign in with Apple on the app's App ID (its bundle ID) and group it with your Services ID under one primary App ID if you also use web sign-in. Add the bundle ID on the project Auth page.
- Nonce: generate a random raw nonce in the app, pass its SHA-256 hex digest to Apple as the request nonce, and send the raw value as nonce. Robodev checks that the token's nonce equals sha256(nonce). Missing → 400 apple_nonce_required.
- Send authorizationCode too. Robodev exchanges it for an Apple refresh token (stored encrypted) so deleting the user can revoke the Apple grant, which App Store guideline 5.1.1(v) requires. A failed exchange never fails sign-in.
- firstName and lastName: Apple shares the name on the first sign-in only. It fills an empty name and never overwrites one.
- Response: 200 { accessToken, refreshToken }, like password login. Errors: 400 invalid-request, apple_nonce_required or apple_email_missing (a new Apple user whose token carries no email); 401 apple_token_invalid (signature, issuer, audience, expiry or nonce); 403 signup_disabled; 409 apple_link_unverified; 503 apple_native_not_configured (Apple Off, or the key or bundle IDs missing).
- robodev dev verifies real identity tokens locally when APPLE_BUNDLE_IDS is set in the project .env (see robodev dev › Apple).
Delete account
DELETE /api/user/me with the user's Bearer deletes their account: the robodev_auth.users row, their refresh tokens and MCP grants. It fires user.deleted, then revokes the user's Apple sign-in when native sign-in stored an Apple token (best effort: a failed revoke never fails the delete), and answers 200. api.auth.deleteAccount() calls it and drops the stored tokens. A missing or invalid token, or an already deleted user, gets 401 identity-not-found; access tokens issued before the delete expire on their own within 15 minutes but no longer load the user. App Store guideline 5.1.1(v) requires in-app account deletion for apps that create accounts, and revoking Sign in with Apple tokens. Deleting a user from the dashboard (or delete_auth_user over MCP) revokes the stored Apple token the same way. robodev dev serves DELETE /api/user/me too, without Apple revocation.
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 or Apple first-time insert.
- onUserSignedIn (user.signed_in) — password login; register (after created); magic-link callback; Google or Apple success (new or existing). provider is google or apple.
- 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; Apple existing user when linked or an empty name was filled.
- onUserPasswordUpdated (user.password_updated) — forgot-password callback after the hash update (no tokens).
- onUserDeleted (user.deleted) — dashboard delete of an Auth user, or DELETE /api/user/me, 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, apple_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 or Apple subjects. GET /api/user/me, refresh rotation, unknown-email magic/forgot, Google start, Apple start, and config/validation gates do not fire hooks. Register welcome email stays in reserved Auth. robodev dev fires the same events, including user.deleted from DELETE /api/user/me. Native iOS Apple sign-in fires the same user.created / user.signed_in / user.updated events as web Apple, with provider apple.