create-bw-app 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/app-manifest.mjs +4 -2
  4. package/src/constants.mjs +95 -18
  5. package/src/doctor.mjs +38 -0
  6. package/src/generator.mjs +75 -19
  7. package/template/base/app/(auth)/auth/confirmed/page.tsx +1 -0
  8. package/template/base/app/(auth)/auth/post-login/page.tsx +1 -0
  9. package/template/base/app/(auth)/auth-provider.tsx +25 -0
  10. package/template/base/app/(auth)/forgot-password/page.tsx +1 -0
  11. package/template/base/app/(auth)/invite/[invitationId]/invite-route-mount.tsx +10 -0
  12. package/template/base/app/(auth)/invite/[invitationId]/page.tsx +1 -0
  13. package/template/base/app/(auth)/layout.tsx +5 -0
  14. package/template/base/app/(auth)/login/page.tsx +1 -0
  15. package/template/base/app/(auth)/reset-password/page.tsx +1 -0
  16. package/template/base/app/(auth)/signup/page.tsx +1 -0
  17. package/template/base/app/(shell)/account/page.tsx +5 -0
  18. package/template/base/app/(shell)/dashboard/dashboard-live-mount.tsx +21 -0
  19. package/template/base/app/(shell)/dashboard/page.tsx +1 -0
  20. package/template/base/app/(shell)/layout.tsx +24 -0
  21. package/template/base/app/(shell)/shell-layout-client.tsx +134 -0
  22. package/template/base/app/api/account/route.ts +2 -0
  23. package/template/base/app/auth/callback/route.ts +1 -0
  24. package/template/base/app/auth/cleanup/route.ts +1 -0
  25. package/template/base/app/layout.tsx +8 -2
  26. package/template/base/config/env.ts +98 -0
  27. package/template/module-manifests/marketing/brightweb.module.json +12 -0
  28. package/template/modules/marketing/app/(shell)/marketing/page.tsx +1 -0
  29. package/template/modules/marketing/app/api/marketing/_handlers.ts +144 -0
  30. package/template/modules/marketing/app/api/marketing/analytics/campaigns/[id]/route.ts +2 -0
  31. package/template/modules/marketing/app/api/marketing/analytics/overview/route.ts +2 -0
  32. package/template/modules/marketing/app/api/marketing/analytics/segments/[id]/route.ts +2 -0
  33. package/template/modules/marketing/app/api/marketing/campaigns/[id]/cancel/route.ts +2 -0
  34. package/template/modules/marketing/app/api/marketing/campaigns/[id]/recipients/route.ts +2 -0
  35. package/template/modules/marketing/app/api/marketing/campaigns/[id]/retry/route.ts +2 -0
  36. package/template/modules/marketing/app/api/marketing/campaigns/[id]/route.ts +2 -0
  37. package/template/modules/marketing/app/api/marketing/campaigns/[id]/schedule/route.ts +2 -0
  38. package/template/modules/marketing/app/api/marketing/campaigns/[id]/send/route.ts +2 -0
  39. package/template/modules/marketing/app/api/marketing/campaigns/[id]/test/route.ts +2 -0
  40. package/template/modules/marketing/app/api/marketing/campaigns/route.ts +2 -0
  41. package/template/modules/marketing/app/api/marketing/segments/[id]/preview/route.ts +2 -0
  42. package/template/modules/marketing/app/api/marketing/segments/[id]/route.ts +2 -0
  43. package/template/modules/marketing/app/api/marketing/segments/preview/route.ts +2 -0
  44. package/template/modules/marketing/app/api/marketing/segments/route.ts +2 -0
  45. package/template/modules/marketing/app/api/marketing/topics/route.ts +2 -0
  46. package/template/modules/marketing/app/api/marketing/unsubscribe/[token]/route.ts +2 -0
  47. package/template/modules/marketing/app/api/marketing/webhooks/resend/route.ts +2 -0
  48. package/template/modules/marketing/app/api/marketing/worker/route.ts +2 -0
  49. package/template/modules/marketing/app/api/marketing/workflows/[id]/activate/route.ts +2 -0
  50. package/template/modules/marketing/app/api/marketing/workflows/[id]/nodes/[nodeId]/route.ts +2 -0
  51. package/template/modules/marketing/app/api/marketing/workflows/[id]/nodes/route.ts +2 -0
  52. package/template/modules/marketing/app/api/marketing/workflows/[id]/pause/route.ts +2 -0
  53. package/template/modules/marketing/app/api/marketing/workflows/[id]/route.ts +2 -0
  54. package/template/modules/marketing/app/api/marketing/workflows/[id]/runs/route.ts +2 -0
  55. package/template/modules/marketing/app/api/marketing/workflows/route.ts +2 -0
  56. package/template/modules/projects/app/(shell)/projects/[id]/board/page.tsx +1 -0
  57. package/template/modules/projects/app/(shell)/projects/[id]/page.tsx +1 -0
  58. package/template/modules/projects/app/(shell)/projects/[id]/tasks/page.tsx +1 -0
  59. package/template/modules/projects/app/(shell)/projects/layout.tsx +5 -0
  60. package/template/modules/projects/app/(shell)/projects/page.tsx +1 -0
  61. package/template/modules/projects/app/(shell)/projects/projects-live-mounts.tsx +27 -0
  62. package/template/modules/projects/app/(shell)/projects/projects-server-mounts.tsx +54 -0
  63. package/template/modules/projects/app/api/projects/[id]/activity/route.ts +2 -0
  64. package/template/modules/projects/app/api/projects/[id]/links/[itemId]/route.ts +2 -0
  65. package/template/modules/projects/app/api/projects/[id]/links/route.ts +2 -0
  66. package/template/modules/projects/app/api/projects/[id]/members/route.ts +2 -0
  67. package/template/modules/projects/app/api/projects/[id]/milestones/[itemId]/route.ts +2 -0
  68. package/template/modules/projects/app/api/projects/[id]/milestones/route.ts +2 -0
  69. package/template/modules/projects/app/api/projects/[id]/route.ts +2 -0
  70. package/template/modules/projects/app/api/projects/[id]/tasks/[itemId]/route.ts +2 -0
  71. package/template/modules/projects/app/api/projects/[id]/tasks/route.ts +2 -0
  72. package/template/modules/projects/app/api/projects/_handlers.ts +86 -0
  73. package/template/modules/projects/app/api/projects/organizations/route.ts +2 -0
  74. package/template/modules/projects/app/api/projects/route.ts +2 -0
  75. package/template/modules/projects/app/api/projects/stats/route.ts +2 -0
  76. package/template/supabase/README.md +3 -0
  77. package/template/supabase/module-registry.json +6 -0
  78. package/template/supabase/modules/core/migrations/20260726180000_enable_rate_limit_counters_rls.sql +2 -0
  79. package/template/supabase/modules/marketing/README.md +12 -0
  80. package/template/supabase/modules/marketing/migrations/20260725120000_marketing_v1.sql +118 -0
  81. package/template/supabase/modules/marketing/migrations/20260725130000_marketing_campaigns.sql +133 -0
  82. package/template/supabase/modules/marketing/migrations/20260726120000_marketing_segments.sql +41 -0
  83. package/template/supabase/modules/marketing/migrations/20260726150000_marketing_workflows.sql +174 -0
  84. package/template/supabase/modules/marketing/migrations/20260726170000_marketing_engine_safety.sql +68 -0
  85. /package/template/modules/admin/app/{admin → (shell)/admin}/users/page.tsx +0 -0
  86. /package/template/modules/crm/app/{crm → (shell)/crm}/layout.tsx +0 -0
  87. /package/template/modules/crm/app/{crm → (shell)/crm}/page.tsx +0 -0
  88. /package/template/modules/crm/app/{crm → (shell)/crm}/report/page.tsx +0 -0
package/README.md CHANGED
@@ -66,11 +66,11 @@ Current updater behavior:
66
66
 
67
67
  - prompts for app type: `platform` or `site`
68
68
  - prompts for project name
69
- - prompts for optional platform modules: `admin`, `crm`, and `projects`
69
+ - prompts for optional platform modules: `admin`, `crm`, `marketing`, and `projects`
70
70
  - prompts to install dependencies immediately
71
71
  - copies a clean Next.js App Router starter template
72
72
  - platform apps include BrightWeb auth, shell wiring, and optional direct package mounts
73
- - generated route files contain direct `@brightweblabs/*` re-exports only
73
+ - generated route files are direct `@brightweblabs/*` re-exports or one-line exports from an app-owned dependency adapter such as Marketing's delivery handler composition
74
74
  - platform apps in published mode also write `supabase/config.toml`, a CLI-ready flat `supabase/migrations/` folder, `supabase/module-registry.json`, `supabase/clients/<slug>/stack.json`, and the resolved shared SQL migrations under `supabase/modules/<module>/migrations`
75
75
  - site apps include a thin Next.js and Tailwind CSS shell with no default feature page or local component library
76
76
  - writes `package.json`, `next.config.ts`, `.gitignore`, and `README.md` for both templates
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-bw-app",
3
3
  "private": false,
4
- "version": "0.13.2",
4
+ "version": "0.14.0",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-bw-app": "bin/create-bw-app.mjs",
@@ -27,6 +27,7 @@ export const MANAGED_APP_FILES = [
27
27
  export const MODULE_PACKAGES = {
28
28
  admin: "@brightweblabs/module-admin",
29
29
  crm: "@brightweblabs/module-crm",
30
+ marketing: "@brightweblabs/module-marketing",
30
31
  orgs: "@brightweblabs/module-orgs",
31
32
  projects: "@brightweblabs/module-projects",
32
33
  };
@@ -36,6 +37,7 @@ const FALLBACK_REQUIRES = {
36
37
  admin: { core: ">=0.3" },
37
38
  orgs: { core: ">=0.3", admin: ">=0.3" },
38
39
  crm: { core: ">=0.3", admin: ">=0.3", orgs: ">=0.1" },
40
+ marketing: { core: ">=0.3", admin: ">=0.3", orgs: ">=0.1", crm: ">=0.6" },
39
41
  projects: { core: ">=0.3", admin: ">=0.3", orgs: ">=0.1" },
40
42
  };
41
43
 
@@ -159,7 +161,7 @@ export async function createInitialAppManifest({ targetDir, slug, template, sele
159
161
  const modules = {};
160
162
  if (template === "platform") {
161
163
  for (const moduleKey of selectedModules) modules[moduleKey] = { version: moduleVersion(moduleKey, versionMap), installedAt: now, exposed: true };
162
- if ((selectedModules.includes("crm") || selectedModules.includes("projects")) && !modules.orgs) {
164
+ if ((selectedModules.includes("crm") || selectedModules.includes("marketing") || selectedModules.includes("projects")) && !modules.orgs) {
163
165
  modules.orgs = { version: moduleVersion("orgs", versionMap), installedAt: now, exposed: true };
164
166
  }
165
167
  }
@@ -250,7 +252,7 @@ export async function readConfiguredModuleFlags(targetDir) {
250
252
  if (!(await pathExists(filePath))) return {};
251
253
  const content = await fs.readFile(filePath, "utf8");
252
254
  const flags = {};
253
- for (const key of ["core-auth", "orgs", "crm", "projects", "admin"]) {
255
+ for (const key of ["core-auth", "orgs", "crm", "marketing", "projects", "admin"]) {
254
256
  const match = content.match(new RegExp(`key:\\s*"${key}"[\\s\\S]*?enabled:\\s*(true|false)`));
255
257
  if (match) flags[key === "core-auth" ? "core" : key] = match[1] === "true";
256
258
  }
package/src/constants.mjs CHANGED
@@ -21,6 +21,12 @@ export const SELECTABLE_MODULES = [
21
21
  packageName: "@brightweblabs/module-crm",
22
22
  templateFolder: "crm",
23
23
  },
24
+ {
25
+ key: "marketing",
26
+ label: "Marketing",
27
+ packageName: "@brightweblabs/module-marketing",
28
+ templateFolder: "marketing",
29
+ },
24
30
  {
25
31
  key: "projects",
26
32
  label: "Projects",
@@ -55,12 +61,12 @@ export const MODULE_STARTER_FILES = {
55
61
  admin: [
56
62
  "app/api/admin/users/route.ts",
57
63
  "app/api/admin/users/roles/route.ts",
58
- "app/admin/users/page.tsx",
64
+ "app/(shell)/admin/users/page.tsx",
59
65
  ],
60
66
  crm: [
61
- "app/crm/layout.tsx",
62
- "app/crm/page.tsx",
63
- "app/crm/report/page.tsx",
67
+ "app/(shell)/crm/layout.tsx",
68
+ "app/(shell)/crm/page.tsx",
69
+ "app/(shell)/crm/report/page.tsx",
64
70
  "app/api/crm/contacts/route.ts",
65
71
  "app/api/crm/organizations/route.ts",
66
72
  "app/api/crm/owners/route.ts",
@@ -68,36 +74,107 @@ export const MODULE_STARTER_FILES = {
68
74
  "app/api/crm/stats/route.ts",
69
75
  "app/api/crm/timeline/route.ts",
70
76
  ],
77
+ marketing: [
78
+ "app/(shell)/marketing/page.tsx",
79
+ "app/api/marketing/_handlers.ts",
80
+ "app/api/marketing/analytics/campaigns/[id]/route.ts",
81
+ "app/api/marketing/analytics/overview/route.ts",
82
+ "app/api/marketing/analytics/segments/[id]/route.ts",
83
+ "app/api/marketing/campaigns/[id]/cancel/route.ts",
84
+ "app/api/marketing/campaigns/[id]/recipients/route.ts",
85
+ "app/api/marketing/campaigns/[id]/retry/route.ts",
86
+ "app/api/marketing/campaigns/[id]/route.ts",
87
+ "app/api/marketing/campaigns/[id]/schedule/route.ts",
88
+ "app/api/marketing/campaigns/[id]/send/route.ts",
89
+ "app/api/marketing/campaigns/[id]/test/route.ts",
90
+ "app/api/marketing/campaigns/route.ts",
91
+ "app/api/marketing/segments/[id]/preview/route.ts",
92
+ "app/api/marketing/segments/[id]/route.ts",
93
+ "app/api/marketing/segments/preview/route.ts",
94
+ "app/api/marketing/segments/route.ts",
95
+ "app/api/marketing/topics/route.ts",
96
+ "app/api/marketing/unsubscribe/[token]/route.ts",
97
+ "app/api/marketing/webhooks/resend/route.ts",
98
+ "app/api/marketing/worker/route.ts",
99
+ "app/api/marketing/workflows/[id]/activate/route.ts",
100
+ "app/api/marketing/workflows/[id]/nodes/[nodeId]/route.ts",
101
+ "app/api/marketing/workflows/[id]/nodes/route.ts",
102
+ "app/api/marketing/workflows/[id]/pause/route.ts",
103
+ "app/api/marketing/workflows/[id]/route.ts",
104
+ "app/api/marketing/workflows/[id]/runs/route.ts",
105
+ "app/api/marketing/workflows/route.ts",
106
+ ],
71
107
  projects: [
108
+ "app/(shell)/projects/layout.tsx",
109
+ "app/(shell)/projects/page.tsx",
110
+ "app/(shell)/projects/projects-live-mounts.tsx",
111
+ "app/(shell)/projects/projects-server-mounts.tsx",
112
+ "app/(shell)/projects/[id]/page.tsx",
113
+ "app/(shell)/projects/[id]/board/page.tsx",
114
+ "app/(shell)/projects/[id]/tasks/page.tsx",
115
+ "app/api/projects/_handlers.ts",
116
+ "app/api/projects/route.ts",
117
+ "app/api/projects/stats/route.ts",
118
+ "app/api/projects/organizations/route.ts",
119
+ "app/api/projects/[id]/route.ts",
120
+ "app/api/projects/[id]/activity/route.ts",
121
+ "app/api/projects/[id]/members/route.ts",
122
+ "app/api/projects/[id]/links/route.ts",
123
+ "app/api/projects/[id]/links/[itemId]/route.ts",
124
+ "app/api/projects/[id]/milestones/route.ts",
125
+ "app/api/projects/[id]/milestones/[itemId]/route.ts",
126
+ "app/api/projects/[id]/tasks/route.ts",
127
+ "app/api/projects/[id]/tasks/[itemId]/route.ts",
72
128
  ],
73
129
  };
74
130
 
75
131
  export const PLATFORM_STARTER_FILES = [
132
+ "app/(auth)/auth-provider.tsx",
133
+ "app/(auth)/layout.tsx",
134
+ "app/(auth)/login/page.tsx",
135
+ "app/(auth)/signup/page.tsx",
136
+ "app/(auth)/forgot-password/page.tsx",
137
+ "app/(auth)/reset-password/page.tsx",
138
+ "app/(auth)/auth/post-login/page.tsx",
139
+ "app/(auth)/auth/confirmed/page.tsx",
140
+ "app/(auth)/invite/[invitationId]/page.tsx",
141
+ "app/(auth)/invite/[invitationId]/invite-route-mount.tsx",
142
+ "app/(shell)/layout.tsx",
143
+ "app/(shell)/shell-layout-client.tsx",
144
+ "app/(shell)/account/page.tsx",
145
+ "app/(shell)/dashboard/dashboard-live-mount.tsx",
146
+ "app/(shell)/dashboard/page.tsx",
147
+ "app/api/account/route.ts",
148
+ "app/auth/callback/route.ts",
149
+ "app/auth/cleanup/route.ts",
150
+ "app/layout.tsx",
151
+ "config/env.ts",
76
152
  "config/shell.overrides.ts",
77
153
  ];
78
154
 
79
155
  export const APP_DEPENDENCY_DEFAULTS = {
80
- "@brightweblabs/app-shell": "^0.6.0",
81
- "@brightweblabs/core-auth": "^0.4.0",
82
- "@brightweblabs/infra": "^0.3.2",
83
- "@brightweblabs/module-admin": "^0.5.0",
84
- "@brightweblabs/module-crm": "^0.8.0",
85
- "@brightweblabs/module-orgs": "^0.3.0",
86
- "@brightweblabs/module-projects": "^0.5.0",
156
+ "@brightweblabs/app-shell": "^0.6.1",
157
+ "@brightweblabs/core-auth": "^0.5.1",
158
+ "@brightweblabs/infra": "^0.3.3",
159
+ "@brightweblabs/module-admin": "^0.5.2",
160
+ "@brightweblabs/module-crm": "^0.8.2",
161
+ "@brightweblabs/module-marketing": "^0.2.1",
162
+ "@brightweblabs/module-orgs": "^0.3.2",
163
+ "@brightweblabs/module-projects": "^0.6.1",
87
164
  "@brightweblabs/theme": "^0.4.0",
88
165
  "@brightweblabs/ui": "^1.1.0",
89
166
  "geist": "1.7.2",
90
167
  "lucide-react": "^1.8.0",
91
- "next": "16.1.6",
92
- "react": "19.2.4",
93
- "react-dom": "19.2.4",
168
+ "next": "^16.0.0",
169
+ "react": "^19.0.0",
170
+ "react-dom": "^19.0.0",
94
171
  };
95
172
 
96
173
  export const SITE_DEPENDENCY_DEFAULTS = {
97
174
  "geist": "1.7.2",
98
- "next": "16.1.6",
99
- "react": "19.2.4",
100
- "react-dom": "19.2.4",
175
+ "next": "^16.0.0",
176
+ "react": "^19.0.0",
177
+ "react-dom": "^19.0.0",
101
178
  };
102
179
 
103
180
  export const APP_DEV_DEPENDENCY_DEFAULTS = {
@@ -131,7 +208,7 @@ Usage:
131
208
  Scaffold options:
132
209
  --template <platform|site> Scaffold a platform app or a standalone site
133
210
  --name, --slug <name> Project name and default directory name
134
- --modules <list> Comma-separated modules: crm,projects,admin
211
+ --modules <list> Comma-separated modules: crm,marketing,projects,admin
135
212
  --output-dir <path> Parent folder for the generated app
136
213
  --target-dir <path> Exact output directory, bypassing slug folder creation
137
214
  --workspace-root <path> BrightWeb workspace root for local mode
package/src/doctor.mjs CHANGED
@@ -7,6 +7,34 @@ import { pathExists, readJsonIfPresent } from "./generator.mjs";
7
7
  import { scaffoldDrift } from "./scaffold.mjs";
8
8
 
9
9
  const HELP = `Usage: bw doctor [options]\n\nOptions:\n --target-dir <path> App directory (defaults to cwd)\n --workspace-root <path> BrightWeb workspace root\n --strict Treat warnings as failures\n --report Stamp lastDoctor in the app manifest\n --help Show this help`;
10
+ const RUNTIME_PACKAGE_NAMES = ["react", "react-dom", "next"];
11
+
12
+ export async function findInstalledRuntimeVersions(targetDir) {
13
+ const storeDir = path.join(targetDir, "node_modules", ".pnpm");
14
+ if (!(await pathExists(storeDir))) {
15
+ return Object.fromEntries(RUNTIME_PACKAGE_NAMES.map((packageName) => [packageName, []]));
16
+ }
17
+
18
+ const entries = await fs.readdir(storeDir, { withFileTypes: true });
19
+ const resolved = {};
20
+ for (const packageName of RUNTIME_PACKAGE_NAMES) {
21
+ const versions = new Set();
22
+ for (const entry of entries) {
23
+ if (!entry.isDirectory() || !entry.name.startsWith(`${packageName}@`)) continue;
24
+ const manifest = await readJsonIfPresent(
25
+ path.join(storeDir, entry.name, "node_modules", packageName, "package.json"),
26
+ );
27
+ const fallbackVersion = entry.name
28
+ .slice(packageName.length + 1)
29
+ .split("_", 1)[0]
30
+ .split("(", 1)[0];
31
+ const version = manifest?.version || fallbackVersion;
32
+ if (version) versions.add(version);
33
+ }
34
+ resolved[packageName] = Array.from(versions).sort();
35
+ }
36
+ return resolved;
37
+ }
10
38
 
11
39
  export async function doctorBrightwebApp(argvOptions = {}, runtimeOptions = {}) {
12
40
  if (argvOptions.help) { output.write(`${HELP}\n`); return { help: true, ok: true }; }
@@ -35,6 +63,16 @@ export async function doctorBrightwebApp(argvOptions = {}, runtimeOptions = {})
35
63
  }
36
64
  add(packageProblems.length ? "FAIL" : "PASS", "packages", packageProblems.join("; ") || "Installed module packages agree with the manifest.");
37
65
 
66
+ const runtimeVersions = await findInstalledRuntimeVersions(targetDir);
67
+ const duplicateRuntimes = Object.entries(runtimeVersions)
68
+ .filter(([, versions]) => versions.length > 1)
69
+ .map(([packageName, versions]) => `${packageName}: ${versions.join(", ")}`);
70
+ add(
71
+ duplicateRuntimes.length ? "FAIL" : "PASS",
72
+ "runtime-singletons",
73
+ duplicateRuntimes.join("; ") || "React, React DOM, and Next resolve to at most one version each.",
74
+ );
75
+
38
76
  const flags = await readConfiguredModuleFlags(targetDir);
39
77
  const exposureProblems = Object.entries(appManifest.modules || {}).filter(([key, entry]) => typeof flags[key] === "boolean" && flags[key] !== entry.exposed).map(([key, entry]) => `${key}: manifest exposed=${entry.exposed}, config enabled=${String(flags[key])}`);
40
78
  add(exposureProblems.length ? "FAIL" : "PASS", "exposure", exposureProblems.join("; ") || "Module exposure flags agree.");
package/src/generator.mjs CHANGED
@@ -28,6 +28,7 @@ const DEFAULT_DB_MODULE_REGISTRY = {
28
28
  admin: { label: "Admin", dependsOn: ["core"] },
29
29
  orgs: { label: "Organizations", dependsOn: ["core", "admin"] },
30
30
  crm: { label: "CRM", dependsOn: ["core", "admin", "orgs"] },
31
+ marketing: { label: "Marketing", dependsOn: ["core", "admin", "orgs", "crm"] },
31
32
  projects: { label: "Projects", dependsOn: ["core", "admin", "orgs"] },
32
33
  },
33
34
  };
@@ -251,6 +252,7 @@ export async function getVersionMap(workspaceRoot) {
251
252
  "@brightweblabs/infra",
252
253
  "@brightweblabs/module-admin",
253
254
  "@brightweblabs/module-crm",
255
+ "@brightweblabs/module-marketing",
254
256
  "@brightweblabs/module-orgs",
255
257
  "@brightweblabs/module-projects",
256
258
  "@brightweblabs/theme",
@@ -264,6 +266,10 @@ export async function getVersionMap(workspaceRoot) {
264
266
  }
265
267
  }
266
268
 
269
+ versionMap.next = APP_DEPENDENCY_DEFAULTS.next;
270
+ versionMap.react = APP_DEPENDENCY_DEFAULTS.react;
271
+ versionMap["react-dom"] = APP_DEPENDENCY_DEFAULTS["react-dom"];
272
+
267
273
  return versionMap;
268
274
  }
269
275
 
@@ -304,10 +310,10 @@ function createPlatformBrandConfigFile({ slug, brandValues }) {
304
310
 
305
311
  export function createPlatformModulesConfigFile(selectedModules) {
306
312
  const selected = new Set(selectedModules);
307
- const orgsEnabled = selected.has("crm") || selected.has("projects");
313
+ const orgsEnabled = selected.has("crm") || selected.has("marketing") || selected.has("projects");
308
314
 
309
315
  return [
310
- 'export type StarterModuleKey = "core-auth" | "orgs" | "crm" | "projects" | "admin";',
316
+ 'export type StarterModuleKey = "core-auth" | "orgs" | "crm" | "marketing" | "projects" | "admin";',
311
317
  "",
312
318
  "export type StarterModuleConfig = {",
313
319
  " key: StarterModuleKey;",
@@ -331,7 +337,7 @@ export function createPlatformModulesConfigFile(selectedModules) {
331
337
  " {",
332
338
  ' key: "orgs",',
333
339
  ' label: "Organizations",',
334
- ' description: "Shared organizations, membership, and invitation foundation for CRM and Projects.",',
340
+ ' description: "Shared organizations, membership, and invitation foundation for CRM, Marketing, and Projects.",',
335
341
  ` enabled: ${String(orgsEnabled)},`,
336
342
  ' packageName: "@brightweblabs/module-orgs",',
337
343
  ' placement: "hidden",',
@@ -346,11 +352,21 @@ export function createPlatformModulesConfigFile(selectedModules) {
346
352
  ' placement: "primary",',
347
353
  " },",
348
354
  " {",
355
+ ' key: "marketing",',
356
+ ' label: "Marketing",',
357
+ ' description: "Consent-first campaigns, segments, analytics, and automated email workflows.",',
358
+ ` enabled: ${String(selected.has("marketing"))},`,
359
+ ' packageName: "@brightweblabs/module-marketing",',
360
+ ' routeHref: "/marketing",',
361
+ ' placement: "primary",',
362
+ " },",
363
+ " {",
349
364
  ' key: "projects",',
350
365
  ' label: "Projects",',
351
366
  ' description: "Project portfolio, detail routes, and work-management server logic.",',
352
367
  ` enabled: ${String(selected.has("projects"))},`,
353
368
  ' packageName: "@brightweblabs/module-projects",',
369
+ ' routeHref: "/projects",',
354
370
  ' placement: "primary",',
355
371
  " },",
356
372
  " {",
@@ -374,15 +390,20 @@ export function createPlatformModulesConfigFile(selectedModules) {
374
390
  function createEnvFileContent() {
375
391
  return [
376
392
  "NEXT_PUBLIC_APP_URL=http://localhost:3000",
393
+ "PUBLIC_APP_URL=http://localhost:3000",
377
394
  "NEXT_PUBLIC_SUPABASE_URL=",
378
395
  "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=",
379
396
  "SUPABASE_SECRET_DEFAULT_KEY=",
380
397
  "RESEND_API_KEY=",
398
+ "RESEND_WEBHOOK_SECRET=",
381
399
  "RESEND_FROM_TRANSACTIONAL=",
382
400
  "RESEND_FROM_MARKETING=",
383
- "CONTACT_TO_EMAIL=",
384
- "RESEND_WEBHOOK_SECRET=",
401
+ "RESEND_FROM_EMAIL=",
402
+ "RESEND_FROM_NAME=",
403
+ "MARKETING_FROM_EMAIL=",
404
+ "MARKETING_FROM_NAME=BrightWeb",
385
405
  "MARKETING_WORKER_SECRET=",
406
+ "CONTACT_TO_EMAIL=",
386
407
  "MARKETING_TEST_EMAIL=",
387
408
  "",
388
409
  ].join("\n");
@@ -436,11 +457,21 @@ function createGitignore() {
436
457
  }
437
458
 
438
459
  function getPlatformStarterRoutes(selectedModules) {
439
- return selectedModules.flatMap((moduleKey) => {
440
- if (moduleKey === "crm") return ["/crm"];
441
- if (moduleKey === "admin") return ["/admin/users"];
442
- return [];
443
- });
460
+ return [
461
+ "/login",
462
+ "/signup",
463
+ "/forgot-password",
464
+ "/reset-password",
465
+ "/account",
466
+ "/dashboard",
467
+ ...selectedModules.flatMap((moduleKey) => {
468
+ if (moduleKey === "crm") return ["/crm"];
469
+ if (moduleKey === "marketing") return ["/marketing"];
470
+ if (moduleKey === "projects") return ["/projects"];
471
+ if (moduleKey === "admin") return ["/admin/users"];
472
+ return [];
473
+ }),
474
+ ];
444
475
  }
445
476
 
446
477
  function getSiteStarterRoutes() {
@@ -675,7 +706,7 @@ export function createAppContextFile({
675
706
  ],
676
707
  packageOwned: [
677
708
  ...CORE_PACKAGES,
678
- ...(selectedModules.includes("crm") || selectedModules.includes("projects") ? [ORGS_PACKAGE_NAME] : []),
709
+ ...(selectedModules.includes("crm") || selectedModules.includes("marketing") || selectedModules.includes("projects") ? [ORGS_PACKAGE_NAME] : []),
679
710
  ...SELECTABLE_MODULES
680
711
  .filter((moduleDefinition) => selectedModules.includes(moduleDefinition.key))
681
712
  .map((moduleDefinition) => moduleDefinition.packageName),
@@ -748,7 +779,7 @@ export function createPackageJson({
748
779
  dependencies[moduleDefinition.packageName] = internalDependencyVersion(moduleDefinition.packageName);
749
780
  }
750
781
  }
751
- if (selectedModules.includes("crm") || selectedModules.includes("projects")) {
782
+ if (selectedModules.includes("crm") || selectedModules.includes("marketing") || selectedModules.includes("projects")) {
752
783
  dependencies[ORGS_PACKAGE_NAME] = internalDependencyVersion(ORGS_PACKAGE_NAME);
753
784
  }
754
785
 
@@ -787,7 +818,7 @@ export function createNextConfig({ template, selectedModules }) {
787
818
  }
788
819
 
789
820
  const transpilePackages = [...CORE_PACKAGES];
790
- if (selectedModules.includes("crm") || selectedModules.includes("projects")) {
821
+ if (selectedModules.includes("crm") || selectedModules.includes("marketing") || selectedModules.includes("projects")) {
791
822
  transpilePackages.push(ORGS_PACKAGE_NAME);
792
823
  }
793
824
 
@@ -836,7 +867,7 @@ export function createShellConfig(selectedModules) {
836
867
  const importLines = [];
837
868
  const registrationLines = [];
838
869
 
839
- if (selectedModules.includes("crm") || selectedModules.includes("projects")) {
870
+ if (selectedModules.includes("crm") || selectedModules.includes("marketing") || selectedModules.includes("projects")) {
840
871
  importLines.push('import { orgsModuleRegistration } from "@brightweblabs/module-orgs/registration";');
841
872
  registrationLines.push(' if (enabled.has("orgs")) registrations.push(orgsModuleRegistration);');
842
873
  }
@@ -851,9 +882,19 @@ export function createShellConfig(selectedModules) {
851
882
  registrationLines.push(' if (enabled.has("crm")) registrations.push(crmModuleRegistration);');
852
883
  }
853
884
 
885
+ if (selectedModules.includes("marketing")) {
886
+ importLines.push('import { marketingModuleRegistration } from "@brightweblabs/module-marketing/registration";');
887
+ registrationLines.push(' if (enabled.has("marketing")) registrations.push(marketingModuleRegistration);');
888
+ }
889
+
890
+ if (selectedModules.includes("projects")) {
891
+ importLines.push('import { projectsPreviewModuleRegistration } from "@brightweblabs/module-projects/registration";');
892
+ registrationLines.push(' if (enabled.has("projects")) registrations.push(projectsPreviewModuleRegistration);');
893
+ }
894
+
854
895
  return [
855
896
  "// MANAGED BY BRIGHTWEB — regenerated by create-bw-app update; put customizations in config/shell.overrides.ts",
856
- 'import { Wrench } from "lucide-react";',
897
+ 'import { LayoutDashboard, Wrench } from "lucide-react";',
857
898
  "import {",
858
899
  " applyShellRegistrationOverrides,",
859
900
  " buildClientAppShellRegistration,",
@@ -867,9 +908,16 @@ export function createShellConfig(selectedModules) {
867
908
  'import { getEnabledStarterModules } from "./modules";',
868
909
  'import { shellRegistrationOverrides } from "./shell.overrides";',
869
910
  "",
911
+ "const dashboardModuleRegistration: ShellModuleRegistration<ShellContextualAction> = {",
912
+ ' key: "dashboard",',
913
+ ' placement: "primary",',
914
+ ' navItems: [{ href: "/dashboard", label: "Dashboard", icon: LayoutDashboard }],',
915
+ ' toolbarRoutes: [{ surface: "dashboard", match: { exact: ["/dashboard"] } }],',
916
+ "};",
917
+ "",
870
918
  "function getStarterModuleRegistrations() {",
871
919
  ' const enabled = new Set(getEnabledStarterModules().map((moduleConfig) => moduleConfig.key));',
872
- " const registrations: ShellModuleRegistration<ShellContextualAction>[] = [];",
920
+ " const registrations: ShellModuleRegistration<ShellContextualAction>[] = [dashboardModuleRegistration];",
873
921
  "",
874
922
  ...registrationLines,
875
923
  "",
@@ -923,6 +971,8 @@ export function createShellConfig(selectedModules) {
923
971
  " shellConfig: builtRegistration.shellConfig,",
924
972
  " shellPreview,",
925
973
  " toolbarRoutes: builtRegistration.toolbarRoutes,",
974
+ " toolbarActions: builtRegistration.toolbarActions,",
975
+ " dashboardContributions: builtRegistration.dashboardContributions,",
926
976
  " };",
927
977
  "}",
928
978
  "",
@@ -1430,13 +1480,19 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
1430
1480
  workspaceMode,
1431
1481
  registry: dbModuleRegistry,
1432
1482
  });
1483
+ const requestedModules = new Set(answers.selectedModules);
1484
+ const selectedModules = dbInstallPlan.resolvedOrder.filter(
1485
+ (moduleKey) =>
1486
+ SELECTABLE_MODULES.some((moduleDefinition) => moduleDefinition.key === moduleKey)
1487
+ && (moduleKey !== "admin" || requestedModules.has("admin")),
1488
+ );
1433
1489
  const install = answers.install && !argvOptions.dryRun;
1434
1490
 
1435
1491
  if (argvOptions.dryRun) {
1436
1492
  output.write(`${renderPlanSummary({
1437
1493
  targetDir,
1438
1494
  dependencyMode,
1439
- selectedModules: answers.selectedModules,
1495
+ selectedModules,
1440
1496
  packageManager,
1441
1497
  workspaceMode,
1442
1498
  install: answers.install,
@@ -1456,7 +1512,7 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
1456
1512
  output.write(`${renderPlanSummary({
1457
1513
  targetDir,
1458
1514
  dependencyMode,
1459
- selectedModules: answers.selectedModules,
1515
+ selectedModules,
1460
1516
  packageManager,
1461
1517
  workspaceMode,
1462
1518
  install: answers.install,
@@ -1476,7 +1532,7 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
1476
1532
  } else {
1477
1533
  await scaffoldPlatformProject({
1478
1534
  targetDir,
1479
- selectedModules: answers.selectedModules,
1535
+ selectedModules,
1480
1536
  versionMap,
1481
1537
  dependencyMode,
1482
1538
  packageManager,
@@ -0,0 +1 @@
1
+ export { AuthConfirmedPage as default } from "@brightweblabs/core-auth/ui";
@@ -0,0 +1 @@
1
+ export { PostLoginPage as default } from "@brightweblabs/core-auth/ui";
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import { AuthUiProvider, createAuthUiClient } from "@brightweblabs/core-auth/ui";
4
+ import { starterBrandConfig } from "../../config/brand";
5
+
6
+ const authClient = createAuthUiClient();
7
+
8
+ export function AuthProvider({ children }: { children: React.ReactNode }) {
9
+ return (
10
+ <AuthUiProvider
11
+ client={authClient}
12
+ signupMode="invite-only"
13
+ layoutVariant="split"
14
+ brand={{
15
+ companyName: starterBrandConfig.companyName,
16
+ helpHref: `mailto:${starterBrandConfig.supportEmail}`,
17
+ splitHeadline: "Operações claras. Decisões com contexto.",
18
+ splitDescription: "Um espaço reservado para equipas e clientes acompanharem projetos, relações e trabalho em curso.",
19
+ logo: <img src="/brand/logo-dark.svg" alt={starterBrandConfig.companyName} width={176} height={44} />,
20
+ }}
21
+ >
22
+ {children}
23
+ </AuthUiProvider>
24
+ );
25
+ }
@@ -0,0 +1 @@
1
+ export { ForgotPasswordPage as default } from "@brightweblabs/core-auth/ui";
@@ -0,0 +1,10 @@
1
+ import { InvitePage } from "@brightweblabs/core-auth/ui";
2
+
3
+ export async function InviteRouteMount({
4
+ params,
5
+ }: {
6
+ params: Promise<{ invitationId: string }>;
7
+ }) {
8
+ const { invitationId } = await params;
9
+ return <InvitePage invitationId={invitationId} />;
10
+ }
@@ -0,0 +1 @@
1
+ export { InviteRouteMount as default } from "./invite-route-mount";
@@ -0,0 +1,5 @@
1
+ import { AuthProvider } from "./auth-provider";
2
+
3
+ export default function AuthLayout({ children }: { children: React.ReactNode }) {
4
+ return <AuthProvider>{children}</AuthProvider>;
5
+ }
@@ -0,0 +1 @@
1
+ export { LoginPage as default } from "@brightweblabs/core-auth/ui";
@@ -0,0 +1 @@
1
+ export { ResetPasswordPage as default } from "@brightweblabs/core-auth/ui";
@@ -0,0 +1 @@
1
+ export { SignupPage as default } from "@brightweblabs/core-auth/ui";
@@ -0,0 +1,5 @@
1
+ import { AccountPage } from "@brightweblabs/core-auth";
2
+
3
+ export default function Page() {
4
+ return <AccountPage />;
5
+ }
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ import { AppDashboard, type DashboardDataClient } from "@brightweblabs/app-shell";
4
+ import { getStarterShellConfig } from "../../../config/shell";
5
+
6
+ async function getJson(path: string) {
7
+ const response = await fetch(path);
8
+ return response.json();
9
+ }
10
+
11
+ const dashboardClient: DashboardDataClient = {
12
+ getOverview: () => Promise.reject(new Error("No aggregate dashboard endpoint configured.")),
13
+ getProjects: () => getJson("/api/projects/stats"),
14
+ getCrm: () => getJson("/api/crm/stats"),
15
+ getTasks: () => Promise.reject(new Error("No task dashboard endpoint configured.")),
16
+ };
17
+
18
+ export function DashboardLiveMount() {
19
+ const { dashboardContributions } = getStarterShellConfig();
20
+ return <AppDashboard client={dashboardClient} contributions={dashboardContributions} />;
21
+ }
@@ -0,0 +1 @@
1
+ export { DashboardLiveMount as default } from "./dashboard-live-mount";
@@ -0,0 +1,24 @@
1
+ import type { ReactNode } from "react";
2
+ import { requireServerPageAccess } from "@brightweblabs/core-auth/server";
3
+ import { ShellLayoutClient, type ShellViewer } from "./shell-layout-client";
4
+
5
+ export const dynamic = "force-dynamic";
6
+
7
+ export default async function ShellLayout({ children }: Readonly<{ children: ReactNode }>) {
8
+ const { supabase, user, role } = await requireServerPageAccess();
9
+ const { data: profile } = await supabase
10
+ .from("profiles")
11
+ .select("first_name, last_name")
12
+ .eq("user_id", user.id)
13
+ .maybeSingle<{ first_name: string | null; last_name: string | null }>();
14
+
15
+ const viewer: ShellViewer = {
16
+ email: user.email ?? null,
17
+ firstName: profile?.first_name ?? null,
18
+ lastName: profile?.last_name ?? null,
19
+ isAdmin: role === "admin",
20
+ isStaff: role === "staff" || role === "admin",
21
+ };
22
+
23
+ return <ShellLayoutClient viewer={viewer}>{children}</ShellLayoutClient>;
24
+ }