Start
Offline dev with robodev dev
Run a created project's schema, APIs, jobs, sockets, and storage on your machine — embedded Postgres, file routes, Swagger, and Robodev Auth — with no Starbase account or hosted project.
robodev dev serves the project you already have on disk. It starts a real Postgres, compiles database.ts plus api/**, jobs/**, hooks/**, sockets/**, and mcp/** with the same esbuild path hosted deploy uses, pushes the schema, and listens on http://localhost:4000. Jobs enqueue and cron onto reserved robodev_jobs tables in that Postgres. Sockets serve on ws://localhost:4000/sockets/…. Storage bytes live under .robodev/dev/storage with metadata in reserved robodev_storage tables. No robodev auth, no project id, and no Starbase account. Google sign-in is the one network hop to Starbase: a project-less identity broker talks to Google and returns a short-lived assertion. Password, magic link, and forgot-password stay offline. hooks/auth.ts runs locally on the same Auth events as hosted, with the dev db, env, and jobs; ctx.email prints to the terminal and ctx.push throws push_not_configured.
Run it
- Run it from the project root, from robodev/, or from apps/fe — it walks up looking for database.ts, robodev/database.ts, or packages/be/database.ts (Tiny).
- Postgres data lives in .robodev/dev/pg, so rows and Auth users survive restarts. That folder is already gitignored.
- Storage bytes live in .robodev/dev/storage (also gitignored). Metadata is in reserved robodev_storage tables on the same Postgres. --database-url still writes bytes under .robodev/dev/storage.
- The Postgres port is picked free on every start unless you pass --db-port.
- Editing database.ts, api/**, jobs/**, hooks/**, sockets/**, or mcp/** recompiles and reloads in place. A successful reload closes open sockets with 1012 deployed so clients reconnect. A compile error keeps the previous build and its sockets serving.
What it serves
- File routes exactly as hosted: api/planets.ts is GET /api/planets, api/planets/[id].ts is GET /api/planets/:id.
- Jobs: ctx.jobs.enqueue and defineJob({ cron }) run after compile. Queue and cron live in reserved robodev_jobs tables on the local Postgres, not in database.ts. Startup logs each job; a zero-job project prints nothing about jobs.
- Sockets: GET /sockets/<name> from sockets/**/*.ts on ws://localhost:{port}. Startup logs each as WS /sockets/…. A zero-socket project prints nothing about sockets.
- Storage: ctx.storage.upload / get / getUrl / delete / list. Public GET /storage/objects/{key} returns 200 and the bytes (no S3 302). Private objects need a local Auth Bearer or a valid exp+sig.
- Robodev Auth: /api/user/auth/register, login, refresh, magic-link, forgot-password, /api/user/me, and Google start + callback (GET /api/user/auth/google and /api/user/auth/google/callback). Magic-link, reset, and email-change codes print to the terminal instead of sending mail. There is no notice to the old address offline.
- GET /openapi.json and Swagger at /_robodev/docs (Scalar at /_robodev/scalar). Dashboard at /_robodev (Overview, APIs, Jobs, Emails, Auth, Storage, Sockets; JSON at /_robodev/state.json).
- GET /_robodev/ready for a readiness check.
- PDF: ctx.pdf.fromHtml POSTs HTML to {STARBASE_URL}/internal/pdf. No local Chromium. See PDF (/docs/pdf).
- CORS is open to localhost, so a Vite frontend on :3000 can call it directly.
With the frontend
Space, Auth chat, and Marketplace have a root dev script that runs the API and apps/fe together, with VITE_API_URL pointed at the local API. Empty's pnpm run dev is robodev dev (local APIs); pnpm run dev:fe is optional local Vite. Backend has no frontend, so its dev script runs robodev dev alone.
You can also run them separately: robodev dev in one terminal, then cd apps/fe && bun dev in another with VITE_API_URL=http://localhost:4000.
Schema changes
The schema is pushed with drizzle-kit, the same non-migration reconcile hosted deploy runs. Non-destructive changes apply on their own. A destructive plan is printed and skipped until you rerun with --force, so a rename never silently drops a column full of local data.
Role parity
Hosted projects query Postgres as a restricted project role, not as the database owner. robodev dev does the same on the embedded Postgres: your routes, jobs, cron, Auth, Storage, MCP OAuth, and the dashboard DB browse all run as robodev_app (no superuser, no CREATEDB, no CREATEROLE). Schema pushes still run as the superuser robodev, and robodev_app is re-granted SELECT, INSERT, UPDATE, and DELETE on public tables after every push, so a new table is usable at once. Statements the hosted role cannot run fail locally too: TRUNCATE on a public table returns permission denied, just as it does after deploy. Use DELETE instead.
- The reserved robodev_auth, robodev_jobs, and robodev_storage schemas are owned by robodev_app, as hosted. A .robodev/dev/pg created by robodev 0.60 or earlier is migrated on the next start; existing rows are kept.
- --db-superuser runs everything as robodev, as before. The banner then says superuser: --db-superuser.
- --database-url skips role handling and connects exactly as the URL says.
When Postgres does not start
If the embedded Postgres cannot initialise or start, robodev dev prints the port, the data folder, and the last lines Postgres wrote, then exits 1. If .robodev/dev/pg/postmaster.pid exists, another robodev dev may still be running for this project, or a previous one crashed and left its Postgres behind. Stop that process, or delete postmaster.pid if no postgres process is running. If the port is taken, pass another --db-port, or --db-port 0 for a free one. On macOS and Linux the embedded Postgres starts with dynamic_shared_memory_type=mmap, so a sandbox that only blocks POSIX shared memory still works; if the sandbox blocks all shared memory, the error says so and robodev dev cannot start there — run it outside the sandbox, pass --database-url, or use robodev openapi to refresh an OpenAPI snapshot without a database. Under Codex, robodev dev detects the sandbox and stops before starting Postgres — see Coding-agent sandboxes. Set ROBODEV_DEBUG=1 for a stack trace.
Coding-agent sandboxes
Coding agents such as Codex, Claude Code, and Cursor often run commands in a sandbox. Those sandboxes usually block the shared memory the embedded Postgres needs, and often binding local ports, so robodev dev cannot start inside them. That is the sandbox, not the project. Codex marks its sandbox with environment variables, and robodev dev checks them before it finds the project or starts anything: it exits 1 with a message for the agent, creates no .robodev/dev folder, and starts no Postgres or Vite.
- CODEX_SANDBOX set (Codex sets seatbelt on macOS) and no --database-url: blocked, because the embedded Postgres needs shared memory.
- CODEX_SANDBOX set with --database-url <url>, network on: runs. A Postgres outside the sandbox needs no shared memory, and the API port can bind.
- CODEX_SANDBOX_NETWORK_DISABLED set (Codex sets 1): blocked, with or without --database-url, because the API cannot listen on its port.
- A variable counts as set when it is non-empty and not 0 or false. Escalated Codex commands run without these variables, so they pass.
- What to do: rerun robodev dev with escalated permissions so it runs outside the sandbox, or ask the user to start it in their own terminal and work against http://localhost:4000. To refresh a committed OpenAPI snapshot, run robodev openapi --out <file>; it needs no database or network, so it works inside the sandbox.
- Other sandboxes do not set these variables. There robodev dev starts as usual and, if shared memory is denied, fails at Postgres start with the hint described above.
- Every starter tells local agents the same: a Local dev with a coding agent section in ROBODEV.md and, in starters with .rulesync, an always-apply robodev-local-dev rule (run rules:gen to emit it into AGENTS.md, CLAUDE.md, and Cursor rules). The hosted builder ignores both and never starts a server.
Flags
- --port <n> — API port (default 4000).
- --host <address> — interface to bind (default 127.0.0.1, or ROBODEV_DEV_HOST). The dev dashboard and DB browse have no authentication, so only bind 0.0.0.0 on a network you trust.
- --db-port <n> — Postgres port (default 0, meaning pick a free one).
- --database-url <url> — use a Postgres you already run instead of the embedded one. Skips role handling (also the way to run inside a sandbox that allows network).
- --db-superuser — run app queries as the embedded Postgres superuser instead of the restricted robodev_app role. See Role parity.
- --force — apply destructive schema changes.
- --no-watch — compile once and serve, without watching files.
- --with-fe — also start apps/fe with the local API pre-wired.
- --tunnel — expose only the API port (--port, default 4000) at a public https URL. Does not expose the frontend.
Public tunnel
robodev dev --tunnel exposes only the API port at https://<id>.tunnels.robodev.povio.dev. It does not expose the web frontend, including the dev server started by --with-fe. To publish port 3000 or any other local port, including from a folder that is not a project, use robodev tunnel (/docs/cli). The id is unguessable. The full https URL is stored as tunnelUrl in gitignored robodev/.robodev and printed when the server starts, so a React Native app that lives with the backend can read that file. The next start reuses the same id. No login: anyone with the CLI can open a tunnel, and anyone with the link can call it. The URL answers only while the command is running. When it stops, the URL stops answering until the next start. If that id is already connected from somewhere else, the new start is refused and does not take over — run robodev tunnel rotate, which writes a new id into robodev/.robodev, stops the old URL, and is what the next start uses.
GET /api/user/auth/google?redirect_uri= (or api.auth.signIn.google()) completes Sign in with Google without a Starbase account, hosted project, or your own Google Cloud OAuth client. The local API 302s to {STARBASE_URL}/internal/auth/google/dev. Starbase talks to Google with the platform client; the consent screen is the Robodev app. After Google, Starbase 302s back to http://localhost:<port>/api/user/auth/google/callback with a short-lived identity assertion. Local Auth upserts robodev_auth.users in that project's embedded Postgres and mints tokens with prj-local / robodev-dev-local-secret.
- STARBASE_URL defaults to https://robodev.povio.dev. Override is process env only (STARBASE_URL=http://localhost:4000 to dogfood a local Starbase). The project .env STARBASE_URL is not read for this hop.
- The app redirect_uri may be a custom scheme (same as hosted), including exp:// from Expo Go. For a request on localhost, Starbase bounces the identity assertion to the loopback callback_uri.
- With --tunnel, a sign-in that comes in through the tunnel URL (a phone, for example) uses https://<id>.tunnels.robodev.povio.dev/api/user/auth/google/callback as the callback, so it finishes on the device. Requests on localhost keep the loopback callback. See React Native and Expo.
- For a tunnel callback, Starbase first shows a page on robodev.povio.dev naming the full tunnel host, which will receive the Google account's name, email and picture. Nothing happens until you press Continue; Cancel returns to the app with error=access_denied. Starbase only accepts a tunnel callback while that tunnel is connected (otherwise 409 tunnel_offline), and if the tunnel drops before Google returns, the app gets error=tunnel_offline. Sign-in has to finish in the same browser session that pressed Continue; a Google link opened anywhere else returns to the app with error=access_denied.
- To use your own Google client instead, put both GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in the project-root .env and register http://localhost:<port>/api/user/auth/google/callback on that client. --port changes that URI. One of the two vars set is 503 google_oauth_unconfigured. With --tunnel, also register https://<id>.tunnels.robodev.povio.dev/api/user/auth/google/callback (the tunnelUrl in robodev/.robodev plus that path); there is no Starbase page in this mode. The id stays the same until robodev tunnel rotate.
- Dogfooding a local Starbase with --tunnel: the CLI's ROBODEV_TUNNEL_HOST (or TUNNEL_HOST) must match the Starbase TUNNEL_HOST (or tunnels.<PUBLIC_URL host>), or Starbase answers 400 invalid_redirect_uri. A phone cannot reach a Starbase on localhost:4000, so device testing needs a Starbase the phone can reach.
- Each robodev dev has its own .robodev/dev/pg. The same Google email can exist in each project independently.
React Native and Expo
A React Native or Expo app on a phone or simulator talks to robodev dev through the tunnel, with @robodev-ai/client 0.8.0 or newer. Password sign-in, API calls, sockets, and Sign in with Google all work. Only the API is tunneled; the web frontend is not. The URL answers only while robodev dev --tunnel runs.
- Run robodev dev --tunnel in the backend project.
- Copy tunnelUrl from robodev/.robodev (also printed as Tunnel in the banner) into EXPO_PUBLIC_ROBODEV_URL in the app's .env. It stays the same across restarts until robodev tunnel rotate.
- Pass a storage to createClient: React Native has no localStorage. Any object with getItem, setItem and removeItem works, sync or async (AsyncStorage, an expo-secure-store adapter, MMKV).
- await client.auth.ready() before showing signed-in state or calling client.socket(). getToken() and socket() read the cached token and are null / unauthenticated until the stored tokens have loaded. Requests wait for it on their own.
expo-secure-store only allows letters, digits, dot, dash and underscore in keys, and the client's keys are robodev:<url>:accessToken and robodev:<url>:refreshToken. Sanitize the key in the adapter:
Sign in with Google opens an in-app browser session with expo-web-browser. Build redirectUri with expo-linking: Linking.createURL("auth") is exp://<lan-ip>:8081/--/auth in Expo Go and <your-scheme>://auth in a development build, and both are accepted. client.auth.signIn.googleUrl({ redirectUri }) is the start URL; client.auth.completeFromUrl(url) stores the tokens from the URL the session returns to, and throws the error value (for example access_denied or tunnel_offline) when sign-in did not complete.
On the way, Starbase shows a page naming your tunnel host (<id>.tunnels.robodev.povio.dev). Check that it is the tunnelUrl from your own robodev/.robodev, then press Continue. See Google above.
What still needs hosted Starbase
- ctx.llm, ctx.agent, and ctx.push throw llm_not_configured / llm_not_configured / push_not_configured. They are not implemented offline.
- ctx.pdf.fromHtml hops to Starbase (STARBASE_URL, default https://robodev.povio.dev). Process env only — the project .env STARBASE_URL is not read. See PDF (/docs/pdf).
- The Jobs dashboard and dead-job retry stay hosted-only. Offline jobs have no retry UI; a dead row stays dead until you enqueue again.
- Real SMTP. ctx.email prints to the terminal.
- Project secrets: ctx.env comes from a .env file at the project root.
- Custom domains, hosted request logs, and the Starbase dashboard. Local request, email, job, Auth, Storage, and Sockets events are on the dashboard at /_robodev.
Next
Deploy the same tree with robodev deploy (/docs/deploy). Local development (/docs/local) covers the hosted-project workflow. CLI reference (/docs/cli) lists every command.
robodev dev and robodev deploy run the same runtime package, so a route that works locally serves the same way once deployed.