Skip to content

Deployment

A Rebase project deploys as one server at one URL (on Rebase Cloud: https://<project>.rebase.website). That server handles:

  • /api/* — the data API, authentication, realtime, and storage
  • everything else — your built frontend/ as a static SPA

There is no separate admin URL: the admin panel is part of your frontend, so where it appears depends on what your frontend is.

Project type Root URL shows Admin panel is at
Default scaffold (rebase init) The admin panel / — the frontend is the admin
Custom product frontend Your app Wherever you mount it, commonly /admin — see Changing the Base URL
Backend-only project Nothing (API only) Not deployed

The generated project already includes a working docker-compose.yml (Postgres + backend + frontend) and the backend//frontend/ Dockerfiles — that generated file is the source of truth; use it as-is rather than hand-writing one. The shape is:

services:
db:
image: postgres:18-alpine
environment:
POSTGRES_USER: rebase
POSTGRES_PASSWORD: ${DATABASE_PASSWORD:-changeme}
POSTGRES_DB: rebase
ports:
- "5432:5432"
backend:
build:
# Context is the PROJECT ROOT so the image can copy
# pnpm-workspace.yaml, backend/, and config/. A `./backend`
# context would fail — the Dockerfile lives at backend/Dockerfile.
context: .
dockerfile: backend/Dockerfile
ports:
- "3001:3001"
env_file: .env
depends_on:
- db
volumes:
postgres_data:
uploads:
docker compose up -d

Bringing the stack up is not enough on its own. The backend boots and auto-creates the auth tables, but it does not create the tables for your own collections — you run that once, explicitly, against the production database. From a checkout of your project (with dependencies installed), set DATABASE_URL to your production database and push the schema:

pnpm run db:push

db:push is the fast option — it applies the schema directly, with no migration files. For a versioned, team workflow, commit migration files with pnpm run db:generate and run pnpm run db:migrate as a release step instead. Whichever you choose, it runs against the production DATABASE_URL from a project checkout (or your CI job), not inside the running container — the production image ships without the CLI.

Before deploying to production, ensure:

Item Details
Database schema Run pnpm run db:push (or pnpm run db:migrate for versioned migrations) against the production database once. The app boots without your collection tables, but every collection errors until they exist.
JWT_SECRET Use a cryptographically strong random string (≥ 32 chars). Never reuse across environments.
DATABASE_URL Use a managed Postgres instance (Neon, Supabase, RDS) with TLS enabled
CORS Configure allowed origins on your backend if frontend and backend are on different domains
Storage volumes Mount persistent volumes for file uploads. Or switch to S3 for production.
HTTPS Terminate TLS at your reverse proxy (nginx, Cloudflare, load balancer)
Registration Set ALLOW_REGISTRATION=false after creating your admin account

In production, the backend can serve the frontend as a static SPA:

import { serveSPA } from "@rebasepro/server";
import path from "path";
// After initializeRebaseBackend()
serveSPA(app, { frontendPath: path.resolve(process.cwd(), "../frontend/dist") });

Build the frontend first:

cd frontend && pnpm build

This way you only need to deploy one server that handles both SPA and API.

Detailed step-by-step guides for each platform:

Platform Type Guide
AWS App Runner / ECS + RDS Deploy on AWS →
Google Cloud Cloud Run + Cloud SQL Deploy on GCP →
Azure Container Apps + PostgreSQL Deploy on Azure →
Hetzner Cloud VPS + Docker Compose Deploy on Hetzner →
Scaleway Serverless Containers Deploy on Scaleway →
Railway PaaS (auto-detect Dockerfile) Deploy on Railway →
Fly.io Container runtime Deploy on Fly.io →

If you want Rebase to run at a sub-path (e.g., /admin):

Frontend — Update the BrowserRouter basename:

<BrowserRouter basename="/admin">
<App />
</BrowserRouter>

Backend — Update the base path:

await initializeRebaseBackend({
// ...
basePath: "/admin/api"
});

The common reason to move the admin to /admin is shipping your own product app at the root of the same deployment. A single Vite entry can serve both, split by URL, so each app is lazy-loaded and product visitors never download the admin bundle:

const isAdmin = window.location.pathname.startsWith("/admin");
const ProductApp = lazy(() => import("./App"));
const AdminApp = lazy(() => import("./AdminApp")); // renders <RebaseAdmin basePath="/admin" />
if (isAdmin) {
// The admin uses useBlocker → needs a data router
const router = createBrowserRouter([{ path: "/admin/*", element: <AdminApp /> }]);
root.render(<RouterProvider router={router} />);
} else {
root.render(<BrowserRouter><ProductApp /></BrowserRouter>);
}

The backend needs no changes for this pattern — the API stays at /api and the SPA catch-all serves index.html for both / and /admin/*.