@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.
- package/README.md +32 -3
- package/bin/rebase.js +67 -1
- package/dist/commands/cloud/auth.d.ts +3 -0
- package/dist/commands/cloud/context.d.ts +151 -0
- package/dist/commands/cloud/databases.d.ts +1 -0
- package/dist/commands/cloud/debug.d.ts +117 -0
- package/dist/commands/cloud/deploy.d.ts +2 -0
- package/dist/commands/cloud/deployments.d.ts +38 -0
- package/dist/commands/cloud/domains.d.ts +1 -0
- package/dist/commands/cloud/env.d.ts +6 -0
- package/dist/commands/cloud/extensions.d.ts +3 -0
- package/dist/commands/cloud/index.d.ts +1 -0
- package/dist/commands/cloud/link.d.ts +5 -0
- package/dist/commands/cloud/orgs.d.ts +1 -0
- package/dist/commands/cloud/power.d.ts +3 -0
- package/dist/commands/cloud/projects.d.ts +13 -0
- package/dist/commands/cloud/resources.d.ts +6 -0
- package/dist/commands/cloud/settings.d.ts +8 -0
- package/dist/commands/init.d.ts +23 -0
- package/dist/commands/skills.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +6083 -1819
- package/dist/index.es.js.map +1 -1
- package/dist/utils/package-manager.d.ts +19 -5
- package/dist/utils/project.d.ts +1 -1
- package/package.json +8 -7
- package/templates/overlays/baas/README.md +55 -0
- package/templates/overlays/baas/backend/package.json +31 -0
- package/templates/overlays/baas/backend/src/index.ts +192 -0
- package/templates/overlays/baas/backend/tsconfig.json +19 -0
- package/templates/overlays/baas/package.json +42 -0
- package/templates/overlays/baas/pnpm-workspace.yaml +9 -0
- package/templates/template/.env.example +25 -3
- package/templates/template/backend/functions/hello.ts +1 -1
- package/templates/template/backend/package.json +6 -7
- package/templates/template/backend/src/env.ts +31 -2
- package/templates/template/backend/src/index.ts +21 -16
- package/templates/template/config/collections/index.ts +20 -0
- package/templates/template/config/collections/presets/blank/index.ts +3 -1
- package/templates/template/config/collections/presets/ecommerce/index.ts +3 -1
- package/templates/template/config/collections/users.ts +2 -0
- package/templates/template/config/package.json +1 -2
- package/templates/template/frontend/package.json +2 -3
- package/templates/template/frontend/src/App.tsx +3 -4
- package/templates/template/frontend/vite.config.ts +1 -1
- package/templates/template/gitignore +31 -0
- package/templates/template/npmrc +10 -0
|
@@ -20,13 +20,27 @@ export interface PMCommands {
|
|
|
20
20
|
workspaceProtocol: string;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
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.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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. */
|
package/dist/utils/project.d.ts
CHANGED
|
@@ -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-
|
|
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.
|
|
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.
|
|
35
|
-
"@rebasepro/
|
|
36
|
-
"@rebasepro/server-
|
|
37
|
-
"@rebasepro/types": "0.9.
|
|
38
|
-
"@rebasepro/server
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
#
|
|
14
|
-
#
|
|
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'))"
|
|
@@ -5,24 +5,23 @@
|
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"dev": "tsx watch --
|
|
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
|
|
15
|
-
"@rebasepro/server-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
17
|
-
import
|
|
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 (
|
|
46
|
-
|
|
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
|
-
|
|
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];
|