@rebasepro/cli 0.11.1-canary.gfd39654 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/bin/rebase.js +21 -0
  2. package/dist/bundle.d.ts +24 -7
  3. package/dist/commands/eject.d.ts +1 -0
  4. package/dist/commands/init.d.ts +19 -15
  5. package/dist/fold-static.d.ts +41 -15
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.es.js +894 -246
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/manifest.d.ts +27 -8
  10. package/package.json +7 -7
  11. package/runtime/dev-server.mjs +0 -1
  12. package/templates/{template/backend → eject}/Dockerfile +16 -4
  13. package/templates/{template → eject}/backend/src/env.ts +0 -1
  14. package/templates/{template → eject}/backend/src/index.ts +41 -27
  15. package/templates/eject/docker-compose.custom.yml +71 -0
  16. package/templates/overlays/baas/backend/package.json +1 -4
  17. package/templates/overlays/baas/backend/tsconfig.json +8 -2
  18. package/templates/overlays/baas/config/index.ts +15 -0
  19. package/templates/overlays/baas/config/package.json +28 -0
  20. package/templates/overlays/baas/package.json +2 -1
  21. package/templates/overlays/baas/pnpm-workspace.yaml +1 -0
  22. package/templates/overlays/baas/rebase.json +2 -6
  23. package/templates/template/.env.example +15 -0
  24. package/templates/template/README.md +56 -22
  25. package/templates/template/ai-instructions.md +1 -0
  26. package/templates/template/backend/functions/hello.ts +45 -14
  27. package/templates/template/backend/package.json +1 -4
  28. package/templates/template/backend/tsconfig.json +23 -2
  29. package/templates/template/config/tsconfig.json +16 -1
  30. package/templates/template/docker-compose.yml +62 -38
  31. package/templates/template/frontend/src/main.tsx +8 -1
  32. package/templates/template/frontend/vite.config.ts +5 -0
  33. package/templates/template/rebase.json +5 -8
  34. package/templates/overlays/baas/backend/src/index.ts +0 -216
  35. package/templates/template/frontend/Dockerfile +0 -52
  36. package/templates/template/frontend/nginx.conf +0 -40
  37. /package/templates/overlays/baas/{backend/src → config}/storage.ts +0 -0
@@ -1,4 +1,4 @@
1
- import type { ManagedCompatibility, RebaseAppConfig, RebaseBackendAppConfig, RebaseProjectManifest } from "@rebasepro/types";
1
+ import { type ManagedCompatibility, type RebaseAppConfig, type RebaseBackendAppConfig, type RebaseProjectManifest } from "@rebasepro/types";
2
2
  /** Runtime range written into new manifests. */
3
3
  export declare const CURRENT_RUNTIME_RANGE = "^1";
4
4
  /** Conventional locations, matching what `rebase init` scaffolds. */
@@ -36,10 +36,12 @@ export declare function validateManifest(raw: unknown): {
36
36
  * the manifest a no-op for existing projects: the synthesized result is what
37
37
  * they would have written by hand.
38
38
  *
39
- * An ejected backend one with its own `src/index.ts` entrypoint is reported
40
- * as a `custom` app rather than a `backend` app. That is not a downgrade; it is
41
- * an accurate description, and it is what keeps such a project deploying exactly
42
- * as it does today.
39
+ * The backend's `runtime` is inferred from whether the repository declares a
40
+ * **Dockerfile** the thing that actually builds an image and from nothing
41
+ * else. It used to be inferred from the presence of `backend/src/index.ts`,
42
+ * which every scaffolded project had whether or not it wanted its own server, so
43
+ * projects predating the manifest silently landed on the custom runtime and paid
44
+ * for it (see `docs/cloud-deploy-workspace-vendoring.md`).
43
45
  */
44
46
  export declare function synthesizeManifest(projectRoot: string): RebaseProjectManifest;
45
47
  export declare function manifestPath(projectRoot: string): string;
@@ -72,12 +74,29 @@ export declare function buildableApps(manifest: RebaseProjectManifest): {
72
74
  * verdict.
73
75
  */
74
76
  export declare function assessManagedCompatibility(manifest: RebaseProjectManifest): ManagedCompatibility;
75
- /** Resolve a backend app's directories against the conventions it omits. */
76
- export declare function resolveBackendPaths(app: RebaseBackendAppConfig): {
77
+ /**
78
+ * Resolve a backend app's directories against the conventions it omits.
79
+ *
80
+ * `hasCollections` replaces the old `mode: "cms" | "baas"` field. Where the
81
+ * collections come from was never an independent choice: either they are
82
+ * declared in code and the bundle ships them, or they are not and the runtime
83
+ * introspects the live database at boot. Declaring it separately only created
84
+ * the contradictory state — code-first declared, no collections anywhere.
85
+ *
86
+ * `hasConfig` is deliberately a **separate** question. A headless project has no
87
+ * `config/collections`, but it may still ship a config package — that is where
88
+ * the `storageAuthorize` hook lives, and storage is not under row-level
89
+ * security, so without one the server refuses to boot with storage enabled.
90
+ * Collapsing the two would leave a headless project nowhere to put it.
91
+ */
92
+ export declare function resolveBackendPaths(app: RebaseBackendAppConfig, projectRoot: string): {
77
93
  config: string;
78
94
  functions: string;
79
95
  crons: string;
80
96
  schema: string;
81
97
  usersCollection: string;
82
- mode: "cms" | "baas";
98
+ /** Whether a config package exists — hooks, storage authorization. */
99
+ hasConfig: boolean;
100
+ /** Whether collections are declared in code, under `<config>/collections`. */
101
+ hasCollections: boolean;
83
102
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/cli",
3
- "version": "0.11.1-canary.gfd39654",
3
+ "version": "0.12.0",
4
4
  "description": "Developer tools for Rebase projects",
5
5
  "main": "./dist/index.es.js",
6
6
  "module": "./dist/index.es.js",
@@ -31,12 +31,12 @@
31
31
  "execa": "^9.6.1",
32
32
  "inquirer": "14.0.2",
33
33
  "jiti": "^2.7.0",
34
- "@rebasepro/agent-skills": "0.11.1-canary.gfd39654",
35
- "@rebasepro/client": "0.11.1-canary.gfd39654",
36
- "@rebasepro/codegen": "0.11.1-canary.gfd39654",
37
- "@rebasepro/server": "0.11.1-canary.gfd39654",
38
- "@rebasepro/server-postgres": "0.11.1-canary.gfd39654",
39
- "@rebasepro/types": "0.11.1-canary.gfd39654"
34
+ "@rebasepro/codegen": "0.12.0",
35
+ "@rebasepro/agent-skills": "0.12.0",
36
+ "@rebasepro/server": "0.12.0",
37
+ "@rebasepro/types": "0.12.0",
38
+ "@rebasepro/client": "0.12.0",
39
+ "@rebasepro/server-postgres": "0.12.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^25.9.3",
@@ -31,7 +31,6 @@ const bundle = createSourceBundle({
31
31
  functions: optional(process.env.REBASE_DEV_FUNCTIONS),
32
32
  crons: optional(process.env.REBASE_DEV_CRONS),
33
33
  schema: optional(process.env.REBASE_DEV_SCHEMA),
34
- mode: process.env.REBASE_DEV_MODE === "baas" ? "baas" : "cms",
35
34
  app: optional(process.env.REBASE_DEV_APP)
36
35
  });
37
36
 
@@ -1,10 +1,22 @@
1
- # ─── Multi-stage production Dockerfile for the Rebase backend ─────────
2
- # Produces a minimal image (~150MB) with only the runtime needed.
1
+ # ─── Your image ───────────────────────────────────────────────────────
2
+ #
3
+ # `rebase eject` wrote this file, and it is yours now — nothing regenerates or
4
+ # updates it. The platform runtime is no longer involved: this image compiles
5
+ # and runs backend/src/index.ts, so CORS, auth wiring, storage and shutdown are
6
+ # configured there rather than by the runtime, and Rebase runtime upgrades do
7
+ # not reach this project.
8
+ #
9
+ # Assumes pnpm, which is what `rebase init` scaffolds a workspace for. On npm,
10
+ # swap the three pnpm lines below for `npm ci` and `npm run build --workspace`.
3
11
  #
4
12
  # Build context: the project root (where pnpm-workspace.yaml lives)
5
13
  # Usage:
6
- # docker build -t my-app-backend -f backend/Dockerfile .
7
- # docker run -p 3001:3001 --env-file .env my-app-backend
14
+ # docker build -t my-app .
15
+ # docker compose -f docker-compose.custom.yml up
16
+ #
17
+ # The managed alternative — no image to build, your project mounted into the
18
+ # published runtime — is docker-compose.yml, which is still here and still
19
+ # works if you change `runtime` back to "managed" in rebase.json.
8
20
 
9
21
  # ── Stage 1: Install + Build ─────────────────────────────────────────
10
22
  FROM node:22-alpine AS builder
@@ -19,7 +19,6 @@ const __dirname = path.dirname(__filename);
19
19
  */
20
20
  function findEnvFile(startDir: string): string | undefined {
21
21
  let dir = startDir;
22
- // eslint-disable-next-line no-constant-condition
23
22
  while (true) {
24
23
  const candidate = path.join(dir, ".env");
25
24
  if (fs.existsSync(candidate)) return candidate;
@@ -12,6 +12,8 @@ import {
12
12
  HonoEnv,
13
13
  listenWithPortRetry,
14
14
  cleanupDevPortFile,
15
+ loadDeclaredStorageSources,
16
+ resolveStorageSources,
15
17
  logger
16
18
  } from "@rebasepro/server";
17
19
  import { createPostgresDatabaseConnection, createPostgresAdapter } from "@rebasepro/server-postgres";
@@ -23,6 +25,13 @@ import usersCollection from "../../config/collections/users.js";
23
25
  const __filename = fileURLToPath(import.meta.url);
24
26
  const __dirname = path.dirname(__filename);
25
27
 
28
+ // Which buckets this project has, read from the `storage` block of its own
29
+ // `rebase.json`. Declared there rather than here so the platform, the console
30
+ // and this process all read one list — a custom image ships the repository, so
31
+ // the file it already contains is the natural place for it. Absent means one
32
+ // default source, configured from the plain S3_*/GCS_* variables.
33
+ const storageSources = loadDeclaredStorageSources(__dirname);
34
+
26
35
  // ─── App ─────────────────────────────────────────────────────────────
27
36
  const app: Hono<HonoEnv> = new Hono<HonoEnv>();
28
37
 
@@ -128,33 +137,21 @@ pass: env.SMTP_PASS! }
128
137
  // production — the upload routes answer 501 STORAGE_NOT_CONFIGURED —
129
138
  // rather than writing to the container filesystem, which is erased on
130
139
  // every restart and redeploy. Uploads that fail loudly are recoverable;
131
- // uploads that succeed into a disk about to be wiped are not.
132
- // Local disk stays the default in development, where it is what you want.
133
- storage: env.STORAGE_TYPE === "s3"
134
- ? {
135
- type: "s3",
136
- bucket: env.S3_BUCKET!,
137
- region: env.S3_REGION || "auto",
138
- accessKeyId: env.S3_ACCESS_KEY_ID || "",
139
- secretAccessKey: env.S3_SECRET_ACCESS_KEY || "",
140
- endpoint: env.S3_ENDPOINT,
141
- forcePathStyle: env.S3_FORCE_PATH_STYLE
142
- }
143
- : env.STORAGE_TYPE === "gcs"
144
- ? {
145
- type: "gcs",
146
- bucket: env.GCS_BUCKET!,
147
- projectId: env.GCS_PROJECT_ID,
148
- keyFilename: env.GCS_KEY_FILENAME
149
- }
150
- // Set FORCE_LOCAL_STORAGE=true only if this deployment really
151
- // does have a durable volume mounted at STORAGE_PATH.
152
- : isProduction && !env.FORCE_LOCAL_STORAGE
153
- ? undefined
154
- : {
155
- type: "local",
156
- basePath: env.STORAGE_PATH || path.resolve(__dirname, "../../uploads")
157
- },
140
+ // uploads that succeed into a disk about to be wiped are not. That rule
141
+ // lives in the runtime, which drops a `local` backend in production
142
+ // unless FORCE_LOCAL_STORAGE says a durable volume really is mounted.
143
+ //
144
+ // One resolver, shared with the managed runtime, so this entrypoint
145
+ // cannot drift from it: every source declared in `rebase.json` is read
146
+ // from `<BASE>__<KEY>` (S3_BUCKET__MEDIA for a source keyed "media"),
147
+ // and a project that declared nothing gets one default source from the
148
+ // plain, unsuffixed variables — exactly as before.
149
+ storage: resolveStorageSources(
150
+ process.env,
151
+ storageSources,
152
+ path.resolve(__dirname, "../../uploads")
153
+ ),
154
+ storageSources,
158
155
  // Storage is not under row-level security, so this hook IS its access
159
156
  // model — the server refuses to boot in production without one, because
160
157
  // "signed in" would otherwise be the only thing between a visitor and
@@ -164,7 +161,24 @@ pass: env.SMTP_PASS! }
164
161
  history: true
165
162
  });
166
163
 
164
+ // ─── Your own routes ──────────────────────────────────────────
165
+ // This is a plain Hono app and everything you add to it is yours — which
166
+ // also means it is outside Rebase's auth. `initializeRebaseBackend` guards
167
+ // the routers it mounts (`/api/data`, `/api/auth`, …); it does not guard
168
+ // this `app`. A route added here is reachable by anyone on the internet
169
+ // until you put a guard in its middleware slot:
170
+ //
171
+ // import { requireAuth, requireAdmin } from "@rebasepro/server";
172
+ // app.get("/admin/report", requireAuth, requireAdmin, handler);
173
+ //
174
+ // `requireAuth` answers 401 without a valid token; `requireAdmin` answers
175
+ // 403 without the `admin` role and must follow `requireAuth`. Note that
176
+ // `c.get("driver")` — the RLS-scoped driver — is only set inside the Rebase
177
+ // routers, so out here reach for `rebase.dataAsAdmin`, which **bypasses
178
+ // RLS** and therefore belongs behind one of those guards.
179
+
167
180
  // ─── Health check ─────────────────────────────────────────────
181
+ // Deliberately public: an orchestrator's probe has no token to send.
168
182
  app.get("/health", async (c) => {
169
183
  const result = await backend.healthCheck();
170
184
  const status = result.healthy ? 200 : 503;
@@ -0,0 +1,71 @@
1
+ # ─── Self-hosting an ejected project ─────────────────────────────────
2
+ #
3
+ # `rebase eject` wrote this. It builds the image described by ./Dockerfile and
4
+ # runs YOUR backend/src/index.ts, rather than mounting a bundle into the
5
+ # published Rebase runtime.
6
+ #
7
+ # docker compose -f docker-compose.custom.yml up -d db
8
+ # rebase db push # once
9
+ # docker compose -f docker-compose.custom.yml up --build
10
+ #
11
+ # What you gave up by ejecting: the platform no longer upgrades the server
12
+ # underneath you, and CORS, auth wiring, storage and shutdown are configured in
13
+ # your entrypoint rather than by the runtime. What you gained: it is your
14
+ # process, and you can put anything you like in it.
15
+ #
16
+ # `docker-compose.yml` is still here and still runs the managed shape. Nothing
17
+ # stops you going back — set `runtime: "managed"` in rebase.json, `rebase build`,
18
+ # and use that file instead.
19
+ # ─────────────────────────────────────────────────────────────────────
20
+
21
+ name: {{PROJECT_NAME}}
22
+
23
+ services:
24
+ db:
25
+ image: postgres:18-alpine
26
+ restart: unless-stopped
27
+ environment:
28
+ POSTGRES_USER: rebase
29
+ POSTGRES_PASSWORD: ${DATABASE_PASSWORD:-changeme}
30
+ POSTGRES_DB: rebase
31
+ ports:
32
+ - "5432:5432"
33
+ volumes:
34
+ - postgres_data:/var/lib/postgresql
35
+ healthcheck:
36
+ test: ["CMD-SHELL", "pg_isready -U rebase -d rebase"]
37
+ interval: 5s
38
+ timeout: 5s
39
+ retries: 10
40
+ start_period: 10s
41
+
42
+ backend:
43
+ build:
44
+ context: .
45
+ dockerfile: Dockerfile
46
+ restart: unless-stopped
47
+ ports:
48
+ - "${PORT:-3001}:3001"
49
+ env_file: .env
50
+ environment:
51
+ DATABASE_URL: postgresql://rebase:${DATABASE_PASSWORD:-changeme}@db:5432/rebase?options=-c%20search_path=public
52
+ ADMIN_CONNECTION_STRING: postgresql://rebase:${DATABASE_PASSWORD:-changeme}@db:5432/rebase?options=-c%20search_path=public
53
+ NODE_ENV: production
54
+ PORT: "3001"
55
+ # Your entrypoint serves the built frontend itself (see the `serveSPA`
56
+ # call in backend/src/index.ts), so this is one container, same origin.
57
+ CORS_ORIGINS: ${CORS_ORIGINS:?set CORS_ORIGINS to the origin you browse to}
58
+ # A durable named volume, which is the case this flag acknowledges.
59
+ STORAGE_PATH: /uploads
60
+ FORCE_LOCAL_STORAGE: "true"
61
+ depends_on:
62
+ db:
63
+ condition: service_healthy
64
+ volumes:
65
+ - uploads:/uploads
66
+
67
+ volumes:
68
+ postgres_data:
69
+ driver: local
70
+ uploads:
71
+ driver: local
@@ -2,12 +2,9 @@
2
2
  "name": "{{PROJECT_NAME}}-backend",
3
3
  "version": "1.0.0",
4
4
  "description": "Rebase BaaS — headless PostgreSQL API",
5
- "main": "src/index.ts",
6
5
  "type": "module",
7
6
  "scripts": {
8
- "dev": "tsx watch --include=\"./functions/**/*\" src/index.ts",
9
- "build": "tsc",
10
- "start": "node dist/index.js"
7
+ "build": "tsc"
11
8
  },
12
9
  "dependencies": {
13
10
  "@rebasepro/server": "workspace:*",
@@ -5,7 +5,6 @@
5
5
  "moduleResolution": "node",
6
6
  "lib": ["ES2022"],
7
7
  "outDir": "./dist",
8
- "rootDir": "./src",
9
8
  "strict": true,
10
9
  "esModuleInterop": true,
11
10
  "allowSyntheticDefaultImports": true,
@@ -15,5 +14,12 @@
15
14
  "declaration": true,
16
15
  "sourceMap": true
17
16
  },
18
- "include": ["src/**/*"]
17
+ // `functions/` as well as `src/`, because this flavour has nothing in src:
18
+ // it declares no collections, so there is no generated schema, and the server
19
+ // entrypoint lives behind `rebase eject`. An include matching only an empty
20
+ // directory is TS18003 — tsc reports "no inputs" as an error, not a no-op.
21
+ //
22
+ // No `rootDir`: it would have to be `.` for both, and letting tsc infer the
23
+ // common root gets there without a second place to keep in sync.
24
+ "include": ["src/**/*", "functions/**/*"]
19
25
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The config package for a headless project.
3
+ *
4
+ * There are no collections here: this flavour introspects them from the live
5
+ * database at boot, which is exactly what "no `config/collections` directory"
6
+ * means to `rebase build`.
7
+ *
8
+ * The package still exists because storage does not run under row-level
9
+ * security and its keys share one flat namespace — so without an access model,
10
+ * a deployment with file storage enabled serves every user's files to every
11
+ * signed-in user. The server refuses to boot in that state, and this is the
12
+ * export it looks for.
13
+ */
14
+
15
+ export { storageAuthorize } from "./storage.js";
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}-config",
3
+ "version": "1.0.0",
4
+ "description": "Storage access control for a headless Rebase project",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "private": true,
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsc --watch",
12
+ "clean": "rm -rf dist"
13
+ },
14
+ "dependencies": {
15
+ "@rebasepro/types": "workspace:*"
16
+ },
17
+ "devDependencies": {
18
+ "typescript": "^5.9.2"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "import": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "./package.json": "./package.json"
27
+ }
28
+ }
@@ -5,7 +5,8 @@
5
5
  "private": true,
6
6
  "type": "module",
7
7
  "workspaces": [
8
- "backend"
8
+ "backend",
9
+ "config"
9
10
  ],
10
11
  "scripts": {
11
12
  "dev": "rebase dev",
@@ -1,5 +1,6 @@
1
1
  packages:
2
2
  - "backend"
3
+ - "config"
3
4
  linkWorkspacePackages: true
4
5
  blockExoticSubdeps: false
5
6
  minimumReleaseAge: 0
@@ -1,14 +1,10 @@
1
1
  {
2
2
  "$schema": "https://rebase.pro/schemas/rebase.json",
3
- "runtime": "^1",
3
+ "rebase": "^1",
4
4
  "apps": {
5
5
  "backend": {
6
6
  "type": "backend",
7
- "mode": "baas"
8
- },
9
- "admin": {
10
- "type": "admin",
11
- "mode": "hosted"
7
+ "runtime": "managed"
12
8
  }
13
9
  }
14
10
  }
@@ -125,6 +125,21 @@ VITE_API_URL=http://localhost:3001
125
125
  # For other providers (Azure Blob, etc.), implement the StorageController
126
126
  # interface and pass it directly in your backend config.
127
127
 
128
+ # --- Several buckets ------------------------------------------------------
129
+ # Declare them in rebase.json, then configure each from the SAME variable names
130
+ # carrying its own suffix. The default source takes no suffix, so everything
131
+ # above keeps working and a single-bucket project declares nothing.
132
+ #
133
+ # rebase.json: "storage": { "media": { "engine": "s3" } }
134
+ #
135
+ # STORAGE_TYPE__MEDIA=s3
136
+ # S3_BUCKET__MEDIA=my-app-media
137
+ # S3_ACCESS_KEY_ID__MEDIA=
138
+ # S3_SECRET_ACCESS_KEY__MEDIA=
139
+ #
140
+ # The separator is a DOUBLE underscore: a single one would collide with real
141
+ # variable names (S3_BUCKET_NAME would parse as bucket "name").
142
+
128
143
  # ── Backups (optional) ────────────────────────────────────────────────────────
129
144
  # Manual backups: `rebase db backup --out ./backups` (or an s3://… URL).
130
145
  # Scheduled backups: set BACKUP_SCHEDULE and add a cron file in backend/crons
@@ -60,37 +60,60 @@ server). Pin a port with `rebase dev --port 3001` if you need a stable one.
60
60
  ## Project Structure
61
61
 
62
62
  ```
63
- ├── frontend/ # React frontend (Vite)
64
- ├── Dockerfile # Production build → nginx
65
- │ └── nginx.conf # SPA routing + compression
66
- ├── backend/ # Hono backend with PostgreSQL
67
- │ ├── Dockerfile # Multi-stage production build
63
+ ├── frontend/ # Your admin panel (React + Vite)
64
+ ├── backend/ # Server code
68
65
  │ ├── functions/ # Custom API endpoints (auto-discovered)
69
- │ └── src/
66
+ │ └── src/ # Generated database schema
70
67
  ├── config/ # Shared collection definitions
71
68
  │ └── collections/ # Schema-as-Code TypeScript files
72
- ├── docker-compose.yml # Production stack (Postgres + Backend + Frontend)
69
+ ├── rebase.json # Which apps this repository contains
70
+ ├── docker-compose.yml # Self-hosting stack (Postgres + the Rebase runtime)
73
71
  ├── .env.example # Environment variable reference
74
72
  └── package.json # Root workspace config
75
73
  ```
76
74
 
75
+ There is no Dockerfile, and that is deliberate: `rebase build` produces a
76
+ **bundle** — your compiled collections, functions, crons and admin assets — and
77
+ the published `rebasepro/server` image boots it. The artifact you self-host is
78
+ the artifact Rebase Cloud runs, so moving between them changes nothing in this
79
+ repository.
80
+
81
+ If you would rather run your own server process, `rebase eject` writes the
82
+ entrypoint, a Dockerfile and a compose file that builds them.
83
+
77
84
  ### Custom Functions
78
85
 
79
86
  Drop a Hono app in `backend/functions/` and it's auto-mounted at `/api/functions/<name>`:
80
87
 
81
88
  ```typescript
82
89
  // backend/functions/hello.ts
83
- import { Hono } from "hono";
84
- const app = new Hono();
85
- app.post("/", async (c) => {
86
- const body = await c.req.json();
87
- return c.json({ message: `Hello, ${body.name}!` });
90
+ import { defineFunction, requireAuth, requireAdmin } from "@rebasepro/server";
91
+
92
+ export default defineFunction((app) => {
93
+ // Deliberately public anyone can call this.
94
+ app.get("/", (c) => c.json({ status: "ok" }));
95
+
96
+ // 401 without a valid token.
97
+ app.post("/", requireAuth, async (c) => {
98
+ const body = await c.req.json();
99
+ return c.json({ message: `Hello, ${body.name}!` });
100
+ });
101
+
102
+ // 401 anonymous, 403 without the `admin` role. Order matters.
103
+ app.get("/stats", requireAuth, requireAdmin, (c) => c.json({ ok: true }));
88
104
  });
89
- export default app;
90
105
  ```
91
106
 
92
107
  Call from the client SDK: `client.call("functions/hello", { name: "World" })`
93
108
 
109
+ **Functions are not authenticated for you.** The functions router parses the
110
+ caller's token into the request context but does not reject anonymous requests —
111
+ webhook receivers have no token to send. So every route here is public until a
112
+ guard says otherwise, and reading `c.get("user")` is not a guard: an anonymous
113
+ caller simply gets `undefined` and the handler runs. Put `requireAuth` /
114
+ `requireAdmin` in the route's own middleware slot rather than in `app.use()`,
115
+ which only covers routes declared below it.
116
+
94
117
  ### Shared Collections
95
118
 
96
119
  Collections are defined once in `config/collections/` and used by both the frontend and backend. This ensures your schema stays in sync across the stack.
@@ -105,23 +128,28 @@ All configuration is managed through a single `.env` file in the project root. B
105
128
 
106
129
  `init` already generated your `.env`. See the comments in `.env.example` for details on each variable, and edit `.env` directly to change any of them.
107
130
 
108
- ## Production Deployment
131
+ ## Self-hosting
132
+
133
+ Two containers: PostgreSQL, and the Rebase runtime with your built project
134
+ mounted into it. There is no application image to build.
109
135
 
110
- The full stack PostgreSQL, backend, and frontend runs from
111
- `docker compose`. The backend image builds and boots, but it does **not**
112
- create your collection tables on its own, so push the schema once the
113
- database is up before (or right after) starting the rest of the stack.
136
+ The runtime creates its auth tables at boot but **not** your collection tables —
137
+ a container restart must not be able to change a schema as a side effect — so
138
+ push the schema once, while the database is up.
114
139
 
115
140
  ```bash
116
- # 1. Start the database and create the tables from your collections
141
+ # 1. Build your project into ./dist-bundle
142
+ pnpm run build # or: npm run build
143
+
144
+ # 2. Start the database and create the tables from your collections
117
145
  docker compose up -d db
118
146
  pnpm run db:push # or: npm run db:push
119
147
 
120
- # 2. Build and start the backend + frontend
121
- docker compose up -d --build
148
+ # 3. Start the runtime
149
+ docker compose up -d
122
150
 
123
151
  # View logs
124
- docker compose logs -f backend
152
+ docker compose logs -f api
125
153
 
126
154
  # Stop
127
155
  docker compose down
@@ -130,6 +158,12 @@ docker compose down
130
158
  docker compose down -v
131
159
  ```
132
160
 
161
+ One container now serves the API at `/api` and the admin at `/` — same origin,
162
+ so there is no CORS between them and no second web server to run.
163
+
164
+ To upgrade Rebase, set `REBASE_VERSION` in `.env` and restart. Your bundle is
165
+ untouched.
166
+
133
167
  > `docker compose` reads the generated `.env` as-is. Set production values
134
168
  > (a strong `DATABASE_PASSWORD`, `JWT_SECRET`, storage credentials, …) by
135
169
  > editing `.env` directly — see `.env.example` for the full list. Do not
@@ -15,3 +15,4 @@ rebase skills install
15
15
  - Step 1: Run `rebase schema generate` to compile collections to the Drizzle schema.
16
16
  - Step 2: Run `rebase db push` (development) or `rebase db generate && rebase db migrate` (production) to apply schema changes to the database.
17
17
  3. **Use the SDK**: Always use the Rebase SDK (`rebase.data.<slug>`) to fetch or modify data. Bypassing it with raw SQL or direct Drizzle/PG queries circumvents model validations, lifecycle hooks, and Row-Level Security (RLS).
18
+ 4. **Guard every custom route**: routes in `backend/functions/` are mounted **without** an auth requirement — webhook receivers need that — so each one is public until you guard it. Import `requireAuth` / `requireAdmin` from `@rebasepro/server` and pass them in the route's own middleware slot (`app.post("/", requireAuth, handler)`), not via `app.use()`, which only covers routes declared below it. Reading `c.get("user")` is not a guard: an anonymous caller gets `undefined` and the handler still runs. See `backend/functions/hello.ts` for all three tiers.