create-bw-app 0.9.4 → 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 +5 -0
- package/package.json +1 -1
- package/src/cli.mjs +5 -0
- package/src/constants.mjs +1 -0
- package/src/generator.mjs +102 -36
- package/src/update.mjs +67 -1
- package/template/base/AGENTS.md +1 -0
- package/template/base/app/playground/auth/page.tsx +1 -1
- package/template/base/app/preview/app-shell/page.tsx +1 -1
- package/template/base/{app/preview → components}/app-shell-preview.tsx +2 -2
- package/template/base/docs/ai/README.md +3 -0
- package/template/base/docs/ai/examples.md +2 -0
- package/template/modules/crm/app/api/crm/_shared/create-module-route-handler.ts +13 -0
- package/template/modules/crm/app/api/crm/contacts/route.ts +6 -4
- package/template/modules/crm/app/api/crm/organizations/route.ts +6 -4
- package/template/modules/crm/app/api/crm/owners/route.ts +6 -4
- package/template/modules/crm/app/api/crm/stats/route.ts +6 -4
- package/template/supabase/README.md +74 -0
- package/template/supabase/clients/README.md +19 -0
- package/template/supabase/module-registry.json +28 -0
- package/template/supabase/modules/admin/README.md +18 -0
- package/template/supabase/modules/admin/migrations/.gitkeep +1 -0
- package/template/supabase/modules/admin/migrations/20260316091000_admin_v1.sql +317 -0
- package/template/supabase/modules/core/README.md +24 -0
- package/template/supabase/modules/core/migrations/.gitkeep +1 -0
- package/template/supabase/modules/core/migrations/20260316090000_core_v1.sql +497 -0
- package/template/supabase/modules/crm/README.md +27 -0
- package/template/supabase/modules/crm/migrations/.gitkeep +1 -0
- package/template/supabase/modules/crm/migrations/20260316092000_crm_v1.sql +392 -0
- package/template/supabase/modules/projects/README.md +22 -0
- package/template/supabase/modules/projects/migrations/.gitkeep +1 -0
- package/template/supabase/modules/projects/migrations/20260316093000_projects_v1.sql +1120 -0
- /package/template/base/{app/playground/auth → components}/auth-playground.tsx +0 -0
package/README.md
CHANGED
|
@@ -41,6 +41,8 @@ pnpm dlx create-bw-app update --target-dir ./apps/client-portal
|
|
|
41
41
|
Current updater behavior:
|
|
42
42
|
|
|
43
43
|
- updates installed `@brightweblabs/*` packages only
|
|
44
|
+
- in published mode, resolves those `@brightweblabs/*` target versions from npm at update time
|
|
45
|
+
- fails the update if npm resolution fails unless you pass `--allow-stale-fallback`
|
|
44
46
|
- re-syncs managed BrightWeb config files such as `next.config.ts`, `config/modules.ts`, and `config/shell.ts`
|
|
45
47
|
- reports missing or drifted starter files and only rewrites them with `--refresh-starters`
|
|
46
48
|
- prints the follow-up install command unless `--install` is passed
|
|
@@ -54,6 +56,8 @@ Current updater behavior:
|
|
|
54
56
|
- prompts to install dependencies immediately
|
|
55
57
|
- copies a clean Next.js App Router starter template
|
|
56
58
|
- platform apps include BrightWeb auth, shell wiring, and optional module starter surfaces
|
|
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`
|
|
57
61
|
- site apps include Next.js, Tailwind CSS v4, and local component primitives
|
|
58
62
|
- writes `package.json`, `next.config.ts`, `.gitignore`, and `README.md` for both templates
|
|
59
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
|
|
@@ -78,3 +82,4 @@ Platform mode always resolves to the `Core + Admin` database baseline. Selecting
|
|
|
78
82
|
- `packages/create-bw-app/template/base`
|
|
79
83
|
- `packages/create-bw-app/template/site/base`
|
|
80
84
|
- `packages/create-bw-app/template/modules`
|
|
85
|
+
- `packages/create-bw-app/template/supabase`
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -42,6 +42,11 @@ function parseArgv(argv) {
|
|
|
42
42
|
continue;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
if (token === "--allow-stale-fallback") {
|
|
46
|
+
options.allowStaleFallback = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
45
50
|
const [rawKey, inlineValue] = token.slice(2).split("=", 2);
|
|
46
51
|
const key = toCamelCase(rawKey);
|
|
47
52
|
const nextValue = inlineValue ?? argv[index + 1];
|
package/src/constants.mjs
CHANGED
|
@@ -136,6 +136,7 @@ Update options:
|
|
|
136
136
|
--target-dir <path> Existing app directory to update (defaults to cwd)
|
|
137
137
|
--workspace-root <path> BrightWeb workspace root for workspace:* apps
|
|
138
138
|
--package-manager <name> Override package manager: pnpm, npm, yarn, or bun
|
|
139
|
+
--allow-stale-fallback Use baked-in BrightWeb package versions if npm lookup fails
|
|
139
140
|
--install Run install after writing package changes
|
|
140
141
|
--refresh-starters Rewrite starter route files from the latest template
|
|
141
142
|
--dry-run Print the update plan without writing 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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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(
|
|
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 =
|
|
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) =>
|
|
213
|
+
.map((candidateKey) => activeRegistry.modules?.[candidateKey]?.label || getModuleLabel(candidateKey))
|
|
209
214
|
.join(", ");
|
|
210
|
-
const moduleLabel =
|
|
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
|
-
"##
|
|
487
|
+
"## Dependency notes",
|
|
473
488
|
"",
|
|
474
|
-
...
|
|
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",
|
|
@@ -631,6 +638,7 @@ export function createAppContextFile({
|
|
|
631
638
|
".env.local",
|
|
632
639
|
],
|
|
633
640
|
appRoutesRoot: "app",
|
|
641
|
+
componentsRoot: "components",
|
|
634
642
|
configRoot: "config",
|
|
635
643
|
brandAssetsRoot: "public/brand",
|
|
636
644
|
},
|
|
@@ -638,6 +646,7 @@ export function createAppContextFile({
|
|
|
638
646
|
ownership: {
|
|
639
647
|
appOwned: [
|
|
640
648
|
"app/**",
|
|
649
|
+
"components/**",
|
|
641
650
|
"config/**",
|
|
642
651
|
"docs/ai/**",
|
|
643
652
|
"public/brand/**",
|
|
@@ -892,20 +901,34 @@ async function copyDirectory(sourceDir, targetDir) {
|
|
|
892
901
|
await fs.cp(sourceDir, targetDir, { recursive: true });
|
|
893
902
|
}
|
|
894
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
|
+
|
|
895
913
|
export async function ensureDirectory(targetDir) {
|
|
896
914
|
await fs.mkdir(targetDir, { recursive: true });
|
|
897
915
|
}
|
|
898
916
|
|
|
899
|
-
|
|
900
|
-
|
|
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);
|
|
901
930
|
const stackPath = path.join(clientDir, "stack.json");
|
|
902
931
|
const migrationsDir = path.join(clientDir, "migrations");
|
|
903
|
-
const registry = await getDbModuleRegistry(workspaceRoot);
|
|
904
|
-
const dbInstallPlan = createDbInstallPlan({
|
|
905
|
-
selectedModules,
|
|
906
|
-
workspaceMode: true,
|
|
907
|
-
registry,
|
|
908
|
-
});
|
|
909
932
|
const enabledModules = dbInstallPlan.resolvedOrder;
|
|
910
933
|
|
|
911
934
|
if (await pathExists(stackPath)) {
|
|
@@ -927,7 +950,9 @@ async function writeWorkspaceClientStack(workspaceRoot, slug, selectedModules) {
|
|
|
927
950
|
enabledModules,
|
|
928
951
|
clientMigrationPath: `supabase/clients/${slug}/migrations`,
|
|
929
952
|
notes: [
|
|
930
|
-
|
|
953
|
+
generatedInWorkspaceMode
|
|
954
|
+
? "Generated by create-bw-app in workspace mode."
|
|
955
|
+
: "Generated by create-bw-app in published mode.",
|
|
931
956
|
`Selected app modules: ${dbInstallPlan.selectedLabels.length > 0 ? dbInstallPlan.selectedLabels.join(", ") : "none"}.`,
|
|
932
957
|
`Resolved database stack: ${enabledModules.map((moduleKey) => getModuleLabel(moduleKey)).join(" -> ")}.`,
|
|
933
958
|
"Platform always resolves to the Core + Admin database baseline; selecting Admin only controls whether the Admin starter UI and package wiring are scaffolded.",
|
|
@@ -940,6 +965,38 @@ async function writeWorkspaceClientStack(workspaceRoot, slug, selectedModules) {
|
|
|
940
965
|
);
|
|
941
966
|
}
|
|
942
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
|
+
|
|
943
1000
|
export async function runInstall(command, cwd) {
|
|
944
1001
|
return new Promise((resolve, reject) => {
|
|
945
1002
|
const child = spawn(command, ["install"], {
|
|
@@ -1084,6 +1141,7 @@ async function scaffoldPlatformProject({
|
|
|
1084
1141
|
workspaceRoot,
|
|
1085
1142
|
answers,
|
|
1086
1143
|
dbInstallPlan,
|
|
1144
|
+
dbRegistry,
|
|
1087
1145
|
}) {
|
|
1088
1146
|
const brandValues = createDerivedBrandValues(answers.slug);
|
|
1089
1147
|
const baseTemplateDir = path.join(TEMPLATE_ROOT, "base");
|
|
@@ -1144,7 +1202,14 @@ async function scaffoldPlatformProject({
|
|
|
1144
1202
|
);
|
|
1145
1203
|
|
|
1146
1204
|
if (workspaceMode) {
|
|
1147
|
-
await
|
|
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
|
+
});
|
|
1148
1213
|
}
|
|
1149
1214
|
}
|
|
1150
1215
|
|
|
@@ -1299,6 +1364,7 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
|
|
|
1299
1364
|
workspaceRoot,
|
|
1300
1365
|
answers,
|
|
1301
1366
|
dbInstallPlan,
|
|
1367
|
+
dbRegistry: dbModuleRegistry,
|
|
1302
1368
|
});
|
|
1303
1369
|
}
|
|
1304
1370
|
|
package/src/update.mjs
CHANGED
|
@@ -76,6 +76,62 @@ function collectInstalledBrightwebPackages(manifest) {
|
|
|
76
76
|
return installed;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
async function resolvePublishedBrightwebVersions(installedBrightwebPackages, options = {}) {
|
|
80
|
+
const packageNames = Array.from(installedBrightwebPackages.keys()).sort();
|
|
81
|
+
|
|
82
|
+
if (packageNames.length === 0) {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
87
|
+
if (typeof fetchImpl !== "function") {
|
|
88
|
+
throw new Error("Published updates require fetch support to resolve BrightWeb package versions from npm.");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const fallbackVersionMap = options.fallbackVersionMap || {};
|
|
92
|
+
const allowStaleFallback = options.allowStaleFallback === true;
|
|
93
|
+
const resolvedVersions = {};
|
|
94
|
+
const failures = [];
|
|
95
|
+
|
|
96
|
+
await Promise.all(
|
|
97
|
+
packageNames.map(async (packageName) => {
|
|
98
|
+
try {
|
|
99
|
+
const response = await fetchImpl(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`);
|
|
100
|
+
if (!response?.ok) {
|
|
101
|
+
throw new Error(`npm registry responded with ${response?.status ?? "an unknown error"}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const payload = await response.json();
|
|
105
|
+
if (!payload?.version || typeof payload.version !== "string") {
|
|
106
|
+
throw new Error("npm registry response did not include a version");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
resolvedVersions[packageName] = `^${payload.version}`;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
const fallbackVersion = fallbackVersionMap[packageName];
|
|
112
|
+
if (allowStaleFallback && fallbackVersion) {
|
|
113
|
+
resolvedVersions[packageName] = fallbackVersion;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
118
|
+
failures.push(`${packageName}: ${message}`);
|
|
119
|
+
}
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (failures.length > 0) {
|
|
124
|
+
const fallbackHint = allowStaleFallback
|
|
125
|
+
? "No baked-in fallback version was available for at least one package."
|
|
126
|
+
: "Re-run with --allow-stale-fallback to use the CLI's baked-in BrightWeb package versions instead.";
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Failed to resolve published BrightWeb package versions from npm.\n${failures.join("\n")}\n${fallbackHint}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return resolvedVersions;
|
|
133
|
+
}
|
|
134
|
+
|
|
79
135
|
function parseConfiguredModules(content) {
|
|
80
136
|
const enabledModules = [];
|
|
81
137
|
|
|
@@ -339,6 +395,13 @@ export async function buildBrightwebAppUpdatePlan(argvOptions = {}, runtimeOptio
|
|
|
339
395
|
const packageManager = detectPackageManager(argvOptions.packageManager || runtimeOptions.packageManager);
|
|
340
396
|
const installedModules = detectInstalledModules(installedBrightwebPackagesMap);
|
|
341
397
|
const versionMap = await getVersionMap(workspaceRoot);
|
|
398
|
+
const brightwebVersionOverrides = dependencyMode === "published"
|
|
399
|
+
? await resolvePublishedBrightwebVersions(installedBrightwebPackagesMap, {
|
|
400
|
+
fetchImpl: runtimeOptions.fetchImpl,
|
|
401
|
+
fallbackVersionMap: versionMap,
|
|
402
|
+
allowStaleFallback: argvOptions.allowStaleFallback || runtimeOptions.allowStaleFallback,
|
|
403
|
+
})
|
|
404
|
+
: {};
|
|
342
405
|
const dbRegistry = await getDbModuleRegistry(workspaceRoot);
|
|
343
406
|
const dbInstallPlan = template === "platform"
|
|
344
407
|
? createDbInstallPlan({
|
|
@@ -356,7 +419,10 @@ export async function buildBrightwebAppUpdatePlan(argvOptions = {}, runtimeOptio
|
|
|
356
419
|
template,
|
|
357
420
|
dependencyMode,
|
|
358
421
|
installedModules,
|
|
359
|
-
versionMap
|
|
422
|
+
versionMap: {
|
|
423
|
+
...versionMap,
|
|
424
|
+
...brightwebVersionOverrides,
|
|
425
|
+
},
|
|
360
426
|
});
|
|
361
427
|
const packageJsonUpdate = mergeManagedPackageUpdates({
|
|
362
428
|
manifest,
|
package/template/base/AGENTS.md
CHANGED
|
@@ -8,6 +8,7 @@ This generated project is a BrightWeb platform starter. Use this file as the loc
|
|
|
8
8
|
- `docs/ai/README.md`: app-specific routing guide for agents.
|
|
9
9
|
- `docs/ai/examples.md`: common setup and customization flows.
|
|
10
10
|
- `docs/ai/app-context.json`: machine-readable app summary for quick discovery.
|
|
11
|
+
- `components/`: local app components used by starter routes and future product surfaces.
|
|
11
12
|
- `config/brand.ts`: client identity, naming, and contact defaults.
|
|
12
13
|
- `config/modules.ts`: selected module set and runtime enablement.
|
|
13
14
|
- `config/client.ts`: starter-facing derived state used by the home page and setup surfaces.
|
|
@@ -12,8 +12,8 @@ import {
|
|
|
12
12
|
type ResolvedClientAppShellConfig,
|
|
13
13
|
} from "@brightweblabs/app-shell";
|
|
14
14
|
import { LayoutTemplate, Sparkles, Users } from "lucide-react";
|
|
15
|
-
import { starterBrandConfig } from "
|
|
16
|
-
import { getStarterShellConfig } from "
|
|
15
|
+
import { starterBrandConfig } from "../config/brand";
|
|
16
|
+
import { getStarterShellConfig } from "../config/shell";
|
|
17
17
|
|
|
18
18
|
const mockUser = {
|
|
19
19
|
email: "admin@starter-client.test",
|
|
@@ -9,6 +9,7 @@ It is intentionally app-scoped. It explains the generated project you are in, no
|
|
|
9
9
|
This app is a normal Next.js App Router project with BrightWeb runtime wiring layered on top.
|
|
10
10
|
|
|
11
11
|
- `app/`: route tree, layouts, pages, starter previews, and playground routes.
|
|
12
|
+
- `components/`: local React components used by starter routes and app-owned product work.
|
|
12
13
|
- `config/`: generated app configuration for brand, env readiness, enabled modules, bootstrap content, and shell registration.
|
|
13
14
|
- `public/brand/`: starter logos used by the shell lockups.
|
|
14
15
|
- `.env.local`: local service configuration for Supabase, Resend, and runtime URLs.
|
|
@@ -18,6 +19,7 @@ This app is a normal Next.js App Router project with BrightWeb runtime wiring la
|
|
|
18
19
|
- `docs/ai/app-context.json`: machine-readable summary of this app's template, starter routes, and first-read files.
|
|
19
20
|
- `docs/ai/examples.md`: common setup and customization workflows.
|
|
20
21
|
- `README.md`: first-run setup steps.
|
|
22
|
+
- `components/`: local app component layer for starter surfaces and future product UI.
|
|
21
23
|
- `config/brand.ts`: client name, product name, support inboxes, and brand color.
|
|
22
24
|
- `config/modules.ts`: module metadata and enablement flags for CRM, Projects, and Admin.
|
|
23
25
|
- `config/client.ts`: aggregated state consumed by starter pages.
|
|
@@ -33,6 +35,7 @@ This app is a normal Next.js App Router project with BrightWeb runtime wiring la
|
|
|
33
35
|
|
|
34
36
|
- Change client identity first in `config/brand.ts`.
|
|
35
37
|
- Check module presence in `config/modules.ts` before editing or creating module-specific routes.
|
|
38
|
+
- Add app-specific UI in `components/` before forking shared package code.
|
|
36
39
|
- Use `config/shell.ts` when navigation or toolbar behavior needs to change.
|
|
37
40
|
- Use `config/bootstrap.ts` and `config/client.ts` when the setup checklist or readiness messaging is wrong.
|
|
38
41
|
- Keep starter validation routes until the real product routes replace their purpose.
|
|
@@ -18,6 +18,7 @@ Goal: get the generated starter running with real credentials.
|
|
|
18
18
|
Goal: update the starter to the real client name and support details.
|
|
19
19
|
|
|
20
20
|
- Edit `config/brand.ts`.
|
|
21
|
+
- Move route-specific presentation into `components/` when the home or preview surfaces need app-owned UI.
|
|
21
22
|
- Check `config/client.ts` or `config/bootstrap.ts` if starter copy still references old defaults.
|
|
22
23
|
- Validate the home page and `/preview/app-shell` after the change.
|
|
23
24
|
|
|
@@ -26,6 +27,7 @@ Goal: update the starter to the real client name and support details.
|
|
|
26
27
|
Goal: move from validation surfaces to product-owned pages.
|
|
27
28
|
|
|
28
29
|
- Build the real routes in `app/` first.
|
|
30
|
+
- Keep reusable route UI in `components/` so the app follows the expected Next.js folder split.
|
|
29
31
|
- Update `config/shell.ts` if navigation or toolbar behavior changes.
|
|
30
32
|
- Remove `/bootstrap`, `/preview/app-shell`, or `/playground/*` only after links and config references are cleaned up.
|
|
31
33
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type RouteHandler = (request: Request) => Response | Promise<Response>;
|
|
2
|
+
|
|
3
|
+
type ModuleWithHandler<THandlerName extends string> = Record<THandlerName, RouteHandler>;
|
|
4
|
+
|
|
5
|
+
export function createModuleRouteHandler<THandlerName extends string>(
|
|
6
|
+
loadModule: () => Promise<ModuleWithHandler<THandlerName>>,
|
|
7
|
+
handlerName: THandlerName,
|
|
8
|
+
): RouteHandler {
|
|
9
|
+
return async function moduleRouteHandler(request: Request) {
|
|
10
|
+
const module = await loadModule();
|
|
11
|
+
return module[handlerName](request);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { createModuleRouteHandler } from "../_shared/create-module-route-handler";
|
|
2
|
+
|
|
1
3
|
export const dynamic = "force-dynamic";
|
|
2
4
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
export const GET = createModuleRouteHandler(
|
|
6
|
+
() => import("@brightweblabs/module-crm"),
|
|
7
|
+
"handleCrmContactsGetRequest",
|
|
8
|
+
);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { createModuleRouteHandler } from "../_shared/create-module-route-handler";
|
|
2
|
+
|
|
1
3
|
export const dynamic = "force-dynamic";
|
|
2
4
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
export const GET = createModuleRouteHandler(
|
|
6
|
+
() => import("@brightweblabs/module-crm"),
|
|
7
|
+
"handleCrmOrganizationsGetRequest",
|
|
8
|
+
);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { createModuleRouteHandler } from "../_shared/create-module-route-handler";
|
|
2
|
+
|
|
1
3
|
export const dynamic = "force-dynamic";
|
|
2
4
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
export const GET = createModuleRouteHandler(
|
|
6
|
+
() => import("@brightweblabs/module-crm"),
|
|
7
|
+
"handleCrmOwnersGetRequest",
|
|
8
|
+
);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { createModuleRouteHandler } from "../_shared/create-module-route-handler";
|
|
2
|
+
|
|
1
3
|
export const dynamic = "force-dynamic";
|
|
2
4
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
export const GET = createModuleRouteHandler(
|
|
6
|
+
() => import("@brightweblabs/module-crm"),
|
|
7
|
+
"handleCrmStatsGetRequest",
|
|
8
|
+
);
|
|
@@ -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
|
+
}
|