@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.
- package/bin/rebase.js +21 -0
- package/dist/bundle.d.ts +24 -7
- package/dist/commands/eject.d.ts +1 -0
- package/dist/commands/init.d.ts +19 -15
- package/dist/fold-static.d.ts +41 -15
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +894 -246
- package/dist/index.es.js.map +1 -1
- package/dist/manifest.d.ts +27 -8
- package/package.json +7 -7
- package/runtime/dev-server.mjs +0 -1
- package/templates/{template/backend → eject}/Dockerfile +16 -4
- package/templates/{template → eject}/backend/src/env.ts +0 -1
- package/templates/{template → eject}/backend/src/index.ts +41 -27
- package/templates/eject/docker-compose.custom.yml +71 -0
- package/templates/overlays/baas/backend/package.json +1 -4
- package/templates/overlays/baas/backend/tsconfig.json +8 -2
- package/templates/overlays/baas/config/index.ts +15 -0
- package/templates/overlays/baas/config/package.json +28 -0
- package/templates/overlays/baas/package.json +2 -1
- package/templates/overlays/baas/pnpm-workspace.yaml +1 -0
- package/templates/overlays/baas/rebase.json +2 -6
- package/templates/template/.env.example +15 -0
- package/templates/template/README.md +56 -22
- package/templates/template/ai-instructions.md +1 -0
- package/templates/template/backend/functions/hello.ts +45 -14
- package/templates/template/backend/package.json +1 -4
- package/templates/template/backend/tsconfig.json +23 -2
- package/templates/template/config/tsconfig.json +16 -1
- package/templates/template/docker-compose.yml +62 -38
- package/templates/template/frontend/src/main.tsx +8 -1
- package/templates/template/frontend/vite.config.ts +5 -0
- package/templates/template/rebase.json +5 -8
- package/templates/overlays/baas/backend/src/index.ts +0 -216
- package/templates/template/frontend/Dockerfile +0 -52
- package/templates/template/frontend/nginx.conf +0 -40
- /package/templates/overlays/baas/{backend/src → config}/storage.ts +0 -0
package/dist/manifest.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
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
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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
|
-
/**
|
|
76
|
-
|
|
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
|
-
|
|
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.
|
|
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/
|
|
35
|
-
"@rebasepro/
|
|
36
|
-
"@rebasepro/
|
|
37
|
-
"@rebasepro/
|
|
38
|
-
"@rebasepro/
|
|
39
|
-
"@rebasepro/
|
|
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",
|
package/runtime/dev-server.mjs
CHANGED
|
@@ -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
|
-
# ───
|
|
2
|
-
#
|
|
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
|
|
7
|
-
# docker
|
|
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
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://rebase.pro/schemas/rebase.json",
|
|
3
|
-
"
|
|
3
|
+
"rebase": "^1",
|
|
4
4
|
"apps": {
|
|
5
5
|
"backend": {
|
|
6
6
|
"type": "backend",
|
|
7
|
-
"
|
|
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
|
|
64
|
-
|
|
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
|
-
├──
|
|
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 {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
##
|
|
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
|
|
111
|
-
|
|
112
|
-
|
|
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.
|
|
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
|
-
#
|
|
121
|
-
docker compose up -d
|
|
148
|
+
# 3. Start the runtime
|
|
149
|
+
docker compose up -d
|
|
122
150
|
|
|
123
151
|
# View logs
|
|
124
|
-
docker compose logs -f
|
|
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.
|