create-bw-app 0.9.5 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,6 +57,7 @@ Current updater behavior:
57
57
  - copies a clean Next.js App Router starter template
58
58
  - platform apps include BrightWeb auth, shell wiring, and optional module starter surfaces
59
59
  - platform apps include a local `components/` folder for app-owned UI alongside the shared BrightWeb packages
60
+ - platform apps in published mode also write `supabase/module-registry.json`, `supabase/clients/<slug>/stack.json`, and the resolved shared SQL migrations under `supabase/modules/<module>/migrations`
60
61
  - site apps include Next.js, Tailwind CSS v4, and local component primitives
61
62
  - writes `package.json`, `next.config.ts`, `.gitignore`, and `README.md` for both templates
62
63
  - platform apps also write `.env.local`, `AGENTS.md`, `docs/ai/README.md`, `docs/ai/examples.md`, `docs/ai/app-context.json`, and generated config files for brand and module state
@@ -81,3 +82,4 @@ Platform mode always resolves to the `Core + Admin` database baseline. Selecting
81
82
  - `packages/create-bw-app/template/base`
82
83
  - `packages/create-bw-app/template/site/base`
83
84
  - `packages/create-bw-app/template/modules`
85
+ - `packages/create-bw-app/template/supabase`
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-bw-app",
3
3
  "private": false,
4
- "version": "0.9.5",
4
+ "version": "0.9.6",
5
5
  "type": "module",
6
6
  "bin": "bin/create-bw-app.mjs",
7
7
  "files": [
package/src/generator.mjs CHANGED
@@ -18,6 +18,7 @@ import {
18
18
 
19
19
  export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
20
20
  export const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, "template");
21
+ const TEMPLATE_SUPABASE_ROOT = path.join(TEMPLATE_ROOT, "supabase");
21
22
  const TEMPLATE_KEY_SET = new Set(TEMPLATE_OPTIONS.map((templateOption) => templateOption.key));
22
23
  const DEFAULT_DB_MODULE_REGISTRY = {
23
24
  modules: {
@@ -112,12 +113,22 @@ export async function readJsonIfPresent(filePath) {
112
113
  }
113
114
 
114
115
  export async function getDbModuleRegistry(workspaceRoot) {
115
- if (!workspaceRoot) {
116
- return DEFAULT_DB_MODULE_REGISTRY;
116
+ const candidatePaths = [];
117
+
118
+ if (workspaceRoot) {
119
+ candidatePaths.push(path.join(workspaceRoot, "supabase", "module-registry.json"));
120
+ }
121
+
122
+ candidatePaths.push(path.join(TEMPLATE_SUPABASE_ROOT, "module-registry.json"));
123
+
124
+ for (const registryPath of candidatePaths) {
125
+ const registry = await readJsonIfPresent(registryPath);
126
+ if (registry) {
127
+ return registry;
128
+ }
117
129
  }
118
130
 
119
- const registryPath = path.join(workspaceRoot, "supabase", "module-registry.json");
120
- return (await readJsonIfPresent(registryPath)) || DEFAULT_DB_MODULE_REGISTRY;
131
+ return DEFAULT_DB_MODULE_REGISTRY;
121
132
  }
122
133
 
123
134
  function resolveModuleOrder(registry, enabledModules) {
@@ -172,20 +183,14 @@ function getModuleLabel(moduleKey) {
172
183
  }
173
184
 
174
185
  export function createDbInstallPlan({ selectedModules, workspaceMode, registry }) {
175
- if (!workspaceMode) {
176
- return {
177
- selectedLabels: getSelectedModuleLabels(selectedModules),
178
- resolvedOrder: [],
179
- notes: [],
180
- };
181
- }
182
-
186
+ void workspaceMode;
187
+ const activeRegistry = registry?.modules ? registry : DEFAULT_DB_MODULE_REGISTRY;
183
188
  const requestedModules = Array.from(new Set(["core", ...selectedModules]));
184
189
  if (!requestedModules.includes("admin")) {
185
190
  requestedModules.push("admin");
186
191
  }
187
192
 
188
- const resolvedOrder = resolveModuleOrder(registry, requestedModules);
193
+ const resolvedOrder = resolveModuleOrder(activeRegistry, requestedModules);
189
194
  const notes = [];
190
195
 
191
196
  if (!selectedModules.includes("admin") && resolvedOrder.includes("admin")) {
@@ -198,16 +203,16 @@ export function createDbInstallPlan({ selectedModules, workspaceMode, registry }
198
203
  }
199
204
 
200
205
  const dependents = resolvedOrder.filter((candidateKey) => {
201
- const dependencyList = registry.modules?.[candidateKey]?.dependsOn || [];
206
+ const dependencyList = activeRegistry.modules?.[candidateKey]?.dependsOn || [];
202
207
  return dependencyList.includes(moduleKey);
203
208
  });
204
209
 
205
210
  if (dependents.length === 0) continue;
206
211
 
207
212
  const dependentLabels = dependents
208
- .map((candidateKey) => registry.modules?.[candidateKey]?.label || getModuleLabel(candidateKey))
213
+ .map((candidateKey) => activeRegistry.modules?.[candidateKey]?.label || getModuleLabel(candidateKey))
209
214
  .join(", ");
210
- const moduleLabel = registry.modules?.[moduleKey]?.label || getModuleLabel(moduleKey);
215
+ const moduleLabel = activeRegistry.modules?.[moduleKey]?.label || getModuleLabel(moduleKey);
211
216
  notes.push(`${moduleLabel} is included because ${dependentLabels} depends on it.`);
212
217
  }
213
218
 
@@ -467,20 +472,22 @@ function createPlatformReadme({
467
472
  "",
468
473
  ...moduleLines,
469
474
  "",
475
+ "## Resolved database stack",
476
+ "",
477
+ ...resolvedDbStackLines,
478
+ "",
470
479
  ...(workspaceMode
480
+ ? []
481
+ : [
482
+ "Bundled Supabase SQL migrations live under `supabase/modules/<module>/migrations`.",
483
+ "",
484
+ ]),
485
+ ...(dependencyNotes.length > 0
471
486
  ? [
472
- "## Resolved database stack",
487
+ "## Dependency notes",
473
488
  "",
474
- ...resolvedDbStackLines,
489
+ ...dependencyNotes,
475
490
  "",
476
- ...(dependencyNotes.length > 0
477
- ? [
478
- "## Dependency notes",
479
- "",
480
- ...dependencyNotes,
481
- "",
482
- ]
483
- : []),
484
491
  ]
485
492
  : []),
486
493
  "## Starter routes",
@@ -894,20 +901,34 @@ async function copyDirectory(sourceDir, targetDir) {
894
901
  await fs.cp(sourceDir, targetDir, { recursive: true });
895
902
  }
896
903
 
904
+ async function copyFileIfPresent(sourcePath, targetPath) {
905
+ if (!(await pathExists(sourcePath))) {
906
+ return;
907
+ }
908
+
909
+ await ensureDirectory(path.dirname(targetPath));
910
+ await fs.copyFile(sourcePath, targetPath);
911
+ }
912
+
897
913
  export async function ensureDirectory(targetDir) {
898
914
  await fs.mkdir(targetDir, { recursive: true });
899
915
  }
900
916
 
901
- async function writeWorkspaceClientStack(workspaceRoot, slug, selectedModules) {
902
- const clientDir = path.join(workspaceRoot, "supabase", "clients", slug);
917
+ function createScopedDbModuleRegistry(registry, moduleKeys) {
918
+ return {
919
+ modules: Object.fromEntries(
920
+ moduleKeys
921
+ .map((moduleKey) => [moduleKey, registry.modules?.[moduleKey]])
922
+ .filter(([, moduleConfig]) => Boolean(moduleConfig)),
923
+ ),
924
+ };
925
+ }
926
+
927
+ async function writeClientStack(baseRoot, slug, dbInstallPlan, options = {}) {
928
+ const generatedInWorkspaceMode = options.workspaceMode === true;
929
+ const clientDir = path.join(baseRoot, "supabase", "clients", slug);
903
930
  const stackPath = path.join(clientDir, "stack.json");
904
931
  const migrationsDir = path.join(clientDir, "migrations");
905
- const registry = await getDbModuleRegistry(workspaceRoot);
906
- const dbInstallPlan = createDbInstallPlan({
907
- selectedModules,
908
- workspaceMode: true,
909
- registry,
910
- });
911
932
  const enabledModules = dbInstallPlan.resolvedOrder;
912
933
 
913
934
  if (await pathExists(stackPath)) {
@@ -929,7 +950,9 @@ async function writeWorkspaceClientStack(workspaceRoot, slug, selectedModules) {
929
950
  enabledModules,
930
951
  clientMigrationPath: `supabase/clients/${slug}/migrations`,
931
952
  notes: [
932
- "Generated by create-bw-app in workspace mode.",
953
+ generatedInWorkspaceMode
954
+ ? "Generated by create-bw-app in workspace mode."
955
+ : "Generated by create-bw-app in published mode.",
933
956
  `Selected app modules: ${dbInstallPlan.selectedLabels.length > 0 ? dbInstallPlan.selectedLabels.join(", ") : "none"}.`,
934
957
  `Resolved database stack: ${enabledModules.map((moduleKey) => getModuleLabel(moduleKey)).join(" -> ")}.`,
935
958
  "Platform always resolves to the Core + Admin database baseline; selecting Admin only controls whether the Admin starter UI and package wiring are scaffolded.",
@@ -942,6 +965,38 @@ async function writeWorkspaceClientStack(workspaceRoot, slug, selectedModules) {
942
965
  );
943
966
  }
944
967
 
968
+ async function writeBundledSupabaseBaseline({ targetDir, slug, dbInstallPlan, registry }) {
969
+ const shippedModuleKeys = dbInstallPlan.resolvedOrder;
970
+ if (shippedModuleKeys.length === 0) {
971
+ return;
972
+ }
973
+
974
+ const targetSupabaseDir = path.join(targetDir, "supabase");
975
+ const targetModulesDir = path.join(targetSupabaseDir, "modules");
976
+ const scopedRegistry = createScopedDbModuleRegistry(registry, shippedModuleKeys);
977
+
978
+ await ensureDirectory(targetModulesDir);
979
+ await copyFileIfPresent(path.join(TEMPLATE_SUPABASE_ROOT, "README.md"), path.join(targetSupabaseDir, "README.md"));
980
+ await copyFileIfPresent(
981
+ path.join(TEMPLATE_SUPABASE_ROOT, "clients", "README.md"),
982
+ path.join(targetSupabaseDir, "clients", "README.md"),
983
+ );
984
+ await fs.writeFile(
985
+ path.join(targetSupabaseDir, "module-registry.json"),
986
+ `${JSON.stringify(scopedRegistry, null, 2)}\n`,
987
+ "utf8",
988
+ );
989
+
990
+ for (const moduleKey of shippedModuleKeys) {
991
+ await copyDirectory(
992
+ path.join(TEMPLATE_SUPABASE_ROOT, "modules", moduleKey),
993
+ path.join(targetModulesDir, moduleKey),
994
+ );
995
+ }
996
+
997
+ await writeClientStack(targetDir, slug, dbInstallPlan);
998
+ }
999
+
945
1000
  export async function runInstall(command, cwd) {
946
1001
  return new Promise((resolve, reject) => {
947
1002
  const child = spawn(command, ["install"], {
@@ -1086,6 +1141,7 @@ async function scaffoldPlatformProject({
1086
1141
  workspaceRoot,
1087
1142
  answers,
1088
1143
  dbInstallPlan,
1144
+ dbRegistry,
1089
1145
  }) {
1090
1146
  const brandValues = createDerivedBrandValues(answers.slug);
1091
1147
  const baseTemplateDir = path.join(TEMPLATE_ROOT, "base");
@@ -1146,7 +1202,14 @@ async function scaffoldPlatformProject({
1146
1202
  );
1147
1203
 
1148
1204
  if (workspaceMode) {
1149
- await writeWorkspaceClientStack(workspaceRoot, answers.slug, selectedModules);
1205
+ await writeClientStack(workspaceRoot, answers.slug, dbInstallPlan, { workspaceMode: true });
1206
+ } else {
1207
+ await writeBundledSupabaseBaseline({
1208
+ targetDir,
1209
+ slug: answers.slug,
1210
+ dbInstallPlan,
1211
+ registry: dbRegistry,
1212
+ });
1150
1213
  }
1151
1214
  }
1152
1215
 
@@ -1301,6 +1364,7 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
1301
1364
  workspaceRoot,
1302
1365
  answers,
1303
1366
  dbInstallPlan,
1367
+ dbRegistry: dbModuleRegistry,
1304
1368
  });
1305
1369
  }
1306
1370
 
@@ -0,0 +1,74 @@
1
+ # Brightweb Supabase Structure
2
+
3
+ This directory is the canonical home for the Brightweb database baseline and its forward migrations.
4
+
5
+ ## Directory map
6
+
7
+ - `module-registry.json`: shared module dependency graph and migration source paths
8
+ - `modules/core`: always-on platform foundations
9
+ - `modules/admin`: RBAC and privileged governance behavior
10
+ - `modules/crm`: organizations, CRM contacts, and invitation flows
11
+ - `modules/projects`: project and work-management data
12
+ - `clients/<client-slug>`: true client-only schema deltas plus the client stack plan
13
+ - `.generated/<client-slug>`: materialized Supabase workdirs produced by `pnpm db:materialize`
14
+
15
+ ## Ownership rule
16
+
17
+ Shared database changes should be authored by ownership area, not by client app:
18
+
19
+ - `core` is applied to every client
20
+ - shared module migrations are applied only when that module is enabled for the client
21
+ - client-specific migrations are the exception, not the default
22
+
23
+ In workspace scaffold mode, `create-bw-app` writes `supabase/clients/<slug>/stack.json` so the generated app modules and the database install plan stay aligned.
24
+
25
+ The module baselines in this repo are the canonical Brightweb v1 install path. Future schema work should extend them with forward migrations instead of carrying historical cleanup sequences.
26
+
27
+ Maintainer references:
28
+
29
+ - `docs/internal/architecture/database-module-migration-structure.md`
30
+ - `docs/internal/architecture/database-migration-authoring-workflow.md`
31
+ - `docs/internal/architecture/database-migration-safety-policy.md`
32
+
33
+ ## Authoring workflow
34
+
35
+ Create a new shared module migration:
36
+
37
+ ```bash
38
+ pnpm db:new core profile_notification_cursor
39
+ pnpm db:new admin role_change_guard
40
+ pnpm db:new crm organization_invite_expiry
41
+ pnpm db:new projects task_due_date_index
42
+ ```
43
+
44
+ Create a client-only migration:
45
+
46
+ ```bash
47
+ pnpm db:new client:acme bespoke_reporting_table
48
+ ```
49
+
50
+ Print the effective apply order for a client:
51
+
52
+ ```bash
53
+ pnpm db:plan acme
54
+ ```
55
+
56
+ Materialize an installable Supabase workdir for a client stack:
57
+
58
+ ```bash
59
+ pnpm db:materialize acme
60
+ ```
61
+
62
+ This writes a generated workdir under `supabase/.generated/<client-slug>` with:
63
+
64
+ - ordered migrations merged from `core`, enabled modules, and client-only deltas
65
+ - a generated `config.toml`
66
+ - a `manifest.json` showing the source file for each materialized migration
67
+
68
+ ## Related READMEs
69
+
70
+ - `supabase/modules/core/README.md`
71
+ - `supabase/modules/admin/README.md`
72
+ - `supabase/modules/crm/README.md`
73
+ - `supabase/modules/projects/README.md`
74
+ - `supabase/clients/README.md`
@@ -0,0 +1,19 @@
1
+ # Client-Specific Migrations
2
+
3
+ This directory is reserved for true client-only schema deltas.
4
+
5
+ ## Rule
6
+
7
+ Only place SQL here when the change:
8
+
9
+ - cannot be reused by other clients
10
+ - should not be part of a shared module
11
+ - is intentionally isolated to a single client deployment
12
+
13
+ ## Expected shape
14
+
15
+ - `clients/<slug>/...`
16
+
17
+ Temporary smoke-test client stacks should be removed after generator verification. This directory is for real client-specific deltas only.
18
+
19
+ If a client-specific migration later proves reusable, move the concept into the appropriate shared module and stop extending the client-only path.
@@ -0,0 +1,28 @@
1
+ {
2
+ "modules": {
3
+ "core": {
4
+ "label": "Core",
5
+ "path": "supabase/modules/core/migrations",
6
+ "dependsOn": [],
7
+ "description": "Shared auth/profile, events, rate limits, and always-on platform foundations."
8
+ },
9
+ "admin": {
10
+ "label": "Admin",
11
+ "path": "supabase/modules/admin/migrations",
12
+ "dependsOn": ["core"],
13
+ "description": "RBAC, user governance, and privileged role-management behavior."
14
+ },
15
+ "crm": {
16
+ "label": "CRM",
17
+ "path": "supabase/modules/crm/migrations",
18
+ "dependsOn": ["core", "admin"],
19
+ "description": "Organizations, CRM contacts, org membership, and invitation flows."
20
+ },
21
+ "projects": {
22
+ "label": "Projects",
23
+ "path": "supabase/modules/projects/migrations",
24
+ "dependsOn": ["core", "admin", "crm"],
25
+ "description": "Projects, tasks, milestones, members, links, and project activity policies."
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,18 @@
1
+ # Admin Migrations
2
+
3
+ `admin` owns user governance, RBAC, and privileged role-management behavior.
4
+
5
+ ## Owns
6
+
7
+ - `roles`
8
+ - `user_role_assignments`
9
+ - `role_change_audit`
10
+ - admin-only helper functions like `admin_set_user_role(...)`
11
+ - privileged profile-field guards
12
+ - default role-assignment triggers/backfills
13
+
14
+ ## Dependency
15
+
16
+ Depends on:
17
+
18
+ - `core` (`profiles`, `current_profile_id()`, auth linkage)