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). 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 }.
  • PUT /api/user/me — Bearer body { name?, email? } → updated profile.
  • 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, and forgot-password 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.
  • "optional" — invalid token becomes user: null; the handler still runs.
  • custom verifier — async ({ headers }) => AuthUser | null. Null → 401.

api/me.ts

import { defineApi, z } from "@robodev-ai/sdk";
export const get = defineApi({
auth: "required",
handler: async ({ user }) => ({ user }),
});

@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.

browser

import { createClient } from "@robodev-ai/client";
const api = createClient({ url: import.meta.env.VITE_API_URL });
await api.auth.signUp({ email, password, name });
await api.auth.signIn.email({ email, password });
const { user } = await api.auth.getUser();
api.auth.signIn.google(); // redirect; then api.consumeTokenFromUrl()
await api.auth.refresh();
await api.auth.signOut();
const me = await api.get("/api/me");
const raw = await api.fetch("/api/me").then((r) => r.json());

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.

Google

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, or http on localhost / 127.0.0.1 / [::1]. 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), then 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.

  • 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; Google existing user when name or google_subject 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 } 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.

hooks/auth.ts

import { defineAuthHook } from "@robodev-ai/sdk";
export default defineAuthHook({
onUserCreated: async (ctx) => {
// Fast DB work here. Enqueue slow work.
void ctx.event.user?.id;
},
onUserSignedIn: async (ctx) => {
void ctx.event.provider;
},
timeoutMs: 10_000,
});