@clubedge/create-clubedge-app 0.30.1 → 0.32.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 +21 -0
- package/README.md +12 -10
- package/package.json +3 -3
- package/template/.env.example +39 -0
- package/template/.github/workflows/ci.yml +22 -0
- package/template/README.md +25 -3
- package/template/SETUP.md +60 -10
- package/template/apps/start/.env.example +39 -0
- package/template/apps/start/package.json +5 -1
- package/template/apps/start/src/components/jobs/jobs-card.tsx +7 -5
- package/template/apps/start/src/env/auth.ts +66 -1
- package/template/apps/start/src/env/production.test.ts +8 -0
- package/template/apps/start/src/env/production.ts +30 -1
- package/template/apps/start/src/routes/api/inngest.ts +16 -0
- package/template/apps/start/src/routes/reset-password.tsx +3 -1
- package/template/apps/start/src/server/auth-email.ts +2 -2
- package/template/apps/start/src/server/auth-forms.ts +3 -1
- package/template/apps/start/src/server/jobs.functions.ts +4 -4
- package/template/apps/start/src/server/jobs.ts +8 -0
- package/template/apps/start/src/server/variants/auth.appwrite.ts +42 -0
- package/template/apps/start/src/server/variants/auth.firebase.ts +49 -0
- package/template/apps/start/src/server/variants/jobs.bullmq.ts +88 -0
- package/template/apps/start/src/server/variants/jobs.inngest.ts +94 -0
- package/template/apps/start/src/start.ts +9 -3
- package/template/apps/web/package.json +5 -1
- package/template/apps/web/src/app/api/inngest/route.ts +15 -0
- package/template/apps/web/src/app/auth/callback/route.ts +3 -1
- package/template/apps/web/src/app/dashboard/_components/jobs-card.tsx +8 -6
- package/template/apps/web/src/app/reset-password/page.tsx +6 -2
- package/template/apps/web/src/env/auth.ts +67 -0
- package/template/apps/web/src/env/production.test.ts +8 -0
- package/template/apps/web/src/env/production.ts +30 -1
- package/template/apps/web/src/server/auth-email.ts +2 -2
- package/template/apps/web/src/server/jobs.ts +8 -0
- package/template/apps/web/src/server/variants/auth.appwrite.ts +48 -0
- package/template/apps/web/src/server/variants/auth.firebase.ts +55 -0
- package/template/apps/web/src/server/variants/jobs.bullmq.ts +88 -0
- package/template/apps/web/src/server/variants/jobs.inngest.ts +97 -0
- package/template/clubedge.template.json +160 -1
- package/template/firebase.json +7 -0
- package/template/package.json +3 -2
- package/template/packages/auth/package.json +3 -2
- package/template/packages/auth/src/conformance.ts +322 -0
- package/template/packages/{auth-better-auth → auth}/src/emails.ts +2 -1
- package/template/packages/auth/src/index.ts +7 -0
- package/template/packages/auth/src/links.test.ts +22 -0
- package/template/packages/auth/src/links.ts +28 -0
- package/template/packages/auth-appwrite/package.json +23 -0
- package/template/packages/auth-appwrite/src/conformance.live.test.ts +82 -0
- package/template/packages/auth-appwrite/src/index.test.ts +204 -0
- package/template/packages/auth-appwrite/src/index.ts +202 -0
- package/template/packages/auth-appwrite/tsconfig.json +4 -0
- package/template/packages/auth-better-auth/package.json +1 -1
- package/template/packages/auth-better-auth/src/conformance.test.ts +49 -0
- package/template/packages/auth-better-auth/src/index.ts +11 -3
- package/template/packages/auth-firebase/package.json +23 -0
- package/template/packages/auth-firebase/src/conformance.test.ts +57 -0
- package/template/packages/auth-firebase/src/index.ts +310 -0
- package/template/packages/auth-firebase/tsconfig.json +4 -0
- package/template/packages/auth-supabase/package.json +1 -1
- package/template/packages/auth-supabase/src/conformance.test.ts +71 -0
- 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/drizzle/0001_auth_user_id_text.sql +3 -0
- package/template/packages/db/drizzle/meta/_journal.json +7 -0
- package/template/packages/db/package.json +1 -1
- package/template/packages/db/src/schema/users.ts +3 -2
- 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/jobs/package.json +3 -2
- package/template/packages/jobs/src/conformance.test.ts +5 -0
- package/template/packages/jobs/src/conformance.ts +267 -0
- package/template/packages/jobs/src/index.test.ts +1 -1
- package/template/packages/jobs/src/index.ts +51 -7
- package/template/packages/jobs-bullmq/package.json +23 -0
- package/template/packages/jobs-bullmq/src/index.test.ts +18 -0
- package/template/packages/jobs-bullmq/src/index.ts +279 -0
- package/template/packages/jobs-bullmq/tsconfig.json +4 -0
- package/template/packages/jobs-inngest/package.json +22 -0
- package/template/packages/jobs-inngest/src/index.test.ts +189 -0
- package/template/packages/jobs-inngest/src/index.ts +307 -0
- package/template/packages/jobs-inngest/tsconfig.json +4 -0
- package/template/packages/jobs-pg-boss/package.json +1 -1
- package/template/packages/jobs-pg-boss/src/index.test.ts +13 -0
- package/template/packages/jobs-pg-boss/src/index.ts +3 -16
- package/template/packages/local-db/package.json +1 -1
- package/template/packages/observability/package.json +1 -1
- package/template/packages/observability-otel/package.json +1 -1
- package/template/packages/observability-sentry/package.json +1 -1
- package/template/packages/payments/package.json +1 -1
- package/template/packages/payments-stripe/package.json +1 -1
- 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 +5219 -2438
- package/template/supabase/config.toml +4 -0
- package/template.lock.json +39 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `create-clubedge-app` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.32.0
|
|
6
|
+
|
|
7
|
+
- **Pluggable authentication**: default to Starter `v0.21.0` (#93). Authentication now has four providers behind one interface, so the pages, settings, payments, and jobs stay the same whichever signs users in:
|
|
8
|
+
- `--auth supabase` and `--auth better-auth`, as before.
|
|
9
|
+
- **`--auth firebase`** (new): Firebase Authentication from the server, with session cookies. `pnpm auth:dev` starts the Auth Emulator, so development needs no Firebase account. Reset and confirmation emails go through the app's email module.
|
|
10
|
+
- **`--auth appwrite`** (new): Appwrite accounts on Appwrite Cloud or your own server, with session cookies and an API key. Appwrite sends its own emails.
|
|
11
|
+
- `@clubedge/auth/conformance` is one test suite every auth provider passes: sign-up and sign-in, sign-out, reset through the emailed link, password changes that sign out other sessions, and account deletion. The Starter's CI runs it against all four, including Appwrite Cloud.
|
|
12
|
+
- Stripe payments work with every provider.
|
|
13
|
+
- Switch an existing project with `pnpm clubedge add auth firebase` or `pnpm clubedge add auth appwrite`. After `pnpm clubedge upgrade`, run `pnpm db:migrate`: `users.auth_user_id` becomes text, since Firebase's and Appwrite's user ids aren't UUIDs.
|
|
14
|
+
|
|
15
|
+
## 0.31.0
|
|
16
|
+
|
|
17
|
+
- **Pluggable background jobs**: default to Starter `v0.20.0` (#91). The jobs module now has three providers behind one interface, so job definitions, routes, and the dashboard's jobs card stay the same whichever runs them:
|
|
18
|
+
- `--jobs pg-boss`: jobs in your PostgreSQL, run by a worker in the app server.
|
|
19
|
+
- **`--jobs bullmq`** (new): jobs in Redis through BullMQ 6, run by a worker in the app server. It needs `--cache redis`, so `deploy` creates Railway's Redis with it.
|
|
20
|
+
- **`--jobs inngest`** (new): Inngest calls the app's `/api/inngest` to run each job, so serverless hosts need no cron. `pnpm jobs:dev` starts its Dev Server, and `check:env` requires `INNGEST_EVENT_KEY` and `INNGEST_SIGNING_KEY` in production.
|
|
21
|
+
- `@clubedge/jobs/conformance` is one test suite every provider passes: retries, delays, throttling, timeouts, history, and the worker. SETUP.md shows how to add your own provider with it.
|
|
22
|
+
- `runDue`'s `failed` now counts every failed attempt, retries included, for every provider.
|
|
23
|
+
- Switch an existing project with `pnpm clubedge add jobs bullmq` or `pnpm clubedge add jobs inngest`.
|
|
24
|
+
- The showcase's SaaS walkthrough waits for Stripe's billing portal to be on screen before moving on (#90). Stripe renders the portal's text under a spinner and fades it in, which Playwright counted as visible.
|
|
25
|
+
|
|
5
26
|
## 0.30.1
|
|
6
27
|
|
|
7
28
|
Railway previews, fixed after running them for real on the Starter's own pull requests.
|
package/README.md
CHANGED
|
@@ -129,7 +129,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git --framework tanstac
|
|
|
129
129
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref main
|
|
130
130
|
|
|
131
131
|
# Reproduce a specific Starter release or commit
|
|
132
|
-
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.
|
|
132
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.21.0
|
|
133
133
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
|
|
134
134
|
|
|
135
135
|
# Test local Starter changes before releasing them
|
|
@@ -148,7 +148,7 @@ pnpm clubedge add email resend payments stripe # several modules as
|
|
|
148
148
|
pnpm clubedge remove storage payments
|
|
149
149
|
pnpm dlx @clubedge/create-clubedge-app add infra local --dry-run
|
|
150
150
|
pnpm dlx @clubedge/create-clubedge-app@latest upgrade # to the newest CLI's Starter release
|
|
151
|
-
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.
|
|
151
|
+
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.21.0 # or a specific tag, branch, or commit
|
|
152
152
|
```
|
|
153
153
|
|
|
154
154
|
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.
|
|
@@ -259,27 +259,27 @@ Projects created with `--infra neon` also have `.github/workflows/preview.yml`,
|
|
|
259
259
|
Each CLI release is tested against one specific Starter release:
|
|
260
260
|
|
|
261
261
|
```text
|
|
262
|
-
create-clubedge-app v0.
|
|
262
|
+
create-clubedge-app v0.32.0
|
|
263
263
|
|
|
|
264
264
|
v
|
|
265
|
-
Clubedge Starter v0.
|
|
266
|
-
commit
|
|
265
|
+
Clubedge Starter v0.21.0
|
|
266
|
+
commit 8f1201f2d526347ccf0365a701aaa20e883f8d8a
|
|
267
267
|
```
|
|
268
268
|
|
|
269
269
|
The current relationship is:
|
|
270
270
|
|
|
271
271
|
| CLI release | Default Starter release | Meaning |
|
|
272
272
|
| --- | --- | --- |
|
|
273
|
-
| `create-clubedge-app@0.
|
|
273
|
+
| `create-clubedge-app@0.32.0` | `clubedge-starter@v0.21.0` | The CLI's required, tested default |
|
|
274
274
|
|
|
275
275
|
This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
|
|
276
276
|
|
|
277
|
-
The `v0.
|
|
278
|
-
`
|
|
277
|
+
The `v0.21.0` tag is expected to resolve to commit
|
|
278
|
+
`8f1201f2d526347ccf0365a701aaa20e883f8d8a`. The CLI verifies this before
|
|
279
279
|
scaffolding with the default reference and fails rather than silently using a
|
|
280
280
|
moved tag.
|
|
281
281
|
|
|
282
|
-
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.
|
|
282
|
+
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.21.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.
|
|
283
283
|
|
|
284
284
|
Every generated project records its CLI version, Starter repository, Starter ref, Starter commit, framework, and modules in `package.json` and `README.md`.
|
|
285
285
|
|
|
@@ -334,7 +334,7 @@ CI packs the CLI with `npm pack`, scaffolds clean projects from the packed tarba
|
|
|
334
334
|
1. Make and validate changes in `clubedge-starter`.
|
|
335
335
|
2. Run lint, typecheck, unit tests, browser tests, and production build.
|
|
336
336
|
3. Update the Starter package versions.
|
|
337
|
-
4. Commit `main`, create a release tag such as `v0.
|
|
337
|
+
4. Commit `main`, create a release tag such as `v0.21.0`, and publish the Starter release.
|
|
338
338
|
|
|
339
339
|
### CLI maintainers
|
|
340
340
|
|
|
@@ -375,6 +375,8 @@ Each phase ends with a Starter release and a CLI release pinned to it. The full
|
|
|
375
375
|
| 0.29.0 | v0.18.0 | `deploy` to Netlify and Render (experimental) |
|
|
376
376
|
| 0.30.0 | v0.19.0 | `--infra neon` with a free Neon database, the `neon` command, `deploy --preview` with a Neon branch per preview |
|
|
377
377
|
| 0.30.1 | v0.19.1 | Railway previews work with a workspace token and answer on the port their domain targets |
|
|
378
|
+
| 0.31.0 | v0.20.0 | Pluggable background jobs: `--jobs bullmq` and `--jobs inngest` next to pg-boss, and a conformance suite every provider passes |
|
|
379
|
+
| 0.32.0 | v0.21.0 | Pluggable authentication: `--auth firebase` and `--auth appwrite` next to Supabase and Better Auth, and a conformance suite every provider passes |
|
|
378
380
|
| next | | Show the Starter's version of a conflicted file next to yours |
|
|
379
381
|
|
|
380
382
|
## Community
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clubedge/create-clubedge-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.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.21.0",
|
|
26
|
+
"starterCommit": "8f1201f2d526347ccf0365a701aaa20e883f8d8a"
|
|
27
27
|
},
|
|
28
28
|
"license": "Apache-2.0",
|
|
29
29
|
"repository": {
|
package/template/.env.example
CHANGED
|
@@ -35,6 +35,27 @@ BETTER_AUTH_SECRET=
|
|
|
35
35
|
BETTER_AUTH_REQUIRE_EMAIL_VERIFICATION=false # clubedge:only email!=none
|
|
36
36
|
# clubedge:end
|
|
37
37
|
|
|
38
|
+
# clubedge:if auth=firebase
|
|
39
|
+
# Firebase Authentication. In development, pnpm auth:dev starts the Auth Emulator, which these
|
|
40
|
+
# values point at. In production, set your project's id and Web API key (Project settings,
|
|
41
|
+
# General) and a service account (Project settings, Service accounts), and remove
|
|
42
|
+
# FIREBASE_AUTH_EMULATOR_HOST.
|
|
43
|
+
FIREBASE_PROJECT_ID=demo-clubedge
|
|
44
|
+
FIREBASE_API_KEY=demo-api-key
|
|
45
|
+
FIREBASE_CLIENT_EMAIL=
|
|
46
|
+
FIREBASE_PRIVATE_KEY=
|
|
47
|
+
FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099
|
|
48
|
+
# clubedge:end
|
|
49
|
+
|
|
50
|
+
# clubedge:if auth=appwrite
|
|
51
|
+
# Appwrite (https://appwrite.io), on Appwrite Cloud or your own server. Create a project, add a
|
|
52
|
+
# Web platform for this app's hostname (localhost in development), and an API key with the
|
|
53
|
+
# sessions.write and users.write scopes. Appwrite sends its own emails (its SMTP settings).
|
|
54
|
+
APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
|
|
55
|
+
APPWRITE_PROJECT_ID=
|
|
56
|
+
APPWRITE_API_KEY=
|
|
57
|
+
# clubedge:end
|
|
58
|
+
|
|
38
59
|
# clubedge:if cache=redis
|
|
39
60
|
# Optional cache and rate limits (Redis protocol; rediss:// enables TLS). Without it, both use
|
|
40
61
|
# process memory. Upstash example: rediss://default:YOUR_PASSWORD@your-database.upstash.io:6379
|
|
@@ -77,6 +98,24 @@ JOBS_WORKER=
|
|
|
77
98
|
CRON_SECRET=
|
|
78
99
|
# clubedge:end
|
|
79
100
|
|
|
101
|
+
# clubedge:if jobs=bullmq
|
|
102
|
+
# Background jobs are stored in Redis (REDIS_URL above) through BullMQ and run by a worker inside
|
|
103
|
+
# the app server. Set JOBS_WORKER=off where no server keeps running (it is off by default on
|
|
104
|
+
# Vercel and Netlify); a scheduler then calls /api/jobs/run with CRON_SECRET (16 or more
|
|
105
|
+
# characters) instead.
|
|
106
|
+
JOBS_WORKER=
|
|
107
|
+
CRON_SECRET=
|
|
108
|
+
# clubedge:end
|
|
109
|
+
|
|
110
|
+
# clubedge:if jobs=inngest
|
|
111
|
+
# Background jobs run by Inngest (https://www.inngest.com), which calls this app's /api/inngest.
|
|
112
|
+
# In development, start its Dev Server with pnpm jobs:dev (http://localhost:8288); INNGEST_DEV=1
|
|
113
|
+
# points the app at it. In production, remove INNGEST_DEV and set the keys of your Inngest app.
|
|
114
|
+
INNGEST_DEV=1
|
|
115
|
+
INNGEST_EVENT_KEY=
|
|
116
|
+
INNGEST_SIGNING_KEY=
|
|
117
|
+
# clubedge:end
|
|
118
|
+
|
|
80
119
|
# clubedge:if observability=otel
|
|
81
120
|
# OpenTelemetry: where traces and error logs go over OTLP/HTTP, such as a collector, Grafana
|
|
82
121
|
# Cloud, or Honeycomb. Empty sends nothing. Headers are key=value pairs separated by commas.
|
|
@@ -9,8 +9,21 @@ permissions:
|
|
|
9
9
|
jobs:
|
|
10
10
|
verify:
|
|
11
11
|
runs-on: ubuntu-24.04
|
|
12
|
+
# clubedge:if jobs=bullmq
|
|
13
|
+
# Redis for the BullMQ provider's tests, which run the jobs conformance suite against it.
|
|
14
|
+
services:
|
|
15
|
+
redis:
|
|
16
|
+
image: redis:7
|
|
17
|
+
ports:
|
|
18
|
+
- 6379:6379
|
|
19
|
+
options: >-
|
|
20
|
+
--health-cmd "redis-cli ping" --health-interval 5s --health-timeout 5s --health-retries 10
|
|
21
|
+
# clubedge:end
|
|
12
22
|
env:
|
|
13
23
|
DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/postgres
|
|
24
|
+
REDIS_URL: redis://127.0.0.1:6379 # clubedge:only jobs=bullmq
|
|
25
|
+
FIREBASE_AUTH_EMULATOR_HOST: 127.0.0.1:9099 # clubedge:only auth=firebase
|
|
26
|
+
FIREBASE_PROJECT_ID: demo-clubedge # clubedge:only auth=firebase
|
|
14
27
|
steps:
|
|
15
28
|
- uses: actions/checkout@v7
|
|
16
29
|
- uses: pnpm/action-setup@v6
|
|
@@ -22,6 +35,15 @@ jobs:
|
|
|
22
35
|
- run: pnpm format:check
|
|
23
36
|
- run: pnpm lint
|
|
24
37
|
- run: pnpm typecheck
|
|
38
|
+
# clubedge:if auth=firebase
|
|
39
|
+
# Firebase's Auth Emulator (firebase.json), for the Firebase provider's conformance tests.
|
|
40
|
+
- name: Start the Firebase Auth Emulator
|
|
41
|
+
run: |
|
|
42
|
+
nohup npx --yes firebase-tools@15 emulators:start --only auth --project demo-clubedge \
|
|
43
|
+
> "$RUNNER_TEMP/firebase.log" 2>&1 &
|
|
44
|
+
timeout 240 bash -c 'until curl --silent --output /dev/null http://127.0.0.1:9099/; do sleep 2; done' \
|
|
45
|
+
|| { cat "$RUNNER_TEMP/firebase.log"; exit 1; }
|
|
46
|
+
# clubedge:end
|
|
25
47
|
- run: pnpm test
|
|
26
48
|
# Build the way the app is developed: with the env example copied into place.
|
|
27
49
|
- run: cp .env.example apps/web/.env.local
|
package/template/README.md
CHANGED
|
@@ -57,6 +57,8 @@ Every combination in the CI matrix is generated as a real project and runs its f
|
|
|
57
57
|
|
|
58
58
|
- Supabase Auth with server-verified sessions in secure cookies, a protected dashboard, and rate-limited sign-in. <!-- clubedge:only auth=supabase -->
|
|
59
59
|
- 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 -->
|
|
60
|
+
- Firebase Authentication through the app's own pages, with server-side session cookies, a protected dashboard, and rate-limited sign-in; the Auth Emulator in development. <!-- clubedge:only auth=firebase -->
|
|
61
|
+
- Appwrite accounts through the app's own pages, on Appwrite Cloud or your own server, with server-side session cookies, a protected dashboard, and rate-limited sign-in. <!-- clubedge:only auth=appwrite -->
|
|
60
62
|
- Password reset by email, and email verification that can be required before sign-in. <!-- clubedge:only auth=better-auth && email!=none -->
|
|
61
63
|
- Password reset by email, sent by Supabase. <!-- clubedge:only auth=supabase -->
|
|
62
64
|
- An account settings page: display name, password change, and account deletion. <!-- clubedge:only auth!=none -->
|
|
@@ -68,6 +70,8 @@ Every combination in the CI matrix is generated as a real project and runs its f
|
|
|
68
70
|
- Fixed-window rate limiter and cache in process memory, suited to a single server instance. <!-- clubedge:only cache=memory -->
|
|
69
71
|
- Subscriptions with Stripe Checkout and the customer portal, kept in sync by verified webhooks, behind a provider-neutral payments interface. <!-- clubedge:only payments=stripe -->
|
|
70
72
|
- Background jobs with retries and schedules, stored in PostgreSQL by pg-boss and run by a worker inside the app server, behind a provider-neutral jobs interface. <!-- clubedge:only jobs=pg-boss -->
|
|
73
|
+
- Background jobs with retries and schedules, stored in Redis by BullMQ and run by a worker inside the app server, behind a provider-neutral jobs interface. <!-- clubedge:only jobs=bullmq -->
|
|
74
|
+
- Background jobs with retries and schedules, run by Inngest, which calls the app for each one, behind a provider-neutral jobs interface. <!-- clubedge:only jobs=inngest -->
|
|
71
75
|
- Server error reporting and request tracing with OpenTelemetry, to any OTLP collector, behind a provider-neutral observability interface. <!-- clubedge:only observability=otel -->
|
|
72
76
|
- Server error reporting and request tracing with Sentry, behind a provider-neutral observability interface. <!-- clubedge:only observability=sentry -->
|
|
73
77
|
|
|
@@ -95,12 +99,12 @@ Pick modules when you create a project, and change them later with `pnpm clubedg
|
|
|
95
99
|
| Module | Options | Default |
|
|
96
100
|
| ------------- | --------------------------------------------------------- | ---------- |
|
|
97
101
|
| Framework | `next`, `tanstack-start` | `next` |
|
|
98
|
-
| Auth | `supabase`, `better-auth`, `none`
|
|
102
|
+
| Auth | `supabase`, `better-auth`, `firebase`, `appwrite`, `none` | `supabase` |
|
|
99
103
|
| Storage | `s3`, `supabase`, `none` | `s3` |
|
|
100
104
|
| Cache | `redis`, `memory` | `redis` |
|
|
101
105
|
| Email | `smtp`, `resend`, `none` | `smtp` |
|
|
102
106
|
| Payments | `stripe`, `none` | `none` |
|
|
103
|
-
| Jobs | `pg-boss`, `none`
|
|
107
|
+
| Jobs | `pg-boss`, `bullmq`, `inngest`, `none` | `none` |
|
|
104
108
|
| Observability | `otel`, `sentry`, `none` | `none` |
|
|
105
109
|
| Local infra | `docker`, `supabase`, `local` (no Docker), `neon`, `none` | `docker` |
|
|
106
110
|
| Deploy | `docker`, `fly`, `railway`, `render`, `vercel`, `netlify` | `docker` |
|
|
@@ -217,6 +221,12 @@ flowchart TB
|
|
|
217
221
|
App -->|auth interface and server sessions| Auth[Better Auth]
|
|
218
222
|
Auth -->|users and sessions| DB
|
|
219
223
|
%% clubedge:end
|
|
224
|
+
%% clubedge:if auth=firebase
|
|
225
|
+
App -->|auth interface and server sessions| Auth[Firebase Authentication]
|
|
226
|
+
%% clubedge:end
|
|
227
|
+
%% clubedge:if auth=appwrite
|
|
228
|
+
App -->|auth interface and server sessions| Auth[Appwrite]
|
|
229
|
+
%% clubedge:end
|
|
220
230
|
%% clubedge:if cache=redis
|
|
221
231
|
App -->|cache and rate limits| Redis[(Redis, or memory)]
|
|
222
232
|
%% clubedge:end
|
|
@@ -240,6 +250,14 @@ flowchart TB
|
|
|
240
250
|
App -->|jobs interface, worker in the server| Jobs[pg-boss]
|
|
241
251
|
Jobs -->|jobs table| DB
|
|
242
252
|
%% clubedge:end
|
|
253
|
+
%% clubedge:if jobs=bullmq
|
|
254
|
+
App -->|jobs interface, worker in the server| Jobs[BullMQ]
|
|
255
|
+
Jobs -->|queues| Redis
|
|
256
|
+
%% clubedge:end
|
|
257
|
+
%% clubedge:if jobs=inngest
|
|
258
|
+
App -->|jobs interface, an event per job| Jobs[Inngest]
|
|
259
|
+
Jobs -->|calls /api/inngest to run each job| App
|
|
260
|
+
%% clubedge:end
|
|
243
261
|
%% clubedge:if observability=otel
|
|
244
262
|
App -->|traces and errors| Otel[OTLP collector]
|
|
245
263
|
%% clubedge:end
|
|
@@ -338,6 +356,8 @@ The TanStack Start app is `apps/start`, run with `pnpm --filter @clubedge/start
|
|
|
338
356
|
- `packages/auth/`: `AuthProvider` and `CookieStore` interfaces <!-- clubedge:only auth!=none -->
|
|
339
357
|
- `packages/auth-supabase/`: Supabase Auth adapter <!-- clubedge:only auth=supabase -->
|
|
340
358
|
- `packages/auth-better-auth/`: Better Auth adapter, with its own tables and migrations <!-- clubedge:only auth=better-auth -->
|
|
359
|
+
- `packages/auth-firebase/`: Firebase Authentication adapter, with session cookies <!-- clubedge:only auth=firebase -->
|
|
360
|
+
- `packages/auth-appwrite/`: Appwrite adapter, with session cookies <!-- clubedge:only auth=appwrite -->
|
|
341
361
|
- `packages/storage/`: `StorageProvider` interface <!-- clubedge:only storage!=none -->
|
|
342
362
|
- `packages/storage-s3/`: S3-compatible storage adapter <!-- clubedge:only storage=s3 -->
|
|
343
363
|
- `packages/storage-supabase/`: Supabase Storage adapter <!-- clubedge:only storage=supabase -->
|
|
@@ -348,8 +368,10 @@ The TanStack Start app is `apps/start`, run with `pnpm --filter @clubedge/start
|
|
|
348
368
|
- `packages/email-resend/`: Resend email provider <!-- clubedge:only email=resend -->
|
|
349
369
|
- `packages/payments/`: `PaymentProvider` interface, billing logic, and the subscription tables <!-- clubedge:only payments!=none -->
|
|
350
370
|
- `packages/payments-stripe/`: Stripe payment provider <!-- clubedge:only payments=stripe -->
|
|
351
|
-
- `packages/jobs/`: `JobQueue` interface, job definitions,
|
|
371
|
+
- `packages/jobs/`: `JobQueue` interface, job definitions, an in-memory queue for tests, and the conformance suite every provider passes <!-- clubedge:only jobs!=none -->
|
|
352
372
|
- `packages/jobs-pg-boss/`: pg-boss job queue <!-- clubedge:only jobs=pg-boss -->
|
|
373
|
+
- `packages/jobs-bullmq/`: BullMQ job queue in Redis <!-- clubedge:only jobs=bullmq -->
|
|
374
|
+
- `packages/jobs-inngest/`: Inngest jobs, and the handler for `/api/inngest` <!-- clubedge:only jobs=inngest -->
|
|
353
375
|
- `packages/observability/`: `ObservabilityProvider` interface and an in-memory provider for tests <!-- clubedge:only observability!=none -->
|
|
354
376
|
- `packages/observability-otel/`: OpenTelemetry provider (OTLP/HTTP traces and error logs) <!-- clubedge:only observability=otel -->
|
|
355
377
|
- `packages/observability-sentry/`: Sentry provider <!-- clubedge:only observability=sentry -->
|
package/template/SETUP.md
CHANGED
|
@@ -43,11 +43,14 @@ The variables are:
|
|
|
43
43
|
- `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` (for sign-in): see [Supabase Auth](#supabase-auth). <!-- clubedge:only framework=next && auth=supabase -->
|
|
44
44
|
- `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY` (for sign-in): see [Supabase Auth](#supabase-auth). <!-- clubedge:only framework=tanstack-start && auth=supabase -->
|
|
45
45
|
- `BETTER_AUTH_SECRET` (for sign-in): see [Better Auth](#better-auth). <!-- clubedge:only auth=better-auth -->
|
|
46
|
+
- `FIREBASE_PROJECT_ID`, `FIREBASE_API_KEY`, and in production a service account: see [Firebase Authentication](#firebase-authentication). <!-- clubedge:only auth=firebase -->
|
|
47
|
+
- `APPWRITE_ENDPOINT`, `APPWRITE_PROJECT_ID`, and `APPWRITE_API_KEY` (for sign-in): see [Appwrite](#appwrite). <!-- clubedge:only auth=appwrite -->
|
|
46
48
|
- `REDIS_URL` (optional): see [Redis](#redis). <!-- clubedge:only cache=redis -->
|
|
47
49
|
- `SMTP_URL` and `EMAIL_FROM` (for sending email): see [Email](#email). <!-- clubedge:only email=smtp -->
|
|
48
50
|
- `RESEND_API_KEY` and `EMAIL_FROM` (for sending email): see [Email](#email). <!-- clubedge:only email=resend -->
|
|
49
51
|
- `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, and `STRIPE_PRICE_<PLAN>` (for payments): see [Payments](#payments). <!-- clubedge:only payments=stripe -->
|
|
50
|
-
- `JOBS_WORKER` and `CRON_SECRET` (optional): see [Background jobs](#background-jobs). <!-- clubedge:only jobs=pg-boss -->
|
|
52
|
+
- `JOBS_WORKER` and `CRON_SECRET` (optional): see [Background jobs](#background-jobs). <!-- clubedge:only jobs=pg-boss|bullmq -->
|
|
53
|
+
- `INNGEST_DEV`, `INNGEST_EVENT_KEY`, and `INNGEST_SIGNING_KEY`: see [Background jobs](#background-jobs). <!-- clubedge:only jobs=inngest -->
|
|
51
54
|
- `OTEL_EXPORTER_OTLP_ENDPOINT` (to send traces and errors): see [Observability](#observability). <!-- clubedge:only observability=otel -->
|
|
52
55
|
- `SENTRY_DSN` (to send errors and traces): see [Observability](#observability). <!-- clubedge:only observability=sentry -->
|
|
53
56
|
- `STORAGE_*` (for file storage): see [File storage](#file-storage). <!-- clubedge:only storage=s3 -->
|
|
@@ -102,6 +105,41 @@ These emails are sent by the `send-email` [background job](#background-jobs), so
|
|
|
102
105
|
|
|
103
106
|
<!-- clubedge:end -->
|
|
104
107
|
|
|
108
|
+
<!-- clubedge:if auth=firebase -->
|
|
109
|
+
|
|
110
|
+
## Firebase Authentication
|
|
111
|
+
|
|
112
|
+
[Firebase Authentication](https://firebase.google.com/docs/auth) checks passwords and keeps the accounts; the app's own pages sign users in, and the server holds the session in an HTTP-only cookie (a Firebase session cookie, checked for revocation on each request). Password reset and email confirmation links are generated by Firebase and sent through the [email module](#email), so they open the app's own pages and need no setup in the Firebase console. <!-- clubedge:only email!=none -->
|
|
113
|
+
[Firebase Authentication](https://firebase.google.com/docs/auth) checks passwords and keeps the accounts; the app's own pages sign users in, and the server holds the session in an HTTP-only cookie (a Firebase session cookie, checked for revocation on each request). Without the email module there is no password reset. <!-- clubedge:only email=none -->
|
|
114
|
+
|
|
115
|
+
In development, `pnpm auth:dev` starts Firebase's Auth Emulator (`firebase.json`, port 9099), which the example environment points at: no Firebase project or account is needed. Accounts live in memory until it stops.
|
|
116
|
+
|
|
117
|
+
In production:
|
|
118
|
+
|
|
119
|
+
1. In the [Firebase console](https://console.firebase.google.com), create a project and turn on **Email/Password** under Authentication, Sign-in method.
|
|
120
|
+
2. Set `FIREBASE_PROJECT_ID` and `FIREBASE_API_KEY` (the Web API key, under Project settings, General).
|
|
121
|
+
3. Under Project settings, Service accounts, generate a private key, and set `FIREBASE_CLIENT_EMAIL` and `FIREBASE_PRIVATE_KEY` from it (the key's line breaks as `\n` are fine).
|
|
122
|
+
4. Remove `FIREBASE_AUTH_EMULATOR_HOST`; `pnpm check:env` refuses it in production.
|
|
123
|
+
|
|
124
|
+
Changing a password or resetting it signs out every other session; deleting an account removes it from Firebase after the app's own data for the user.
|
|
125
|
+
|
|
126
|
+
<!-- clubedge:end -->
|
|
127
|
+
|
|
128
|
+
<!-- clubedge:if auth=appwrite -->
|
|
129
|
+
|
|
130
|
+
## Appwrite
|
|
131
|
+
|
|
132
|
+
[Appwrite](https://appwrite.io) keeps the accounts, on Appwrite Cloud or your own server; the app's own pages sign users in, and the server holds the session's secret in an HTTP-only cookie (`a_session_<project>`). Appwrite sends the email confirmation and password recovery emails itself, through its SMTP settings, with links back to the app's `/auth/callback` and `/reset-password`.
|
|
133
|
+
|
|
134
|
+
1. Create a project in [Appwrite Cloud](https://cloud.appwrite.io) (free tier) or on your Appwrite server.
|
|
135
|
+
2. Under Overview, Platforms, add a **Web** platform for the app's hostname: `localhost` in development, your domain in production. Appwrite only sends links to registered hostnames.
|
|
136
|
+
3. Under Overview, Integrations, API keys, create a key with the `sessions.write` and `users.write` scopes.
|
|
137
|
+
4. Set `APPWRITE_ENDPOINT` (such as `https://cloud.appwrite.io/v1`), `APPWRITE_PROJECT_ID`, and `APPWRITE_API_KEY`. The key stays on the server.
|
|
138
|
+
|
|
139
|
+
Changing or resetting a password signs out every other session; deleting an account removes the Appwrite user after the app's own data for them. A self-hosted Appwrite sends email only once its SMTP settings (`_APP_SMTP_HOST` and the rest) are set.
|
|
140
|
+
|
|
141
|
+
<!-- clubedge:end -->
|
|
142
|
+
|
|
105
143
|
## Local services
|
|
106
144
|
|
|
107
145
|
<!-- clubedge:if infra=docker -->
|
|
@@ -264,11 +302,13 @@ Turn on the customer portal in the dashboard's billing settings, so "Manage bill
|
|
|
264
302
|
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.
|
|
265
303
|
|
|
266
304
|
<!-- clubedge:end -->
|
|
267
|
-
<!-- clubedge:if jobs
|
|
305
|
+
<!-- clubedge:if jobs!=none -->
|
|
268
306
|
|
|
269
307
|
## Background jobs
|
|
270
308
|
|
|
271
|
-
Jobs run outside requests, with retries, in a worker that starts inside the app server: `pnpm dev` and the production server both run it, so there is no separate process to deploy. [pg-boss](https://github.com/timgit/pg-boss) stores them in your PostgreSQL, and creates and upgrades its own tables in the `pgboss` schema when the app starts.
|
|
309
|
+
Jobs run outside requests, with retries, in a worker that starts inside the app server: `pnpm dev` and the production server both run it, so there is no separate process to deploy. [pg-boss](https://github.com/timgit/pg-boss) stores them in your PostgreSQL, and creates and upgrades its own tables in the `pgboss` schema when the app starts. <!-- clubedge:only jobs=pg-boss -->
|
|
310
|
+
Jobs run outside requests, with retries, in a worker that starts inside the app server: `pnpm dev` and the production server both run it, so there is no separate process to deploy. [BullMQ](https://docs.bullmq.io) stores them in Redis (`REDIS_URL`, Redis 6.2 or newer), which the cache also uses; keep it on a Redis that doesn't evict keys, since jobs are data. <!-- clubedge:only jobs=bullmq -->
|
|
311
|
+
[Inngest](https://www.inngest.com) runs the jobs: the app sends an event for each one, and Inngest calls the app's `/api/inngest` to run it, with retries and schedules, so nothing runs between requests and serverless hosts need no cron. In development, start Inngest's Dev Server next to `pnpm dev` with `pnpm jobs:dev`, and open its dashboard at <http://localhost:8288> to see every run; `INNGEST_DEV=1` points the app at it. In production, create an app in Inngest, set `INNGEST_EVENT_KEY` and `INNGEST_SIGNING_KEY`, remove `INNGEST_DEV`, and sync the deployed `/api/inngest` URL in Inngest's dashboard (Inngest's Vercel and Netlify integrations do it on each deploy). <!-- clubedge:only jobs=inngest -->
|
|
272
312
|
|
|
273
313
|
Define a job in `apps/web/src/jobs` and add it to the list in `apps/web/src/jobs/index.ts`:
|
|
274
314
|
|
|
@@ -284,12 +324,21 @@ export const welcomeEmail = defineJob<{ userId: string }>({
|
|
|
284
324
|
});
|
|
285
325
|
```
|
|
286
326
|
|
|
287
|
-
Queue it from server code with `getJobs().enqueue(welcomeEmail, { userId })`, from `@/server/jobs`. To send any email in the background, enqueue `sendEmailJob` from `@/jobs` with the message, as the auth emails do.
|
|
288
|
-
Queue it from server code with `getJobs().enqueue(welcomeEmail, { userId })`, from `@/server/jobs`.
|
|
327
|
+
Queue it from server code with `getJobs().enqueue(welcomeEmail, { userId })`, from `@/server/jobs`. To send any email in the background, enqueue `sendEmailJob` from `@/jobs` with the message, as the auth emails do. <!-- clubedge:only email!=none -->
|
|
328
|
+
Queue it from server code with `getJobs().enqueue(welcomeEmail, { userId })`, from `@/server/jobs`. <!-- clubedge:only email=none -->
|
|
289
329
|
|
|
290
|
-
|
|
330
|
+
A thrown error fails the attempt and the job is retried (`retryLimit`, `retryDelaySeconds`, `retryBackoff`). An attempt that runs past `timeoutSeconds`, or whose server stops, is retried too, so make jobs safe to run twice. Add `schedule: "0 3 * * *"` (cron syntax, UTC) to run a job on a schedule. Restart `pnpm dev` after adding or changing a job. The dashboard's jobs card queues the sample job from `apps/web/src/jobs/sample.ts` and lists its recent runs; remove both when you add your own.
|
|
291
331
|
|
|
292
|
-
|
|
332
|
+
Where no server keeps running, as on Vercel and Netlify, there is no worker: `JOBS_WORKER` defaults to `off` there, and you can set it yourself anywhere. Set `CRON_SECRET` to 16 or more random characters and call `GET /api/jobs/run` every minute or so with the header `Authorization: Bearer <CRON_SECRET>`, from Vercel Cron, a Netlify scheduled function, or any scheduler. Each call runs the jobs that are due. <!-- clubedge:only jobs=pg-boss|bullmq -->
|
|
333
|
+
Inngest keeps the job history: the jobs card reads it from Inngest's API (the Dev Server's locally), and Inngest's dashboard shows each run's attempts, output, and errors. <!-- clubedge:only jobs=inngest -->
|
|
334
|
+
|
|
335
|
+
Jobs go through the provider-neutral `JobQueue` interface in `packages/jobs`, so job definitions, the routes, and the jobs card stay the same whichever provider runs them. Switch providers with `pnpm clubedge add jobs pg-boss`, `bullmq`, or `inngest`. To add another, implement `JobQueue` in a new package, return it from `apps/web/src/server/jobs.ts`, and prove it with the shared conformance suite, as each provider's tests do:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
import { describeJobQueue } from "@clubedge/jobs/conformance";
|
|
339
|
+
|
|
340
|
+
describeJobQueue("my-queue", (jobs, { worker }) => createMyQueue({ jobs, worker }));
|
|
341
|
+
```
|
|
293
342
|
|
|
294
343
|
<!-- clubedge:end -->
|
|
295
344
|
<!-- clubedge:if observability!=none -->
|
|
@@ -433,7 +482,7 @@ Vercel only runs the app: the database, Redis, file storage, and email come from
|
|
|
433
482
|
|
|
434
483
|
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).
|
|
435
484
|
|
|
436
|
-
Vercel runs no worker for [background jobs](#background-jobs). Set `CRON_SECRET` and add `"crons": [{ "path": "/api/jobs/run", "schedule": "* * * * *" }]` to `vercel.json`; Vercel Cron sends the secret itself. Cron jobs more frequent than daily need a paid Vercel plan; otherwise call the endpoint from another scheduler. <!-- clubedge:only jobs=pg-boss -->
|
|
485
|
+
Vercel runs no worker for [background jobs](#background-jobs). Set `CRON_SECRET` and add `"crons": [{ "path": "/api/jobs/run", "schedule": "* * * * *" }]` to `vercel.json`; Vercel Cron sends the secret itself. Cron jobs more frequent than daily need a paid Vercel plan; otherwise call the endpoint from another scheduler. <!-- clubedge:only jobs=pg-boss|bullmq -->
|
|
437
486
|
|
|
438
487
|
<!-- clubedge:end -->
|
|
439
488
|
<!-- clubedge:if deploy=netlify -->
|
|
@@ -445,7 +494,7 @@ Netlify only runs the app: the database, Redis, file storage, and email come fro
|
|
|
445
494
|
|
|
446
495
|
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).
|
|
447
496
|
|
|
448
|
-
Netlify runs no worker for [background jobs](#background-jobs). Set `JOBS_WORKER=off` and `CRON_SECRET`, then call `/api/jobs/run` every minute from a [scheduled function](https://docs.netlify.com/build/functions/scheduled-functions/) or another scheduler, with the header `Authorization: Bearer <CRON_SECRET>`. <!-- clubedge:only jobs=pg-boss -->
|
|
497
|
+
Netlify runs no worker for [background jobs](#background-jobs). Set `JOBS_WORKER=off` and `CRON_SECRET`, then call `/api/jobs/run` every minute from a [scheduled function](https://docs.netlify.com/build/functions/scheduled-functions/) or another scheduler, with the header `Authorization: Bearer <CRON_SECRET>`. <!-- clubedge:only jobs=pg-boss|bullmq -->
|
|
449
498
|
|
|
450
499
|
<!-- clubedge:end -->
|
|
451
500
|
|
|
@@ -489,7 +538,8 @@ Production needs these variables:
|
|
|
489
538
|
- `RESEND_API_KEY` and `EMAIL_FROM`; without them, production refuses to send email. <!-- clubedge:only email=resend -->
|
|
490
539
|
- `STRIPE_SECRET_KEY` (live mode), `STRIPE_WEBHOOK_SECRET` from the production webhook endpoint, and `STRIPE_PRICE_<PLAN>` for each plan. <!-- clubedge:only payments=stripe -->
|
|
491
540
|
- `REDIS_URL`, when you run more than one instance. <!-- clubedge:only cache=redis -->
|
|
492
|
-
- `CRON_SECRET`, where no worker runs and a scheduler calls `/api/jobs/run`. <!-- clubedge:only jobs=pg-boss -->
|
|
541
|
+
- `CRON_SECRET`, where no worker runs and a scheduler calls `/api/jobs/run`. <!-- clubedge:only jobs=pg-boss|bullmq -->
|
|
542
|
+
- `INNGEST_EVENT_KEY` and `INNGEST_SIGNING_KEY`, from your Inngest app; `INNGEST_DEV` unset. <!-- clubedge:only jobs=inngest -->
|
|
493
543
|
- `OTEL_EXPORTER_OTLP_ENDPOINT`, and `OTEL_EXPORTER_OTLP_HEADERS` if your collector needs a key. <!-- clubedge:only observability=otel -->
|
|
494
544
|
- `SENTRY_DSN`, and `SENTRY_ENVIRONMENT` to tell environments apart. <!-- clubedge:only observability=sentry -->
|
|
495
545
|
- The `STORAGE_*` variables for your S3-compatible bucket. <!-- clubedge:only storage=s3 -->
|
|
@@ -37,6 +37,27 @@ BETTER_AUTH_SECRET=
|
|
|
37
37
|
BETTER_AUTH_REQUIRE_EMAIL_VERIFICATION=false # clubedge:only email!=none
|
|
38
38
|
# clubedge:end
|
|
39
39
|
|
|
40
|
+
# clubedge:if auth=firebase
|
|
41
|
+
# Firebase Authentication. In development, pnpm auth:dev starts the Auth Emulator, which these
|
|
42
|
+
# values point at. In production, set your project's id and Web API key (Project settings,
|
|
43
|
+
# General) and a service account (Project settings, Service accounts), and remove
|
|
44
|
+
# FIREBASE_AUTH_EMULATOR_HOST.
|
|
45
|
+
FIREBASE_PROJECT_ID=demo-clubedge
|
|
46
|
+
FIREBASE_API_KEY=demo-api-key
|
|
47
|
+
FIREBASE_CLIENT_EMAIL=
|
|
48
|
+
FIREBASE_PRIVATE_KEY=
|
|
49
|
+
FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099
|
|
50
|
+
# clubedge:end
|
|
51
|
+
|
|
52
|
+
# clubedge:if auth=appwrite
|
|
53
|
+
# Appwrite (https://appwrite.io), on Appwrite Cloud or your own server. Create a project, add a
|
|
54
|
+
# Web platform for this app's hostname (localhost in development), and an API key with the
|
|
55
|
+
# sessions.write and users.write scopes. Appwrite sends its own emails (its SMTP settings).
|
|
56
|
+
APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
|
|
57
|
+
APPWRITE_PROJECT_ID=
|
|
58
|
+
APPWRITE_API_KEY=
|
|
59
|
+
# clubedge:end
|
|
60
|
+
|
|
40
61
|
# clubedge:if cache=redis
|
|
41
62
|
# Optional cache and rate limits (Redis protocol; rediss:// enables TLS). Without it, both use
|
|
42
63
|
# process memory. Upstash example: rediss://default:YOUR_PASSWORD@your-database.upstash.io:6379
|
|
@@ -79,6 +100,24 @@ JOBS_WORKER=
|
|
|
79
100
|
CRON_SECRET=
|
|
80
101
|
# clubedge:end
|
|
81
102
|
|
|
103
|
+
# clubedge:if jobs=bullmq
|
|
104
|
+
# Background jobs are stored in Redis (REDIS_URL above) through BullMQ and run by a worker inside
|
|
105
|
+
# the app server. Set JOBS_WORKER=off where no server keeps running (it is off by default on
|
|
106
|
+
# Vercel and Netlify); a scheduler then calls /api/jobs/run with CRON_SECRET (16 or more
|
|
107
|
+
# characters) instead.
|
|
108
|
+
JOBS_WORKER=
|
|
109
|
+
CRON_SECRET=
|
|
110
|
+
# clubedge:end
|
|
111
|
+
|
|
112
|
+
# clubedge:if jobs=inngest
|
|
113
|
+
# Background jobs run by Inngest (https://www.inngest.com), which calls this app's /api/inngest.
|
|
114
|
+
# In development, start its Dev Server with pnpm jobs:dev (http://localhost:8288); INNGEST_DEV=1
|
|
115
|
+
# points the app at it. In production, remove INNGEST_DEV and set the keys of your Inngest app.
|
|
116
|
+
INNGEST_DEV=1
|
|
117
|
+
INNGEST_EVENT_KEY=
|
|
118
|
+
INNGEST_SIGNING_KEY=
|
|
119
|
+
# clubedge:end
|
|
120
|
+
|
|
82
121
|
# clubedge:if observability=otel
|
|
83
122
|
# OpenTelemetry: where traces and error logs go over OTLP/HTTP, such as a collector, Grafana
|
|
84
123
|
# Cloud, or Honeycomb. Empty sends nothing. Headers are key=value pairs separated by commas.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clubedge/start",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@clubedge/auth": "workspace:*",
|
|
17
|
+
"@clubedge/auth-appwrite": "workspace:*",
|
|
17
18
|
"@clubedge/auth-better-auth": "workspace:*",
|
|
19
|
+
"@clubedge/auth-firebase": "workspace:*",
|
|
18
20
|
"@clubedge/auth-supabase": "workspace:*",
|
|
19
21
|
"@clubedge/cache": "workspace:*",
|
|
20
22
|
"@clubedge/cache-redis": "workspace:*",
|
|
@@ -24,6 +26,8 @@
|
|
|
24
26
|
"@clubedge/email-resend": "workspace:*",
|
|
25
27
|
"@clubedge/email-smtp": "workspace:*",
|
|
26
28
|
"@clubedge/jobs": "workspace:*",
|
|
29
|
+
"@clubedge/jobs-bullmq": "workspace:*",
|
|
30
|
+
"@clubedge/jobs-inngest": "workspace:*",
|
|
27
31
|
"@clubedge/jobs-pg-boss": "workspace:*",
|
|
28
32
|
"@clubedge/observability": "workspace:*",
|
|
29
33
|
"@clubedge/observability-otel": "workspace:*",
|
|
@@ -11,6 +11,12 @@ import {
|
|
|
11
11
|
CardTitle,
|
|
12
12
|
} from "@clubedge/ui/components/card";
|
|
13
13
|
|
|
14
|
+
const runnerDescriptions = {
|
|
15
|
+
worker: "Work runs outside requests in this server's worker, with retries.",
|
|
16
|
+
scheduler: "This server runs no worker: jobs run when /api/jobs/run is called.",
|
|
17
|
+
service: "A jobs service runs each job by calling this app, with retries.",
|
|
18
|
+
};
|
|
19
|
+
|
|
14
20
|
const timeFormat = new Intl.DateTimeFormat("en", { timeStyle: "medium", timeZone: "UTC" });
|
|
15
21
|
|
|
16
22
|
/** Starts the sample job and lists the current user's recent runs. The form posts to /jobs/sample. */
|
|
@@ -22,11 +28,7 @@ export function JobsCard({ status }: { status: JobsStatus }) {
|
|
|
22
28
|
<CardHeader className="flex-row items-start justify-between gap-3 border-b px-5 py-5 sm:px-6">
|
|
23
29
|
<div className="grid gap-1">
|
|
24
30
|
<CardTitle className="text-base">Background jobs</CardTitle>
|
|
25
|
-
<CardDescription>
|
|
26
|
-
{status.worker
|
|
27
|
-
? "Work runs outside requests in this server's worker, with retries."
|
|
28
|
-
: "This server runs no worker: jobs run when /api/jobs/run is called."}
|
|
29
|
-
</CardDescription>
|
|
31
|
+
<CardDescription>{runnerDescriptions[status.runner]}</CardDescription>
|
|
30
32
|
</div>
|
|
31
33
|
<span className="grid size-9 place-items-center rounded-lg bg-primary/10 text-primary">
|
|
32
34
|
<ListChecks aria-hidden="true" className="size-4" />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { optional, parseEnv } from "./server";
|
|
3
|
-
// clubedge:if auth=better-auth
|
|
3
|
+
// clubedge:if auth=better-auth|firebase|appwrite
|
|
4
4
|
import { getAppUrl } from "./server";
|
|
5
5
|
// clubedge:end
|
|
6
6
|
|
|
@@ -60,3 +60,68 @@ export function getBetterAuthEnv(): BetterAuthEnv | null {
|
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
// clubedge:end
|
|
63
|
+
|
|
64
|
+
// clubedge:if auth=firebase
|
|
65
|
+
const firebaseAuthSchema = z.object({
|
|
66
|
+
FIREBASE_PROJECT_ID: optional(z.string()),
|
|
67
|
+
// The project's Web API key: Project settings, General.
|
|
68
|
+
FIREBASE_API_KEY: optional(z.string()),
|
|
69
|
+
// A service account: Project settings, Service accounts. Not needed with the emulator.
|
|
70
|
+
FIREBASE_CLIENT_EMAIL: optional(z.string()),
|
|
71
|
+
FIREBASE_PRIVATE_KEY: optional(z.string()),
|
|
72
|
+
// The Auth Emulator (pnpm auth:dev) in development, such as 127.0.0.1:9099.
|
|
73
|
+
FIREBASE_AUTH_EMULATOR_HOST: optional(z.string()),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export interface FirebaseAuthEnv {
|
|
77
|
+
projectId: string;
|
|
78
|
+
apiKey: string;
|
|
79
|
+
clientEmail?: string;
|
|
80
|
+
privateKey?: string;
|
|
81
|
+
emulatorHost?: string;
|
|
82
|
+
baseUrl: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Firebase Auth settings, or null until the project id and Web API key are set. */
|
|
86
|
+
export function getFirebaseAuthEnv(): FirebaseAuthEnv | null {
|
|
87
|
+
const env = parseEnv(firebaseAuthSchema, "Firebase Auth");
|
|
88
|
+
if (!env.FIREBASE_PROJECT_ID || !env.FIREBASE_API_KEY) return null;
|
|
89
|
+
return {
|
|
90
|
+
projectId: env.FIREBASE_PROJECT_ID,
|
|
91
|
+
apiKey: env.FIREBASE_API_KEY,
|
|
92
|
+
...(env.FIREBASE_CLIENT_EMAIL ? { clientEmail: env.FIREBASE_CLIENT_EMAIL } : {}),
|
|
93
|
+
...(env.FIREBASE_PRIVATE_KEY ? { privateKey: env.FIREBASE_PRIVATE_KEY } : {}),
|
|
94
|
+
...(env.FIREBASE_AUTH_EMULATOR_HOST ? { emulatorHost: env.FIREBASE_AUTH_EMULATOR_HOST } : {}),
|
|
95
|
+
baseUrl: getAppUrl(),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// clubedge:end
|
|
99
|
+
|
|
100
|
+
// clubedge:if auth=appwrite
|
|
101
|
+
const appwriteAuthSchema = z.object({
|
|
102
|
+
// The API endpoint, such as https://cloud.appwrite.io/v1 or your server's /v1.
|
|
103
|
+
APPWRITE_ENDPOINT: optional(z.url()),
|
|
104
|
+
APPWRITE_PROJECT_ID: optional(z.string()),
|
|
105
|
+
// An API key with the sessions.write and users.write scopes (Overview, Integrations, API keys).
|
|
106
|
+
APPWRITE_API_KEY: optional(z.string()),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export interface AppwriteAuthEnv {
|
|
110
|
+
endpoint: string;
|
|
111
|
+
projectId: string;
|
|
112
|
+
apiKey: string;
|
|
113
|
+
baseUrl: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Appwrite settings, or null until the endpoint, project, and API key are set. */
|
|
117
|
+
export function getAppwriteAuthEnv(): AppwriteAuthEnv | null {
|
|
118
|
+
const env = parseEnv(appwriteAuthSchema, "Appwrite");
|
|
119
|
+
if (!env.APPWRITE_ENDPOINT || !env.APPWRITE_PROJECT_ID || !env.APPWRITE_API_KEY) return null;
|
|
120
|
+
return {
|
|
121
|
+
endpoint: env.APPWRITE_ENDPOINT,
|
|
122
|
+
projectId: env.APPWRITE_PROJECT_ID,
|
|
123
|
+
apiKey: env.APPWRITE_API_KEY,
|
|
124
|
+
baseUrl: getAppUrl(),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// clubedge:end
|
|
@@ -55,6 +55,14 @@ describe("production env check", () => {
|
|
|
55
55
|
).toEqual(["DATABASE_URL_DIRECT points at Neon's pooler: use the host without -pooler"]);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
const hasInngest = requiredVariables({}).some(({ key }) => key === "INNGEST_SIGNING_KEY");
|
|
59
|
+
it.runIf(hasInngest)("refuses Inngest's Dev Server in production", () => {
|
|
60
|
+
expect(checkProductionEnv({ ...complete(), INNGEST_DEV: "1" }).invalid).toEqual([
|
|
61
|
+
"INNGEST_DEV is set: production uses Inngest Cloud's keys, so remove it",
|
|
62
|
+
]);
|
|
63
|
+
expect(checkProductionEnv({ ...complete(), INNGEST_DEV: "0" }).invalid).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
|
|
58
66
|
it("rejects a URL that isn't absolute", () => {
|
|
59
67
|
const env = { ...complete(), APP_URL: "my-app.vercel.app" };
|
|
60
68
|
expect(checkProductionEnv(env).invalid).toEqual(["APP_URL is not an absolute URL"]);
|