Guides

Self-host a project

Run a Robodev project outside hosted Starbase: one process from @robodev-ai/runtime/server that serves its APIs, Robodev Auth, storage, and frontend on your own Postgres, in Docker or on any Node or Bun host.

@robodev-ai/runtime/server runs one project the way hosted Starbase and robodev dev do: the same compile, loadModules, route matching, validation, Robodev Auth, hooks/auth.ts, and schema push, because they are the same runtime. You bring Postgres, a JWT secret, the public URL, and where storage bytes and email go. The project code (database.ts, api/**, hooks/**) does not change, and robodev dev stays the local loop.

The server

server.ts

import { createProjectServer } from "@robodev-ai/runtime/server";
import { createS3BlobStore } from "@robodev-ai/runtime/server/s3";
import { createSmtpEmailClient } from "@robodev-ai/runtime/server/smtp";

const server = await createProjectServer({
  build: { buildDir: ".robodev-build", backendDir: "robodev" },
  databaseUrl: process.env.DATABASE_URL!,
  projectId: "my-app",
  jwtSecret: process.env.JWT_SECRET!, // openssl rand -hex 32
  publicUrl: "https://app.example.com",
  storage: createS3BlobStore({ bucket: "my-app-files", region: "eu-central-1" }),
  email: createSmtpEmailClient({ host: "email-smtp.eu-central-1.amazonaws.com", user: process.env.SMTP_USER, pass: process.env.SMTP_PASS, from: "My App <no-reply@example.com>" }),
  env: { STRIPE_KEY: process.env.STRIPE_KEY! }, // ctx.env
  static: { dir: "apps/fe/dist" },
  trustProxy: true,
});

// Bun
Bun.serve({ port: 8080, fetch: (req, srv) => server.fetch(req, { clientIp: srv.requestIP(req)?.address }) });
// Node: import { serveNode } from "@robodev-ai/runtime/server"; await serveNode(server.fetch, { port: 8080 });
  • buildProject({ backendDir, buildDir }) compiles at image build time; loadProjectBuild loads it at boot (read-only filesystem is fine). Without a build and with backendDir set, createProjectServer compiles first.
  • Schema: database.ts is pushed on boot under a Postgres advisory lock, so replicas apply it once. A change that would lose data throws instead of applying; pass schema: { allowDestructive: true } once, after a backup. schema: { mode: "off" } plus a release step that calls syncProjectSchema does the same out of band. Any table in public that database.ts does not define counts as a drop: give the project its own database.
  • Storage: robodev_storage metadata plus a BlobStore for the bytes — { dir } for a volume, or createS3BlobStore (peer dependency @aws-sdk/client-s3; credentials from the AWS default chain). Object URLs stay on your server (/storage/objects/<key>, private ones signed), so the bucket stays private and needs no CORS.
  • Email: createSmtpEmailClient (peer dependency nodemailer) for SES, Postmark, and any SMTP provider; the console otherwise. ctx.email templates are not supported.
  • Auth emails: magic-link, reset, and email-change codes are mailed with the same link and default wording hosted Starbase uses (<appUrl>?type=…&code=…). resolveAuthEmail(slug, vars) returns your own subject and text, for example a link to your /reset-password page.
  • Static: static: { dir } serves a built SPA with the history fallback; /api/** stays the API. Without it, every path goes to the file routes, as on robodev dev.
  • GET /healthz checks the database, for load balancer health checks. Every response carries X-Request-Id; with logRequests (default on) each request logs one JSON line.

What it leaves out

  • No dev dashboard, DB browser, or MCP endpoint, and no reflected CORS: corsOrigins is an allowlist, empty by default.
  • Google and Apple sign-in go through the Starbase identity broker, so they answer 404 (disabledAuthProviders). Password, magic link, and forgot password work.
  • ctx.llm, ctx.agent, ctx.pdf, and ctx.push are hosted services and throw *_not_configured. Jobs and sockets are not started; pass your own through clients.
  • The /api/user/auth/* rate limit (authRateLimitPerMinute, default 30 per IP and route) is per process.

Pin @robodev-ai/runtime and @robodev-ai/sdk to the versions your robodev CLI uses (robodev 0.66.0: runtime 0.21.0, sdk 0.20.0), so robodev dev and the server run the same runtime.