@clubedge/create-clubedge-app 0.10.0 → 0.12.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 (87) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +13 -11
  3. package/package.json +3 -3
  4. package/template/.env.example +9 -0
  5. package/template/.github/workflows/ci.yml +18 -0
  6. package/template/.github/workflows/deploy.yml +109 -0
  7. package/template/Dockerfile +5 -0
  8. package/template/README.md +10 -0
  9. package/template/SETUP.md +113 -1
  10. package/template/_gitignore +3 -0
  11. package/template/apps/start/.env.example +9 -0
  12. package/template/apps/start/Dockerfile +5 -0
  13. package/template/apps/start/netlify.toml +6 -0
  14. package/template/apps/start/package.json +3 -1
  15. package/template/apps/start/scripts/check-services.ts +20 -0
  16. package/template/apps/start/src/components/billing/billing-card.tsx +83 -0
  17. package/template/apps/start/src/config/plans.ts +16 -0
  18. package/template/apps/start/src/routes/api/ready.ts +23 -0
  19. package/template/apps/start/src/routes/api/webhooks/payments.ts +31 -0
  20. package/template/apps/start/src/routes/billing/checkout.ts +11 -0
  21. package/template/apps/start/src/routes/billing/portal.ts +11 -0
  22. package/template/apps/start/src/routes/dashboard.tsx +16 -2
  23. package/template/apps/start/src/server/billing-forms.ts +50 -0
  24. package/template/apps/start/src/server/billing.functions.ts +29 -0
  25. package/template/apps/start/src/server/payments.ts +60 -0
  26. package/template/apps/start/src/server/users.ts +6 -1
  27. package/template/apps/start/src/start.ts +3 -1
  28. package/template/apps/start/vercel.json +5 -0
  29. package/template/apps/web/netlify.toml +6 -0
  30. package/template/apps/web/package.json +3 -1
  31. package/template/apps/web/scripts/check-services.ts +20 -0
  32. package/template/apps/web/src/app/api/ready/route.ts +18 -0
  33. package/template/apps/web/src/app/api/webhooks/payments/route.ts +23 -0
  34. package/template/apps/web/src/app/dashboard/_components/billing-actions.ts +44 -0
  35. package/template/apps/web/src/app/dashboard/_components/billing-card.tsx +89 -0
  36. package/template/apps/web/src/app/dashboard/page.tsx +7 -0
  37. package/template/apps/web/src/config/plans.ts +16 -0
  38. package/template/apps/web/src/server/payments.ts +60 -0
  39. package/template/apps/web/src/server/users.ts +6 -1
  40. package/template/apps/web/vercel.json +5 -0
  41. package/template/clubedge.template.json +66 -0
  42. package/template/e2e/payments.spec.ts +51 -0
  43. package/template/e2e/ready.spec.ts +11 -0
  44. package/template/e2e/stripe-fake.ts +95 -0
  45. package/template/fly.toml +25 -0
  46. package/template/package.json +2 -1
  47. package/template/packages/auth/package.json +1 -1
  48. package/template/packages/auth-better-auth/package.json +7 -1
  49. package/template/packages/auth-supabase/package.json +1 -1
  50. package/template/packages/cache/package.json +1 -1
  51. package/template/packages/cache-redis/package.json +1 -1
  52. package/template/packages/core/package.json +1 -1
  53. package/template/packages/db/package.json +7 -1
  54. package/template/packages/db/src/client.test.ts +28 -0
  55. package/template/packages/db/src/client.ts +19 -0
  56. package/template/packages/db/src/index.ts +2 -2
  57. package/template/packages/db/src/users.ts +23 -4
  58. package/template/packages/email/package.json +1 -1
  59. package/template/packages/email-resend/package.json +1 -1
  60. package/template/packages/email-smtp/package.json +1 -1
  61. package/template/packages/local-db/package.json +1 -1
  62. package/template/packages/payments/drizzle/0000_initial.sql +24 -0
  63. package/template/packages/payments/drizzle/meta/0000_snapshot.json +171 -0
  64. package/template/packages/payments/drizzle/meta/_journal.json +13 -0
  65. package/template/packages/payments/drizzle.config.ts +12 -0
  66. package/template/packages/payments/package.json +35 -0
  67. package/template/packages/payments/scripts/migrate.ts +14 -0
  68. package/template/packages/payments/src/billing.test.ts +151 -0
  69. package/template/packages/payments/src/billing.ts +125 -0
  70. package/template/packages/payments/src/index.ts +17 -0
  71. package/template/packages/payments/src/schema.ts +57 -0
  72. package/template/packages/payments/src/types.ts +67 -0
  73. package/template/packages/payments/tsconfig.json +4 -0
  74. package/template/packages/payments-stripe/package.json +22 -0
  75. package/template/packages/payments-stripe/src/index.test.ts +156 -0
  76. package/template/packages/payments-stripe/src/index.ts +141 -0
  77. package/template/packages/payments-stripe/tsconfig.json +4 -0
  78. package/template/packages/storage/package.json +1 -1
  79. package/template/packages/storage-s3/package.json +1 -1
  80. package/template/packages/storage-supabase/package.json +1 -1
  81. package/template/packages/ui/package.json +1 -1
  82. package/template/pnpm-lock.yaml +63 -0
  83. package/template/railway.json +13 -0
  84. package/template/render.yaml +74 -0
  85. package/template/scripts/build-migrator.mjs +51 -0
  86. package/template/scripts/migrate.mjs +30 -0
  87. package/template.lock.json +47 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to `create-clubedge-app` are documented here.
4
4
 
5
+ ## 0.12.0
6
+
7
+ - Default to Starter `v0.11.0`, which adds payments (`--payments stripe`): Stripe Checkout, the customer portal, and verified webhooks behind a provider-neutral payments interface. `--payments` needs no CLI code: it comes from the Starter's manifest.
8
+ - Test a payments project in CI and in the release smoke test, and add the payments module in the change-modules job.
9
+
10
+ ## 0.11.0
11
+
12
+ - 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.
13
+ - Test Fly.io and Vercel projects in CI and a Netlify project in the release smoke test.
14
+
5
15
  ## 0.10.0
6
16
 
7
17
  - 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.
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.10.0
15
+ create-clubedge-app v0.12.0
16
16
  |
17
17
  v
18
- Clubedge Starter v0.9.0
19
- commit e858d0b5b60bbb05f1b6aed3f4d9e841ea2df2a5
18
+ Clubedge Starter v0.11.0
19
+ commit b5637afcf3748e03bb7ecb1f1941a4e6c17cfa65
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.10.0` | `clubedge-starter@v0.9.0` | The CLI's required, tested default |
26
+ | `create-clubedge-app@0.12.0` | `clubedge-starter@v0.11.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.9.0` tag is expected to resolve to commit
31
- `e858d0b5b60bbb05f1b6aed3f4d9e841ea2df2a5`. The CLI verifies this before
30
+ The `v0.11.0` tag is expected to resolve to commit
31
+ `b5637afcf3748e03bb7ecb1f1941a4e6c17cfa65`. 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.9.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.11.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,7 +85,7 @@ 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, caching, email, and local infrastructure 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.
88
+ Authentication, file storage, caching, email, payments, 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
  | ----------- | ------------------------------------------ | ---------- |
@@ -93,6 +93,8 @@ Authentication, file storage, caching, email, and local infrastructure are modul
93
93
  | `--storage` | `s3`, `supabase` (needs Supabase Auth), `none` | `s3` |
94
94
  | `--cache` | `redis` (falls back to memory without `REDIS_URL`), `memory` | `redis` |
95
95
  | `--email` | `smtp` (any provider, or a local inbox), `resend`, `none` | `smtp` |
96
+ | `--payments` | `stripe`, `none` (needs an auth module) | `none` |
97
+ | `--deploy` | `docker` (any container host), `fly`, `railway`, `render`, `vercel`, `netlify` | `docker` |
96
98
  | `--infra` | `docker` (Docker Compose), `supabase` (Supabase CLI), `local` (no Docker: PGlite and a local folder), `none` | `docker` |
97
99
 
98
100
  ```sh
@@ -137,7 +139,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git --framework tanstac
137
139
  pnpm dlx @clubedge/create-clubedge-app my-app --ref main
138
140
 
139
141
  # Reproduce a specific Starter release or commit
140
- pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.9.0
142
+ pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.11.0
141
143
  pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
142
144
 
143
145
  # Test local Starter changes before releasing them
@@ -154,7 +156,7 @@ pnpm dlx @clubedge/create-clubedge-app add cache redis # or switch: add aut
154
156
  pnpm dlx @clubedge/create-clubedge-app remove storage
155
157
  pnpm dlx @clubedge/create-clubedge-app add infra local --dry-run
156
158
  pnpm dlx @clubedge/create-clubedge-app@latest upgrade # to the newest CLI's Starter release
157
- pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.9.0 # or a specific tag, branch, or commit
159
+ pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.11.0 # or a specific tag, branch, or commit
158
160
  ```
159
161
 
160
162
  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.
@@ -205,7 +207,7 @@ CI packs the CLI with `npm pack`, scaffolds a clean project from the packed tarb
205
207
  1. Make and validate changes in `clubedge-starter`.
206
208
  2. Run lint, typecheck, unit tests, browser tests, and production build.
207
209
  3. Update the Starter package versions.
208
- 4. Commit `main`, create a release tag such as `v0.9.0`, and publish the Starter release.
210
+ 4. Commit `main`, create a release tag such as `v0.11.0`, and publish the Starter release.
209
211
 
210
212
  ### CLI maintainers
211
213
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/create-clubedge-app",
3
- "version": "0.10.0",
3
+ "version": "0.12.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.9.0",
26
- "starterCommit": "e858d0b5b60bbb05f1b6aed3f4d9e841ea2df2a5"
25
+ "starterRef": "v0.11.0",
26
+ "starterCommit": "b5637afcf3748e03bb7ecb1f1941a4e6c17cfa65"
27
27
  },
28
28
  "license": "Apache-2.0",
29
29
  "repository": {
@@ -51,6 +51,15 @@ RESEND_API_KEY=
51
51
  EMAIL_FROM=App <no-reply@example.com>
52
52
  # clubedge:end
53
53
 
54
+ # clubedge:if payments=stripe
55
+ # Stripe (https://dashboard.stripe.com/apikeys): use test-mode keys during development. The
56
+ # webhook secret comes from `stripe listen` locally, or from the endpoint's settings in production.
57
+ STRIPE_SECRET_KEY=
58
+ STRIPE_WEBHOOK_SECRET=
59
+ # The Stripe price for each plan in src/config/plans.ts (STRIPE_PRICE_<PLAN>).
60
+ STRIPE_PRICE_PRO=
61
+ # clubedge:end
62
+
54
63
  # clubedge:if storage=s3
55
64
  # S3-compatible storage (AWS S3, Cloudflare R2, MinIO)
56
65
  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
@@ -27,8 +27,16 @@ This repository contains the same application for two frameworks, Next.js (`apps
27
27
  - Fixed-window rate limiter and cache in process memory, suited to a single server instance. <!-- clubedge:only cache=memory -->
28
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
29
  - Email through Resend; development prints emails to the server log until the API key is set. <!-- clubedge:only email=resend -->
30
+ - Subscriptions with Stripe Checkout and the customer portal, kept in sync by verified webhooks, behind a provider-neutral payments interface. <!-- clubedge:only payments=stripe -->
30
31
  - Zod environment validation, security headers, structured errors, and `/api/health`.
31
32
  - Docker support, GitHub Actions CI, Vitest, Playwright, ESLint, and Prettier.
33
+ - A production image that includes its own migrator (`node /app/migrate.mjs`), for running migrations before each release.
34
+ - A manual deploy workflow for the chosen platform, and a readiness endpoint (`/api/ready`) that checks the database.
35
+ - Deployment to Fly.io with `fly.toml`: migrations run as the release command. <!-- clubedge:only deploy=fly -->
36
+ - Deployment to Railway with `railway.json`: migrations run before each deploy. <!-- clubedge:only deploy=railway -->
37
+ - Deployment to Render with a `render.yaml` Blueprint: migrations run before each deploy. <!-- clubedge:only deploy=render -->
38
+ - Deployment to Vercel with `apps/web/vercel.json`: migrations run during the build. <!-- clubedge:only deploy=vercel -->
39
+ - Deployment to Netlify with `apps/web/netlify.toml`: migrations run during the build. <!-- clubedge:only deploy=netlify -->
32
40
 
33
41
  ## Architecture
34
42
 
@@ -214,6 +222,8 @@ The TanStack Start app is `apps/start`, run with `pnpm --filter @clubedge/start
214
222
  - `packages/email/`: `EmailProvider` interface with console and in-memory providers <!-- clubedge:only email!=none -->
215
223
  - `packages/email-smtp/`: SMTP email provider <!-- clubedge:only email=smtp -->
216
224
  - `packages/email-resend/`: Resend email provider <!-- clubedge:only email=resend -->
225
+ - `packages/payments/`: `PaymentProvider` interface, billing logic, and the subscription tables <!-- clubedge:only payments!=none -->
226
+ - `packages/payments-stripe/`: Stripe payment provider <!-- clubedge:only payments=stripe -->
217
227
  - `packages/ui/`: shared shadcn/ui components, utilities, and global theme styles
218
228
  - `.github/`: CI workflow, issue forms, and pull request template
219
229
  - `SETUP.md`, `CONTRIBUTING.md`, `SECURITY.md`: setup, contribution, and security guides
package/template/SETUP.md CHANGED
@@ -45,6 +45,7 @@ The variables are:
45
45
  - `REDIS_URL` (optional): see [Redis](#redis). <!-- clubedge:only cache=redis -->
46
46
  - `SMTP_URL` and `EMAIL_FROM` (for sending email): see [Email](#email). <!-- clubedge:only email=smtp -->
47
47
  - `RESEND_API_KEY` and `EMAIL_FROM` (for sending email): see [Email](#email). <!-- clubedge:only email=resend -->
48
+ - `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, and `STRIPE_PRICE_<PLAN>` (for payments): see [Payments](#payments). <!-- clubedge:only payments=stripe -->
48
49
  - `STORAGE_*` (for file storage): see [File storage](#file-storage). <!-- clubedge:only storage=s3 -->
49
50
  - `SUPABASE_STORAGE_BUCKET` (for file storage): see [File storage](#file-storage). <!-- clubedge:only storage=supabase -->
50
51
 
@@ -56,7 +57,7 @@ Start the development server:
56
57
  pnpm dev
57
58
  ```
58
59
 
59
- The app is available at <http://localhost:3000> and the liveness endpoint at <http://localhost:3000/api/health>.
60
+ The app is available at <http://localhost:3000>, the liveness endpoint at <http://localhost:3000/api/health>, and the readiness endpoint, which also checks the database, at <http://localhost:3000/api/ready>.
60
61
 
61
62
  <!-- clubedge:if auth=supabase -->
62
63
 
@@ -208,6 +209,33 @@ Set `RESEND_API_KEY` to a [Resend](https://resend.com) API key and `EMAIL_FROM`
208
209
 
209
210
  Without a provider configured, development prints each email to the server log, so you can open links from the terminal, and production refuses to send. `pnpm check:services` confirms that the provider accepts the settings without sending anything.
210
211
 
212
+ <!-- clubedge:end -->
213
+ <!-- clubedge:if payments!=none -->
214
+
215
+ ## Payments
216
+
217
+ The plans users can buy are in `apps/web/src/config/plans.ts`, with the app's own ids. The dashboard's billing card offers checkout for each plan, then shows the subscription and opens billing management. Server code reads a user's plan with `getBilling().subscriptionFor(userId)` and `isEntitled` from `@/server/payments`, and the `subscriptions` table keeps it in sync through the provider's webhook at `/api/webhooks/payments`.
218
+
219
+ <!-- clubedge:end -->
220
+ <!-- clubedge:if payments=stripe -->
221
+
222
+ [Stripe](https://stripe.com) runs checkout and billing management. In test mode:
223
+
224
+ 1. Copy the secret key from the dashboard's API keys into `STRIPE_SECRET_KEY`.
225
+ 2. Create a recurring price for each plan and put its id in `STRIPE_PRICE_<PLAN>`, such as `STRIPE_PRICE_PRO`.
226
+ 3. Forward webhooks to your machine with the [Stripe CLI](https://docs.stripe.com/stripe-cli), and put the signing secret it prints in `STRIPE_WEBHOOK_SECRET`:
227
+
228
+ ```sh
229
+ stripe listen --forward-to localhost:3000/api/webhooks/payments
230
+ ```
231
+
232
+ Turn on the customer portal in the dashboard's billing settings, so "Manage billing" works. In production, add a webhook endpoint for `https://<your-domain>/api/webhooks/payments` with the events `checkout.session.completed` and `customer.subscription.created`, `.updated`, `.deleted`, `.paused`, and `.resumed`, and use its signing secret. Every webhook's signature is verified, each event is applied once, and subscription events read the subscription's current state from Stripe, so retried or out-of-order deliveries are safe.
233
+
234
+ <!-- clubedge:end -->
235
+ <!-- clubedge:if payments!=none -->
236
+
237
+ Payments go through the provider-neutral `PaymentProvider` interface in `packages/payments`. To use another provider, implement that interface in a new package and return it from `apps/web/src/server/payments.ts`; billing logic, the tables, the routes, and the billing card stay the same.
238
+
211
239
  <!-- clubedge:end -->
212
240
  <!-- clubedge:if storage!=none -->
213
241
 
@@ -262,6 +290,90 @@ pnpm docker:start
262
290
  The image runs the Next.js standalone output as an unprivileged user. Ensure the configured database and provider hosts are reachable from the container. <!-- clubedge:only framework=next -->
263
291
  The image runs the self-contained Nitro server bundle as an unprivileged user, with no `node_modules`. Ensure the configured database and provider hosts are reachable from the container. <!-- clubedge:only framework=tanstack-start -->
264
292
 
293
+ The image also contains the production migrator, which applies every package's migrations with only `DATABASE_URL`:
294
+
295
+ ```sh
296
+ docker run --rm --env DATABASE_URL="postgresql://..." <image> node /app/migrate.mjs
297
+ ```
298
+
299
+ Run it before a new version starts serving; it skips migrations that are already applied.
300
+
301
+ ## Deploy
302
+
303
+ <!-- clubedge:if deploy=docker -->
304
+
305
+ Run the production image on any host or orchestrator that runs containers: build it with `pnpm docker:build` or in CI, push it to a registry, and start it with the production environment. Before each release, run the migrator above as a one-off task (a Kubernetes Job, an ECS task, or `docker run`), then roll out the new containers. Point the platform's health check at `/api/health`.
306
+
307
+ <!-- clubedge:end -->
308
+ <!-- clubedge:if deploy=fly -->
309
+
310
+ `fly.toml` deploys the production image to [Fly.io](https://fly.io):
311
+
312
+ ```sh
313
+ fly launch --no-deploy # creates the app; keep the existing fly.toml
314
+ fly secrets set DATABASE_URL="postgresql://..."
315
+ fly deploy
316
+ ```
317
+
318
+ Set the other production variables listed below with `fly secrets set`. Each deploy runs `node /app/migrate.mjs` as its release command, in a temporary machine, before the new version replaces the old one; a failed migration stops the deploy. Health checks call `/api/health`.
319
+
320
+ <!-- clubedge:end -->
321
+ <!-- clubedge:if deploy=railway -->
322
+
323
+ `railway.json` deploys the production image to [Railway](https://railway.com): create a project from this repository, add the production variables listed below to the service, and deploy. Railway runs `node /app/migrate.mjs` as the pre-deploy command, so a failed migration stops the release, and checks `/api/health` before switching traffic.
324
+
325
+ <!-- clubedge:end -->
326
+ <!-- clubedge:if deploy=render -->
327
+
328
+ `render.yaml` is a [Render](https://render.com) Blueprint: in the dashboard, choose New, then Blueprint, and select this repository. Render asks for each variable marked `sync: false` and generates `BETTER_AUTH_SECRET`. <!-- clubedge:only auth=better-auth -->
329
+ `render.yaml` is a [Render](https://render.com) Blueprint: in the dashboard, choose New, then Blueprint, and select this repository. Render asks for each variable marked `sync: false`. <!-- clubedge:only auth!=better-auth -->
330
+
331
+ Render runs `node /app/migrate.mjs` as the pre-deploy command before each deploy goes live, and a failed migration stops the deploy. Pre-deploy commands need a paid instance type. On the free type, remove `preDeployCommand` from `render.yaml` and apply migrations yourself before deploying, from your machine or CI, with `DATABASE_URL` set to the production database: `pnpm db:migrate`, or the migrator in the image as shown above. Health checks call `/api/health`.
332
+
333
+ <!-- clubedge:end -->
334
+ <!-- clubedge:if deploy=vercel -->
335
+
336
+ `apps/web/vercel.json` deploys the app to [Vercel](https://vercel.com). Import the repository, set the project's Root Directory to `apps/web`, add the production variables listed below, and deploy. Vercel installs the whole pnpm workspace and builds the app. <!-- clubedge:only framework=next -->
337
+ `apps/web/vercel.json` deploys the app to [Vercel](https://vercel.com). Import the repository, set the project's Root Directory to `apps/web`, add the production variables listed below, and deploy. The build detects Vercel and writes Vercel's Build Output, with server routes as functions. <!-- clubedge:only framework=tanstack-start -->
338
+
339
+ Vercel has no step that runs before a release, so the build command applies migrations (`pnpm db:migrate`) against the `DATABASE_URL` of the deployment's environment. Give Preview deployments their own database, or remove the migration step from `buildCommand` and run migrations yourself. Serverless functions open a connection per instance: use your provider's pooled connection URL (for example Supabase's transaction pooler or Neon's pooled endpoint).
340
+
341
+ <!-- clubedge:end -->
342
+ <!-- clubedge:if deploy=netlify -->
343
+
344
+ `apps/web/netlify.toml` deploys the app to [Netlify](https://www.netlify.com). Import the repository, set the site's Base directory to `apps/web`, add the production variables listed below, and deploy. Netlify installs its Next.js runtime automatically. <!-- clubedge:only framework=next -->
345
+ `apps/web/netlify.toml` deploys the app to [Netlify](https://www.netlify.com). Import the repository, set the site's Base directory to `apps/web`, add the production variables listed below, and deploy. The build detects Netlify and writes its functions and static assets. <!-- clubedge:only framework=tanstack-start -->
346
+
347
+ Netlify has no step that runs before a release, so the build command applies migrations (`pnpm db:migrate`) against the `DATABASE_URL` of the deploy context. Give deploy previews their own database, or remove the migration step from the build command and run migrations yourself. Serverless functions open a connection per instance: use your provider's pooled connection URL (for example Supabase's transaction pooler or Neon's pooled endpoint).
348
+
349
+ <!-- clubedge:end -->
350
+
351
+ `.github/workflows/deploy.yml` deploys on demand: in GitHub, open Actions, then Deploy, then Run workflow.
352
+
353
+ - It builds the production image and publishes it to this repository's GitHub Container Registry as `ghcr.io/<owner>/<repository>`, tagged with the commit and `latest`, using the workflow's own token. Run the migrator from that image before rolling it out. <!-- clubedge:only deploy=docker -->
354
+ - It needs the secret `FLY_API_TOKEN`, from `fly tokens create deploy`. <!-- clubedge:only deploy=fly -->
355
+ - It needs the secret `RAILWAY_TOKEN`, a project token from the project's settings, and the variable `RAILWAY_SERVICE`, the service's name. <!-- clubedge:only deploy=railway -->
356
+ - It needs the secret `RENDER_DEPLOY_HOOK_URL`, the deploy hook from the service's settings. <!-- clubedge:only deploy=render -->
357
+ - It needs the secrets `VERCEL_TOKEN`, from your account settings, and `VERCEL_ORG_ID` and `VERCEL_PROJECT_ID`, which `vercel link` writes to `.vercel/project.json`. <!-- clubedge:only deploy=vercel -->
358
+ - It needs the secrets `NETLIFY_AUTH_TOKEN`, a personal access token, and `NETLIFY_SITE_ID`, from the site's settings. <!-- clubedge:only deploy=netlify -->
359
+
360
+ `/api/health` answers as long as the app runs, so platforms use it to decide whether to restart the app. `/api/ready` also checks that the database answers, returning 200 or 503 without details: point load balancers and readiness probes at it, so traffic waits while the database is unreachable instead of the app restarting.
361
+
362
+ Production needs these variables:
363
+
364
+ - `DATABASE_URL`, pointing at a managed PostgreSQL database.
365
+ - `NEXT_PUBLIC_APP_URL`, the deployed origin. <!-- clubedge:only framework=next -->
366
+ - `APP_URL`, the deployed origin. <!-- clubedge:only framework=tanstack-start -->
367
+ - `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`, from the Supabase project. <!-- clubedge:only framework=next && auth=supabase -->
368
+ - `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY`, from the Supabase project. <!-- clubedge:only framework=tanstack-start && auth=supabase -->
369
+ - `BETTER_AUTH_SECRET`, at least 32 random characters, different from development. <!-- clubedge:only auth=better-auth -->
370
+ - `SMTP_URL` and `EMAIL_FROM`; without them, production refuses to send email. <!-- clubedge:only email=smtp -->
371
+ - `RESEND_API_KEY` and `EMAIL_FROM`; without them, production refuses to send email. <!-- clubedge:only email=resend -->
372
+ - `STRIPE_SECRET_KEY` (live mode), `STRIPE_WEBHOOK_SECRET` from the production webhook endpoint, and `STRIPE_PRICE_<PLAN>` for each plan. <!-- clubedge:only payments=stripe -->
373
+ - `REDIS_URL`, when you run more than one instance. <!-- clubedge:only cache=redis -->
374
+ - The `STORAGE_*` variables for your S3-compatible bucket. <!-- clubedge:only storage=s3 -->
375
+ - `SUPABASE_STORAGE_BUCKET`. <!-- clubedge:only storage=supabase -->
376
+
265
377
  ## Checks
266
378
 
267
379
  Run the checks relevant to your change before opening a pull request:
@@ -22,3 +22,6 @@ supabase/.temp/
22
22
  supabase/.branches/
23
23
  # Local database and files for --infra local
24
24
  .data/
25
+ # Build output of the Vercel and Netlify presets
26
+ .vercel/
27
+ .netlify/
@@ -53,6 +53,15 @@ RESEND_API_KEY=
53
53
  EMAIL_FROM=App <no-reply@example.com>
54
54
  # clubedge:end
55
55
 
56
+ # clubedge:if payments=stripe
57
+ # Stripe (https://dashboard.stripe.com/apikeys): use test-mode keys during development. The
58
+ # webhook secret comes from `stripe listen` locally, or from the endpoint's settings in production.
59
+ STRIPE_SECRET_KEY=
60
+ STRIPE_WEBHOOK_SECRET=
61
+ # The Stripe price for each plan in src/config/plans.ts (STRIPE_PRICE_<PLAN>).
62
+ STRIPE_PRICE_PRO=
63
+ # clubedge:end
64
+
56
65
  # clubedge:if storage=s3
57
66
  # S3-compatible storage (AWS S3, Cloudflare R2, MinIO)
58
67
  STORAGE_BUCKET= # clubedge:only infra!=docker
@@ -25,6 +25,10 @@ COPY --from=deps /app/ .
25
25
  COPY --from=pruner /app/out/full/ .
26
26
  COPY tsconfig.base.json ./
27
27
  RUN pnpm build
28
+ # The production migrator (node /app/migrate.mjs): one bundled file plus every package's
29
+ # migrations, so release commands can migrate before a new version starts.
30
+ COPY scripts/migrate.mjs scripts/build-migrator.mjs ./scripts/
31
+ RUN node scripts/build-migrator.mjs /app/.migrator
28
32
 
29
33
  # Nitro bundles the server and its dependencies into .output, so no node_modules are needed.
30
34
  FROM node:22-alpine AS runner
@@ -33,6 +37,7 @@ ENV NODE_ENV=production HOST=0.0.0.0 PORT=3000
33
37
  WORKDIR /app
34
38
  RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 app
35
39
  COPY --from=builder --chown=app:nodejs /app/${APP_DIR}/.output ./.output
40
+ COPY --from=builder --chown=app:nodejs /app/.migrator ./
36
41
  USER app
37
42
  EXPOSE 3000
38
43
  CMD ["node", ".output/server/index.mjs"]
@@ -0,0 +1,6 @@
1
+ # Netlify (https://docs.netlify.com/configure-builds/file-based-configuration/). Set the site's
2
+ # base directory to apps/web; the build detects Netlify and writes its functions and assets.
3
+ [build]
4
+ # Migrations run during the build, against the DATABASE_URL set for the deploy context.
5
+ command = "pnpm --dir ../.. db:migrate && pnpm run build"
6
+ publish = "dist"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/start",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -22,6 +22,8 @@
22
22
  "@clubedge/email": "workspace:*",
23
23
  "@clubedge/email-resend": "workspace:*",
24
24
  "@clubedge/email-smtp": "workspace:*",
25
+ "@clubedge/payments": "workspace:*",
26
+ "@clubedge/payments-stripe": "workspace:*",
25
27
  "@clubedge/storage": "workspace:*",
26
28
  "@clubedge/storage-s3": "workspace:*",
27
29
  "@clubedge/storage-supabase": "workspace:*",
@@ -14,6 +14,9 @@ import { createSmtpEmail } from "@clubedge/email-smtp";
14
14
  // clubedge:if email=resend
15
15
  import { createResendEmail } from "@clubedge/email-resend";
16
16
  // clubedge:end
17
+ // clubedge:if payments=stripe
18
+ import { createStripePayments } from "@clubedge/payments-stripe";
19
+ // clubedge:end
17
20
  // clubedge:if storage=s3 && infra=local
18
21
  import { createFileSystemStorage } from "@clubedge/storage/file-system";
19
22
  // clubedge:end
@@ -100,6 +103,23 @@ async function main() {
100
103
  },
101
104
  },
102
105
  // clubedge:end
106
+ // clubedge:if payments=stripe
107
+ {
108
+ name: "Payments (Stripe)",
109
+ skip: env.STRIPE_SECRET_KEY ? undefined : "STRIPE_SECRET_KEY is not set",
110
+ async run() {
111
+ // A read-only request that checks the key; nothing is charged or created.
112
+ const stripe = createStripePayments({
113
+ secretKey: env.STRIPE_SECRET_KEY!,
114
+ webhookSecret: env.STRIPE_WEBHOOK_SECRET || "unused",
115
+ prices: {},
116
+ apiBase: env.STRIPE_API_BASE || undefined,
117
+ });
118
+ await stripe.verify();
119
+ return "secret key accepted";
120
+ },
121
+ },
122
+ // clubedge:end
103
123
  // clubedge:if storage=s3 && infra=local
104
124
  {
105
125
  name: "Local folder storage",