@rebasepro/cli 0.11.1-canary.gfd39654 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/bin/rebase.js +21 -0
  2. package/dist/bundle.d.ts +24 -7
  3. package/dist/commands/eject.d.ts +1 -0
  4. package/dist/commands/init.d.ts +19 -15
  5. package/dist/fold-static.d.ts +41 -15
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.es.js +894 -246
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/manifest.d.ts +27 -8
  10. package/package.json +7 -7
  11. package/runtime/dev-server.mjs +0 -1
  12. package/templates/{template/backend → eject}/Dockerfile +16 -4
  13. package/templates/{template → eject}/backend/src/env.ts +0 -1
  14. package/templates/{template → eject}/backend/src/index.ts +41 -27
  15. package/templates/eject/docker-compose.custom.yml +71 -0
  16. package/templates/overlays/baas/backend/package.json +1 -4
  17. package/templates/overlays/baas/backend/tsconfig.json +8 -2
  18. package/templates/overlays/baas/config/index.ts +15 -0
  19. package/templates/overlays/baas/config/package.json +28 -0
  20. package/templates/overlays/baas/package.json +2 -1
  21. package/templates/overlays/baas/pnpm-workspace.yaml +1 -0
  22. package/templates/overlays/baas/rebase.json +2 -6
  23. package/templates/template/.env.example +15 -0
  24. package/templates/template/README.md +56 -22
  25. package/templates/template/ai-instructions.md +1 -0
  26. package/templates/template/backend/functions/hello.ts +45 -14
  27. package/templates/template/backend/package.json +1 -4
  28. package/templates/template/backend/tsconfig.json +23 -2
  29. package/templates/template/config/tsconfig.json +16 -1
  30. package/templates/template/docker-compose.yml +62 -38
  31. package/templates/template/frontend/src/main.tsx +8 -1
  32. package/templates/template/frontend/vite.config.ts +5 -0
  33. package/templates/template/rebase.json +5 -8
  34. package/templates/overlays/baas/backend/src/index.ts +0 -216
  35. package/templates/template/frontend/Dockerfile +0 -52
  36. package/templates/template/frontend/nginx.conf +0 -40
  37. /package/templates/overlays/baas/{backend/src → config}/storage.ts +0 -0
@@ -1,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
- * POST /api/functions/hello
8
- * GET /api/functions/hello
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
- app.post("/", async (c) => {
27
- const body = await c.req.json().catch(() => ({}));
28
- const user = c.get("user");
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
- const userId = (user && typeof user === "object" && "userId" in user)
31
- ? user.userId
32
- : "anonymous";
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 ${userId}!</p>`,
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: userId
77
+ user: uidOf(c.get("user"))
49
78
  });
50
79
  });
51
80
 
52
- app.get("/", (c) => {
53
- return c.json({ status: "ok",
54
- endpoint: "hello" });
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
- "dev": "tsx watch --include=\"../config/**/*\" --include=\"./functions/**/*\" src/index.ts",
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
- "include": ["src/**/*", "../config/**/*", "drizzle.config.ts"]
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
- # ─── Rebase Production Docker Compose ──────────────────────────────
1
+ # ─── Self-hosting this project ───────────────────────────────────────
2
2
  #
3
- # Quick start:
4
- # cp .env.example .env # Edit with your secrets
5
- # docker compose up -d # Start everything
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
- # This runs:
8
- # 1. PostgreSQL 18 with persistent data
9
- # 2. Backend (Node.js / Hono) with health checks
10
- # 3. Frontend (nginx) serving the Vite build
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
- # ── Backend ──────────────────────────────────────────────────────────
49
- backend:
50
- build:
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
- # The server refuses to boot with local storage under NODE_ENV=production,
64
- # because the container filesystem is destroyed on the next restart and
65
- # uploads go with it. That does not apply here: the `uploads` volume below
66
- # is a durable named volume mounted at the storage path, which is exactly
67
- # the case the check tells you to acknowledge with this flag. Switch to
68
- # STORAGE_TYPE=s3 or gcs and drop this line if you move storage off-box.
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
- - uploads:/app/backend/uploads
75
-
76
- # ── Frontend ─────────────────────────────────────────────────────────
77
- frontend:
78
- build:
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
- "runtime": "^1",
3
+ "rebase": "^1",
4
4
  "apps": {
5
5
  "backend": {
6
- "type": "backend"
6
+ "type": "backend",
7
+ "runtime": "managed"
7
8
  },
8
- "web": {
9
+ "admin": {
9
10
  "type": "static",
10
11
  "root": "frontend",
11
12
  "build": "npm run build --workspace frontend",
12
13
  "output": "frontend/dist",
13
- "spa": true
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;"]