@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
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { defineFunction } from "@rebasepro/server";
|
|
1
|
+
import { defineFunction, requireAuth, requireAdmin } from "@rebasepro/server";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Example custom function route.
|
|
5
5
|
*
|
|
6
6
|
* This file is auto-discovered by Rebase and mounted at:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* GET /api/functions/hello (public)
|
|
8
|
+
* POST /api/functions/hello (signed in)
|
|
9
|
+
* GET /api/functions/hello/stats (admins only)
|
|
9
10
|
*
|
|
10
11
|
* Call from the client SDK:
|
|
11
12
|
* const result = await client.call("functions/hello", { name: "World" });
|
|
@@ -15,6 +16,20 @@ import { defineFunction } from "@rebasepro/server";
|
|
|
15
16
|
* singleton via the injected context — use any Hono middleware, define any
|
|
16
17
|
* HTTP methods, access the request/response directly.
|
|
17
18
|
*
|
|
19
|
+
* **Custom functions are not authenticated for you.** The functions router
|
|
20
|
+
* parses the caller's token and puts the result in the context, but it does
|
|
21
|
+
* not reject anonymous requests — a webhook receiver (Stripe, GitHub) has no
|
|
22
|
+
* token to send, and that has to keep working. So every route in this folder
|
|
23
|
+
* is public until you say otherwise, and reading `c.get("user")` is not a
|
|
24
|
+
* check: an anonymous caller just gets `undefined` and the handler runs anyway.
|
|
25
|
+
*
|
|
26
|
+
* Say otherwise with `requireAuth` / `requireAdmin`, in the route's own
|
|
27
|
+
* middleware slot as below. `requireAuth` answers 401 without a valid token.
|
|
28
|
+
* `requireAdmin` answers 403 without the `admin` role and must come *after*
|
|
29
|
+
* `requireAuth` — on its own it has no user to inspect. Prefer the per-route
|
|
30
|
+
* slot over `app.use("/*", requireAuth)`: `use()` only covers routes declared
|
|
31
|
+
* *below* it, so a route appended later above it is silently unprotected.
|
|
32
|
+
*
|
|
18
33
|
* `rebase.dataAsAdmin` gives you admin-level access to your data and
|
|
19
34
|
* **bypasses RLS** — use it only for trusted admin work. For request-scoped /
|
|
20
35
|
* RLS-scoped data access, use c.get("user") and c.get("driver"), which carry
|
|
@@ -23,19 +38,33 @@ import { defineFunction } from "@rebasepro/server";
|
|
|
23
38
|
export default defineFunction((app, { rebase }) => {
|
|
24
39
|
void rebase; // available for dataAsAdmin/auth/storage/email — see commented usage below
|
|
25
40
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
41
|
+
/** The caller's id, or undefined when nobody is signed in. */
|
|
42
|
+
const uidOf = (user: unknown): string | undefined =>
|
|
43
|
+
(typeof user === "object" && user !== null && "uid" in user)
|
|
44
|
+
? (user as { uid?: string }).uid
|
|
45
|
+
: undefined;
|
|
46
|
+
|
|
47
|
+
// ── Public ────────────────────────────────────────────────────────────
|
|
48
|
+
// Deliberately public: no guard, so anyone can call it. That is a fine
|
|
49
|
+
// choice for health probes, webhook receivers and public content — the
|
|
50
|
+
// point is that it is a choice, written down, and not the default you got
|
|
51
|
+
// by forgetting.
|
|
52
|
+
app.get("/", (c) => {
|
|
53
|
+
return c.json({ status: "ok",
|
|
54
|
+
endpoint: "hello" });
|
|
55
|
+
});
|
|
29
56
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
57
|
+
// ── Signed in ─────────────────────────────────────────────────────────
|
|
58
|
+
// `requireAuth` replies 401 before the handler runs, so the user here is
|
|
59
|
+
// guaranteed rather than hoped for.
|
|
60
|
+
app.post("/", requireAuth, async (c) => {
|
|
61
|
+
const body = await c.req.json().catch(() => ({}));
|
|
33
62
|
|
|
34
63
|
// Access any Rebase service via the injected `rebase`:
|
|
35
64
|
// await rebase.email.send({
|
|
36
65
|
// to: "admin@example.com",
|
|
37
66
|
// subject: "Function called",
|
|
38
|
-
// html: `<p>Hello from ${
|
|
67
|
+
// html: `<p>Hello from ${uidOf(c.get("user"))}!</p>`,
|
|
39
68
|
// });
|
|
40
69
|
//
|
|
41
70
|
// Admin-scoped data (bypasses RLS — trusted work only):
|
|
@@ -45,12 +74,14 @@ export default defineFunction((app, { rebase }) => {
|
|
|
45
74
|
|
|
46
75
|
return c.json({
|
|
47
76
|
message: `Hello, ${body.name || "World"}!`,
|
|
48
|
-
user:
|
|
77
|
+
user: uidOf(c.get("user"))
|
|
49
78
|
});
|
|
50
79
|
});
|
|
51
80
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
81
|
+
// ── Admins only ───────────────────────────────────────────────────────
|
|
82
|
+
// Order matters: `requireAuth` first (401 for anonymous), then
|
|
83
|
+
// `requireAdmin` (403 for a signed-in non-admin).
|
|
84
|
+
app.get("/stats", requireAuth, requireAdmin, (c) => {
|
|
85
|
+
return c.json({ admin: uidOf(c.get("user")) });
|
|
55
86
|
});
|
|
56
87
|
});
|
|
@@ -2,12 +2,9 @@
|
|
|
2
2
|
"name": "{{PROJECT_NAME}}-backend",
|
|
3
3
|
"version": "1.0.0",
|
|
4
4
|
"description": "Rebase backend with PostgreSQL",
|
|
5
|
-
"main": "src/index.ts",
|
|
6
5
|
"type": "module",
|
|
7
6
|
"scripts": {
|
|
8
|
-
"
|
|
9
|
-
"build": "rebase schema generate --collections ../config/collections && tsc",
|
|
10
|
-
"start": "node dist/backend/src/index.js"
|
|
7
|
+
"build": "rebase schema generate --collections ../config/collections && tsc"
|
|
11
8
|
},
|
|
12
9
|
"dependencies": {
|
|
13
10
|
"{{PROJECT_NAME}}-config": "*",
|
|
@@ -13,7 +13,28 @@
|
|
|
13
13
|
"resolveJsonModule": true,
|
|
14
14
|
"declaration": true,
|
|
15
15
|
"sourceMap": true,
|
|
16
|
-
"jsx": "preserve"
|
|
16
|
+
"jsx": "preserve",
|
|
17
|
+
// Pin the ambient type libraries.
|
|
18
|
+
//
|
|
19
|
+
// Unset, TypeScript sweeps every `node_modules/@types` it can reach and
|
|
20
|
+
// treats each folder as an implicit type library. Under pnpm that reaches
|
|
21
|
+
// the virtual store, where packages hoisted for peer resolution live —
|
|
22
|
+
// `dompurify` among them, pulled in transitively by the admin editor. The
|
|
23
|
+
// backend has no dependency on it and cannot resolve its entry point, so
|
|
24
|
+
// `rebase build` failed the whole bundle with:
|
|
25
|
+
//
|
|
26
|
+
// error TS2688: Cannot find type definition file for 'dompurify'
|
|
27
|
+
//
|
|
28
|
+
// `frontend/tsconfig.json` has always pinned its own list, which is why the
|
|
29
|
+
// frontend never saw this. Naming what this program needs is both the fix
|
|
30
|
+
// and the faster compile.
|
|
31
|
+
"types": ["node"]
|
|
17
32
|
},
|
|
18
|
-
|
|
33
|
+
// `functions/` is included so `tsc` actually checks your custom routes. It
|
|
34
|
+
// was omitted here (the baas flavour has always had it), which meant the one
|
|
35
|
+
// place you write server code by hand was the one place a type error went
|
|
36
|
+
// unnoticed until runtime. The inferred common root is already the project
|
|
37
|
+
// root because of `../config`, so this adds no `rootDir` and moves nothing
|
|
38
|
+
// that dist/ already contains.
|
|
39
|
+
"include": ["src/**/*", "functions/**/*", "../config/**/*", "drizzle.config.ts"]
|
|
19
40
|
}
|
|
@@ -13,7 +13,22 @@
|
|
|
13
13
|
"outDir": "./dist",
|
|
14
14
|
"skipLibCheck": true,
|
|
15
15
|
"forceConsistentCasingInFileNames": true,
|
|
16
|
-
"resolveJsonModule": true
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
// Pin the ambient type libraries.
|
|
18
|
+
//
|
|
19
|
+
// Unset, TypeScript sweeps every `node_modules/@types` it can reach and
|
|
20
|
+
// treats each folder as an implicit type library. Under pnpm that reaches
|
|
21
|
+
// the virtual store, where packages hoisted for peer resolution live —
|
|
22
|
+
// `dompurify` among them, pulled in transitively by the admin editor. The
|
|
23
|
+
// backend has no dependency on it and cannot resolve its entry point, so
|
|
24
|
+
// `rebase build` failed the whole bundle with:
|
|
25
|
+
//
|
|
26
|
+
// error TS2688: Cannot find type definition file for 'dompurify'
|
|
27
|
+
//
|
|
28
|
+
// `frontend/tsconfig.json` has always pinned its own list, which is why the
|
|
29
|
+
// frontend never saw this. Naming what this program needs is both the fix
|
|
30
|
+
// and the faster compile.
|
|
31
|
+
"types": ["node"]
|
|
17
32
|
},
|
|
18
33
|
"include": ["**/*.ts"],
|
|
19
34
|
"exclude": ["node_modules", "dist"]
|
|
@@ -1,17 +1,32 @@
|
|
|
1
|
-
# ───
|
|
1
|
+
# ─── Self-hosting this project ───────────────────────────────────────
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
3
|
+
# Two containers: PostgreSQL, and the Rebase runtime with your built project
|
|
4
|
+
# mounted into it. There is no application image to build here — the runtime is
|
|
5
|
+
# the same published image every Rebase deployment runs, and your project
|
|
6
|
+
# travels as a bundle.
|
|
6
7
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# That is the point: the artifact you self-host is the artifact Rebase Cloud
|
|
9
|
+
# runs. Nothing about this repository changes when you move between them; only
|
|
10
|
+
# the destination does, and that lives in `.rebase/cloud.json`, which is not
|
|
11
|
+
# committed.
|
|
12
|
+
#
|
|
13
|
+
# rebase build # produces ./dist-bundle
|
|
14
|
+
# docker compose up -d db
|
|
15
|
+
# rebase db push # create the collection tables, once
|
|
16
|
+
# docker compose up
|
|
17
|
+
#
|
|
18
|
+
# One container then serves the API at /api and the admin at / — same origin, so
|
|
19
|
+
# there is no CORS to configure between them and no second web server.
|
|
11
20
|
#
|
|
12
21
|
# For development, use `rebase dev` instead.
|
|
22
|
+
#
|
|
23
|
+
# To upgrade Rebase, change REBASE_VERSION and restart. Your bundle is untouched.
|
|
24
|
+
# To run your OWN server code instead, `rebase eject` — it writes the entrypoint,
|
|
25
|
+
# a Dockerfile and a compose file that builds them.
|
|
13
26
|
# ─────────────────────────────────────────────────────────────────────
|
|
14
27
|
|
|
28
|
+
name: {{PROJECT_NAME}}
|
|
29
|
+
|
|
15
30
|
services:
|
|
16
31
|
# ── PostgreSQL ───────────────────────────────────────────────────────
|
|
17
32
|
db:
|
|
@@ -21,20 +36,25 @@ services:
|
|
|
21
36
|
POSTGRES_USER: rebase
|
|
22
37
|
POSTGRES_PASSWORD: ${DATABASE_PASSWORD:-changeme}
|
|
23
38
|
POSTGRES_DB: rebase
|
|
39
|
+
# Published so `rebase db push` can reach it from the host. Remove this
|
|
40
|
+
# mapping once the schema is in place if the database should not be
|
|
41
|
+
# reachable from outside the compose network.
|
|
24
42
|
ports:
|
|
25
43
|
- "5432:5432"
|
|
26
44
|
volumes:
|
|
27
45
|
- postgres_data:/var/lib/postgresql
|
|
28
46
|
healthcheck:
|
|
47
|
+
# The runtime must not start before the database can answer, or its first
|
|
48
|
+
# boot fails on a connection refused and the container restarts for no
|
|
49
|
+
# reason a reader would understand.
|
|
29
50
|
test: ["CMD-SHELL", "pg_isready -U rebase -d rebase"]
|
|
30
51
|
interval: 5s
|
|
31
52
|
timeout: 5s
|
|
32
53
|
retries: 10
|
|
33
54
|
start_period: 10s
|
|
34
|
-
# Production tuning (adjust for your workload)
|
|
35
55
|
command:
|
|
36
56
|
- "postgres"
|
|
37
|
-
- "-c"
|
|
57
|
+
- "-c"
|
|
38
58
|
- "shared_buffers=256MB"
|
|
39
59
|
- "-c"
|
|
40
60
|
- "max_connections=100"
|
|
@@ -45,44 +65,48 @@ services:
|
|
|
45
65
|
- "-c"
|
|
46
66
|
- "log_min_duration_statement=1000"
|
|
47
67
|
|
|
48
|
-
# ──
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
context: .
|
|
52
|
-
dockerfile: backend/Dockerfile
|
|
68
|
+
# ── The Rebase runtime, booting your bundle ──────────────────────────
|
|
69
|
+
api:
|
|
70
|
+
image: rebasepro/server:${REBASE_VERSION:-latest}
|
|
53
71
|
restart: unless-stopped
|
|
72
|
+
depends_on:
|
|
73
|
+
db:
|
|
74
|
+
condition: service_healthy
|
|
54
75
|
ports:
|
|
55
76
|
- "${PORT:-3001}:3001"
|
|
56
|
-
env_file: .env
|
|
57
77
|
environment:
|
|
58
|
-
# Override DATABASE_URL to point to the Docker network service
|
|
59
78
|
DATABASE_URL: postgresql://rebase:${DATABASE_PASSWORD:-changeme}@db:5432/rebase?options=-c%20search_path=public
|
|
60
79
|
ADMIN_CONNECTION_STRING: postgresql://rebase:${DATABASE_PASSWORD:-changeme}@db:5432/rebase?options=-c%20search_path=public
|
|
80
|
+
JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET in .env — `rebase init` generates one}
|
|
81
|
+
REBASE_SERVICE_KEY: ${REBASE_SERVICE_KEY:?set REBASE_SERVICE_KEY in .env — `rebase init` generates one}
|
|
61
82
|
NODE_ENV: production
|
|
62
83
|
PORT: "3001"
|
|
63
|
-
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
|
|
84
|
+
|
|
85
|
+
# The origins a browser will load the admin from. With the admin folded
|
|
86
|
+
# into the bundle it is served from this very container, so this is just
|
|
87
|
+
# your own address — but it is still required, because an API that guesses
|
|
88
|
+
# its allowed origins is one that eventually allows the wrong one.
|
|
89
|
+
CORS_ORIGINS: ${CORS_ORIGINS:?set CORS_ORIGINS to the origin you browse to, e.g. http://localhost:3001}
|
|
90
|
+
|
|
91
|
+
# Auth tables are created at boot. Collection tables are not: run
|
|
92
|
+
# `rebase db push` once, against the database above. A container restart
|
|
93
|
+
# must not be able to change a schema as a side effect.
|
|
94
|
+
REBASE_MIGRATE_ON_BOOT: ${REBASE_MIGRATE_ON_BOOT:-ensure}
|
|
95
|
+
|
|
96
|
+
# Uploads land on the named volume below, which survives restarts. That is
|
|
97
|
+
# the case FORCE_LOCAL_STORAGE exists to acknowledge — without a durable
|
|
98
|
+
# mount the server refuses local storage in production, because the
|
|
99
|
+
# container filesystem is destroyed on the next deploy and every uploaded
|
|
100
|
+
# file goes with it. Switch to STORAGE_TYPE=s3 or gcs and drop both lines
|
|
101
|
+
# if you move storage off-box.
|
|
102
|
+
STORAGE_PATH: /uploads
|
|
69
103
|
FORCE_LOCAL_STORAGE: "true"
|
|
70
|
-
depends_on:
|
|
71
|
-
db:
|
|
72
|
-
condition: service_healthy
|
|
73
104
|
volumes:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
context: .
|
|
80
|
-
dockerfile: frontend/Dockerfile
|
|
81
|
-
restart: unless-stopped
|
|
82
|
-
ports:
|
|
83
|
-
- "80:80"
|
|
84
|
-
depends_on:
|
|
85
|
-
- backend
|
|
105
|
+
# Your built project. Writable, because the runtime installs the bundle's
|
|
106
|
+
# declared dependencies into it on first start — `rebase build` emits a
|
|
107
|
+
# package.json but not a node_modules.
|
|
108
|
+
- ./dist-bundle:/bundle
|
|
109
|
+
- uploads:/uploads
|
|
86
110
|
|
|
87
111
|
volumes:
|
|
88
112
|
postgres_data:
|
|
@@ -8,12 +8,19 @@ window.addEventListener("unhandledrejection", (event: PromiseRejectionEvent) =>
|
|
|
8
8
|
console.error("[Rebase] Unhandled promise rejection:", event.reason);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
+
// Where this app is mounted, from the `path` declared in rebase.json.
|
|
12
|
+
//
|
|
13
|
+
// `rebase build` passes it to Vite as `base` (REBASE_APP_BASE), and Vite exposes
|
|
14
|
+
// it back as BASE_URL — so the assets, the router and the server all agree on
|
|
15
|
+
// one value without it being written down three times. At "/" this is "".
|
|
16
|
+
const basename = import.meta.env.BASE_URL.replace(/\/$/, "");
|
|
17
|
+
|
|
11
18
|
const router = createBrowserRouter([
|
|
12
19
|
{
|
|
13
20
|
path: "/*",
|
|
14
21
|
element: <App/>
|
|
15
22
|
}
|
|
16
|
-
]);
|
|
23
|
+
], { basename });
|
|
17
24
|
|
|
18
25
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|
19
26
|
<React.StrictMode>
|
|
@@ -7,6 +7,11 @@ import { rebaseCollectionsPlugin } from "@rebasepro/app/vitePlugin";
|
|
|
7
7
|
|
|
8
8
|
export default defineConfig({
|
|
9
9
|
envDir: path.resolve(__dirname, ".."),
|
|
10
|
+
// The public path this app is served under, from its `path` in rebase.json.
|
|
11
|
+
// `rebase build` sets REBASE_APP_BASE; without this line an app declared at
|
|
12
|
+
// "/admin" would emit assets rooted at "/" and render a blank page. The
|
|
13
|
+
// build refuses to ship that — see docs/apps-and-runtimes.md §4.2.
|
|
14
|
+
base: process.env.REBASE_APP_BASE ?? "/",
|
|
10
15
|
// Force a single copy of React and React Router across the app and all
|
|
11
16
|
// @rebasepro/* packages. Without this, a locally `link:`ed Rebase checkout
|
|
12
17
|
// resolves its own copies of react-router, producing "multiple copies of
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://rebase.pro/schemas/rebase.json",
|
|
3
|
-
"
|
|
3
|
+
"rebase": "^1",
|
|
4
4
|
"apps": {
|
|
5
5
|
"backend": {
|
|
6
|
-
"type": "backend"
|
|
6
|
+
"type": "backend",
|
|
7
|
+
"runtime": "managed"
|
|
7
8
|
},
|
|
8
|
-
"
|
|
9
|
+
"admin": {
|
|
9
10
|
"type": "static",
|
|
10
11
|
"root": "frontend",
|
|
11
12
|
"build": "npm run build --workspace frontend",
|
|
12
13
|
"output": "frontend/dist",
|
|
13
|
-
"
|
|
14
|
-
},
|
|
15
|
-
"admin": {
|
|
16
|
-
"type": "admin",
|
|
17
|
-
"mode": "hosted"
|
|
14
|
+
"path": "/"
|
|
18
15
|
}
|
|
19
16
|
}
|
|
20
17
|
}
|
|
@@ -1,216 +0,0 @@
|
|
|
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
|
-
import { storageAuthorize } from "./storage.js";
|
|
19
|
-
|
|
20
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
-
const __dirname = path.dirname(__filename);
|
|
22
|
-
|
|
23
|
-
// ─── App ─────────────────────────────────────────────────────────────
|
|
24
|
-
const app: Hono<HonoEnv> = new Hono<HonoEnv>();
|
|
25
|
-
|
|
26
|
-
const isProduction = env.NODE_ENV === "production";
|
|
27
|
-
const allowedOrigins = isProduction
|
|
28
|
-
? (() => {
|
|
29
|
-
const origins = env.CORS_ORIGINS || env.FRONTEND_URL;
|
|
30
|
-
if (!origins) {
|
|
31
|
-
throw new Error(
|
|
32
|
-
"CORS_ORIGINS or FRONTEND_URL must be set in production. " +
|
|
33
|
-
"Example: CORS_ORIGINS=https://yourdomain.com"
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
return origins.split(",").map(s => s.trim());
|
|
37
|
-
})()
|
|
38
|
-
: [];
|
|
39
|
-
|
|
40
|
-
// In dev we still restrict which origins are reflected. Because `credentials`
|
|
41
|
-
// is enabled, reflecting an arbitrary Origin would let any website the
|
|
42
|
-
// developer happens to visit make credentialed cross-origin requests to this
|
|
43
|
-
// dev server (and read the responses) using the developer's session. So dev
|
|
44
|
-
// reflects only localhost origins; requests with no Origin (curl, same-origin)
|
|
45
|
-
// are unaffected.
|
|
46
|
-
const isLocalhostOrigin = (origin: string): boolean => {
|
|
47
|
-
try {
|
|
48
|
-
const { hostname } = new URL(origin);
|
|
49
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
50
|
-
} catch {
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
app.use("/*", cors({
|
|
56
|
-
origin: (origin) => {
|
|
57
|
-
if (isProduction) return allowedOrigins.includes(origin) ? origin : null;
|
|
58
|
-
if (!origin) return "*";
|
|
59
|
-
return isLocalhostOrigin(origin) ? origin : null;
|
|
60
|
-
},
|
|
61
|
-
credentials: true
|
|
62
|
-
}));
|
|
63
|
-
|
|
64
|
-
app.use("/*", secureHeaders());
|
|
65
|
-
|
|
66
|
-
// ─── Database ────────────────────────────────────────────────────────
|
|
67
|
-
const databaseUrl = env.DATABASE_URL;
|
|
68
|
-
|
|
69
|
-
const { db, pool, connectionString } = createPostgresDatabaseConnection(databaseUrl);
|
|
70
|
-
|
|
71
|
-
// ─── Start ───────────────────────────────────────────────────────────
|
|
72
|
-
async function startServer() {
|
|
73
|
-
const jwtSecret = env.JWT_SECRET;
|
|
74
|
-
const PORT = env.PORT;
|
|
75
|
-
const server = createServer(getRequestListener(app.fetch));
|
|
76
|
-
|
|
77
|
-
const backend = await initializeRebaseBackend({
|
|
78
|
-
// BaaS mode: every RLS-protected table is served over REST. There are
|
|
79
|
-
// no collection files to write or keep in sync — change the schema with
|
|
80
|
-
// a migration and the API follows.
|
|
81
|
-
//
|
|
82
|
-
// Your database's own row-level security is the whole authorization
|
|
83
|
-
// model here: requests run as the `rebase_user` role, so a table
|
|
84
|
-
// without RLS has no rules at all and is not served. Protect one with:
|
|
85
|
-
// ALTER TABLE mytable ENABLE ROW LEVEL SECURITY;
|
|
86
|
-
// CREATE POLICY mytable_read ON mytable FOR SELECT TO public USING (true);
|
|
87
|
-
mode: "baas",
|
|
88
|
-
functionsDir: path.resolve(__dirname, "../functions"),
|
|
89
|
-
server,
|
|
90
|
-
app,
|
|
91
|
-
database: createPostgresAdapter({
|
|
92
|
-
connection: db,
|
|
93
|
-
adminConnectionString: env.ADMIN_CONNECTION_STRING || databaseUrl,
|
|
94
|
-
connectionString
|
|
95
|
-
}),
|
|
96
|
-
auth: {
|
|
97
|
-
// No `collection` here: BaaS mode has no collection files, and the
|
|
98
|
-
// auth adapter owns its own user tables.
|
|
99
|
-
jwtSecret,
|
|
100
|
-
accessExpiresIn: env.JWT_ACCESS_EXPIRES_IN,
|
|
101
|
-
refreshExpiresIn: env.JWT_REFRESH_EXPIRES_IN,
|
|
102
|
-
serviceKey: env.REBASE_SERVICE_KEY,
|
|
103
|
-
cookieAuth: { sameSite: "Lax" },
|
|
104
|
-
google: env.GOOGLE_CLIENT_ID
|
|
105
|
-
? { clientId: env.GOOGLE_CLIENT_ID }
|
|
106
|
-
: undefined,
|
|
107
|
-
allowRegistration: env.ALLOW_REGISTRATION,
|
|
108
|
-
email: env.SMTP_HOST
|
|
109
|
-
? {
|
|
110
|
-
from: env.SMTP_FROM || `${env.APP_NAME} <noreply@rebase.pro>`,
|
|
111
|
-
smtp: {
|
|
112
|
-
host: env.SMTP_HOST,
|
|
113
|
-
port: env.SMTP_PORT,
|
|
114
|
-
secure: env.SMTP_SECURE,
|
|
115
|
-
auth: env.SMTP_USER
|
|
116
|
-
? { user: env.SMTP_USER,
|
|
117
|
-
pass: env.SMTP_PASS! }
|
|
118
|
-
: undefined,
|
|
119
|
-
name: env.SMTP_NAME
|
|
120
|
-
},
|
|
121
|
-
appName: env.APP_NAME,
|
|
122
|
-
resetPasswordUrl: env.FRONTEND_URL
|
|
123
|
-
}
|
|
124
|
-
: undefined
|
|
125
|
-
},
|
|
126
|
-
// File storage is opt-in. With no bucket configured, storage is OFF in
|
|
127
|
-
// production — the upload routes answer 501 STORAGE_NOT_CONFIGURED —
|
|
128
|
-
// rather than writing to the container filesystem, which is erased on
|
|
129
|
-
// every restart and redeploy. Uploads that fail loudly are recoverable;
|
|
130
|
-
// uploads that succeed into a disk about to be wiped are not.
|
|
131
|
-
// Local disk stays the default in development, where it is what you want.
|
|
132
|
-
storage: env.STORAGE_TYPE === "s3"
|
|
133
|
-
? {
|
|
134
|
-
type: "s3",
|
|
135
|
-
bucket: env.S3_BUCKET!,
|
|
136
|
-
region: env.S3_REGION || "auto",
|
|
137
|
-
accessKeyId: env.S3_ACCESS_KEY_ID || "",
|
|
138
|
-
secretAccessKey: env.S3_SECRET_ACCESS_KEY || "",
|
|
139
|
-
endpoint: env.S3_ENDPOINT,
|
|
140
|
-
forcePathStyle: env.S3_FORCE_PATH_STYLE
|
|
141
|
-
}
|
|
142
|
-
: env.STORAGE_TYPE === "gcs"
|
|
143
|
-
? {
|
|
144
|
-
type: "gcs",
|
|
145
|
-
bucket: env.GCS_BUCKET!,
|
|
146
|
-
projectId: env.GCS_PROJECT_ID,
|
|
147
|
-
keyFilename: env.GCS_KEY_FILENAME
|
|
148
|
-
}
|
|
149
|
-
// Set FORCE_LOCAL_STORAGE=true only if this deployment really
|
|
150
|
-
// does have a durable volume mounted at STORAGE_PATH.
|
|
151
|
-
: isProduction && !env.FORCE_LOCAL_STORAGE
|
|
152
|
-
? undefined
|
|
153
|
-
: {
|
|
154
|
-
type: "local",
|
|
155
|
-
basePath: env.STORAGE_PATH || path.resolve(__dirname, "../../uploads")
|
|
156
|
-
},
|
|
157
|
-
// Storage is not under row-level security, so this hook IS its access
|
|
158
|
-
// model — the server refuses to boot in production without one, because
|
|
159
|
-
// "signed in" would otherwise be the only thing between a caller and every
|
|
160
|
-
// file in the bucket. See storage.ts; the default scopes each caller to
|
|
161
|
-
// `users/<uid>/` and is meant to be replaced with your own rule.
|
|
162
|
-
storageAuthorize,
|
|
163
|
-
history: true,
|
|
164
|
-
enableSwagger: true
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// ─── Health check ─────────────────────────────────────────────
|
|
168
|
-
app.get("/health", async (c) => {
|
|
169
|
-
const result = await backend.healthCheck();
|
|
170
|
-
const status = result.healthy ? 200 : 503;
|
|
171
|
-
return c.json({
|
|
172
|
-
status: result.healthy ? "ok" : "degraded",
|
|
173
|
-
latencyMs: result.latencyMs,
|
|
174
|
-
...(result.details ? { details: result.details } : {})
|
|
175
|
-
}, status);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// No serveSPA: this is a headless API. Point any frontend at it over HTTP.
|
|
179
|
-
|
|
180
|
-
if (!isProduction) {
|
|
181
|
-
// Dev mode: retry the next port if the current one is in use
|
|
182
|
-
const projectRoot = path.resolve(__dirname, "../..");
|
|
183
|
-
const actualPort = await listenWithPortRetry(server, PORT, { portFileDir: projectRoot, serviceKey: env.REBASE_SERVICE_KEY });
|
|
184
|
-
|
|
185
|
-
// Clean up port file on exit
|
|
186
|
-
const cleanup = () => cleanupDevPortFile(projectRoot);
|
|
187
|
-
process.on("SIGINT", cleanup);
|
|
188
|
-
process.on("SIGTERM", cleanup);
|
|
189
|
-
process.on("exit", cleanup);
|
|
190
|
-
|
|
191
|
-
logger.info(`API running at http://localhost:${actualPort}`);
|
|
192
|
-
// Docs are only mounted once there is something to document; with no
|
|
193
|
-
// servable tables the URL would 404, so don't advertise it.
|
|
194
|
-
if (backend.collectionRegistry.getCollections().length > 0) {
|
|
195
|
-
logger.info(`API docs at http://localhost:${actualPort}/api/swagger`);
|
|
196
|
-
}
|
|
197
|
-
} else {
|
|
198
|
-
server.listen(PORT, () => {
|
|
199
|
-
logger.info(`API running at http://localhost:${PORT}`);
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// ─── Graceful Shutdown ───────────────────────────────────────────────
|
|
204
|
-
// Drains HTTP, stops crons, tears down realtime, then closes the pool.
|
|
205
|
-
// Guards against double signals and force-exits if shutdown hangs.
|
|
206
|
-
installShutdownHandlers(backend, {
|
|
207
|
-
onCleanup: () => pool.end()
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
startServer().catch(err => {
|
|
212
|
-
logger.error("Failed to start server", { error: err instanceof Error ? err : new Error(String(err)) });
|
|
213
|
-
process.exit(1);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
export { app };
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# ─── Multi-stage production Dockerfile for the Rebase frontend ────────
|
|
2
|
-
# Builds the Vite app, then serves via nginx for proper caching/compression.
|
|
3
|
-
#
|
|
4
|
-
# Build context: the project root (where pnpm-workspace.yaml lives)
|
|
5
|
-
# Usage:
|
|
6
|
-
# docker build -t my-app-frontend -f frontend/Dockerfile .
|
|
7
|
-
|
|
8
|
-
# ── Stage 1: Install + Build ─────────────────────────────────────────
|
|
9
|
-
FROM node:24-alpine AS builder
|
|
10
|
-
|
|
11
|
-
ENV PNPM_HOME="/pnpm"
|
|
12
|
-
ENV PATH="$PNPM_HOME:$PATH"
|
|
13
|
-
RUN corepack enable
|
|
14
|
-
|
|
15
|
-
RUN apk add --no-cache python3 make g++
|
|
16
|
-
|
|
17
|
-
WORKDIR /app
|
|
18
|
-
|
|
19
|
-
# Copy workspace root files
|
|
20
|
-
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
|
|
21
|
-
|
|
22
|
-
# Copy workspace packages
|
|
23
|
-
COPY frontend ./frontend
|
|
24
|
-
COPY config ./config
|
|
25
|
-
|
|
26
|
-
# Install dependencies (skip scripts to avoid @ariga/atlas binary download,
|
|
27
|
-
# which is a backend-only dependency that may fail on certain platforms)
|
|
28
|
-
RUN pnpm install --frozen-lockfile --ignore-scripts
|
|
29
|
-
RUN pnpm rebuild esbuild
|
|
30
|
-
|
|
31
|
-
# Build config first, then frontend
|
|
32
|
-
RUN pnpm --filter "*-config" run build
|
|
33
|
-
RUN pnpm --filter "*-frontend" run build
|
|
34
|
-
|
|
35
|
-
# ── Stage 2: Serve with nginx ────────────────────────────────────────
|
|
36
|
-
FROM nginx:1.27-alpine AS runtime
|
|
37
|
-
|
|
38
|
-
# Remove default nginx page
|
|
39
|
-
RUN rm -rf /usr/share/nginx/html/*
|
|
40
|
-
|
|
41
|
-
# Copy built assets
|
|
42
|
-
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
|
|
43
|
-
|
|
44
|
-
# Custom nginx config for SPA routing + compression
|
|
45
|
-
COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
|
|
46
|
-
|
|
47
|
-
EXPOSE 80
|
|
48
|
-
|
|
49
|
-
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
|
50
|
-
CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1
|
|
51
|
-
|
|
52
|
-
CMD ["nginx", "-g", "daemon off;"]
|