@rebasepro/cli 0.0.1-canary.4d4fb3e → 0.0.1-canary.ca2cb6e

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 (35) hide show
  1. package/dist/commands/cli.test.d.ts +1 -0
  2. package/dist/commands/dev.test.d.ts +1 -0
  3. package/dist/commands/doctor.d.ts +1 -0
  4. package/dist/commands/generate_sdk.d.ts +1 -1
  5. package/dist/commands/init.test.d.ts +1 -0
  6. package/dist/index.cjs +188 -62
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.es.js +188 -62
  9. package/dist/index.es.js.map +1 -1
  10. package/dist/utils/project.test.d.ts +1 -0
  11. package/package.json +67 -66
  12. package/templates/template/.env.template +22 -1
  13. package/templates/template/README.md +2 -2
  14. package/templates/template/backend/Dockerfile +6 -6
  15. package/templates/template/backend/functions/hello.ts +24 -6
  16. package/templates/template/backend/package.json +2 -2
  17. package/templates/template/backend/src/env.ts +52 -0
  18. package/templates/template/backend/src/index.ts +86 -34
  19. package/templates/template/backend/tsconfig.json +1 -1
  20. package/templates/template/config/collections/authors.ts +45 -0
  21. package/templates/template/config/collections/index.ts +5 -0
  22. package/templates/template/{shared → config}/collections/posts.ts +39 -1
  23. package/templates/template/config/collections/tags.ts +27 -0
  24. package/templates/template/{shared → config}/package.json +1 -1
  25. package/templates/template/docker-compose.yml +12 -0
  26. package/templates/template/frontend/Dockerfile +3 -3
  27. package/templates/template/frontend/package.json +2 -2
  28. package/templates/template/frontend/src/App.tsx +11 -12
  29. package/templates/template/frontend/src/main.tsx +2 -2
  30. package/templates/template/frontend/vite.config.ts +1 -1
  31. package/templates/template/pnpm-workspace.yaml +1 -1
  32. package/templates/template/scripts/example.ts +91 -0
  33. package/templates/template/shared/collections/index.ts +0 -3
  34. /package/templates/template/{shared → config}/index.ts +0 -0
  35. /package/templates/template/{shared → config}/tsconfig.json +0 -0
@@ -7,7 +7,7 @@
7
7
  # docker run -p 3001:3001 --env-file .env my-app-backend
8
8
 
9
9
  # ── Stage 1: Install + Build ─────────────────────────────────────────
10
- FROM node:24-alpine AS builder
10
+ FROM node:22-alpine AS builder
11
11
 
12
12
  ENV PNPM_HOME="/pnpm"
13
13
  ENV PATH="$PNPM_HOME:$PATH"
@@ -23,20 +23,20 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
23
23
 
24
24
  # Copy workspace packages
25
25
  COPY backend ./backend
26
- COPY shared ./shared
26
+ COPY config ./config
27
27
 
28
28
  # Install all deps (including devDependencies for build)
29
29
  RUN pnpm install --frozen-lockfile
30
30
 
31
- # Build shared first, then backend
32
- RUN pnpm --filter "*-shared" run build
31
+ # Build config first, then backend
32
+ RUN pnpm --filter "*-config" run build
33
33
  RUN pnpm --filter "*-backend" run build
34
34
 
35
35
  # Prune dev dependencies for a smaller runtime
36
36
  RUN pnpm install --frozen-lockfile --prod
37
37
 
38
38
  # ── Stage 2: Production Runtime ──────────────────────────────────────
39
- FROM node:24-alpine AS runtime
39
+ FROM node:22-alpine AS runtime
40
40
 
41
41
  ENV PNPM_HOME="/pnpm"
42
42
  ENV PATH="$PNPM_HOME:$PATH"
@@ -53,7 +53,7 @@ WORKDIR /app
53
53
  COPY --from=builder /app/package.json /app/pnpm-lock.yaml /app/pnpm-workspace.yaml ./
54
54
  COPY --from=builder /app/node_modules ./node_modules
55
55
  COPY --from=builder /app/backend ./backend
56
- COPY --from=builder /app/shared ./shared
56
+ COPY --from=builder /app/config ./config
57
57
 
58
58
  # Create uploads directory
59
59
  RUN mkdir -p /app/backend/uploads && chown -R rebase:rebase /app
@@ -1,4 +1,6 @@
1
1
  import { Hono } from "hono";
2
+ import type { HonoEnv } from "@rebasepro/server-core";
3
+ import { rebase } from "@rebasepro/server-core";
2
4
 
3
5
  /**
4
6
  * Example custom function route.
@@ -12,23 +14,39 @@ import { Hono } from "hono";
12
14
  *
13
15
  * This is a standard Hono app — use any Hono middleware,
14
16
  * define any HTTP methods, access the request/response directly.
15
- * The authenticated user and RLS-scoped driver are available
16
- * via c.get("user") and c.get("driver").
17
+ *
18
+ * The `rebase` singleton gives you admin-level access to all
19
+ * app-scoped services (data, auth, storage, email) from anywhere.
20
+ * For request-scoped / RLS-scoped access, use c.get("user") and c.get("driver").
17
21
  */
18
- const app = new Hono();
22
+ const app = new Hono<HonoEnv>();
19
23
 
20
24
  app.post("/", async (c) => {
21
25
  const body = await c.req.json().catch(() => ({}));
22
- const user = c.get("user") as any;
26
+ const user = c.get("user");
27
+
28
+ const userId = (user && typeof user === "object" && "userId" in user)
29
+ ? user.userId
30
+ : "anonymous";
31
+
32
+ // Access any Rebase service — just import and use:
33
+ // await rebase.email?.send({
34
+ // to: "admin@example.com",
35
+ // subject: "Function called",
36
+ // html: `<p>Hello from ${userId}!</p>`,
37
+ // });
38
+ //
39
+ // const authors = await rebase.data.authors.find({ limit: 5 });
23
40
 
24
41
  return c.json({
25
42
  message: `Hello, ${body.name || "World"}!`,
26
- user: user?.userId || "anonymous",
43
+ user: userId
27
44
  });
28
45
  });
29
46
 
30
47
  app.get("/", (c) => {
31
- return c.json({ status: "ok", endpoint: "hello" });
48
+ return c.json({ status: "ok",
49
+ endpoint: "hello" });
32
50
  });
33
51
 
34
52
  export default app;
@@ -5,12 +5,12 @@
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
7
7
  "scripts": {
8
- "dev": "tsx watch --watch=\"../shared/**/*\" src/index.ts",
8
+ "dev": "tsx watch --watch=\"../config/**/*\" src/index.ts",
9
9
  "build": "tsc --noEmit",
10
10
  "start": "tsx src/index.ts"
11
11
  },
12
12
  "dependencies": {
13
- "{{PROJECT_NAME}}-shared": "0.0.1",
13
+ "{{PROJECT_NAME}}-config": "workspace:*",
14
14
  "@rebasepro/server-core": "workspace:*",
15
15
  "@rebasepro/server-postgresql": "workspace:*",
16
16
  "drizzle-orm": "^0.44.4",
@@ -0,0 +1,52 @@
1
+ import { z } from "zod";
2
+ import * as dotenv from "dotenv";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ dotenv.config({ path: path.resolve(__dirname, "../../.env") });
10
+
11
+ const envSchema = z.object({
12
+ NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
13
+ PORT: z.string().default("3001").transform(Number),
14
+ DATABASE_URL: z.string().url("DATABASE_URL must be a valid URL"),
15
+ ADMIN_CONNECTION_STRING: z.string().url().optional(),
16
+ JWT_SECRET: z.string().min(32, "JWT_SECRET must be at least 32 characters long"),
17
+ JWT_ACCESS_EXPIRES_IN: z.string().default("1h"),
18
+ JWT_REFRESH_EXPIRES_IN: z.string().default("30d"),
19
+ GOOGLE_CLIENT_ID: z.string().optional(),
20
+ ALLOW_REGISTRATION: z.enum(["true", "false", ""]).default("false").transform(v => v === "true"),
21
+ CORS_ORIGINS: z.string().optional(),
22
+ FRONTEND_URL: z.string().optional(),
23
+ DB_POOL_MAX: z.string().default("20").transform(Number),
24
+ DB_POOL_IDLE_TIMEOUT: z.string().default("30000").transform(Number),
25
+ DB_POOL_CONNECT_TIMEOUT: z.string().default("10000").transform(Number),
26
+ FORCE_LOCAL_STORAGE: z.enum(["true", "false", ""]).optional().transform(v => v === "true"),
27
+ STORAGE_TYPE: z.enum(["local", "s3"]).default("local"),
28
+ STORAGE_PATH: z.string().optional(),
29
+ S3_BUCKET: z.string().optional(),
30
+ S3_REGION: z.string().optional(),
31
+ S3_ACCESS_KEY_ID: z.string().optional(),
32
+ S3_SECRET_ACCESS_KEY: z.string().optional(),
33
+ S3_ENDPOINT: z.string().url().optional(),
34
+ S3_FORCE_PATH_STYLE: z.enum(["true", "false", ""]).optional().transform(v => v === "true"),
35
+ /**
36
+ * Service key for admin-level script / server-to-server authentication.
37
+ * When set, scripts can send `Authorization: Bearer <key>` to get admin access.
38
+ * Generate with: node -e "console.log(require('crypto').randomBytes(48).toString('base64'))"
39
+ */
40
+ REBASE_SERVICE_KEY: z.string().min(32, "REBASE_SERVICE_KEY must be at least 32 characters").optional()
41
+ }).superRefine((data, ctx) => {
42
+ if (data.NODE_ENV === "production" && !data.CORS_ORIGINS && !data.FRONTEND_URL) {
43
+ ctx.addIssue({
44
+ code: z.ZodIssueCode.custom,
45
+ message: "CORS_ORIGINS or FRONTEND_URL must be set in production to secure the API.",
46
+ path: ["CORS_ORIGINS"]
47
+ });
48
+ }
49
+ });
50
+
51
+ // Parse and export
52
+ export const env = envSchema.parse(process.env);
@@ -8,88 +8,140 @@ import { fileURLToPath } from "url";
8
8
  import {
9
9
  initializeRebaseBackend,
10
10
  serveSPA,
11
- HonoEnv
11
+ HonoEnv,
12
+ listenWithPortRetry,
13
+ cleanupDevPortFile,
14
+ logger
12
15
  } from "@rebasepro/server-core";
13
16
  import { createPostgresDatabaseConnection, createPostgresBootstrapper } from "@rebasepro/server-postgresql";
14
17
  import { enums, relations, tables } from "./schema.generated";
15
- import * as dotenv from "dotenv";
18
+ import { env } from "./env";
16
19
 
17
20
  const __filename = fileURLToPath(import.meta.url);
18
21
  const __dirname = path.dirname(__filename);
19
22
 
20
- dotenv.config({ path: path.resolve(__dirname, "../../.env") });
21
-
22
23
  // ─── App ─────────────────────────────────────────────────────────────
23
24
  const app = new Hono<HonoEnv>();
24
25
 
26
+ const isProduction = env.NODE_ENV === "production";
27
+ const allowedOrigins = isProduction
28
+ ? (env.CORS_ORIGINS || env.FRONTEND_URL || "https://yourdomain.com").split(",").map(s => s.trim())
29
+ : [];
30
+
25
31
  app.use("/*", cors({
26
- origin: process.env.NODE_ENV === "production"
27
- ? [process.env.FRONTEND_URL || "https://yourdomain.com"]
28
- : ["http://localhost:5173", "http://localhost:3000"],
32
+ origin: (origin) => {
33
+ if (!isProduction) return origin || "*";
34
+ return allowedOrigins.includes(origin) ? origin : null;
35
+ },
29
36
  credentials: true
30
37
  }));
31
38
 
32
39
  app.use("/*", secureHeaders());
33
40
 
34
41
  // ─── Database ────────────────────────────────────────────────────────
35
- const databaseUrl = process.env.DATABASE_URL;
36
- if (!databaseUrl) throw new Error("DATABASE_URL is not set");
42
+ const databaseUrl = env.DATABASE_URL;
37
43
 
38
- const { db, connectionString } = createPostgresDatabaseConnection(databaseUrl);
44
+ const { db, pool, connectionString } = createPostgresDatabaseConnection(databaseUrl);
39
45
 
40
46
  // ─── Start ───────────────────────────────────────────────────────────
41
47
  async function startServer() {
42
- const jwtSecret = process.env.JWT_SECRET;
43
- if (!jwtSecret) throw new Error("JWT_SECRET is not set");
44
-
45
- const PORT = parseInt(process.env.PORT || "3001", 10);
48
+ const jwtSecret = env.JWT_SECRET;
49
+ const PORT = env.PORT;
46
50
  const server = createServer(getRequestListener(app.fetch));
47
51
 
48
52
  const backend = await initializeRebaseBackend({
49
- collectionsDir: path.resolve(__dirname, "../../shared/collections"),
53
+ collectionsDir: path.resolve(__dirname, "../../config/collections"),
50
54
  functionsDir: path.resolve(__dirname, "../functions"),
51
55
  server,
52
56
  app,
53
57
  bootstrappers: [
54
58
  createPostgresBootstrapper({
55
59
  connection: db,
56
- schema: { tables, enums, relations },
57
- adminConnectionString: process.env.ADMIN_CONNECTION_STRING || databaseUrl,
60
+ schema: { tables,
61
+ enums,
62
+ relations },
63
+ adminConnectionString: env.ADMIN_CONNECTION_STRING || databaseUrl,
58
64
  connectionString
59
65
  })
60
66
  ],
61
67
  auth: {
62
68
  jwtSecret,
63
- accessExpiresIn: process.env.JWT_ACCESS_EXPIRES_IN || "1h",
64
- refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || "30d",
65
- google: process.env.GOOGLE_CLIENT_ID
66
- ? { clientId: process.env.GOOGLE_CLIENT_ID }
69
+ accessExpiresIn: env.JWT_ACCESS_EXPIRES_IN,
70
+ refreshExpiresIn: env.JWT_REFRESH_EXPIRES_IN,
71
+ serviceKey: env.REBASE_SERVICE_KEY,
72
+ google: env.GOOGLE_CLIENT_ID
73
+ ? { clientId: env.GOOGLE_CLIENT_ID }
67
74
  : undefined,
68
75
  seedDefaultRoles: true,
69
- allowRegistration: process.env.ALLOW_REGISTRATION === "true"
76
+ allowRegistration: env.ALLOW_REGISTRATION
70
77
  },
71
- storage: {
72
- type: "local",
73
- basePath: path.resolve(__dirname, "../../uploads")
74
- },
75
- history: true,
78
+ storage: env.STORAGE_TYPE === "s3"
79
+ ? {
80
+ type: "s3",
81
+ bucket: env.S3_BUCKET!,
82
+ region: env.S3_REGION || "auto",
83
+ accessKeyId: env.S3_ACCESS_KEY_ID || "",
84
+ secretAccessKey: env.S3_SECRET_ACCESS_KEY || "",
85
+ endpoint: env.S3_ENDPOINT,
86
+ forcePathStyle: env.S3_FORCE_PATH_STYLE
87
+ }
88
+ : {
89
+ type: "local",
90
+ basePath: env.STORAGE_PATH || path.resolve(__dirname, "../../uploads")
91
+ },
92
+ history: true
76
93
  });
77
94
 
78
- // ─── Your routes ─────────────────────────────────────────────
79
- app.get("/health", (c) => c.json({ status: "ok" }));
95
+ // ─── Health check ─────────────────────────────────────────────
96
+ app.get("/health", async (c) => {
97
+ const result = await backend.healthCheck();
98
+ const status = result.healthy ? 200 : 503;
99
+ return c.json({
100
+ status: result.healthy ? "ok" : "degraded",
101
+ latencyMs: result.latencyMs,
102
+ ...(result.details ? { details: result.details } : {})
103
+ }, status);
104
+ });
80
105
 
81
106
  // Serve the frontend in production
82
- if (process.env.NODE_ENV === "production") {
107
+ if (isProduction) {
83
108
  serveSPA(app, { frontendPath: path.join(__dirname, "../../frontend/dist") });
84
109
  }
85
110
 
86
- server.listen(PORT, () => {
87
- console.log(`🚀 Server running at http://localhost:${PORT}`);
88
- });
111
+ if (!isProduction) {
112
+ // Dev mode: retry the next port if the current one is in use
113
+ const projectRoot = path.resolve(__dirname, "../..");
114
+ const actualPort = await listenWithPortRetry(server, PORT, { portFileDir: projectRoot });
115
+
116
+ // Clean up port file on exit
117
+ const cleanup = () => cleanupDevPortFile(projectRoot);
118
+ process.on("SIGINT", cleanup);
119
+ process.on("SIGTERM", cleanup);
120
+ process.on("exit", cleanup);
121
+
122
+ logger.info(`Server running at http://localhost:${actualPort}`);
123
+ } else {
124
+ server.listen(PORT, () => {
125
+ logger.info(`Server running at http://localhost:${PORT}`);
126
+ });
127
+ }
128
+
129
+ // ─── Graceful Shutdown ───────────────────────────────────────────────
130
+ // Uses the framework's built-in shutdown() which drains connections,
131
+ // stops the cron scheduler, and force-exits after 15s timeout.
132
+ const gracefulShutdown = async (signal: string) => {
133
+ logger.info(`Received ${signal}, shutting down...`);
134
+ await backend.shutdown();
135
+ await pool.end();
136
+ logger.info("Database pool closed");
137
+ process.exit(0);
138
+ };
139
+ process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
140
+ process.on("SIGINT", () => gracefulShutdown("SIGINT"));
89
141
  }
90
142
 
91
143
  startServer().catch(err => {
92
- console.error("Failed to start server:", err);
144
+ logger.error("Failed to start server", { error: err instanceof Error ? err : new Error(String(err)) });
93
145
  process.exit(1);
94
146
  });
95
147
 
@@ -15,5 +15,5 @@
15
15
  "sourceMap": true,
16
16
  "jsx": "preserve"
17
17
  },
18
- "include": ["src/**/*", "../shared/**/*", "drizzle.config.ts"]
18
+ "include": ["src/**/*", "../config/**/*", "drizzle.config.ts"]
19
19
  }
@@ -0,0 +1,45 @@
1
+ import { EntityCollection } from "@rebasepro/types";
2
+
3
+ const authorsCollection: EntityCollection = {
4
+ name: "Authors",
5
+ singularName: "Author",
6
+ slug: "authors",
7
+ table: "authors",
8
+ icon: "Person",
9
+ properties: {
10
+ id: {
11
+ name: "ID",
12
+ type: "number",
13
+ isId: "increment"
14
+ },
15
+ name: {
16
+ name: "Name",
17
+ type: "string",
18
+ validation: {
19
+ required: true
20
+ }
21
+ },
22
+ email: {
23
+ name: "Email",
24
+ type: "string",
25
+ validation: {
26
+ required: true
27
+ }
28
+ },
29
+ picture: {
30
+ name: "Picture",
31
+ type: "string",
32
+ storage: {
33
+ storagePath: "author_pictures/"
34
+ }
35
+ }
36
+ },
37
+ propertiesOrder: [
38
+ "id",
39
+ "name",
40
+ "email",
41
+ "picture"
42
+ ]
43
+ };
44
+
45
+ export default authorsCollection;
@@ -0,0 +1,5 @@
1
+ import postsCollection from "./posts";
2
+ import authorsCollection from "./authors";
3
+ import tagsCollection from "./tags";
4
+
5
+ export const collections = [postsCollection, authorsCollection, tagsCollection];
@@ -1,4 +1,6 @@
1
1
  import { EntityCollection } from "@rebasepro/types";
2
+ import authorsCollection from "./authors";
3
+ import tagsCollection from "./tags";
2
4
 
3
5
  const postsCollection: EntityCollection = {
4
6
  name: "Posts",
@@ -46,8 +48,44 @@ const postsCollection: EntityCollection = {
46
48
  color: "greenLight"
47
49
  }
48
50
  ]
51
+ },
52
+ author: {
53
+ name: "Author",
54
+ type: "relation",
55
+ relationName: "author",
56
+ relation: {
57
+ relationName: "author",
58
+ cardinality: "one",
59
+ direction: "owning",
60
+ target: () => authorsCollection
61
+ }
62
+ },
63
+ tags: {
64
+ name: "Tags",
65
+ type: "relation",
66
+ relationName: "tags",
67
+ relation: {
68
+ relationName: "tags",
69
+ cardinality: "many",
70
+ direction: "owning",
71
+ target: () => tagsCollection
72
+ }
73
+ }
74
+ },
75
+ relations: [
76
+ {
77
+ relationName: "author",
78
+ target: () => authorsCollection,
79
+ cardinality: "one",
80
+ direction: "owning"
81
+ },
82
+ {
83
+ relationName: "tags",
84
+ target: () => tagsCollection,
85
+ cardinality: "many",
86
+ direction: "owning"
49
87
  }
50
- }
88
+ ]
51
89
  };
52
90
 
53
91
  export default postsCollection;
@@ -0,0 +1,27 @@
1
+ import { EntityCollection } from "@rebasepro/types";
2
+
3
+ const tagsCollection: EntityCollection = {
4
+ name: "Tags",
5
+ singularName: "Tag",
6
+ slug: "tags",
7
+ table: "tags",
8
+ icon: "Tag",
9
+ properties: {
10
+ id: {
11
+ name: "ID",
12
+ type: "number",
13
+ validation: {
14
+ required: true
15
+ }
16
+ },
17
+ name: {
18
+ name: "Tag Name",
19
+ type: "string",
20
+ validation: {
21
+ required: true
22
+ }
23
+ }
24
+ }
25
+ };
26
+
27
+ export default tagsCollection;
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "{{PROJECT_NAME}}-shared",
2
+ "name": "{{PROJECT_NAME}}-config",
3
3
  "version": "1.0.0",
4
4
  "description": "Shared collections for frontend and backend",
5
5
  "main": "dist/index.js",
@@ -63,6 +63,7 @@ services:
63
63
  PORT: "3001"
64
64
  ALLOW_REGISTRATION: ${ALLOW_REGISTRATION:-false}
65
65
  FRONTEND_URL: ${FRONTEND_URL:-http://localhost}
66
+ CORS_ORIGINS: ${CORS_ORIGINS:-}
66
67
  # Optional Google OAuth
67
68
  GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
68
69
  # Optional SMTP
@@ -73,6 +74,17 @@ services:
73
74
  SMTP_PASS: ${SMTP_PASS:-}
74
75
  SMTP_FROM: ${SMTP_FROM:-}
75
76
  APP_NAME: ${APP_NAME:-Rebase}
77
+ # Storage
78
+ STORAGE_TYPE: ${STORAGE_TYPE:-local}
79
+ STORAGE_PATH: ${STORAGE_PATH:-/app/backend/uploads}
80
+ S3_BUCKET: ${S3_BUCKET:-}
81
+ S3_REGION: ${S3_REGION:-}
82
+ S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-}
83
+ S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-}
84
+ S3_ENDPOINT: ${S3_ENDPOINT:-}
85
+ S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE:-}
86
+ # Service key for scripts & server-to-server admin access
87
+ REBASE_SERVICE_KEY: ${REBASE_SERVICE_KEY:-}
76
88
  depends_on:
77
89
  db:
78
90
  condition: service_healthy
@@ -21,13 +21,13 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
21
21
 
22
22
  # Copy workspace packages
23
23
  COPY frontend ./frontend
24
- COPY shared ./shared
24
+ COPY config ./config
25
25
 
26
26
  # Install dependencies
27
27
  RUN pnpm install --frozen-lockfile
28
28
 
29
- # Build shared first, then frontend
30
- RUN pnpm --filter "*-shared" run build
29
+ # Build config first, then frontend
30
+ RUN pnpm --filter "*-config" run build
31
31
  RUN pnpm --filter "*-frontend" run build
32
32
 
33
33
  # ── Stage 2: Serve with nginx ────────────────────────────────────────
@@ -8,12 +8,12 @@
8
8
  "@rebasepro/core": "workspace:*",
9
9
  "@rebasepro/plugin-data-enhancement": "workspace:*",
10
10
  "@rebasepro/client": "workspace:*",
11
- "@rebasepro/cms": "workspace:*",
11
+ "@rebasepro/admin": "workspace:*",
12
12
  "@rebasepro/studio": "workspace:*",
13
13
  "@rebasepro/types": "workspace:*",
14
14
  "@rebasepro/ui": "workspace:*",
15
15
  "@fontsource/jetbrains-mono": "^5.2.5",
16
- "{{PROJECT_NAME}}-shared": "0.0.1",
16
+ "{{PROJECT_NAME}}-config": "workspace:*",
17
17
  "react": "^19.0.0",
18
18
  "react-dom": "^19.0.0",
19
19
  "react-router": "^7.0.0",
@@ -25,14 +25,14 @@ import {
25
25
  useBuildModeController,
26
26
  useBuildNavigationStateController
27
27
  } from "@rebasepro/core";
28
- import { RebaseRoute } from "@rebasepro/cms";
28
+ import { RebaseRoute } from "@rebasepro/admin";
29
29
  import { CircularProgressCenter } from "@rebasepro/ui";
30
30
  import { collections } from "virtual:rebase-collections";
31
31
  import { Route, Outlet } from "react-router-dom";
32
32
  import { createRebaseClient } from "@rebasepro/client";
33
33
 
34
34
  // Configuration from environment
35
- const API_URL = import.meta.env.VITE_API_URL || "http://localhost:3001";
35
+ const API_URL = import.meta.env.VITE_API_URL || (import.meta.env.DEV ? "http://localhost:3001" : undefined);
36
36
  const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID;
37
37
 
38
38
  export function App() {
@@ -40,8 +40,7 @@ export function App() {
40
40
  const userConfigPersistence = useBuildLocalConfigurationPersistence();
41
41
 
42
42
  const rebaseClient = React.useMemo(() => createRebaseClient({
43
- baseUrl: API_URL,
44
- websocketUrl: API_URL.replace(/^http/, "ws")
43
+ baseUrl: API_URL
45
44
  }), [API_URL]);
46
45
 
47
46
  const authController = useRebaseAuthController({
@@ -89,7 +88,7 @@ export function App() {
89
88
  >
90
89
  {({ loading }) => {
91
90
  if (loading || authController.initialLoading) {
92
- return <CircularProgressCenter />;
91
+ return <CircularProgressCenter/>;
93
92
  }
94
93
 
95
94
  if (!authController.user) {
@@ -106,15 +105,15 @@ export function App() {
106
105
  <RebaseRoutes>
107
106
  <Route element={
108
107
  <Scaffold autoOpenDrawer={false}>
109
- <AppBar />
110
- <Drawer />
111
- <Outlet />
112
- <SideDialogs />
108
+ <AppBar/>
109
+ <Drawer/>
110
+ <Outlet/>
111
+ <SideDialogs/>
113
112
  </Scaffold>
114
113
  }>
115
- <Route path={"/"} element={<ContentHomePage />} />
116
- <Route path={"/c/*"} element={<RebaseRoute />} />
117
- <Route path={"*"} element={<NotFoundPage />} />
114
+ <Route path={"/"} element={<ContentHomePage/>}/>
115
+ <Route path={"/c/*"} element={<RebaseRoute/>}/>
116
+ <Route path={"*"} element={<NotFoundPage/>}/>
118
117
  </Route>
119
118
  </RebaseRoutes>
120
119
  );
@@ -7,12 +7,12 @@ import "./index.css";
7
7
  const router = createBrowserRouter([
8
8
  {
9
9
  path: "/*",
10
- element: <App />
10
+ element: <App/>
11
11
  }
12
12
  ]);
13
13
 
14
14
  ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
15
15
  <React.StrictMode>
16
- <RouterProvider router={router} />
16
+ <RouterProvider router={router}/>
17
17
  </React.StrictMode>
18
18
  );
@@ -21,6 +21,6 @@ export default defineConfig({
21
21
  svgr(),
22
22
  react({}),
23
23
  tailwindcss(),
24
- rebaseCollectionsPlugin({ collectionsDir: "../shared/collections" })
24
+ rebaseCollectionsPlugin({ collectionsDir: "../config/collections" })
25
25
  ]
26
26
  });
@@ -1,4 +1,4 @@
1
1
  packages:
2
2
  - "frontend"
3
3
  - "backend"
4
- - "shared"
4
+ - "config"