@rebasepro/cli 0.9.0 → 0.9.1-canary.0de22e0

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 (47) hide show
  1. package/README.md +32 -3
  2. package/bin/rebase.js +67 -1
  3. package/dist/commands/cloud/auth.d.ts +3 -0
  4. package/dist/commands/cloud/context.d.ts +151 -0
  5. package/dist/commands/cloud/databases.d.ts +1 -0
  6. package/dist/commands/cloud/debug.d.ts +117 -0
  7. package/dist/commands/cloud/deploy.d.ts +2 -0
  8. package/dist/commands/cloud/deployments.d.ts +38 -0
  9. package/dist/commands/cloud/domains.d.ts +1 -0
  10. package/dist/commands/cloud/env.d.ts +6 -0
  11. package/dist/commands/cloud/extensions.d.ts +3 -0
  12. package/dist/commands/cloud/index.d.ts +1 -0
  13. package/dist/commands/cloud/link.d.ts +5 -0
  14. package/dist/commands/cloud/orgs.d.ts +1 -0
  15. package/dist/commands/cloud/power.d.ts +3 -0
  16. package/dist/commands/cloud/projects.d.ts +13 -0
  17. package/dist/commands/cloud/resources.d.ts +6 -0
  18. package/dist/commands/cloud/settings.d.ts +8 -0
  19. package/dist/commands/init.d.ts +23 -0
  20. package/dist/commands/skills.d.ts +1 -1
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.es.js +6083 -1819
  23. package/dist/index.es.js.map +1 -1
  24. package/dist/utils/package-manager.d.ts +19 -5
  25. package/dist/utils/project.d.ts +1 -1
  26. package/package.json +8 -7
  27. package/templates/overlays/baas/README.md +55 -0
  28. package/templates/overlays/baas/backend/package.json +31 -0
  29. package/templates/overlays/baas/backend/src/index.ts +192 -0
  30. package/templates/overlays/baas/backend/tsconfig.json +19 -0
  31. package/templates/overlays/baas/package.json +42 -0
  32. package/templates/overlays/baas/pnpm-workspace.yaml +9 -0
  33. package/templates/template/.env.example +25 -3
  34. package/templates/template/backend/functions/hello.ts +1 -1
  35. package/templates/template/backend/package.json +6 -7
  36. package/templates/template/backend/src/env.ts +31 -2
  37. package/templates/template/backend/src/index.ts +21 -16
  38. package/templates/template/config/collections/index.ts +20 -0
  39. package/templates/template/config/collections/presets/blank/index.ts +3 -1
  40. package/templates/template/config/collections/presets/ecommerce/index.ts +3 -1
  41. package/templates/template/config/collections/users.ts +2 -0
  42. package/templates/template/config/package.json +1 -2
  43. package/templates/template/frontend/package.json +2 -3
  44. package/templates/template/frontend/src/App.tsx +3 -4
  45. package/templates/template/frontend/vite.config.ts +1 -1
  46. package/templates/template/gitignore +31 -0
  47. package/templates/template/npmrc +10 -0
@@ -20,13 +20,27 @@ export interface PMCommands {
20
20
  workspaceProtocol: string;
21
21
  }
22
22
  /**
23
- * Detect the package manager from the environment or the target directory.
23
+ * Whether pnpm is runnable on this machine.
24
+ *
25
+ * Used to decide whether a fresh project can be scaffolded with pnpm. Kept
26
+ * cheap and non-interactive (short timeout, output discarded) so it never
27
+ * hangs detection if a corepack shim misbehaves.
28
+ */
29
+ export declare function isPnpmAvailable(): boolean;
30
+ /**
31
+ * Detect the package manager for a Rebase project.
32
+ *
33
+ * Rebase recommends pnpm, so detection prefers it. Crucially, *how the CLI was
34
+ * invoked* (`npx` vs `pnpm dlx`, i.e. `npm_config_user_agent`) is deliberately
35
+ * ignored: running `npx @rebasepro/cli init` says nothing about how the user
36
+ * wants to manage the project they're creating, and letting it pin the scaffold
37
+ * to npm is what made every `npx`-invoked project an npm project.
24
38
  *
25
39
  * Detection order:
26
- * 1. Explicit override (if provided)
27
- * 2. `npm_config_user_agent` env var (set by npm/pnpm when running via `npx`/`pnpm dlx`)
28
- * 3. Lock-file presence in the target directory
29
- * 4. Default to pnpm (Rebase's recommended PM)
40
+ * 1. An existing lock file — an explicit choice we always respect
41
+ * (`pnpm-lock.yaml` wins over `package-lock.json` when both are present).
42
+ * 2. pnpm, whenever it is installed.
43
+ * 3. npm, only as a fallback when pnpm is genuinely unavailable.
30
44
  */
31
45
  export declare function detectPackageManager(targetDir?: string): PackageManager;
32
46
  /** Build the command helpers for a given package manager. */
@@ -11,7 +11,7 @@ export declare function findProjectRoot(startDir?: string): string | null;
11
11
  */
12
12
  export declare function findBackendDir(projectRoot: string): string | null;
13
13
  /**
14
- * Detect the active backend plugin (e.g. @rebasepro/server-postgresql) from the backend's package.json.
14
+ * Detect the active backend plugin (e.g. @rebasepro/server-postgres) from the backend's package.json.
15
15
  */
16
16
  export declare function getActiveBackendPlugin(backendDir: string): string | null;
17
17
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/cli",
3
- "version": "0.9.0",
3
+ "version": "0.9.1-canary.0de22e0",
4
4
  "description": "Developer tools for Rebase projects",
5
5
  "main": "./dist/index.es.js",
6
6
  "module": "./dist/index.es.js",
@@ -31,11 +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.9.0",
35
- "@rebasepro/sdk-generator": "0.9.0",
36
- "@rebasepro/server-core": "0.9.0",
37
- "@rebasepro/types": "0.9.0",
38
- "@rebasepro/server-postgresql": "0.9.0"
34
+ "@rebasepro/agent-skills": "0.9.1-canary.0de22e0",
35
+ "@rebasepro/client": "0.9.1-canary.0de22e0",
36
+ "@rebasepro/server-postgres": "0.9.1-canary.0de22e0",
37
+ "@rebasepro/types": "0.9.1-canary.0de22e0",
38
+ "@rebasepro/server": "0.9.1-canary.0de22e0",
39
+ "@rebasepro/codegen": "0.9.1-canary.0de22e0"
39
40
  },
40
41
  "devDependencies": {
41
42
  "@types/node": "^25.9.3",
@@ -64,7 +65,7 @@
64
65
  "scripts": {
65
66
  "test": "vitest run",
66
67
  "test:e2e": "vitest run --config vitest.e2e.config.ts",
67
- "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.json",
68
+ "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.json && node ../../scripts/assert-build-output.mjs",
68
69
  "clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f"
69
70
  }
70
71
  }
@@ -0,0 +1,55 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ A headless Rebase backend: a REST API, auth, storage, realtime and backups over
4
+ your PostgreSQL database.
5
+
6
+ There are no collection files. The server reads your database schema at boot and
7
+ serves your tables, so the API follows your migrations — change the schema and
8
+ the endpoints change with it.
9
+
10
+ ## Serving a table
11
+
12
+ A table is served once it has an authorization model: row-level security enabled
13
+ plus at least one policy. Until then the server skips it — deliberately, so a new
14
+ table is never exposed just by existing — and logs each table it skipped and why.
15
+
16
+ ```sql
17
+ ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
18
+ CREATE POLICY your_table_owner ON your_table
19
+ FOR ALL USING (user_id = auth.uid());
20
+ ```
21
+
22
+ `auth.uid()`, `auth.roles()` and `auth.jwt()` are provided by Rebase and read the
23
+ identity of the authenticated request.
24
+
25
+ To serve unprotected tables anyway (development only), set
26
+ `baas: { unprotectedTables: "serve" }` in `backend/src/index.ts`.
27
+
28
+ ## Run it
29
+
30
+ ```bash
31
+ pnpm install
32
+ pnpm dev
33
+ ```
34
+
35
+ - API: `http://localhost:3001/api/data/<table>`
36
+ - Docs: `http://localhost:3001/api/swagger`
37
+ - Health: `http://localhost:3001/health`
38
+
39
+ Set `DATABASE_URL` in `.env` to point at your database.
40
+
41
+ ## Use it from an app
42
+
43
+ ```ts
44
+ import { createRebaseClient } from "@rebasepro/client";
45
+
46
+ const rebase = createRebaseClient({ baseUrl: "http://localhost:3001" });
47
+ const posts = await rebase.data.collection("posts").find();
48
+ ```
49
+
50
+ ## Adding an admin UI later
51
+
52
+ Nothing here locks you out of it. Switch `mode` to `"cms"` in
53
+ `backend/src/index.ts`, add a `config/collections` directory, and add a frontend
54
+ that renders them. See MODULAR-ARCHITECTURE.md in the Rebase repo for the three
55
+ adoption modes.
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}-backend",
3
+ "version": "1.0.0",
4
+ "description": "Rebase BaaS — headless PostgreSQL API",
5
+ "main": "src/index.ts",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "tsx watch --include=\"./functions/**/*\" src/index.ts",
9
+ "build": "tsc",
10
+ "start": "node dist/index.js"
11
+ },
12
+ "dependencies": {
13
+ "@rebasepro/server": "workspace:*",
14
+ "@rebasepro/server-postgres": "workspace:*",
15
+ "@rebasepro/types": "workspace:*",
16
+ "drizzle-orm": "^0.45.2",
17
+ "hono": "^4.12.10",
18
+ "@hono/node-server": "^1.19.12",
19
+ "pg": "^8.11.3",
20
+ "ws": "^8.16.0",
21
+ "dotenv": "^16.0.0",
22
+ "zod": "^4.4.3"
23
+ },
24
+ "devDependencies": {
25
+ "@types/pg": "^8.6.5",
26
+ "@types/node": "^20.10.5",
27
+ "@types/ws": "^8.5.10",
28
+ "tsx": "^4.20.6",
29
+ "typescript": "^5.9.2"
30
+ }
31
+ }
@@ -0,0 +1,192 @@
1
+ import { Hono } from "hono";
2
+ import { cors } from "hono/cors";
3
+ import { secureHeaders } from "hono/secure-headers";
4
+ import { getRequestListener } from "@hono/node-server";
5
+ import { createServer } from "http";
6
+ import path from "path";
7
+ import { fileURLToPath } from "url";
8
+ import {
9
+ initializeRebaseBackend,
10
+ installShutdownHandlers,
11
+ HonoEnv,
12
+ listenWithPortRetry,
13
+ cleanupDevPortFile,
14
+ logger
15
+ } from "@rebasepro/server";
16
+ import { createPostgresDatabaseConnection, createPostgresAdapter } from "@rebasepro/server-postgres";
17
+ import { env } from "./env.js";
18
+
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = path.dirname(__filename);
21
+
22
+ // ─── App ─────────────────────────────────────────────────────────────
23
+ const app: Hono<HonoEnv> = new Hono<HonoEnv>();
24
+
25
+ const isProduction = env.NODE_ENV === "production";
26
+ const allowedOrigins = isProduction
27
+ ? (() => {
28
+ const origins = env.CORS_ORIGINS || env.FRONTEND_URL;
29
+ if (!origins) {
30
+ throw new Error(
31
+ "CORS_ORIGINS or FRONTEND_URL must be set in production. " +
32
+ "Example: CORS_ORIGINS=https://yourdomain.com"
33
+ );
34
+ }
35
+ return origins.split(",").map(s => s.trim());
36
+ })()
37
+ : [];
38
+
39
+ // In dev we still restrict which origins are reflected. Because `credentials`
40
+ // is enabled, reflecting an arbitrary Origin would let any website the
41
+ // developer happens to visit make credentialed cross-origin requests to this
42
+ // dev server (and read the responses) using the developer's session. So dev
43
+ // reflects only localhost origins; requests with no Origin (curl, same-origin)
44
+ // are unaffected.
45
+ const isLocalhostOrigin = (origin: string): boolean => {
46
+ try {
47
+ const { hostname } = new URL(origin);
48
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
49
+ } catch {
50
+ return false;
51
+ }
52
+ };
53
+
54
+ app.use("/*", cors({
55
+ origin: (origin) => {
56
+ if (isProduction) return allowedOrigins.includes(origin) ? origin : null;
57
+ if (!origin) return "*";
58
+ return isLocalhostOrigin(origin) ? origin : null;
59
+ },
60
+ credentials: true
61
+ }));
62
+
63
+ app.use("/*", secureHeaders());
64
+
65
+ // ─── Database ────────────────────────────────────────────────────────
66
+ const databaseUrl = env.DATABASE_URL;
67
+
68
+ const { db, pool, connectionString } = createPostgresDatabaseConnection(databaseUrl);
69
+
70
+ // ─── Start ───────────────────────────────────────────────────────────
71
+ async function startServer() {
72
+ const jwtSecret = env.JWT_SECRET;
73
+ const PORT = env.PORT;
74
+ const server = createServer(getRequestListener(app.fetch));
75
+
76
+ const backend = await initializeRebaseBackend({
77
+ // BaaS mode: every RLS-protected table is served over REST. There are
78
+ // no collection files to write or keep in sync — change the schema with
79
+ // a migration and the API follows.
80
+ //
81
+ // Your database's own row-level security is the whole authorization
82
+ // model here: requests run as the `rebase_user` role, so a table
83
+ // without RLS has no rules at all and is not served. Protect one with:
84
+ // ALTER TABLE mytable ENABLE ROW LEVEL SECURITY;
85
+ // CREATE POLICY mytable_read ON mytable FOR SELECT TO public USING (true);
86
+ mode: "baas",
87
+ functionsDir: path.resolve(__dirname, "../functions"),
88
+ server,
89
+ app,
90
+ database: createPostgresAdapter({
91
+ connection: db,
92
+ adminConnectionString: env.ADMIN_CONNECTION_STRING || databaseUrl,
93
+ connectionString
94
+ }),
95
+ auth: {
96
+ // No `collection` here: BaaS mode has no collection files, and the
97
+ // auth adapter owns its own user tables.
98
+ jwtSecret,
99
+ accessExpiresIn: env.JWT_ACCESS_EXPIRES_IN,
100
+ refreshExpiresIn: env.JWT_REFRESH_EXPIRES_IN,
101
+ serviceKey: env.REBASE_SERVICE_KEY,
102
+ cookieAuth: { sameSite: "Lax" },
103
+ google: env.GOOGLE_CLIENT_ID
104
+ ? { clientId: env.GOOGLE_CLIENT_ID }
105
+ : undefined,
106
+ allowRegistration: env.ALLOW_REGISTRATION,
107
+ email: env.SMTP_HOST
108
+ ? {
109
+ from: env.SMTP_FROM || `${env.APP_NAME} <noreply@rebase.pro>`,
110
+ smtp: {
111
+ host: env.SMTP_HOST,
112
+ port: env.SMTP_PORT,
113
+ secure: env.SMTP_SECURE,
114
+ auth: env.SMTP_USER
115
+ ? { user: env.SMTP_USER,
116
+ pass: env.SMTP_PASS! }
117
+ : undefined,
118
+ name: env.SMTP_NAME
119
+ },
120
+ appName: env.APP_NAME,
121
+ resetPasswordUrl: env.FRONTEND_URL
122
+ }
123
+ : undefined
124
+ },
125
+ storage: env.STORAGE_TYPE === "s3"
126
+ ? {
127
+ type: "s3",
128
+ bucket: env.S3_BUCKET!,
129
+ region: env.S3_REGION || "auto",
130
+ accessKeyId: env.S3_ACCESS_KEY_ID || "",
131
+ secretAccessKey: env.S3_SECRET_ACCESS_KEY || "",
132
+ endpoint: env.S3_ENDPOINT,
133
+ forcePathStyle: env.S3_FORCE_PATH_STYLE
134
+ }
135
+ : {
136
+ type: "local",
137
+ basePath: env.STORAGE_PATH || path.resolve(__dirname, "../../uploads")
138
+ },
139
+ history: true,
140
+ enableSwagger: true
141
+ });
142
+
143
+ // ─── Health check ─────────────────────────────────────────────
144
+ app.get("/health", async (c) => {
145
+ const result = await backend.healthCheck();
146
+ const status = result.healthy ? 200 : 503;
147
+ return c.json({
148
+ status: result.healthy ? "ok" : "degraded",
149
+ latencyMs: result.latencyMs,
150
+ ...(result.details ? { details: result.details } : {})
151
+ }, status);
152
+ });
153
+
154
+ // No serveSPA: this is a headless API. Point any frontend at it over HTTP.
155
+
156
+ if (!isProduction) {
157
+ // Dev mode: retry the next port if the current one is in use
158
+ const projectRoot = path.resolve(__dirname, "../..");
159
+ const actualPort = await listenWithPortRetry(server, PORT, { portFileDir: projectRoot, serviceKey: env.REBASE_SERVICE_KEY });
160
+
161
+ // Clean up port file on exit
162
+ const cleanup = () => cleanupDevPortFile(projectRoot);
163
+ process.on("SIGINT", cleanup);
164
+ process.on("SIGTERM", cleanup);
165
+ process.on("exit", cleanup);
166
+
167
+ logger.info(`API running at http://localhost:${actualPort}`);
168
+ // Docs are only mounted once there is something to document; with no
169
+ // servable tables the URL would 404, so don't advertise it.
170
+ if (backend.collectionRegistry.getCollections().length > 0) {
171
+ logger.info(`API docs at http://localhost:${actualPort}/api/swagger`);
172
+ }
173
+ } else {
174
+ server.listen(PORT, () => {
175
+ logger.info(`API running at http://localhost:${PORT}`);
176
+ });
177
+ }
178
+
179
+ // ─── Graceful Shutdown ───────────────────────────────────────────────
180
+ // Drains HTTP, stops crons, tears down realtime, then closes the pool.
181
+ // Guards against double signals and force-exits if shutdown hangs.
182
+ installShutdownHandlers(backend, {
183
+ onCleanup: () => pool.end()
184
+ });
185
+ }
186
+
187
+ startServer().catch(err => {
188
+ logger.error("Failed to start server", { error: err instanceof Error ? err : new Error(String(err)) });
189
+ process.exit(1);
190
+ });
191
+
192
+ export { app };
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "node",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "allowSyntheticDefaultImports": true,
12
+ "skipLibCheck": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "resolveJsonModule": true,
15
+ "declaration": true,
16
+ "sourceMap": true
17
+ },
18
+ "include": ["src/**/*"]
19
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "1.0.0",
4
+ "description": "Rebase BaaS — headless PostgreSQL API",
5
+ "private": true,
6
+ "type": "module",
7
+ "workspaces": [
8
+ "backend"
9
+ ],
10
+ "scripts": {
11
+ "dev": "rebase dev",
12
+ "build": "rebase build",
13
+ "start": "rebase start",
14
+ "db:migrate": "rebase db migrate",
15
+ "schema:introspect": "rebase schema introspect",
16
+ "generate:sdk": "rebase generate-sdk",
17
+ "skills:install": "rebase skills install",
18
+ "example": "tsx scripts/example.ts",
19
+ "deploy": "rebase build && rebase start"
20
+ },
21
+ "devDependencies": {
22
+ "@rebasepro/cli": "workspace:*",
23
+ "@rebasepro/types": "workspace:*",
24
+ "concurrently": "^8.2.2",
25
+ "tsx": "^4.20.6",
26
+ "typescript": "^5.9.2"
27
+ },
28
+ "dependencies": {
29
+ "@rebasepro/client": "workspace:*",
30
+ "dotenv": "^16.0.0"
31
+ },
32
+ "engines": {
33
+ "node": ">=18.0.0"
34
+ },
35
+ "pnpm": {
36
+ "onlyBuiltDependencies": [
37
+ "esbuild",
38
+ "sharp",
39
+ "@ariga/atlas"
40
+ ]
41
+ }
42
+ }
@@ -0,0 +1,9 @@
1
+ packages:
2
+ - "backend"
3
+ linkWorkspacePackages: true
4
+ blockExoticSubdeps: false
5
+ minimumReleaseAge: 0
6
+ allowBuilds:
7
+ esbuild: true
8
+ sharp: true
9
+ "@ariga/atlas": true
@@ -7,11 +7,21 @@
7
7
  # Database connection string (required)
8
8
  # You can use PostgreSQL (postgresql://) or MongoDB (mongodb:// or mongodb+srv://).
9
9
  # The backend will automatically detect the database type.
10
- DATABASE_URL=postgresql://rebase:changeme@localhost:5432/rebase?options=-c%20search_path=public
10
+ # sslmode=disable matches the local docker-compose database, which has no TLS;
11
+ # schema tooling (atlas) would otherwise default to requiring SSL. Remove it
12
+ # when pointing at a managed/cloud database.
13
+ DATABASE_URL=postgresql://rebase:changeme@localhost:5432/rebase?options=-c%20search_path=public&sslmode=disable
11
14
  # DATABASE_URL=mongodb://localhost:27017/rebase
12
15
  #
13
- # Local Postgres without SSL? If you see "SSL is not enabled on the server",
14
- # append &sslmode=disable to the connection string, e.g.:
16
+ # Seeing "SSL is not enabled on the server"? Something in your environment
17
+ # asks for SSL almost always a global PGSSLMODE=require
18
+ # (set once for a cloud database, then inherited by every local project), or an
19
+ # sslmode=require copied in with a connection string.
20
+ #
21
+ # Prefer unsetting PGSSLMODE for this project. Rebase deliberately does not
22
+ # downgrade SSL for you when you have asked for it, not even on localhost:
23
+ # quietly ignoring a security setting is worse than a clear failure. To opt out
24
+ # explicitly for a local database, say so:
15
25
  # DATABASE_URL=postgresql://rebase:changeme@localhost:5432/rebase?options=-c%20search_path=public&sslmode=disable
16
26
 
17
27
  # Separate admin connection string for migrations and schema operations (optional)
@@ -97,6 +107,18 @@ VITE_API_URL=http://localhost:3001
97
107
  # For native GCS SDK or other providers (Azure Blob, etc.), implement the
98
108
  # StorageController interface and pass it directly in your backend config.
99
109
 
110
+ # ── Backups (optional) ────────────────────────────────────────────────────────
111
+ # Manual backups: `rebase db backup --out ./backups` (or an s3://… URL).
112
+ # Scheduled backups: set BACKUP_SCHEDULE and add a cron file in backend/crons
113
+ # that default-exports createBackupCron (from @rebasepro/server-postgres).
114
+ # Backups contain secrets & PII — use a PRIVATE destination with encryption-at-rest.
115
+ # BACKUP_SCHEDULE=0 3 * * * # cron expression; unset ⇒ scheduled backups off
116
+ # BACKUP_DESTINATION=./backups # local path, or s3://bucket/prefix / gs://bucket/prefix
117
+ # BACKUP_RETENTION_DAYS=14 # delete backups older than N days (unset/0 ⇒ keep all)
118
+ # BACKUP_KEEP_MINIMUM=3 # always retain at least N most-recent backups
119
+ # PG_DUMP_PATH= # override pg_dump binary (must match server major version)
120
+ # PG_RESTORE_PATH= # override pg_restore binary
121
+
100
122
  # ── Admin Service Key (optional) ──────────────────────────────────────────────
101
123
  # When set, scripts authenticate with: Authorization: Bearer <key>
102
124
  # Generate with: node -e "console.log(require('crypto').randomBytes(48).toString('base64'))"
@@ -1,4 +1,4 @@
1
- import { defineFunction } from "@rebasepro/server-core";
1
+ import { defineFunction } from "@rebasepro/server";
2
2
 
3
3
  /**
4
4
  * Example custom function route.
@@ -5,24 +5,23 @@
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
7
7
  "scripts": {
8
- "dev": "tsx watch --watch=\"../config/**/*\" src/index.ts",
8
+ "dev": "tsx watch --include=\"../config/**/*\" --include=\"./functions/**/*\" src/index.ts",
9
9
  "build": "rebase schema generate --collections ../config/collections && tsc",
10
10
  "start": "node dist/backend/src/index.js"
11
11
  },
12
12
  "dependencies": {
13
13
  "{{PROJECT_NAME}}-config": "*",
14
- "@rebasepro/server-core": "workspace:*",
15
- "@rebasepro/server-postgresql": "workspace:*",
16
- "@rebasepro/admin": "workspace:*",
14
+ "@rebasepro/server": "workspace:*",
15
+ "@rebasepro/server-postgres": "workspace:*",
17
16
  "@rebasepro/types": "workspace:*",
18
17
  "drizzle-orm": "^0.45.2",
19
- "hono": "^4.12.10",
18
+ "hono": "^4.12.25",
20
19
  "@hono/node-server": "^1.19.12",
21
20
  "pg": "^8.11.3",
22
21
  "ws": "^8.16.0",
23
- "react-compiler-runtime": "^1.0.0",
24
22
  "dotenv": "^16.0.0",
25
- "zod": "^4.4.3"
23
+ "zod": "^4.4.3",
24
+ "ts-morph": "28.0.0"
26
25
  },
27
26
  "devDependencies": {
28
27
  "@types/pg": "^8.6.5",
@@ -1,13 +1,42 @@
1
1
  import * as dotenv from "dotenv";
2
+ import fs from "fs";
2
3
  import path from "path";
3
4
  import { fileURLToPath } from "url";
4
- import { loadEnv } from "@rebasepro/server-core";
5
+ import { loadEnv } from "@rebasepro/server";
5
6
  import { z } from "zod";
6
7
 
7
8
  const __filename = fileURLToPath(import.meta.url);
8
9
  const __dirname = path.dirname(__filename);
9
10
 
10
- dotenv.config({ path: path.resolve(__dirname, "../../.env") });
11
+ /**
12
+ * Locate the project's `.env` by walking up the directory tree.
13
+ *
14
+ * A fixed relative path can't work in both modes: in dev this file runs from
15
+ * source at `backend/src/env.ts`, but the compiled output lives at
16
+ * `backend/dist/backend/src/env.js` — two directories deeper — so
17
+ * `../../.env` points at a file that doesn't exist in production. Walking up
18
+ * finds the root `.env` from either location.
19
+ */
20
+ function findEnvFile(startDir: string): string | undefined {
21
+ let dir = startDir;
22
+ // eslint-disable-next-line no-constant-condition
23
+ while (true) {
24
+ const candidate = path.join(dir, ".env");
25
+ if (fs.existsSync(candidate)) return candidate;
26
+ const parent = path.dirname(dir);
27
+ if (parent === dir) return undefined;
28
+ dir = parent;
29
+ }
30
+ }
31
+
32
+ // `rebase start` sets DOTENV_CONFIG_PATH to the project's .env; honor it first,
33
+ // then fall back to searching up from this file. When neither is found (e.g. a
34
+ // production host that injects env vars directly), skip dotenv entirely and let
35
+ // the real process environment flow through.
36
+ const envPath = process.env.DOTENV_CONFIG_PATH || findEnvFile(__dirname);
37
+ if (envPath && fs.existsSync(envPath)) {
38
+ dotenv.config({ path: envPath });
39
+ }
11
40
 
12
41
  export const env = loadEnv({
13
42
  extend: z.object({
@@ -13,9 +13,8 @@ import {
13
13
  listenWithPortRetry,
14
14
  cleanupDevPortFile,
15
15
  logger
16
- } from "@rebasepro/server-core";
17
- import type { SecurityRule } from "@rebasepro/types";
18
- import { createPostgresDatabaseConnection, createPostgresAdapter } from "@rebasepro/server-postgresql";
16
+ } from "@rebasepro/server";
17
+ import { createPostgresDatabaseConnection, createPostgresAdapter } from "@rebasepro/server-postgres";
19
18
  import { enums, relations, tables } from "./schema.generated.js";
20
19
  import { env } from "./env.js";
21
20
  import usersCollection from "../../config/collections/users.js";
@@ -40,10 +39,26 @@ const allowedOrigins = isProduction
40
39
  })()
41
40
  : [];
42
41
 
42
+ // In dev we still restrict which origins are reflected. Because `credentials`
43
+ // is enabled, reflecting an arbitrary Origin would let any website the
44
+ // developer happens to visit make credentialed cross-origin requests to this
45
+ // dev server (and read the responses) using the developer's session. So dev
46
+ // reflects only localhost origins; requests with no Origin (curl, same-origin)
47
+ // are unaffected.
48
+ const isLocalhostOrigin = (origin: string): boolean => {
49
+ try {
50
+ const { hostname } = new URL(origin);
51
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
52
+ } catch {
53
+ return false;
54
+ }
55
+ };
56
+
43
57
  app.use("/*", cors({
44
58
  origin: (origin) => {
45
- if (!isProduction) return origin || "*";
46
- return allowedOrigins.includes(origin) ? origin : null;
59
+ if (isProduction) return allowedOrigins.includes(origin) ? origin : null;
60
+ if (!origin) return "*";
61
+ return isLocalhostOrigin(origin) ? origin : null;
47
62
  },
48
63
  credentials: true
49
64
  }));
@@ -61,15 +76,6 @@ async function startServer() {
61
76
  const PORT = env.PORT;
62
77
  const server = createServer(getRequestListener(app.fetch));
63
78
 
64
- // Default security rules for collections that don't define their own.
65
- // Authenticated users can read all rows; only admins can write.
66
- const defaultSecurityRules: SecurityRule[] = [
67
- { operation: "select",
68
- access: "public" },
69
- { operations: ["insert", "update", "delete"],
70
- roles: ["admin"] }
71
- ];
72
-
73
79
  const backend = await initializeRebaseBackend({
74
80
  collectionsDir: path.resolve(__dirname, "../../config/collections"),
75
81
  functionsDir: path.resolve(__dirname, "../functions"),
@@ -131,8 +137,7 @@ pass: env.SMTP_PASS! }
131
137
  type: "local",
132
138
  basePath: env.STORAGE_PATH || path.resolve(__dirname, "../../uploads")
133
139
  },
134
- history: true,
135
- defaultSecurityRules
140
+ history: true
136
141
  });
137
142
 
138
143
  // ─── Health check ─────────────────────────────────────────────
@@ -2,5 +2,25 @@ import postsCollection from "./posts.js";
2
2
  import authorsCollection from "./authors.js";
3
3
  import tagsCollection from "./tags.js";
4
4
  import usersCollection from "./users.js";
5
+ import type { SecurityRule } from "@rebasepro/types";
5
6
 
6
7
  export const collections = [postsCollection, authorsCollection, tagsCollection, usersCollection];
8
+
9
+ /**
10
+ * Applied to any collection in this directory that declares no
11
+ * `securityRules` of its own: anyone can read, only admins can write.
12
+ *
13
+ * These live here, next to the collections, because `rebase db push`
14
+ * generates the Postgres policies from these files — that is what actually
15
+ * enforces access. A default declared on the server could never reach the
16
+ * database.
17
+ *
18
+ * A collection that declares neither its own rules nor inherits these is
19
+ * locked by default: admin-only.
20
+ */
21
+ export const defaultSecurityRules: SecurityRule[] = [
22
+ { operation: "select",
23
+ access: "public" },
24
+ { operations: ["insert", "update", "delete"],
25
+ roles: ["admin"] }
26
+ ];
@@ -1,3 +1,5 @@
1
- import usersCollection from "../../users.js";
1
+ // Resolves after `rebase init` copies this file into config/collections/,
2
+ // next to the shared users.ts — not from inside presets/, which never runs.
3
+ import usersCollection from "./users.js";
2
4
 
3
5
  export const collections = [usersCollection];