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

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 (38) 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.d.ts +2 -0
  6. package/dist/commands/init.test.d.ts +1 -0
  7. package/dist/index.cjs +295 -73
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.es.js +295 -73
  10. package/dist/index.es.js.map +1 -1
  11. package/dist/utils/project.test.d.ts +1 -0
  12. package/package.json +67 -66
  13. package/templates/template/.env.template +22 -1
  14. package/templates/template/README.md +2 -2
  15. package/templates/template/backend/Dockerfile +6 -6
  16. package/templates/template/backend/drizzle.config.ts +15 -3
  17. package/templates/template/backend/functions/hello.ts +24 -6
  18. package/templates/template/backend/package.json +2 -2
  19. package/templates/template/backend/src/env.ts +52 -0
  20. package/templates/template/backend/src/index.ts +86 -34
  21. package/templates/template/backend/tsconfig.json +1 -1
  22. package/templates/template/config/collections/authors.ts +45 -0
  23. package/templates/template/config/collections/index.ts +5 -0
  24. package/templates/template/{shared → config}/collections/posts.ts +39 -1
  25. package/templates/template/config/collections/tags.ts +27 -0
  26. package/templates/template/{shared → config}/package.json +1 -1
  27. package/templates/template/docker-compose.yml +12 -0
  28. package/templates/template/frontend/Dockerfile +3 -3
  29. package/templates/template/frontend/package.json +2 -2
  30. package/templates/template/frontend/src/App.tsx +21 -101
  31. package/templates/template/frontend/src/main.tsx +2 -2
  32. package/templates/template/frontend/vite.config.ts +32 -3
  33. package/templates/template/package.json +5 -4
  34. package/templates/template/pnpm-workspace.yaml +1 -1
  35. package/templates/template/scripts/example.ts +91 -0
  36. package/templates/template/shared/collections/index.ts +0 -3
  37. /package/templates/template/{shared → config}/index.ts +0 -0
  38. /package/templates/template/{shared → config}/tsconfig.json +0 -0
@@ -66,7 +66,7 @@ Backend (Hono + PostgreSQL) on port 3001, frontend (Vite + React) on port 5173.
66
66
  │ ├── Dockerfile # Multi-stage production build
67
67
  │ ├── functions/ # Custom API endpoints (auto-discovered)
68
68
  │ └── src/
69
- ├── shared/ # Shared collection definitions
69
+ ├── config/ # Shared collection definitions
70
70
  │ └── collections/ # Schema-as-Code TypeScript files
71
71
  ├── docker-compose.yml # Production stack (Postgres + Backend + Frontend)
72
72
  ├── .env.template # Environment variable template
@@ -92,7 +92,7 @@ Call from the client SDK: `client.call("functions/hello", { name: "World" })`
92
92
 
93
93
  ### Shared Collections
94
94
 
95
- Collections are defined once in `shared/collections/` and used by both the frontend and backend. This ensures your schema stays in sync across the stack.
95
+ Collections are defined once in `config/collections/` and used by both the frontend and backend. This ensures your schema stays in sync across the stack.
96
96
 
97
97
  ## Production Deployment
98
98
 
@@ -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
@@ -7,8 +7,9 @@ if (!process.env.DATABASE_URL) {
7
7
  throw new Error("DATABASE_URL is not set. Make sure .env file exists in the project root and contains DATABASE_URL");
8
8
  }
9
9
 
10
- // Extract table names from the generated schema
11
- // This ensures drizzle-kit ONLY manages tables defined in the schema
10
+ // Extract table names from the generated schema.
11
+ // This ensures drizzle-kit ONLY manages tables defined in the schema.
12
+ // Any tables in the database that are NOT part of the Rebase schema are left untouched.
12
13
  const tableNames = Object.values(tables).map(table => getTableName(table as Table));
13
14
 
14
15
  export default defineConfig({
@@ -18,5 +19,16 @@ export default defineConfig({
18
19
  dbCredentials: {
19
20
  url: process.env.DATABASE_URL
20
21
  },
21
- tablesFilter: tableNames
22
+ // Only manage tables defined in the generated schema.
23
+ // Unmapped tables in the database are completely ignored.
24
+ tablesFilter: tableNames,
25
+ // Restrict drizzle-kit to the public schema only — tables in other schemas
26
+ // (e.g. extensions, custom schemas) are never touched.
27
+ schemaFilter: ["public"],
28
+ // Prevent drizzle-kit from managing roles not defined in the schema
29
+ entities: {
30
+ roles: false
31
+ },
32
+ // If PostGIS or other extensions create helper tables, ignore them
33
+ extensionsFilters: ["postgis"]
22
34
  });
@@ -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",