@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.
- package/CHANGELOG.md +10 -0
- package/README.md +13 -11
- package/package.json +3 -3
- package/template/.env.example +9 -0
- package/template/.github/workflows/ci.yml +18 -0
- package/template/.github/workflows/deploy.yml +109 -0
- package/template/Dockerfile +5 -0
- package/template/README.md +10 -0
- package/template/SETUP.md +113 -1
- package/template/_gitignore +3 -0
- package/template/apps/start/.env.example +9 -0
- package/template/apps/start/Dockerfile +5 -0
- package/template/apps/start/netlify.toml +6 -0
- package/template/apps/start/package.json +3 -1
- package/template/apps/start/scripts/check-services.ts +20 -0
- package/template/apps/start/src/components/billing/billing-card.tsx +83 -0
- package/template/apps/start/src/config/plans.ts +16 -0
- package/template/apps/start/src/routes/api/ready.ts +23 -0
- package/template/apps/start/src/routes/api/webhooks/payments.ts +31 -0
- package/template/apps/start/src/routes/billing/checkout.ts +11 -0
- package/template/apps/start/src/routes/billing/portal.ts +11 -0
- package/template/apps/start/src/routes/dashboard.tsx +16 -2
- package/template/apps/start/src/server/billing-forms.ts +50 -0
- package/template/apps/start/src/server/billing.functions.ts +29 -0
- package/template/apps/start/src/server/payments.ts +60 -0
- package/template/apps/start/src/server/users.ts +6 -1
- package/template/apps/start/src/start.ts +3 -1
- package/template/apps/start/vercel.json +5 -0
- package/template/apps/web/netlify.toml +6 -0
- package/template/apps/web/package.json +3 -1
- package/template/apps/web/scripts/check-services.ts +20 -0
- package/template/apps/web/src/app/api/ready/route.ts +18 -0
- package/template/apps/web/src/app/api/webhooks/payments/route.ts +23 -0
- package/template/apps/web/src/app/dashboard/_components/billing-actions.ts +44 -0
- package/template/apps/web/src/app/dashboard/_components/billing-card.tsx +89 -0
- package/template/apps/web/src/app/dashboard/page.tsx +7 -0
- package/template/apps/web/src/config/plans.ts +16 -0
- package/template/apps/web/src/server/payments.ts +60 -0
- package/template/apps/web/src/server/users.ts +6 -1
- package/template/apps/web/vercel.json +5 -0
- package/template/clubedge.template.json +66 -0
- package/template/e2e/payments.spec.ts +51 -0
- package/template/e2e/ready.spec.ts +11 -0
- package/template/e2e/stripe-fake.ts +95 -0
- package/template/fly.toml +25 -0
- package/template/package.json +2 -1
- package/template/packages/auth/package.json +1 -1
- package/template/packages/auth-better-auth/package.json +7 -1
- package/template/packages/auth-supabase/package.json +1 -1
- package/template/packages/cache/package.json +1 -1
- package/template/packages/cache-redis/package.json +1 -1
- package/template/packages/core/package.json +1 -1
- package/template/packages/db/package.json +7 -1
- package/template/packages/db/src/client.test.ts +28 -0
- package/template/packages/db/src/client.ts +19 -0
- package/template/packages/db/src/index.ts +2 -2
- package/template/packages/db/src/users.ts +23 -4
- package/template/packages/email/package.json +1 -1
- package/template/packages/email-resend/package.json +1 -1
- package/template/packages/email-smtp/package.json +1 -1
- package/template/packages/local-db/package.json +1 -1
- package/template/packages/payments/drizzle/0000_initial.sql +24 -0
- package/template/packages/payments/drizzle/meta/0000_snapshot.json +171 -0
- package/template/packages/payments/drizzle/meta/_journal.json +13 -0
- package/template/packages/payments/drizzle.config.ts +12 -0
- package/template/packages/payments/package.json +35 -0
- package/template/packages/payments/scripts/migrate.ts +14 -0
- package/template/packages/payments/src/billing.test.ts +151 -0
- package/template/packages/payments/src/billing.ts +125 -0
- package/template/packages/payments/src/index.ts +17 -0
- package/template/packages/payments/src/schema.ts +57 -0
- package/template/packages/payments/src/types.ts +67 -0
- package/template/packages/payments/tsconfig.json +4 -0
- package/template/packages/payments-stripe/package.json +22 -0
- package/template/packages/payments-stripe/src/index.test.ts +156 -0
- package/template/packages/payments-stripe/src/index.ts +141 -0
- package/template/packages/payments-stripe/tsconfig.json +4 -0
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage-s3/package.json +1 -1
- package/template/packages/storage-supabase/package.json +1 -1
- package/template/packages/ui/package.json +1 -1
- package/template/pnpm-lock.yaml +63 -0
- package/template/railway.json +13 -0
- package/template/render.yaml +74 -0
- package/template/scripts/build-migrator.mjs +51 -0
- package/template/scripts/migrate.mjs +30 -0
- 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.
|
|
15
|
+
create-clubedge-app v0.12.0
|
|
16
16
|
|
|
|
17
17
|
v
|
|
18
|
-
Clubedge Starter v0.
|
|
19
|
-
commit
|
|
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.
|
|
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.
|
|
31
|
-
`
|
|
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.
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
26
|
-
"starterCommit": "
|
|
25
|
+
"starterRef": "v0.11.0",
|
|
26
|
+
"starterCommit": "b5637afcf3748e03bb7ecb1f1941a4e6c17cfa65"
|
|
27
27
|
},
|
|
28
28
|
"license": "Apache-2.0",
|
|
29
29
|
"repository": {
|
package/template/.env.example
CHANGED
|
@@ -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
|
package/template/Dockerfile
CHANGED
|
@@ -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
|
package/template/README.md
CHANGED
|
@@ -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
|
|
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:
|
package/template/_gitignore
CHANGED
|
@@ -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.
|
|
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",
|