@clubedge/create-clubedge-app 0.9.0 → 0.11.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 (104) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +14 -12
  3. package/dist/cli/args.js +56 -27
  4. package/dist/cli/ui.js +27 -6
  5. package/dist/core/env.js +10 -3
  6. package/dist/index.js +19 -4
  7. package/package.json +3 -3
  8. package/template/.env.example +20 -0
  9. package/template/.github/workflows/ci.yml +18 -0
  10. package/template/.github/workflows/deploy.yml +109 -0
  11. package/template/Dockerfile +5 -0
  12. package/template/README.md +21 -1
  13. package/template/SETUP.md +122 -5
  14. package/template/_gitignore +3 -0
  15. package/template/apps/start/.env.example +20 -0
  16. package/template/apps/start/Dockerfile +5 -0
  17. package/template/apps/start/netlify.toml +6 -0
  18. package/template/apps/start/package.json +4 -1
  19. package/template/apps/start/scripts/check-services.ts +38 -0
  20. package/template/apps/start/src/components/auth/auth-form-controls.tsx +8 -2
  21. package/template/apps/start/src/components/auth/auth-page.tsx +50 -0
  22. package/template/apps/start/src/components/auth/notice.tsx +33 -0
  23. package/template/apps/start/src/env/auth.ts +9 -1
  24. package/template/apps/start/src/lib/login-url.ts +10 -1
  25. package/template/apps/start/src/routes/api/ready.ts +23 -0
  26. package/template/apps/start/src/routes/auth/forgot-password.ts +11 -0
  27. package/template/apps/start/src/routes/auth/reset-password.ts +11 -0
  28. package/template/apps/start/src/routes/forgot-password.tsx +46 -0
  29. package/template/apps/start/src/routes/login.tsx +23 -32
  30. package/template/apps/start/src/routes/reset-password.tsx +52 -0
  31. package/template/apps/start/src/server/auth-email.ts +18 -0
  32. package/template/apps/start/src/server/auth-forms.ts +58 -2
  33. package/template/apps/start/src/server/auth.functions.ts +9 -5
  34. package/template/apps/start/src/server/email.ts +39 -0
  35. package/template/apps/start/src/server/variants/auth.better-auth.ts +8 -1
  36. package/template/apps/start/src/server/variants/email.resend.ts +39 -0
  37. package/template/apps/start/vercel.json +5 -0
  38. package/template/apps/web/netlify.toml +6 -0
  39. package/template/apps/web/package.json +4 -1
  40. package/template/apps/web/scripts/check-services.ts +38 -0
  41. package/template/apps/web/src/app/actions/auth.ts +49 -2
  42. package/template/apps/web/src/app/api/ready/route.ts +18 -0
  43. package/template/apps/web/src/app/auth/callback/route.ts +11 -2
  44. package/template/apps/web/src/app/forgot-password/page.tsx +40 -0
  45. package/template/apps/web/src/app/login/_components/auth-form-controls.tsx +8 -2
  46. package/template/apps/web/src/app/login/_components/auth-page.tsx +50 -0
  47. package/template/apps/web/src/app/login/_components/notice.tsx +33 -0
  48. package/template/apps/web/src/app/login/page.tsx +23 -32
  49. package/template/apps/web/src/app/reset-password/page.tsx +47 -0
  50. package/template/apps/web/src/env/auth.ts +4 -0
  51. package/template/apps/web/src/lib/login-url.ts +10 -1
  52. package/template/apps/web/src/server/auth-email.ts +18 -0
  53. package/template/apps/web/src/server/email.ts +39 -0
  54. package/template/apps/web/src/server/variants/auth.better-auth.ts +8 -1
  55. package/template/apps/web/src/server/variants/email.resend.ts +39 -0
  56. package/template/apps/web/vercel.json +5 -0
  57. package/template/clubedge.template.json +80 -2
  58. package/template/docker-compose.dev.yml +13 -0
  59. package/template/e2e/email-verification.spec.ts +29 -0
  60. package/template/e2e/inbox.ts +53 -0
  61. package/template/e2e/password-reset.spec.ts +48 -0
  62. package/template/e2e/ready.spec.ts +11 -0
  63. package/template/fly.toml +25 -0
  64. package/template/package.json +2 -1
  65. package/template/packages/auth/package.json +1 -1
  66. package/template/packages/auth/src/types.ts +19 -1
  67. package/template/packages/auth-better-auth/package.json +7 -1
  68. package/template/packages/auth-better-auth/src/emails.ts +78 -0
  69. package/template/packages/auth-better-auth/src/index.test.ts +133 -2
  70. package/template/packages/auth-better-auth/src/index.ts +135 -7
  71. package/template/packages/auth-supabase/package.json +1 -1
  72. package/template/packages/auth-supabase/src/index.test.ts +45 -0
  73. package/template/packages/auth-supabase/src/index.ts +12 -0
  74. package/template/packages/cache/package.json +1 -1
  75. package/template/packages/cache-redis/package.json +1 -1
  76. package/template/packages/core/package.json +1 -1
  77. package/template/packages/db/package.json +7 -1
  78. package/template/packages/db/src/client.test.ts +28 -0
  79. package/template/packages/db/src/client.ts +19 -0
  80. package/template/packages/db/src/index.ts +1 -1
  81. package/template/packages/email/package.json +18 -0
  82. package/template/packages/email/src/index.test.ts +57 -0
  83. package/template/packages/email/src/index.ts +50 -0
  84. package/template/packages/email/tsconfig.json +4 -0
  85. package/template/packages/email-resend/package.json +21 -0
  86. package/template/packages/email-resend/src/index.test.ts +56 -0
  87. package/template/packages/email-resend/src/index.ts +56 -0
  88. package/template/packages/email-resend/tsconfig.json +4 -0
  89. package/template/packages/email-smtp/package.json +22 -0
  90. package/template/packages/email-smtp/src/index.test.ts +73 -0
  91. package/template/packages/email-smtp/src/index.ts +26 -0
  92. package/template/packages/email-smtp/tsconfig.json +4 -0
  93. package/template/packages/local-db/package.json +1 -1
  94. package/template/packages/storage/package.json +1 -1
  95. package/template/packages/storage-s3/package.json +1 -1
  96. package/template/packages/storage-supabase/package.json +1 -1
  97. package/template/packages/ui/package.json +1 -1
  98. package/template/pnpm-lock.yaml +56 -0
  99. package/template/railway.json +13 -0
  100. package/template/render.yaml +66 -0
  101. package/template/scripts/build-migrator.mjs +51 -0
  102. package/template/scripts/migrate.mjs +30 -0
  103. package/template/supabase/config.toml +6 -1
  104. package/template.lock.json +48 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to `create-clubedge-app` are documented here.
4
4
 
5
+ ## 0.11.0
6
+
7
+ - Default to Starter `v0.10.0`, which adds the deploy module (`--deploy docker|fly|railway|render|vercel|netlify`), a migrator in the production image, a manual deploy workflow, and the `/api/ready` readiness endpoint. `--deploy` needs no CLI code: it comes from the Starter's manifest.
8
+ - Test Fly.io and Vercel projects in CI and a Netlify project in the release smoke test.
9
+
10
+ ## 0.10.0
11
+
12
+ - Default to Starter `v0.9.0`, which adds the email module (`--email smtp|resend|none`), password reset for both auth providers, and email verification for Better Auth. `--email` needs no CLI code: it comes from the Starter's manifest.
13
+ - Fix a doubled blank line in `.env.local` when `add` or `remove` swaps one section of variables for another, such as switching `--email smtp` to `resend`.
14
+ - Take module flags from the Starter's manifest: any `--<module> <option>` flag is accepted and validated against the modules the Starter describes, so new Starter modules need no CLI release. A mistyped module lists the Starter's modules; an unknown flag without a value is still an error.
15
+ - Build the help text's framework and module lines, and the "customize the included services" prompt, from the Starter's manifest.
16
+
5
17
  ## 0.9.0
6
18
 
7
19
  - Default to Starter `v0.8.0`, whose projects have a `clubedge` script: `pnpm clubedge add`, `remove`, and `upgrade`.
package/README.md CHANGED
@@ -12,27 +12,27 @@ The CLI downloads a tested Starter release, names the generated project, creates
12
12
  Each CLI release is tested against one specific Starter release:
13
13
 
14
14
  ```text
15
- create-clubedge-app v0.9.0
15
+ create-clubedge-app v0.11.0
16
16
  |
17
17
  v
18
- Clubedge Starter v0.8.0
19
- commit 0b77577a3fe77617c84e250d8b220169b72a297e
18
+ Clubedge Starter v0.10.0
19
+ commit 9d8fec71b3851c2d615735fb37edb6bb0e568761
20
20
  ```
21
21
 
22
22
  The current relationship is:
23
23
 
24
24
  | CLI release | Default Starter release | Meaning |
25
25
  | --- | --- | --- |
26
- | `create-clubedge-app@0.9.0` | `clubedge-starter@v0.8.0` | The CLI's required, tested default |
26
+ | `create-clubedge-app@0.11.0` | `clubedge-starter@v0.10.0` | The CLI's required, tested default |
27
27
 
28
28
  This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
29
29
 
30
- The `v0.8.0` tag is expected to resolve to commit
31
- `0b77577a3fe77617c84e250d8b220169b72a297e`. The CLI verifies this before
30
+ The `v0.10.0` tag is expected to resolve to commit
31
+ `9d8fec71b3851c2d615735fb37edb6bb0e568761`. The CLI verifies this before
32
32
  scaffolding with the default reference and fails rather than silently using a
33
33
  moved tag.
34
34
 
35
- When the Starter changes, its maintainers first publish a new versioned release, such as `v0.8.0`. The CLI is then updated to use that release, all generated-project checks must pass, and a new CLI version is published. This keeps Starter and CLI releases independently versioned while preserving a tested compatibility relationship.
35
+ When the Starter changes, its maintainers first publish a new versioned release, such as `v0.10.0`. The CLI is then updated to use that release, all generated-project checks must pass, and a new CLI version is published. This keeps Starter and CLI releases independently versioned while preserving a tested compatibility relationship.
36
36
 
37
37
  ## Requirements
38
38
 
@@ -85,13 +85,15 @@ Either way, the app lives in `apps/web` and uses the same shared packages, so th
85
85
 
86
86
  ### Choosing services
87
87
 
88
- Authentication, file storage, and caching are modules. Each has a default, and anything you leave out is removed completely: its package, its provider SDK, its environment variables, its documentation, and its lockfile entries.
88
+ Authentication, file storage, caching, email, local infrastructure, and deployment are modules. Each has a default, and anything you leave out is removed completely: its package, its provider SDK, its environment variables, its documentation, and its lockfile entries. The Starter's `clubedge.template.json` defines the modules, so a module a newer Starter adds gets its `--<module>` flag without a CLI change; `--help` lists the modules of the Starter this CLI release creates.
89
89
 
90
90
  | Flag | Options | Default |
91
91
  | ----------- | ------------------------------------------ | ---------- |
92
92
  | `--auth` | `supabase`, `better-auth`, `none` | `supabase` |
93
93
  | `--storage` | `s3`, `supabase` (needs Supabase Auth), `none` | `s3` |
94
94
  | `--cache` | `redis` (falls back to memory without `REDIS_URL`), `memory` | `redis` |
95
+ | `--email` | `smtp` (any provider, or a local inbox), `resend`, `none` | `smtp` |
96
+ | `--deploy` | `docker` (any container host), `fly`, `railway`, `render`, `vercel`, `netlify` | `docker` |
95
97
  | `--infra` | `docker` (Docker Compose), `supabase` (Supabase CLI), `local` (no Docker: PGlite and a local folder), `none` | `docker` |
96
98
 
97
99
  ```sh
@@ -136,7 +138,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git --framework tanstac
136
138
  pnpm dlx @clubedge/create-clubedge-app my-app --ref main
137
139
 
138
140
  # Reproduce a specific Starter release or commit
139
- pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.8.0
141
+ pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.10.0
140
142
  pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
141
143
 
142
144
  # Test local Starter changes before releasing them
@@ -153,7 +155,7 @@ pnpm dlx @clubedge/create-clubedge-app add cache redis # or switch: add aut
153
155
  pnpm dlx @clubedge/create-clubedge-app remove storage
154
156
  pnpm dlx @clubedge/create-clubedge-app add infra local --dry-run
155
157
  pnpm dlx @clubedge/create-clubedge-app@latest upgrade # to the newest CLI's Starter release
156
- pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.8.0 # or a specific tag, branch, or commit
158
+ pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.10.0 # or a specific tag, branch, or commit
157
159
  ```
158
160
 
159
161
  The CLI renders the project twice, from the Starter commit and modules recorded in your `package.json` and from the new state, and merges the difference into your project. The new state has other modules for `add` and `remove`, and the newer Starter with the same modules for `upgrade`. Modules a newer Starter introduces start as `none` where possible, so an upgrade never adds a service on its own.
@@ -204,7 +206,7 @@ CI packs the CLI with `npm pack`, scaffolds a clean project from the packed tarb
204
206
  1. Make and validate changes in `clubedge-starter`.
205
207
  2. Run lint, typecheck, unit tests, browser tests, and production build.
206
208
  3. Update the Starter package versions.
207
- 4. Commit `main`, create a release tag such as `v0.8.0`, and publish the Starter release.
209
+ 4. Commit `main`, create a release tag such as `v0.10.0`, and publish the Starter release.
208
210
 
209
211
  ### CLI maintainers
210
212
 
@@ -218,7 +220,7 @@ The required CI job always tests the pinned default. The scheduled compatibility
218
220
 
219
221
  ## Contributing
220
222
 
221
- See [CONTRIBUTING.md](CONTRIBUTING.md) for development and pull request guidance. Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md).
223
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development and pull request guidance, and [ROADMAP.md](ROADMAP.md) for the planned phases. Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md).
222
224
 
223
225
  ## License
224
226
 
package/dist/cli/args.js CHANGED
@@ -1,32 +1,70 @@
1
1
  import { parseArgs } from "node:util";
2
- /** Module flags the CLI accepts. Which options exist is up to the Starter's manifest. */
3
- export const MODULE_FLAGS = ["auth", "storage", "cache", "infra"];
4
2
  export class UsageError extends Error {
5
3
  name = "UsageError";
6
4
  }
5
+ /** The CLI's own long options; every other --name value flag selects a module option. */
6
+ const OPTIONS = {
7
+ framework: { type: "string" },
8
+ ref: { type: "string" },
9
+ "template-dir": { type: "string" },
10
+ install: { type: "boolean", default: true },
11
+ git: { type: "boolean", default: true },
12
+ yes: { type: "boolean", short: "y", default: false },
13
+ "dry-run": { type: "boolean", default: false },
14
+ force: { type: "boolean", default: false },
15
+ help: { type: "boolean", short: "h", default: false },
16
+ version: { type: "boolean", short: "v", default: false },
17
+ };
18
+ const isOwnOption = (name) => name in OPTIONS || (name.startsWith("no-") && OPTIONS[name.slice(3)]?.type === "boolean");
19
+ /**
20
+ * Takes module flags (`--email smtp` or `--email=smtp`) out of the arguments, leaving the CLI's
21
+ * own options for the strict parser. A flag that is neither needs a value, so a stray unknown
22
+ * flag such as --verbose is still reported.
23
+ */
24
+ function extractModuleFlags(argv) {
25
+ const rest = [];
26
+ const modules = {};
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const argument = argv[i];
29
+ if (argument === "--") {
30
+ rest.push(...argv.slice(i));
31
+ break;
32
+ }
33
+ const match = /^--([^=]+)(?:=(.*))?$/.exec(argument);
34
+ if (!match || isOwnOption(match[1])) {
35
+ rest.push(argument);
36
+ // A string option's value is never a flag of its own.
37
+ if (match && match[2] === undefined && OPTIONS[match[1]]?.type === "string") {
38
+ if (argv[i + 1] !== undefined)
39
+ rest.push(argv[++i]);
40
+ }
41
+ continue;
42
+ }
43
+ const [, name, inline] = match;
44
+ if (!/^[a-z][a-z0-9-]*$/.test(name))
45
+ throw new UsageError(`Unknown option --${name}. Run with --help to see available options.`);
46
+ const next = argv[i + 1];
47
+ const value = inline ?? (next !== undefined && !next.startsWith("-") ? argv[++i] : undefined);
48
+ if (value === undefined) {
49
+ throw new UsageError(`Unknown option --${name}. Module flags take an option, such as --${name} none. Run with --help to see available options.`);
50
+ }
51
+ if (!value.trim())
52
+ throw new UsageError(`--${name} requires an option, such as none.`);
53
+ modules[name] = value.trim();
54
+ }
55
+ return { rest, modules };
56
+ }
7
57
  export function parseCliArgs(argv) {
58
+ const { rest, modules } = extractModuleFlags(argv);
8
59
  let parsed;
9
60
  try {
10
61
  parsed = parseArgs({
11
- args: argv,
62
+ args: rest,
12
63
  allowPositionals: true,
13
64
  allowNegative: true,
14
65
  strict: true,
15
66
  options: {
16
- framework: { type: "string" },
17
- auth: { type: "string" },
18
- storage: { type: "string" },
19
- cache: { type: "string" },
20
- infra: { type: "string" },
21
- ref: { type: "string" },
22
- "template-dir": { type: "string" },
23
- install: { type: "boolean", default: true },
24
- git: { type: "boolean", default: true },
25
- yes: { type: "boolean", short: "y", default: false },
26
- "dry-run": { type: "boolean", default: false },
27
- force: { type: "boolean", default: false },
28
- help: { type: "boolean", short: "h", default: false },
29
- version: { type: "boolean", short: "v", default: false },
67
+ ...OPTIONS,
30
68
  },
31
69
  });
32
70
  }
@@ -39,7 +77,7 @@ export function parseCliArgs(argv) {
39
77
  if (command.name === "create" && positionals.length > 1) {
40
78
  throw new UsageError("Only one project directory can be provided.");
41
79
  }
42
- if (command.name !== "create" && (values.framework !== undefined || MODULE_FLAGS.some((id) => values[id]))) {
80
+ if (command.name !== "create" && (values.framework !== undefined || Object.keys(modules).length)) {
43
81
  throw new UsageError("--framework and module flags only apply when creating a project; use add or remove to change one module.");
44
82
  }
45
83
  if ((command.name === "add" || command.name === "remove") && values.ref !== undefined) {
@@ -48,15 +86,6 @@ export function parseCliArgs(argv) {
48
86
  if (values.framework !== undefined && !values.framework.trim()) {
49
87
  throw new UsageError("--framework requires a framework id, such as next or tanstack-start.");
50
88
  }
51
- const modules = {};
52
- for (const module of MODULE_FLAGS) {
53
- const value = values[module];
54
- if (value === undefined)
55
- continue;
56
- if (!value.trim())
57
- throw new UsageError(`--${module} requires an option, such as none.`);
58
- modules[module] = value.trim();
59
- }
60
89
  if (values.ref !== undefined && !values.ref.trim()) {
61
90
  throw new UsageError("--ref requires a tag, branch, or commit.");
62
91
  }
package/dist/cli/ui.js CHANGED
@@ -52,7 +52,32 @@ export function showBanner() {
52
52
  console.log(` ${dim("By Clubedge")} ${dim("·")} ${paint(gradientFrom, siteUrl)}`);
53
53
  console.log("");
54
54
  }
55
- export function helpText() {
55
+ /** "a (default), b, or c" */
56
+ function choices(ids, defaultId) {
57
+ const labels = ids.map((id) => (id === defaultId ? `${id} (default)` : id));
58
+ return labels.length > 1 ? `${labels.slice(0, -1).join(", ")}${labels.length > 2 ? "," : ""} or ${labels.at(-1)}` : labels[0] ?? "";
59
+ }
60
+ /**
61
+ * The framework and module flags of a Starter, from its manifest, so new modules appear in the
62
+ * help without CLI changes. Without a manifest, a generic line explains module flags.
63
+ */
64
+ export function selectionHelp(manifest) {
65
+ const line = (flag, text) => ` ${flag.padEnd(24)}${text}`;
66
+ if (!manifest) {
67
+ return [
68
+ line("--framework <id>", "App framework, such as next or tanstack-start"),
69
+ line("--<module> <option>", "A module option, such as --auth none; the Starter lists its modules"),
70
+ ].join("\n");
71
+ }
72
+ const frameworks = Object.keys(manifest.frameworks);
73
+ return [
74
+ ...(frameworks.length > 1
75
+ ? [line("--framework <id>", `App framework: ${choices(frameworks, manifest.defaultFramework)}`)]
76
+ : []),
77
+ ...Object.entries(manifest.modules).map(([id, module]) => line(`--${id} <id>`, `${module.name}: ${choices(Object.keys(module.options), module.default)}`)),
78
+ ].join("\n");
79
+ }
80
+ export function helpText(manifest = null) {
56
81
  return `
57
82
  create-clubedge-app ${cliVersion}
58
83
 
@@ -74,11 +99,7 @@ Usage:
74
99
  markers. Commit first so you can review the result with git diff.
75
100
 
76
101
  Options:
77
- --framework <id> App framework: next (default) or tanstack-start
78
- --auth <id> Authentication: supabase (default), better-auth, or none
79
- --storage <id> File storage: s3 (default), supabase, or none
80
- --cache <id> Cache and rate limits: redis (default) or memory
81
- --infra <id> Local services: docker (default), supabase, local, or none
102
+ ${selectionHelp(manifest)}
82
103
  --ref <ref> Download this Starter tag, branch, or commit from GitHub instead
83
104
  (upgrade: the Starter to upgrade to)
84
105
  --template-dir <path> Scaffold from a local Starter checkout (for Starter development)
package/dist/core/env.js CHANGED
@@ -76,10 +76,17 @@ export function updateEnvFile(local, baseExample, targetExample) {
76
76
  while (anchor >= 0 && !keyOf(target[anchor]))
77
77
  anchor--;
78
78
  const position = anchor >= 0 ? find(lines, keyOf(target[anchor])) : -1;
79
- if (position === -1)
79
+ if (position === -1) {
80
80
  appended.push(...commentsAbove(target, at), line);
81
- else
82
- lines.splice(position + 1, 0, ...target.slice(anchor + 1, at + 1));
81
+ return;
82
+ }
83
+ const between = target.slice(anchor + 1, at + 1);
84
+ // A removed section can leave its blank separator after the anchor; reuse it rather than
85
+ // adding a second one.
86
+ // The last element is the end of the file after its final newline, not a blank line.
87
+ const followedByBlank = position + 1 < lines.length - 1 && lines[position + 1].trim() === "";
88
+ const reuseBlank = between[0]?.trim() === "" && followedByBlank;
89
+ lines.splice(position + (reuseBlank ? 2 : 1), 0, ...(reuseBlank ? between.slice(1) : between));
83
90
  });
84
91
  let text = lines.join(eol);
85
92
  // Removed variables can leave blank lines in a row, as removed template blocks do.
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@ import { dim, helpText, showBanner } from "./cli/ui.js";
7
7
  import { assertTargetIsUsable, executePlan } from "./core/execute.js";
8
8
  import { readTemplateManifest } from "./core/manifest.js";
9
9
  import { createPlan, describePlan, selectFramework } from "./core/plan.js";
10
- import { resolveTemplateSource } from "./core/source.js";
11
- import { cliUrl, cliVersion, siteUrl } from "./package-info.js";
10
+ import { readBundledTemplate, resolveTemplateSource } from "./core/source.js";
11
+ import { cliUrl, cliVersion, siteUrl, starterPin } from "./package-info.js";
12
12
  import { changeProject } from "./modify.js";
13
13
  import { initializeGit } from "./steps/git.js";
14
14
  import { detectLauncher, installDependencies, resolvePnpm } from "./steps/install.js";
@@ -63,7 +63,9 @@ async function chooseModules(manifest, requested, interactive) {
63
63
  if (!interactive || unasked.length === 0)
64
64
  return requested;
65
65
  const customize = await confirm({
66
- message: "Customize the included services (auth, storage, cache, local infrastructure)?",
66
+ message: `Customize the included services (${Object.values(manifest.modules)
67
+ .map((module) => module.name.toLowerCase())
68
+ .join(", ")})?`,
67
69
  initialValue: false,
68
70
  });
69
71
  if (isCancel(customize))
@@ -193,6 +195,19 @@ async function createProject(args) {
193
195
  later +
194
196
  `${dim("Docs")} ${cliUrl}\n${dim("Clubedge")} ${siteUrl}`);
195
197
  }
198
+ /** The bundled Starter's manifest, for the help text; null in a checkout without a bundle. */
199
+ async function bundledManifest() {
200
+ try {
201
+ const bundled = await readBundledTemplate();
202
+ // A checkout can hold an older bundle; only the pinned release describes what this CLI creates.
203
+ if (!bundled || bundled.commit !== starterPin.starterCommit)
204
+ return null;
205
+ return await readTemplateManifest(bundled.root);
206
+ }
207
+ catch {
208
+ return null;
209
+ }
210
+ }
196
211
  export async function main(argv = process.argv.slice(2)) {
197
212
  try {
198
213
  const args = parseCliArgs(argv);
@@ -202,7 +217,7 @@ export async function main(argv = process.argv.slice(2)) {
202
217
  }
203
218
  showBanner();
204
219
  if (args.help) {
205
- console.log(helpText());
220
+ console.log(helpText(await bundledManifest()));
206
221
  return;
207
222
  }
208
223
  assertSupportedNode();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/create-clubedge-app",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Create a Next.js or TanStack Start application from the Clubedge Starter reference implementation.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.9.0",
@@ -22,8 +22,8 @@
22
22
  },
23
23
  "clubedge": {
24
24
  "starterRepository": "Clubedge/clubedge-starter",
25
- "starterRef": "v0.8.0",
26
- "starterCommit": "0b77577a3fe77617c84e250d8b220169b72a297e"
25
+ "starterRef": "v0.10.0",
26
+ "starterCommit": "9d8fec71b3851c2d615735fb37edb6bb0e568761"
27
27
  },
28
28
  "license": "Apache-2.0",
29
29
  "repository": {
@@ -22,6 +22,8 @@ NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
22
22
  # Better Auth keeps users and sessions in DATABASE_URL (run pnpm db:migrate first).
23
23
  # Generate a secret with: openssl rand -base64 32
24
24
  BETTER_AUTH_SECRET=
25
+ # true refuses sign-in until the email address is confirmed through the emailed link. # clubedge:only email!=none
26
+ BETTER_AUTH_REQUIRE_EMAIL_VERIFICATION=false # clubedge:only email!=none
25
27
  # clubedge:end
26
28
 
27
29
  # clubedge:if cache=redis
@@ -31,6 +33,24 @@ REDIS_URL= # clubedge:only infra!=docker
31
33
  REDIS_URL=redis://localhost:6379 # clubedge:only infra=docker
32
34
  # clubedge:end
33
35
 
36
+ # clubedge:if email=smtp
37
+ # Email over SMTP: any provider's SMTP endpoint (smtps:// for TLS). Without it, development
38
+ # prints emails to the server log and production refuses to send.
39
+ SMTP_URL= # clubedge:only infra!=docker|supabase
40
+ # Mailpit from docker-compose.dev.yml; read the emails at http://localhost:8025. # clubedge:only infra=docker
41
+ SMTP_URL=smtp://localhost:1025 # clubedge:only infra=docker
42
+ # The local Supabase inbox; read the emails at http://127.0.0.1:54324. # clubedge:only infra=supabase
43
+ SMTP_URL=smtp://127.0.0.1:54325 # clubedge:only infra=supabase
44
+ EMAIL_FROM=App <no-reply@example.com>
45
+ # clubedge:end
46
+
47
+ # clubedge:if email=resend
48
+ # Resend (https://resend.com). EMAIL_FROM must use a domain verified in Resend. Without the
49
+ # key, development prints emails to the server log and production refuses to send.
50
+ RESEND_API_KEY=
51
+ EMAIL_FROM=App <no-reply@example.com>
52
+ # clubedge:end
53
+
34
54
  # clubedge:if storage=s3
35
55
  # S3-compatible storage (AWS S3, Cloudflare R2, MinIO)
36
56
  STORAGE_BUCKET= # clubedge:only infra!=docker
@@ -58,10 +58,28 @@ jobs:
58
58
 
59
59
  docker:
60
60
  runs-on: ubuntu-latest
61
+ services:
62
+ postgres:
63
+ image: postgres:17
64
+ env:
65
+ POSTGRES_USER: postgres
66
+ POSTGRES_PASSWORD: postgres
67
+ POSTGRES_DB: app
68
+ ports:
69
+ - 5432:5432
70
+ options: >-
71
+ --health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 10
61
72
  steps:
62
73
  - uses: actions/checkout@v4
63
74
  - name: Build the production image
64
75
  run: docker build --tag app:ci .
76
+ - name: Run the image's migrations twice; the second run changes nothing
77
+ run: |
78
+ set -euo pipefail
79
+ for run in 1 2; do
80
+ docker run --rm --network host --env DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/app app:ci node /app/migrate.mjs
81
+ done
82
+ PGPASSWORD=postgres psql --host 127.0.0.1 --username postgres --dbname app --command "select count(*) from public.users"
65
83
  - name: Start the image and check health
66
84
  run: |
67
85
  set -euo pipefail
@@ -0,0 +1,109 @@
1
+ # Deploys on demand: Actions > Deploy > Run workflow. SETUP.md lists the secrets it needs.
2
+ name: Deploy
3
+ on:
4
+ workflow_dispatch:
5
+ permissions:
6
+ contents: read
7
+ concurrency:
8
+ group: deploy
9
+ cancel-in-progress: false
10
+ jobs:
11
+ # clubedge:if deploy=docker
12
+ # Builds the production image and publishes it to this repository's GitHub Container Registry
13
+ # as ghcr.io/<owner>/<repository>, tagged with the commit and latest. No secret is needed.
14
+ image:
15
+ runs-on: ubuntu-latest
16
+ permissions:
17
+ contents: read
18
+ packages: write
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: docker/setup-buildx-action@v3
22
+ - uses: docker/login-action@v3
23
+ with:
24
+ registry: ghcr.io
25
+ username: ${{ github.actor }}
26
+ password: ${{ secrets.GITHUB_TOKEN }}
27
+ - id: meta
28
+ uses: docker/metadata-action@v5
29
+ with:
30
+ images: ghcr.io/${{ github.repository }}
31
+ tags: |
32
+ type=sha
33
+ type=raw,value=latest
34
+ - uses: docker/build-push-action@v6
35
+ with:
36
+ context: .
37
+ push: true
38
+ tags: ${{ steps.meta.outputs.tags }}
39
+ labels: ${{ steps.meta.outputs.labels }}
40
+ # clubedge:end
41
+ # clubedge:if deploy=fly
42
+ fly:
43
+ runs-on: ubuntu-latest
44
+ steps:
45
+ - uses: actions/checkout@v4
46
+ - uses: superfly/flyctl-actions/setup-flyctl@master
47
+ # Builds on Fly's builders and runs the release command (migrations) before switching.
48
+ - run: flyctl deploy --remote-only
49
+ env:
50
+ FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
51
+ # clubedge:end
52
+ # clubedge:if deploy=railway
53
+ railway:
54
+ runs-on: ubuntu-latest
55
+ steps:
56
+ - uses: actions/checkout@v4
57
+ # Uploads this checkout; Railway builds the image and runs the pre-deploy migrations.
58
+ - run: npx --yes @railway/cli@4 up --ci --service "$RAILWAY_SERVICE"
59
+ env:
60
+ RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
61
+ RAILWAY_SERVICE: ${{ vars.RAILWAY_SERVICE }}
62
+ # clubedge:end
63
+ # clubedge:if deploy=render
64
+ render:
65
+ runs-on: ubuntu-latest
66
+ steps:
67
+ # The service's deploy hook starts a deploy of its branch, with its pre-deploy migrations.
68
+ - run: curl --fail --silent --show-error --request POST "$RENDER_DEPLOY_HOOK_URL"
69
+ env:
70
+ RENDER_DEPLOY_HOOK_URL: ${{ secrets.RENDER_DEPLOY_HOOK_URL }}
71
+ # clubedge:end
72
+ # clubedge:if deploy=vercel
73
+ vercel:
74
+ runs-on: ubuntu-latest
75
+ env:
76
+ VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
77
+ VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
78
+ VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
79
+ steps:
80
+ - uses: actions/checkout@v4
81
+ - uses: pnpm/action-setup@v4
82
+ - uses: actions/setup-node@v4
83
+ with:
84
+ node-version: 22
85
+ cache: pnpm
86
+ # Builds here with the project's production settings and variables (including the
87
+ # migrations in its build command), then uploads the result.
88
+ - run: npx --yes vercel@latest pull --yes --environment=production --token="$VERCEL_TOKEN"
89
+ - run: npx --yes vercel@latest build --prod --token="$VERCEL_TOKEN"
90
+ - run: npx --yes vercel@latest deploy --prebuilt --prod --token="$VERCEL_TOKEN"
91
+ # clubedge:end
92
+ # clubedge:if deploy=netlify
93
+ netlify:
94
+ runs-on: ubuntu-latest
95
+ env:
96
+ NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
97
+ NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
98
+ steps:
99
+ - uses: actions/checkout@v4
100
+ - uses: pnpm/action-setup@v4
101
+ - uses: actions/setup-node@v4
102
+ with:
103
+ node-version: 22
104
+ cache: pnpm
105
+ - run: pnpm install --frozen-lockfile
106
+ # Builds with netlify.toml (including its migrations) and the site's variables, then deploys.
107
+ - run: npx --yes netlify-cli@latest deploy --build --prod
108
+ working-directory: apps/web
109
+ # clubedge:end
@@ -18,6 +18,10 @@ COPY --from=deps /app/ .
18
18
  COPY --from=pruner /app/out/full/ .
19
19
  COPY tsconfig.base.json ./
20
20
  RUN pnpm build
21
+ # The production migrator (node /app/migrate.mjs): one bundled file plus every package's
22
+ # migrations, so release commands can migrate before a new version starts.
23
+ COPY scripts/migrate.mjs scripts/build-migrator.mjs ./scripts/
24
+ RUN node scripts/build-migrator.mjs /app/.migrator
21
25
 
22
26
  FROM node:22-alpine AS runner
23
27
  ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 HOSTNAME=0.0.0.0 PORT=3000
@@ -26,6 +30,7 @@ RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
26
30
  COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
27
31
  COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
28
32
  COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
33
+ COPY --from=builder --chown=nextjs:nodejs /app/.migrator ./
29
34
  WORKDIR /app/apps/web
30
35
  USER nextjs
31
36
  EXPOSE 3000
@@ -7,7 +7,7 @@ Clubedge Starter is a production-oriented, modular React application foundation.
7
7
 
8
8
  <!-- clubedge:if starter-repository -->
9
9
 
10
- This repository contains the same application for two frameworks, Next.js (`apps/web`) and TanStack Start (`apps/start`), plus every optional module. [create-clubedge-app](https://github.com/Clubedge/create-clubedge-app) generates a project with one framework and only the modules you choose; sections of this README marked for a framework or module appear only in projects that use it. Contributions and bug reports are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md). Repository maintainers can use [PUBLISHING.md](PUBLISHING.md) for the GitHub launch checklist.
10
+ This repository contains the same application for two frameworks, Next.js (`apps/web`) and TanStack Start (`apps/start`), plus every optional module. [create-clubedge-app](https://github.com/Clubedge/create-clubedge-app) generates a project with one framework and only the modules you choose; sections of this README marked for a framework or module appear only in projects that use it. Contributions and bug reports are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md). Repository maintainers can use [PUBLISHING.md](PUBLISHING.md) for the GitHub launch checklist, and [ROADMAP.md](ROADMAP.md) describes the planned phases.
11
11
 
12
12
  <!-- clubedge:end -->
13
13
 
@@ -19,12 +19,23 @@ This repository contains the same application for two frameworks, Next.js (`apps
19
19
  - Drizzle ORM and PostgreSQL schema, migrations, and seed commands.
20
20
  - Supabase Auth with server-verified sessions in secure cookies, a protected dashboard, and rate-limited sign-in. <!-- clubedge:only auth=supabase -->
21
21
  - Better Auth with users and sessions in your own PostgreSQL, secure session cookies, a protected dashboard, and rate-limited sign-in. <!-- clubedge:only auth=better-auth -->
22
+ - Password reset by email, and email verification that can be required before sign-in. <!-- clubedge:only auth=better-auth && email!=none -->
23
+ - Password reset by email, sent by Supabase. <!-- clubedge:only auth=supabase -->
22
24
  - S3-compatible storage adapter for AWS S3, Cloudflare R2, and MinIO. <!-- clubedge:only storage=s3 -->
23
25
  - Supabase Storage adapter that uses the signed-in user's session. <!-- clubedge:only storage=supabase -->
24
26
  - Fixed-window rate limiter and cache on Redis, falling back to process memory without `REDIS_URL`. <!-- clubedge:only cache=redis -->
25
27
  - Fixed-window rate limiter and cache in process memory, suited to a single server instance. <!-- clubedge:only cache=memory -->
28
+ - Email over SMTP, for any provider or a local inbox; development prints emails to the server log until SMTP is configured. <!-- clubedge:only email=smtp -->
29
+ - Email through Resend; development prints emails to the server log until the API key is set. <!-- clubedge:only email=resend -->
26
30
  - Zod environment validation, security headers, structured errors, and `/api/health`.
27
31
  - Docker support, GitHub Actions CI, Vitest, Playwright, ESLint, and Prettier.
32
+ - A production image that includes its own migrator (`node /app/migrate.mjs`), for running migrations before each release.
33
+ - A manual deploy workflow for the chosen platform, and a readiness endpoint (`/api/ready`) that checks the database.
34
+ - Deployment to Fly.io with `fly.toml`: migrations run as the release command. <!-- clubedge:only deploy=fly -->
35
+ - Deployment to Railway with `railway.json`: migrations run before each deploy. <!-- clubedge:only deploy=railway -->
36
+ - Deployment to Render with a `render.yaml` Blueprint: migrations run before each deploy. <!-- clubedge:only deploy=render -->
37
+ - Deployment to Vercel with `apps/web/vercel.json`: migrations run during the build. <!-- clubedge:only deploy=vercel -->
38
+ - Deployment to Netlify with `apps/web/netlify.toml`: migrations run during the build. <!-- clubedge:only deploy=netlify -->
28
39
 
29
40
  ## Architecture
30
41
 
@@ -47,6 +58,12 @@ flowchart TB
47
58
  %% clubedge:if cache=redis
48
59
  App -->|cache and rate limits| Redis[(Redis, or memory)]
49
60
  %% clubedge:end
61
+ %% clubedge:if email=smtp
62
+ App -->|email interface| SMTP[SMTP server, or the log]
63
+ %% clubedge:end
64
+ %% clubedge:if email=resend
65
+ App -->|email interface| Resend[Resend, or the log]
66
+ %% clubedge:end
50
67
  %% clubedge:if storage=s3
51
68
  App -->|storage interface| S3[S3-compatible storage: AWS S3, R2, MinIO]
52
69
  %% clubedge:end
@@ -201,6 +218,9 @@ The TanStack Start app is `apps/start`, run with `pnpm --filter @clubedge/start
201
218
  - `packages/storage-supabase/`: Supabase Storage adapter <!-- clubedge:only storage=supabase -->
202
219
  - `packages/cache/`: `Cache` and `RateLimiter` interfaces with in-memory implementations
203
220
  - `packages/cache-redis/`: Redis rate limiter and cache <!-- clubedge:only cache=redis -->
221
+ - `packages/email/`: `EmailProvider` interface with console and in-memory providers <!-- clubedge:only email!=none -->
222
+ - `packages/email-smtp/`: SMTP email provider <!-- clubedge:only email=smtp -->
223
+ - `packages/email-resend/`: Resend email provider <!-- clubedge:only email=resend -->
204
224
  - `packages/ui/`: shared shadcn/ui components, utilities, and global theme styles
205
225
  - `.github/`: CI workflow, issue forms, and pull request template
206
226
  - `SETUP.md`, `CONTRIBUTING.md`, `SECURITY.md`: setup, contribution, and security guides