@clubedge/create-clubedge-app 0.14.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +122 -76
  3. package/package.json +3 -3
  4. package/template/README.md +179 -51
  5. package/template/apps/start/package.json +1 -1
  6. package/template/apps/start/src/nitro/plugins/jobs.ts +2 -2
  7. package/template/apps/start/src/server/jobs.test.ts +27 -0
  8. package/template/apps/start/src/server/jobs.ts +20 -8
  9. package/template/apps/start/src/server/observability.test.ts +23 -0
  10. package/template/apps/start/src/server/observability.ts +10 -2
  11. package/template/apps/start/src/server/variants/observability.sentry.ts +10 -2
  12. package/template/apps/web/package.json +1 -1
  13. package/template/apps/web/src/server/jobs.ts +20 -8
  14. package/template/apps/web/src/server/observability.ts +10 -2
  15. package/template/apps/web/src/server/variants/observability.sentry.ts +10 -2
  16. package/template/clubedge.template.json +3 -0
  17. package/template/package.json +1 -1
  18. package/template/packages/auth/package.json +1 -1
  19. package/template/packages/auth-better-auth/package.json +1 -1
  20. package/template/packages/auth-supabase/package.json +1 -1
  21. package/template/packages/cache/package.json +1 -1
  22. package/template/packages/cache-redis/package.json +1 -1
  23. package/template/packages/core/package.json +1 -1
  24. package/template/packages/db/package.json +1 -1
  25. package/template/packages/email/package.json +1 -1
  26. package/template/packages/email-resend/package.json +1 -1
  27. package/template/packages/email-smtp/package.json +1 -1
  28. package/template/packages/jobs/package.json +1 -1
  29. package/template/packages/jobs-pg-boss/package.json +1 -1
  30. package/template/packages/local-db/package.json +1 -1
  31. package/template/packages/observability/package.json +1 -1
  32. package/template/packages/observability-otel/package.json +1 -1
  33. package/template/packages/observability-sentry/package.json +1 -1
  34. package/template/packages/payments/package.json +1 -1
  35. package/template/packages/payments-stripe/package.json +1 -1
  36. package/template/packages/storage/package.json +1 -1
  37. package/template/packages/storage-s3/package.json +1 -1
  38. package/template/packages/storage-supabase/package.json +1 -1
  39. package/template/packages/ui/package.json +1 -1
  40. package/template.lock.json +4 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  All notable changes to `create-clubedge-app` are documented here.
4
4
 
5
+ ## 0.14.1
6
+
7
+ - Default to Starter `v0.13.1`. In projects with background jobs or observability, a configuration error such as a missing `DATABASE_URL` no longer stops the server at startup; pages that need no database keep working, and the error is reported once.
8
+
5
9
  ## 0.14.0
6
10
 
7
11
  - Default to Starter `v0.13.0`, which adds observability (`--observability otel|sentry`): request tracing and server error reporting through OpenTelemetry, to any OTLP collector, or Sentry, behind provider-neutral hooks in `@clubedge/core`. `--observability` needs no CLI code: it comes from the Starter's manifest.
package/README.md CHANGED
@@ -1,76 +1,57 @@
1
+ <div align="center">
2
+
1
3
  # create-clubedge-app
2
4
 
3
- [![CI](https://github.com/Clubedge/create-clubedge-app/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Clubedge/create-clubedge-app/actions/workflows/ci.yml)
4
- [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE)
5
+ **Create a production-ready Next.js or TanStack Start app with one command, and keep it up to date.**
5
6
 
6
- Create a Next.js or TanStack Start application from the [Clubedge Starter](https://github.com/Clubedge/clubedge-starter) reference implementation.
7
+ Pick only the modules you need: auth, storage, email, payments, jobs, observability, deployment.<br>
8
+ Add, remove, or upgrade them later without losing your own changes.
7
9
 
8
- The CLI downloads a tested Starter release, names the generated project, creates `apps/web/.env.local` from the example, initializes Git, and installs dependencies. The Starter remains the source of truth for the generated architecture.
10
+ [![npm](https://img.shields.io/npm/v/@clubedge/create-clubedge-app?style=flat-square)](https://www.npmjs.com/package/@clubedge/create-clubedge-app)
11
+ [![npm downloads](https://img.shields.io/npm/dm/@clubedge/create-clubedge-app?style=flat-square)](https://www.npmjs.com/package/@clubedge/create-clubedge-app)
12
+ [![CI](https://img.shields.io/github/actions/workflow/status/Clubedge/create-clubedge-app/ci.yml?branch=main&label=CI&style=flat-square)](https://github.com/Clubedge/create-clubedge-app/actions/workflows/ci.yml)
13
+ [![Release](https://img.shields.io/github/v/release/Clubedge/create-clubedge-app?style=flat-square)](https://github.com/Clubedge/create-clubedge-app/releases)
14
+ [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue?style=flat-square)](LICENSE)
9
15
 
10
- ## Version relationship
16
+ [Website](https://starter.clubedge.live) ·
17
+ [Clubedge Starter](https://github.com/Clubedge/clubedge-starter) ·
18
+ [Changelog](CHANGELOG.md) ·
19
+ [Roadmap](ROADMAP.md) ·
20
+ [Contributing](CONTRIBUTING.md)
11
21
 
12
- Each CLI release is tested against one specific Starter release:
22
+ </div>
13
23
 
14
- ```text
15
- create-clubedge-app v0.14.0
16
- |
17
- v
18
- Clubedge Starter v0.13.0
19
- commit 62fb8d1b1e13ed8e8e134bd1e53f2bfdde2c6a50
24
+ ```sh
25
+ pnpm dlx @clubedge/create-clubedge-app my-app
20
26
  ```
21
27
 
22
- The current relationship is:
23
-
24
- | CLI release | Default Starter release | Meaning |
25
- | --- | --- | --- |
26
- | `create-clubedge-app@0.14.0` | `clubedge-starter@v0.13.0` | The CLI's required, tested default |
27
-
28
- This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
28
+ ## Why create-clubedge-app
29
29
 
30
- The `v0.13.0` tag is expected to resolve to commit
31
- `62fb8d1b1e13ed8e8e134bd1e53f2bfdde2c6a50`. The CLI verifies this before
32
- scaffolding with the default reference and fails rather than silently using a
33
- moved tag.
30
+ Copying a starter is easy; living with it is not. You delete the services you don't use by hand, and once you've changed the code, every later improvement to the starter has to be copied over by hand too.
34
31
 
35
- When the Starter changes, its maintainers first publish a new versioned release, such as `v0.13.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.
32
+ create-clubedge-app generates a project from a tested release of [Clubedge Starter](https://github.com/Clubedge/clubedge-starter) with only the modules you choose. A module you leave out is removed completely: its package, its provider SDK, its environment variables, its documentation, and its lockfile entries. The project records exactly what it was generated from, so later the CLI can add a module, remove one, or upgrade to a newer Starter release by merging the change into your code. Files you never touched are updated; your edits are kept.
36
33
 
37
- ## Requirements
34
+ Every CLI release is pinned to one Starter release and verified against it, so the same command always produces the same project.
38
35
 
39
- - Node.js 22.12 or newer.
40
- - pnpm 10.9.0 or newer. The generated project is a pnpm workspace, so dependencies are installed with pnpm even when you start the CLI with `npx`; the CLI falls back to `corepack pnpm` when `pnpm` is not on your path.
41
- - Git, when using the default Git initialization.
42
- - Internet access to install dependencies. The default Starter release ships inside the CLI package, so GitHub is only contacted for `--ref`.
36
+ ## Quick start
43
37
 
44
- ## Usage
38
+ You need Node.js 22.12 or newer and pnpm 10.9.0 or newer. Git is used for the default repository initialization.
45
39
 
46
40
  ```sh
47
41
  pnpm dlx @clubedge/create-clubedge-app my-app
48
- ```
49
-
50
- Or run it with npm:
51
-
52
- ```sh
53
- npx @clubedge/create-clubedge-app my-app
54
- ```
55
-
56
- After scaffolding:
57
-
58
- ```sh
59
42
  cd my-app
60
43
  pnpm dev
61
44
  ```
62
45
 
63
- The generated project includes `.env.local`; fill in `DATABASE_URL` to start the app and configure Supabase Auth or other providers as needed. The root route is a project-named landing page, and the reference dashboard is available at `/dashboard`. See its `SETUP.md` for database, Redis, storage, and Docker setup.
46
+ Or start it with npm: `npx @clubedge/create-clubedge-app my-app`. The generated project is a pnpm workspace, so dependencies are installed with pnpm either way; the CLI falls back to `corepack pnpm` when `pnpm` is not on your path. Without a destination, the CLI asks where to create the project.
64
47
 
65
- When no destination is provided, the CLI asks where to create the project:
48
+ The generated project includes `apps/web/.env.local`, created from the example and pointing at the local services you chose. Its `SETUP.md` describes every variable and provider. The root route is a landing page named after your project, and the dashboard is at `/dashboard`.
66
49
 
67
- ```sh
68
- pnpm dlx @clubedge/create-clubedge-app
69
- ```
50
+ The default Starter release ships inside the CLI package, so GitHub is only contacted when you pass `--ref`. Internet access is needed to install dependencies.
70
51
 
71
- ### Choosing a framework
52
+ ## Choosing a framework
72
53
 
73
- The Starter ships the same application for two React frameworks. Next.js is the default; pick TanStack Start with `--framework`, or choose interactively when the Starter offers more than one:
54
+ The Starter ships the same application for two React frameworks. Next.js is the default; pick TanStack Start with `--framework`, or choose interactively:
74
55
 
75
56
  ```sh
76
57
  pnpm dlx @clubedge/create-clubedge-app my-app --framework tanstack-start
@@ -83,9 +64,9 @@ pnpm dlx @clubedge/create-clubedge-app my-app --framework tanstack-start
83
64
 
84
65
  Either way, the app lives in `apps/web` and uses the same shared packages, so the project layout, root scripts, and Docker commands are identical.
85
66
 
86
- ### Choosing services
67
+ ## Choosing modules
87
68
 
88
- Authentication, file storage, caching, email, payments, background jobs, observability, 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.
69
+ Authentication, file storage, caching, email, payments, background jobs, observability, local infrastructure, and deployment are modules. Each has a default, and anything you leave out is removed completely. 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
70
 
90
71
  | Flag | Options | Default |
91
72
  | ----------- | ------------------------------------------ | ---------- |
@@ -101,35 +82,40 @@ Authentication, file storage, caching, email, payments, background jobs, observa
101
82
 
102
83
  ```sh
103
84
  # Only the monorepo, UI, database, and tooling
104
- pnpm dlx @clubedge/create-clubedge-app my-app --auth none --storage none --cache memory
85
+ pnpm dlx @clubedge/create-clubedge-app my-app --auth none --storage none --cache memory --email none
86
+
87
+ # A SaaS starting point: your own auth, payments, and background jobs, no Docker needed
88
+ pnpm dlx @clubedge/create-clubedge-app my-app --auth better-auth --payments stripe --jobs pg-boss --infra local
105
89
  ```
106
90
 
107
- Interactive runs ask whether to customize these after the framework question. The options come from the Starter's manifest, so `--help` lists the ones the bundled Starter offers.
91
+ Interactive runs ask whether to customize these after the framework question.
108
92
 
109
93
  ## Options
110
94
 
111
95
  ```text
112
- Usage: pnpm dlx @clubedge/create-clubedge-app [project-directory] [options]
113
-
114
- --framework <id> App framework: next (default) or tanstack-start
115
- --auth <id> Authentication: supabase (default), better-auth, or none
116
- --storage <id> File storage: s3 (default), supabase, or none
117
- --cache <id> Cache and rate limits: redis (default) or memory
118
- --infra <id> Local services: docker (default), supabase, local, or none
119
- --ref <ref> Download this Starter tag, branch, or commit from GitHub instead
120
- --template-dir <path> Scaffold from a local Starter checkout (for Starter development)
121
- --dry-run Show what would be created without writing anything
122
- -y, --yes Accept defaults and never prompt (default directory: my-app)
123
- --no-install Skip dependency installation
124
- --no-git Skip Git initialization
125
- -h, --help Show help
126
- -v, --version Show the CLI version
96
+ Usage:
97
+ pnpm dlx @clubedge/create-clubedge-app [project-directory] [options]
98
+ pnpm dlx @clubedge/create-clubedge-app add [module] [option] (inside a project)
99
+ pnpm dlx @clubedge/create-clubedge-app remove [module] (inside a project)
100
+ pnpm dlx @clubedge/create-clubedge-app upgrade [--ref <tag>] (inside a project)
101
+
102
+ Options:
103
+ --framework <id> App framework, such as next or tanstack-start
104
+ --<module> <option> A module option, such as --auth none; the Starter lists its modules
105
+ --ref <ref> Download this Starter tag, branch, or commit from GitHub instead
106
+ (upgrade: the Starter to upgrade to)
107
+ --template-dir <path> Scaffold from a local Starter checkout (for Starter development)
108
+ --dry-run Show what would be created or changed without writing anything
109
+ --force add/remove: change a project with uncommitted changes or without Git
110
+ -y, --yes Accept defaults and never prompt (default directory: my-app)
111
+ --no-install Skip dependency installation
112
+ --no-git Skip Git repository initialization
113
+ -h, --help Show this help
114
+ -v, --version Show the CLI version
127
115
  ```
128
116
 
129
117
  The target directory must be empty or not exist. The CLI never deletes or overwrites existing files, and if scaffolding fails it removes whatever it wrote, so a failed run leaves nothing behind.
130
118
 
131
- Every generated project records its CLI version, Starter repository, Starter ref, Starter commit, framework, and modules in `package.json` and `README.md`.
132
-
133
119
  ```sh
134
120
  # Preview the files and edits without writing anything
135
121
  pnpm dlx @clubedge/create-clubedge-app my-app --dry-run
@@ -141,7 +127,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git --framework tanstac
141
127
  pnpm dlx @clubedge/create-clubedge-app my-app --ref main
142
128
 
143
129
  # Reproduce a specific Starter release or commit
144
- pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.13.0
130
+ pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.13.1
145
131
  pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
146
132
 
147
133
  # Test local Starter changes before releasing them
@@ -158,7 +144,7 @@ pnpm dlx @clubedge/create-clubedge-app add cache redis # or switch: add aut
158
144
  pnpm dlx @clubedge/create-clubedge-app remove storage
159
145
  pnpm dlx @clubedge/create-clubedge-app add infra local --dry-run
160
146
  pnpm dlx @clubedge/create-clubedge-app@latest upgrade # to the newest CLI's Starter release
161
- pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.13.0 # or a specific tag, branch, or commit
147
+ pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.13.1 # or a specific tag, branch, or commit
162
148
  ```
163
149
 
164
150
  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.
@@ -171,6 +157,35 @@ The CLI renders the project twice, from the Starter commit and modules recorded
171
157
 
172
158
  The project must be a Git repository without uncommitted changes, so you can review the result with `git diff` and undo it. Pass `--force` to skip this check. Add `--no-install` to skip `pnpm install`. Afterward, run `pnpm db:migrate` and `pnpm check:services` if the change added tables or services.
173
159
 
160
+ ## Version relationship
161
+
162
+ Each CLI release is tested against one specific Starter release:
163
+
164
+ ```text
165
+ create-clubedge-app v0.14.1
166
+ |
167
+ v
168
+ Clubedge Starter v0.13.1
169
+ commit 28bfbf98c85ae500ace6a062dd10d67746e1a330
170
+ ```
171
+
172
+ The current relationship is:
173
+
174
+ | CLI release | Default Starter release | Meaning |
175
+ | --- | --- | --- |
176
+ | `create-clubedge-app@0.14.1` | `clubedge-starter@v0.13.1` | The CLI's required, tested default |
177
+
178
+ This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
179
+
180
+ The `v0.13.1` tag is expected to resolve to commit
181
+ `28bfbf98c85ae500ace6a062dd10d67746e1a330`. The CLI verifies this before
182
+ scaffolding with the default reference and fails rather than silently using a
183
+ moved tag.
184
+
185
+ When the Starter changes, its maintainers first publish a new versioned release, such as `v0.13.1`. 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.
186
+
187
+ Every generated project records its CLI version, Starter repository, Starter ref, Starter commit, framework, and modules in `package.json` and `README.md`.
188
+
174
189
  ## How it works
175
190
 
176
191
  Each CLI release bundles one Starter release. When the release is built, `scripts/bundle-template.mjs` clones the pinned Starter tag, verifies that it resolves to the pinned commit, and copies its tracked files into the package (`template/` and `template.lock.json`). Running the CLI then needs no GitHub access for the default Starter.
@@ -182,11 +197,23 @@ The CLI first builds a plan (the files to copy and the edits to make), which `--
182
197
  ```text
183
198
  src/
184
199
  cli/ argument parsing and terminal output
185
- core/ manifest, template sources, customizations, planning, and execution
200
+ core/ manifest, template sources, customizations, planning, merging, and execution
186
201
  steps/ Git initialization and dependency installation
187
202
  utils/ file listing and copying, processes, and naming
188
203
  ```
189
204
 
205
+ ## Documentation
206
+
207
+ | I want to | Read |
208
+ | --- | --- |
209
+ | Configure a generated project | Its `SETUP.md`, also in the [Starter repository](https://github.com/Clubedge/clubedge-starter/blob/main/SETUP.md) |
210
+ | Change modules or upgrade a project | [Change an existing project](#change-an-existing-project) |
211
+ | Reproduce an exact Starter version | [Version relationship](#version-relationship) |
212
+ | See what changed in each release | [CHANGELOG.md](CHANGELOG.md) |
213
+ | See what is planned next | [ROADMAP.md](ROADMAP.md) |
214
+ | Contribute code | [CONTRIBUTING.md](CONTRIBUTING.md) |
215
+ | Report a security problem | [SECURITY.md](SECURITY.md) |
216
+
190
217
  ## Development
191
218
 
192
219
  ```sh
@@ -200,7 +227,7 @@ node ./bin/create.mjs my-app --template-dir ../clubedge-starter --dry-run
200
227
 
201
228
  Without a bundled template, a local build downloads the pinned Starter release from GitHub and verifies its commit. Run `pnpm bundle-template` to bundle it, or `pnpm bundle-template --from ../clubedge-starter` to bundle a local checkout.
202
229
 
203
- CI packs the CLI with `npm pack`, scaffolds a clean project from the packed tarball, installs its dependencies, runs linting, typechecking, unit tests, browser checks, and a production build, then starts the generated app and checks the landing page, sign-up page, dashboard, health endpoint, logo, and favicon. A scheduled, non-blocking compatibility job also checks Starter `main`.
230
+ CI packs the CLI with `npm pack`, scaffolds clean projects from the packed tarball, installs their dependencies, runs linting, typechecking, unit tests, browser checks, and a production build, then starts each generated app and checks the landing page, sign-up page, dashboard, health endpoint, logo, and favicon. It also adds and removes modules in an existing project and upgrades a project from an older Starter. A scheduled, non-blocking compatibility job checks Starter `main`.
204
231
 
205
232
  ## Release workflow
206
233
 
@@ -209,7 +236,7 @@ CI packs the CLI with `npm pack`, scaffolds a clean project from the packed tarb
209
236
  1. Make and validate changes in `clubedge-starter`.
210
237
  2. Run lint, typecheck, unit tests, browser tests, and production build.
211
238
  3. Update the Starter package versions.
212
- 4. Commit `main`, create a release tag such as `v0.13.0`, and publish the Starter release.
239
+ 4. Commit `main`, create a release tag such as `v0.13.1`, and publish the Starter release.
213
240
 
214
241
  ### CLI maintainers
215
242
 
@@ -221,9 +248,28 @@ CI packs the CLI with `npm pack`, scaffolds a clean project from the packed tarb
221
248
 
222
249
  The required CI job always tests the pinned default. The scheduled compatibility job tests Starter `main` early, but unreleased Starter changes must not change the stable CLI default.
223
250
 
224
- ## Contributing
251
+ ## Roadmap
252
+
253
+ Each phase ends with a Starter release and a CLI release pinned to it. The full plan is in [ROADMAP.md](ROADMAP.md).
225
254
 
226
- See [CONTRIBUTING.md](CONTRIBUTING.md) for development and pull request guidance, and [ROADMAP.md](ROADMAP.md) for the planned phases. Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md).
255
+ | CLI release | Starter release | Theme |
256
+ | --- | --- | --- |
257
+ | 0.5.0 to 0.9.0 | v0.5.0 to v0.8.0 | Optional modules, local services, `add`, `remove`, and `upgrade` |
258
+ | 0.10.0 | v0.9.0 | Module flags from the Starter's manifest, and email |
259
+ | 0.11.0 | v0.10.0 | Deployment |
260
+ | 0.12.0 | v0.11.0 | Payments |
261
+ | 0.13.0 | v0.12.0 | Background jobs |
262
+ | 0.14.0 | v0.13.0 | Observability |
263
+ | next | | `status` and `doctor` commands, several modules in one change, macOS CI |
264
+
265
+ ## Community
266
+
267
+ - **[Issues](https://github.com/Clubedge/create-clubedge-app/issues)** for problems creating, changing, or upgrading a project.
268
+ - **[Clubedge Starter issues](https://github.com/Clubedge/clubedge-starter/issues)** for problems in the generated application itself.
269
+ - **[Pull requests](https://github.com/Clubedge/create-clubedge-app/pulls)** are welcome; start with [CONTRIBUTING.md](CONTRIBUTING.md).
270
+ - **Security problems** go through the private process in [SECURITY.md](SECURITY.md), not public issues.
271
+
272
+ Everyone taking part is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
227
273
 
228
274
  ## License
229
275
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/create-clubedge-app",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
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.13.0",
26
- "starterCommit": "62fb8d1b1e13ed8e8e134bd1e53f2bfdde2c6a50"
25
+ "starterRef": "v0.13.1",
26
+ "starterCommit": "28bfbf98c85ae500ace6a062dd10d67746e1a330"
27
27
  },
28
28
  "license": "Apache-2.0",
29
29
  "repository": {
@@ -1,38 +1,75 @@
1
+ <!-- clubedge:if starter-repository -->
2
+ <div align="center">
3
+
1
4
  # Clubedge Starter
2
5
 
3
- [![CI](https://github.com/Clubedge/clubedge-starter/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Clubedge/clubedge-starter/actions/workflows/ci.yml)
4
- [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE)
6
+ **A production-ready full-stack TypeScript starter that ships only the modules you choose.**
5
7
 
6
- Clubedge Starter is a production-oriented, modular React application foundation. It brings together a pnpm monorepo, shared shadcn/ui components, PostgreSQL access through Drizzle, replaceable infrastructure providers, and a working example dashboard. It provides engineering conventions and a reference implementation; review its security and deployment choices for your application before production use.
8
+ Next.js or TanStack Start on PostgreSQL, with optional auth, storage, email, payments,<br>
9
+ background jobs, observability, and deployment, each behind an interface you can replace.
7
10
 
8
- <!-- clubedge:if starter-repository -->
11
+ [![CI](https://img.shields.io/github/actions/workflow/status/Clubedge/clubedge-starter/ci.yml?branch=main&label=CI&style=flat-square)](https://github.com/Clubedge/clubedge-starter/actions/workflows/ci.yml)
12
+ [![Generated projects](https://img.shields.io/github/actions/workflow/status/Clubedge/clubedge-starter/starter.yml?branch=main&label=generated%20projects&style=flat-square)](https://github.com/Clubedge/clubedge-starter/actions/workflows/starter.yml)
13
+ [![Release](https://img.shields.io/github/v/release/Clubedge/clubedge-starter?style=flat-square)](https://github.com/Clubedge/clubedge-starter/releases)
14
+ [![npm](https://img.shields.io/npm/v/@clubedge/create-clubedge-app?label=create-clubedge-app&style=flat-square)](https://www.npmjs.com/package/@clubedge/create-clubedge-app)
15
+ [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue?style=flat-square)](LICENSE)
16
+
17
+ [Website](https://starter.clubedge.live) ·
18
+ [Create a project](https://github.com/Clubedge/create-clubedge-app) ·
19
+ [Setup guide](SETUP.md) ·
20
+ [Roadmap](ROADMAP.md) ·
21
+ [Releases](https://github.com/Clubedge/clubedge-starter/releases)
22
+
23
+ </div>
24
+
25
+ ```sh
26
+ pnpm dlx @clubedge/create-clubedge-app my-app
27
+ ```
9
28
 
10
- This repository contains the same application for two frameworks, Next.js (`apps/web`) and TanStack Start (`apps/start`), plus every optional module. [create-clubedge-app](https://github.com/Clubedge/create-clubedge-app) generates a project with one framework and only the modules you choose; sections of this README marked for a framework or module appear only in projects that use it. Contributions and bug reports are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md). Repository maintainers can use [PUBLISHING.md](PUBLISHING.md) for the GitHub launch checklist, and [ROADMAP.md](ROADMAP.md) describes the planned phases.
29
+ ## Why Clubedge Starter
11
30
 
31
+ Most starters give you one stack and leave you to rip out what you don't need. Removing a provider means hunting down its package, its environment variables, its docs, and its lockfile entries, and once you have changed the code, a newer version of the starter is something you can only copy from by hand.
32
+
33
+ Clubedge Starter is built the other way around. Every service is an optional module, and a module you leave out is removed completely. Each provider sits behind an interface in its own package, so you can swap Supabase Auth for Better Auth, or OpenTelemetry for Sentry, without touching your pages. And because [create-clubedge-app](https://github.com/Clubedge/create-clubedge-app) records exactly what your project was generated from, it can add, remove, or upgrade modules later by merging the change into your code, keeping your edits.
34
+
35
+ Every combination in the CI matrix is generated as a real project and runs its full pipeline: install, lint, typecheck, unit tests, browser tests against real local services, a production build, and a smoke test.
36
+
37
+ <!-- In generated projects, the block below supplies the title that create-clubedge-app replaces with the project's name; its comment keeps it hidden here. -->
38
+ <!-- clubedge:end -->
39
+ <!-- clubedge:if framework=next|tanstack-start
40
+ # Clubedge Starter
12
41
  <!-- clubedge:end -->
13
42
 
14
43
  ## Features
15
44
 
45
+ **Application**
46
+
16
47
  - Next.js App Router with React, TypeScript, Tailwind CSS 4, and shadcn/ui using Base UI primitives. <!-- clubedge:only framework=next -->
17
48
  - TanStack Start with React, TypeScript, Vite, Tailwind CSS 4, and shadcn/ui using Base UI primitives. <!-- clubedge:only framework=tanstack-start -->
18
49
  - pnpm workspaces and Turborepo, with the web app in `apps/web` and framework-agnostic packages under `packages/`.
19
50
  - Drizzle ORM and PostgreSQL schema, migrations, and seed commands.
51
+ - Zod environment validation, security headers, structured errors, and `/api/health`.
52
+ - A landing page and a working dashboard built from the shared UI package.
53
+
54
+ **Modules**
55
+
20
56
  - Supabase Auth with server-verified sessions in secure cookies, a protected dashboard, and rate-limited sign-in. <!-- clubedge:only auth=supabase -->
21
57
  - Better Auth with users and sessions in your own PostgreSQL, secure session cookies, a protected dashboard, and rate-limited sign-in. <!-- clubedge:only auth=better-auth -->
22
58
  - Password reset by email, and email verification that can be required before sign-in. <!-- clubedge:only auth=better-auth && email!=none -->
23
59
  - Password reset by email, sent by Supabase. <!-- clubedge:only auth=supabase -->
60
+ - 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 -->
61
+ - Email through Resend; development prints emails to the server log until the API key is set. <!-- clubedge:only email=resend -->
24
62
  - S3-compatible storage adapter for AWS S3, Cloudflare R2, and MinIO. <!-- clubedge:only storage=s3 -->
25
63
  - Supabase Storage adapter that uses the signed-in user's session. <!-- clubedge:only storage=supabase -->
26
64
  - Fixed-window rate limiter and cache on Redis, falling back to process memory without `REDIS_URL`. <!-- clubedge:only cache=redis -->
27
65
  - Fixed-window rate limiter and cache in process memory, suited to a single server instance. <!-- clubedge:only cache=memory -->
28
- - Email over SMTP, for any provider or a local inbox; development prints emails to the server log until SMTP is configured. <!-- clubedge:only email=smtp -->
29
- - Email through Resend; development prints emails to the server log until the API key is set. <!-- clubedge:only email=resend -->
30
66
  - Subscriptions with Stripe Checkout and the customer portal, kept in sync by verified webhooks, behind a provider-neutral payments interface. <!-- clubedge:only payments=stripe -->
31
67
  - 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 -->
32
68
  - Server error reporting and request tracing with OpenTelemetry, to any OTLP collector, behind a provider-neutral observability interface. <!-- clubedge:only observability=otel -->
33
69
  - Server error reporting and request tracing with Sentry, behind a provider-neutral observability interface. <!-- clubedge:only observability=sentry -->
34
- - Zod environment validation, security headers, structured errors, and `/api/health`.
35
- - Docker support, GitHub Actions CI, Vitest, Playwright, ESLint, and Prettier.
70
+
71
+ **Production**
72
+
36
73
  - A production image that includes its own migrator (`node /app/migrate.mjs`), for running migrations before each release.
37
74
  - A manual deploy workflow for the chosen platform, and a readiness endpoint (`/api/ready`) that checks the database.
38
75
  - Deployment to Fly.io with `fly.toml`: migrations run as the release command. <!-- clubedge:only deploy=fly -->
@@ -41,52 +78,42 @@ This repository contains the same application for two frameworks, Next.js (`apps
41
78
  - Deployment to Vercel with `apps/web/vercel.json`: migrations run during the build. <!-- clubedge:only deploy=vercel -->
42
79
  - Deployment to Netlify with `apps/web/netlify.toml`: migrations run during the build. <!-- clubedge:only deploy=netlify -->
43
80
 
44
- ## Architecture
81
+ **Quality**
45
82
 
46
- ```mermaid
47
- flowchart TB
48
- %% clubedge:if framework=next
49
- Browser --> App[Next.js application]
50
- %% clubedge:end
51
- %% clubedge:if framework=tanstack-start
52
- Browser --> App[TanStack Start application]
53
- %% clubedge:end
54
- App -->|Drizzle ORM| DB[(PostgreSQL)]
55
- %% clubedge:if auth=supabase
56
- App -->|auth interface and server sessions| Auth[Supabase Auth]
57
- %% clubedge:end
58
- %% clubedge:if auth=better-auth
59
- App -->|auth interface and server sessions| Auth[Better Auth]
60
- Auth -->|users and sessions| DB
61
- %% clubedge:end
62
- %% clubedge:if cache=redis
63
- App -->|cache and rate limits| Redis[(Redis, or memory)]
64
- %% clubedge:end
65
- %% clubedge:if email=smtp
66
- App -->|email interface| SMTP[SMTP server, or the log]
67
- %% clubedge:end
68
- %% clubedge:if email=resend
69
- App -->|email interface| Resend[Resend, or the log]
70
- %% clubedge:end
71
- %% clubedge:if storage=s3
72
- App -->|storage interface| S3[S3-compatible storage: AWS S3, R2, MinIO]
73
- %% clubedge:end
74
- %% clubedge:if storage=supabase
75
- App -->|storage interface| SupabaseStorage[Supabase Storage]
76
- %% clubedge:end
77
- ```
83
+ - GitHub Actions CI with Vitest, Playwright, ESLint, and Prettier.
84
+ - `pnpm check:services`, which proves the app can reach every configured service with a real round trip.
78
85
 
79
- ### Architecture principles
86
+ <!-- clubedge:if starter-repository -->
80
87
 
81
- - **PostgreSQL is the source of truth for application data.** Drizzle centralizes application schema, queries, and migrations.
82
- - **Authentication stays separate from application data.** The auth provider owns credentials and sessions; application tables are managed by Drizzle. <!-- clubedge:only auth!=none -->
83
- - **Providers sit behind interfaces.** Interfaces live in `packages/{auth,storage,cache}` and each provider in its own package, so replacing a provider does not touch application code.
84
- - **Shared packages are framework-agnostic.** Packages under `packages/` receive configuration and request cookies as arguments and never import a web framework. ESLint enforces these boundaries.
85
- - **One composition root.** `apps/web/src/server/` is the only place that reads environment variables and chooses providers. Routes and components call it rather than a provider SDK.
86
- - **Keep the baseline focused.** Additional providers, queues, and generator tooling should be added when a real use case calls for them.
88
+ ## Modules
89
+
90
+ Pick modules when you create a project, and change them later with `pnpm clubedge add` and `pnpm clubedge remove`.
91
+
92
+ | Module | Options | Default |
93
+ | ------------- | --------------------------------------------------------- | ---------- |
94
+ | Framework | `next`, `tanstack-start` | `next` |
95
+ | Auth | `supabase`, `better-auth`, `none` | `supabase` |
96
+ | Storage | `s3`, `supabase`, `none` | `s3` |
97
+ | Cache | `redis`, `memory` | `redis` |
98
+ | Email | `smtp`, `resend`, `none` | `smtp` |
99
+ | Payments | `stripe`, `none` | `none` |
100
+ | Jobs | `pg-boss`, `none` | `none` |
101
+ | Observability | `otel`, `sentry`, `none` | `none` |
102
+ | Local infra | `docker`, `supabase`, `local` (no Docker), `none` | `docker` |
103
+ | Deploy | `docker`, `fly`, `railway`, `render`, `vercel`, `netlify` | `docker` |
104
+
105
+ A module you leave out takes its packages, provider SDK, environment variables, documentation, and lockfile entries with it.
106
+
107
+ <!-- clubedge:end -->
87
108
 
88
109
  ## Quick start
89
110
 
111
+ <!-- clubedge:if starter-repository -->
112
+
113
+ To start a new application, create a project with the CLI as shown above; it contains only your modules. The steps below run this repository itself, with every module, for working on the Starter.
114
+
115
+ <!-- clubedge:end -->
116
+
90
117
  ### Requirements
91
118
 
92
119
  - Node.js 22.12 or newer (Node.js 22 LTS recommended).
@@ -156,6 +183,65 @@ pnpm dev
156
183
 
157
184
  Open <http://localhost:3000> for the landing page, <http://localhost:3000/dashboard> for the dashboard, or <http://localhost:3000/api/health> for the liveness endpoint. The pages render without connecting to providers; database and provider operations require their configuration.
158
185
 
186
+ > [!NOTE]
187
+ > This is a foundation and a reference implementation. Review its security and deployment choices for your application before you go to production; [SETUP.md](SETUP.md#before-deploying) has a checklist.
188
+
189
+ ## How it works
190
+
191
+ ```mermaid
192
+ flowchart TB
193
+ %% clubedge:if framework=next
194
+ Browser --> App[Next.js application]
195
+ %% clubedge:end
196
+ %% clubedge:if framework=tanstack-start
197
+ Browser --> App[TanStack Start application]
198
+ %% clubedge:end
199
+ App -->|Drizzle ORM| DB[(PostgreSQL)]
200
+ %% clubedge:if auth=supabase
201
+ App -->|auth interface and server sessions| Auth[Supabase Auth]
202
+ %% clubedge:end
203
+ %% clubedge:if auth=better-auth
204
+ App -->|auth interface and server sessions| Auth[Better Auth]
205
+ Auth -->|users and sessions| DB
206
+ %% clubedge:end
207
+ %% clubedge:if cache=redis
208
+ App -->|cache and rate limits| Redis[(Redis, or memory)]
209
+ %% clubedge:end
210
+ %% clubedge:if email=smtp
211
+ App -->|email interface| SMTP[SMTP server, or the log]
212
+ %% clubedge:end
213
+ %% clubedge:if email=resend
214
+ App -->|email interface| Resend[Resend, or the log]
215
+ %% clubedge:end
216
+ %% clubedge:if storage=s3
217
+ App -->|storage interface| S3[S3-compatible storage: AWS S3, R2, MinIO]
218
+ %% clubedge:end
219
+ %% clubedge:if storage=supabase
220
+ App -->|storage interface| SupabaseStorage[Supabase Storage]
221
+ %% clubedge:end
222
+ %% clubedge:if payments=stripe
223
+ App -->|payments interface| Stripe[Stripe Checkout and portal]
224
+ Stripe -->|verified webhooks| App
225
+ %% clubedge:end
226
+ %% clubedge:if jobs=pg-boss
227
+ App -->|jobs interface, worker in the server| Jobs[pg-boss]
228
+ Jobs -->|jobs table| DB
229
+ %% clubedge:end
230
+ %% clubedge:if observability=otel
231
+ App -->|traces and errors| Otel[OTLP collector]
232
+ %% clubedge:end
233
+ %% clubedge:if observability=sentry
234
+ App -->|traces and errors| Sentry[Sentry]
235
+ %% clubedge:end
236
+ ```
237
+
238
+ - **PostgreSQL is the source of truth for application data.** Drizzle centralizes application schema, queries, and migrations.
239
+ - **Authentication stays separate from application data.** The auth provider owns credentials and sessions; application tables are managed by Drizzle. <!-- clubedge:only auth!=none -->
240
+ - **Providers sit behind interfaces.** Each interface lives in its own package under `packages/`, and each provider in another, so replacing a provider does not touch application code.
241
+ - **Shared packages are framework-agnostic.** Packages under `packages/` receive configuration and request cookies as arguments and never import a web framework. ESLint enforces these boundaries.
242
+ - **One composition root.** `apps/web/src/server/` is the only place that reads environment variables and chooses providers. Routes and components call it rather than a provider SDK.
243
+ - **Keep the baseline focused.** Additional providers and tooling should be added when a real use case calls for them.
244
+
159
245
  ## Common commands
160
246
 
161
247
  Run these from the repository root:
@@ -198,13 +284,28 @@ Change the project later:
198
284
 
199
285
  These commands merge the change into your code instead of overwriting it: files you never touched are updated, your edits are kept, and lines you both changed get conflict markers. Commit first, because they need a clean Git tree so that `git diff` shows the result. Add `--dry-run` to preview a change.
200
286
 
287
+ ## Documentation
288
+
289
+ | I want to | Read |
290
+ | ------------------------------------------- | --------------------------------------------- |
291
+ | Configure the environment and each provider | [SETUP.md](SETUP.md) |
292
+ | Run the local services | [Local services](SETUP.md#local-services) |
293
+ | Deploy the app | [Deploy](SETUP.md#deploy) |
294
+ | Check the app before going live | [Before deploying](SETUP.md#before-deploying) |
295
+ | Fix a common problem | [Troubleshooting](SETUP.md#troubleshooting) |
296
+ | Contribute code | [CONTRIBUTING.md](CONTRIBUTING.md) |
297
+ | Get help | [SUPPORT.md](SUPPORT.md) |
298
+ | Report a security problem | [SECURITY.md](SECURITY.md) |
299
+
201
300
  <!-- clubedge:if starter-repository -->
202
301
 
203
302
  ## Working on this repository
204
303
 
304
+ This repository contains the same application for two frameworks, Next.js (`apps/web`) and TanStack Start (`apps/start`), plus every optional module. Sections of this README marked for a framework or module appear only in projects that use it.
305
+
205
306
  The TanStack Start app is `apps/start`, run with `pnpm --filter @clubedge/start dev`. It reads `apps/start/.env.local`, copied from `apps/start/.env.example`. Run the browser suite against it with `E2E_APP=start pnpm test:e2e`, and build its image with `docker build -f apps/start/Dockerfile --build-arg APP_DIR=apps/start --build-arg APP_PACKAGE=@clubedge/start .`.
206
307
 
207
- `clubedge.template.json` tells create-clubedge-app what each framework and module owns: packages, files, variant files under `variants/` that replace a default, and files with `clubedge:if` blocks. Write those blocks so the default selection keeps every block; use a variant file when two options need different code. `.github/workflows/starter.yml` generates projects for several combinations and runs their full pipeline.
308
+ `clubedge.template.json` tells create-clubedge-app what each framework and module owns: packages, files, variant files under `variants/` that replace a default, and files with `clubedge:if` blocks. Write those blocks so the default selection keeps every block; use a variant file when two options need different code. `.github/workflows/starter.yml` generates projects for several combinations and runs their full pipeline. Maintainers can use [PUBLISHING.md](PUBLISHING.md) for the GitHub launch checklist.
208
309
 
209
310
  <!-- clubedge:end -->
210
311
 
@@ -212,7 +313,7 @@ The TanStack Start app is `apps/start`, run with `pnpm --filter @clubedge/start
212
313
 
213
314
  - `apps/web/`: the web app, with routes, UI, and the composition root in `src/server/`
214
315
  - `e2e/`: Playwright browser checks
215
- - `packages/core/`: errors, `Result` type, and HTTP helpers (no dependencies)
316
+ - `packages/core/`: errors, `Result` type, HTTP helpers, and the error reporting and tracing hooks (no dependencies)
216
317
  - `packages/db/`: Drizzle schema, client factory, migrations, and seed script
217
318
  - `packages/auth/`: `AuthProvider` and `CookieStore` interfaces <!-- clubedge:only auth!=none -->
218
319
  - `packages/auth-supabase/`: Supabase Auth adapter <!-- clubedge:only auth=supabase -->
@@ -250,6 +351,33 @@ pnpm dlx shadcn@latest add badge -c apps/web
250
351
 
251
352
  Shared components are generated under `packages/ui/src/components` and can be imported from `@clubedge/ui/components/<component>`. The app and `packages/ui` `components.json` files intentionally share the `base-nova` style, neutral color base, Lucide icons, Tailwind v4 stylesheet, and RTL-aware generation setting. The app currently renders English in LTR; set the root document's `lang` and `dir` for the locale used by your application. Put app-only components in `apps/web/src/components`.
252
353
 
354
+ <!-- clubedge:if starter-repository -->
355
+
356
+ ## Roadmap
357
+
358
+ Each phase ends with a Starter release and a create-clubedge-app release pinned to it. The full plan is in [ROADMAP.md](ROADMAP.md).
359
+
360
+ | Release | Theme |
361
+ | ---------------- | ---------------------------------------------------------------------------------------- |
362
+ | v0.5.0 to v0.8.0 | Optional modules, local services with and without Docker, `add`, `remove`, and `upgrade` |
363
+ | v0.9.0 | Email, password reset, and email verification |
364
+ | v0.10.0 | Deployment to Docker hosts, Fly.io, Railway, Render, Vercel, and Netlify |
365
+ | v0.11.0 | Payments with Stripe |
366
+ | v0.12.0 | Background jobs with pg-boss |
367
+ | v0.13.0 | Observability with OpenTelemetry or Sentry |
368
+ | next | Browser error reporting, Sentry source maps, and auth emails through the job queue |
369
+
370
+ ## Community
371
+
372
+ - **[Issues](https://github.com/Clubedge/clubedge-starter/issues)** for bugs and feature requests, using the issue forms.
373
+ - **[Pull requests](https://github.com/Clubedge/clubedge-starter/pulls)** are welcome; start with [CONTRIBUTING.md](CONTRIBUTING.md).
374
+ - **[create-clubedge-app](https://github.com/Clubedge/create-clubedge-app)** for problems creating, changing, or upgrading a project.
375
+ - **Security problems** go through the private process in [SECURITY.md](SECURITY.md), not public issues.
376
+
377
+ Everyone taking part is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
378
+
379
+ <!-- clubedge:end -->
380
+
253
381
  ## Contributing
254
382
 
255
383
  Please read [CONTRIBUTING.md](CONTRIBUTING.md) before opening an issue or pull request. For local changes, run the relevant checks before submitting. You can report vulnerabilities privately using the process in [SECURITY.md](SECURITY.md).
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/start",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,8 +1,8 @@
1
1
  import { definePlugin } from "nitro";
2
- import { getJobs, startJobsWorker } from "@/server/jobs";
2
+ import { startJobsWorker, stopJobsWorker } from "@/server/jobs";
3
3
 
4
4
  /** Starts the background jobs worker with the server, and stops it with the server. */
5
5
  export default definePlugin((nitroApp) => {
6
6
  startJobsWorker();
7
- nitroApp.hooks.hook("close", () => getJobs().stop());
7
+ nitroApp.hooks.hook("close", stopJobsWorker);
8
8
  });
@@ -0,0 +1,27 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { startJobsWorker } from "./jobs";
3
+
4
+ // The server starts the worker before handling any request. A configuration mistake must be
5
+ // reported, not stop the server, so pages that need no database keep working.
6
+ const holder = globalThis as typeof globalThis & { clubedgeJobs?: unknown };
7
+
8
+ afterEach(() => {
9
+ delete holder.clubedgeJobs;
10
+ vi.unstubAllEnvs();
11
+ vi.restoreAllMocks();
12
+ });
13
+
14
+ describe("startJobsWorker", () => {
15
+ it("reports a missing DATABASE_URL instead of failing to start", () => {
16
+ const log = vi.spyOn(console, "error").mockImplementation(() => {});
17
+ vi.stubEnv("DATABASE_URL", "");
18
+ vi.stubEnv("JOBS_WORKER", "on");
19
+
20
+ expect(() => startJobsWorker()).not.toThrow();
21
+ expect(log).toHaveBeenCalledWith(
22
+ "Background jobs: the worker did not start.",
23
+ expect.any(Error),
24
+ );
25
+ expect(holder.clubedgeJobs).toBeUndefined();
26
+ });
27
+ });
@@ -53,15 +53,27 @@ export function getJobs(): JobQueue {
53
53
 
54
54
  /** Starts the worker when this server should run one, retrying until the database answers. */
55
55
  export function startJobsWorker() {
56
- if (!isWorkerEnabled()) return;
56
+ let jobs: JobQueue;
57
+ try {
58
+ if (!isWorkerEnabled()) return;
59
+ jobs = getJobs();
60
+ } catch (error) {
61
+ // Invalid configuration, such as a missing DATABASE_URL, is reported once; the rest of the
62
+ // app keeps serving instead of failing to start.
63
+ reportError(error, { message: "Background jobs: the worker did not start." });
64
+ return;
65
+ }
57
66
  const start = () =>
58
- getJobs()
59
- .startWorker()
60
- .catch((error: unknown) => {
61
- reportError(error, {
62
- message: "Background jobs: the worker could not start, retrying in 5 seconds.",
63
- });
64
- setTimeout(start, 5_000).unref();
67
+ jobs.startWorker().catch((error: unknown) => {
68
+ reportError(error, {
69
+ message: "Background jobs: the worker could not start, retrying in 5 seconds.",
65
70
  });
71
+ setTimeout(start, 5_000).unref();
72
+ });
66
73
  void start();
67
74
  }
75
+
76
+ /** Stops the worker, if this process created a queue. */
77
+ export async function stopJobsWorker() {
78
+ await globalForJobs.clubedgeJobs?.stop();
79
+ }
@@ -0,0 +1,23 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { startObservability } from "./observability";
3
+
4
+ // The server starts observability before handling any request. An invalid setting must leave it
5
+ // off with a report, not stop the server.
6
+ const holder = globalThis as typeof globalThis & { clubedgeObservability?: unknown };
7
+
8
+ afterEach(() => {
9
+ delete holder.clubedgeObservability;
10
+ vi.unstubAllEnvs();
11
+ vi.restoreAllMocks();
12
+ });
13
+
14
+ describe("startObservability", () => {
15
+ it("leaves observability off when its settings are invalid", () => {
16
+ const log = vi.spyOn(console, "error").mockImplementation(() => {});
17
+ vi.stubEnv("OTEL_EXPORTER_OTLP_ENDPOINT", "not a url");
18
+ vi.stubEnv("SENTRY_DSN", "not a url");
19
+
20
+ expect(startObservability()).toBeNull();
21
+ expect(log).toHaveBeenCalledWith("Observability did not start.", expect.any(Error));
22
+ });
23
+ });
@@ -1,6 +1,6 @@
1
1
  import "@tanstack/react-start/server-only";
2
2
  import { z } from "zod";
3
- import { setTelemetry } from "@clubedge/core";
3
+ import { reportError, setTelemetry } from "@clubedge/core";
4
4
  import type { ObservabilityProvider } from "@clubedge/observability";
5
5
  import { createOtelObservability, parseOtlpHeaders } from "@clubedge/observability-otel";
6
6
  import { siteConfig } from "@/config/site";
@@ -22,7 +22,15 @@ const globalForObservability = globalThis as typeof globalThis & {
22
22
  export function startObservability(): ObservabilityProvider | null {
23
23
  if (globalForObservability.clubedgeObservability !== undefined)
24
24
  return globalForObservability.clubedgeObservability;
25
- const env = parseEnv(observabilityEnvSchema, "Observability");
25
+ let env: z.infer<typeof observabilityEnvSchema>;
26
+ try {
27
+ env = parseEnv(observabilityEnvSchema, "Observability");
28
+ } catch (error) {
29
+ // An invalid variable leaves observability off rather than stopping the server.
30
+ reportError(error, { message: "Observability did not start." });
31
+ globalForObservability.clubedgeObservability = null;
32
+ return null;
33
+ }
26
34
  const provider = env.OTEL_EXPORTER_OTLP_ENDPOINT
27
35
  ? createOtelObservability({
28
36
  endpoint: env.OTEL_EXPORTER_OTLP_ENDPOINT,
@@ -1,6 +1,6 @@
1
1
  import "@tanstack/react-start/server-only";
2
2
  import { z } from "zod";
3
- import { setTelemetry } from "@clubedge/core";
3
+ import { reportError, setTelemetry } from "@clubedge/core";
4
4
  import type { ObservabilityProvider } from "@clubedge/observability";
5
5
  import { createSentryObservability } from "@clubedge/observability-sentry";
6
6
  import { optional, parseEnv } from "@/env/server";
@@ -25,7 +25,15 @@ const globalForObservability = globalThis as typeof globalThis & {
25
25
  export function startObservability(): ObservabilityProvider | null {
26
26
  if (globalForObservability.clubedgeObservability !== undefined)
27
27
  return globalForObservability.clubedgeObservability;
28
- const env = parseEnv(observabilityEnvSchema, "Observability");
28
+ let env: z.infer<typeof observabilityEnvSchema>;
29
+ try {
30
+ env = parseEnv(observabilityEnvSchema, "Observability");
31
+ } catch (error) {
32
+ // An invalid variable leaves observability off rather than stopping the server.
33
+ reportError(error, { message: "Observability did not start." });
34
+ globalForObservability.clubedgeObservability = null;
35
+ return null;
36
+ }
29
37
  const provider = env.SENTRY_DSN
30
38
  ? createSentryObservability({
31
39
  dsn: env.SENTRY_DSN,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/web",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "scripts": {
@@ -53,15 +53,27 @@ export function getJobs(): JobQueue {
53
53
 
54
54
  /** Starts the worker when this server should run one, retrying until the database answers. */
55
55
  export function startJobsWorker() {
56
- if (!isWorkerEnabled()) return;
56
+ let jobs: JobQueue;
57
+ try {
58
+ if (!isWorkerEnabled()) return;
59
+ jobs = getJobs();
60
+ } catch (error) {
61
+ // Invalid configuration, such as a missing DATABASE_URL, is reported once; the rest of the
62
+ // app keeps serving instead of failing to start.
63
+ reportError(error, { message: "Background jobs: the worker did not start." });
64
+ return;
65
+ }
57
66
  const start = () =>
58
- getJobs()
59
- .startWorker()
60
- .catch((error: unknown) => {
61
- reportError(error, {
62
- message: "Background jobs: the worker could not start, retrying in 5 seconds.",
63
- });
64
- setTimeout(start, 5_000).unref();
67
+ jobs.startWorker().catch((error: unknown) => {
68
+ reportError(error, {
69
+ message: "Background jobs: the worker could not start, retrying in 5 seconds.",
65
70
  });
71
+ setTimeout(start, 5_000).unref();
72
+ });
66
73
  void start();
67
74
  }
75
+
76
+ /** Stops the worker, if this process created a queue. */
77
+ export async function stopJobsWorker() {
78
+ await globalForJobs.clubedgeJobs?.stop();
79
+ }
@@ -1,6 +1,6 @@
1
1
  import "server-only";
2
2
  import { z } from "zod";
3
- import { setTelemetry } from "@clubedge/core";
3
+ import { reportError, setTelemetry } from "@clubedge/core";
4
4
  import type { ObservabilityProvider } from "@clubedge/observability";
5
5
  import { createOtelObservability, parseOtlpHeaders } from "@clubedge/observability-otel";
6
6
  import { siteConfig } from "@/config/site";
@@ -22,7 +22,15 @@ const globalForObservability = globalThis as typeof globalThis & {
22
22
  export function startObservability(): ObservabilityProvider | null {
23
23
  if (globalForObservability.clubedgeObservability !== undefined)
24
24
  return globalForObservability.clubedgeObservability;
25
- const env = parseEnv(observabilityEnvSchema, "Observability");
25
+ let env: z.infer<typeof observabilityEnvSchema>;
26
+ try {
27
+ env = parseEnv(observabilityEnvSchema, "Observability");
28
+ } catch (error) {
29
+ // An invalid variable leaves observability off rather than stopping the server.
30
+ reportError(error, { message: "Observability did not start." });
31
+ globalForObservability.clubedgeObservability = null;
32
+ return null;
33
+ }
26
34
  const provider = env.OTEL_EXPORTER_OTLP_ENDPOINT
27
35
  ? createOtelObservability({
28
36
  endpoint: env.OTEL_EXPORTER_OTLP_ENDPOINT,
@@ -1,6 +1,6 @@
1
1
  import "server-only";
2
2
  import { z } from "zod";
3
- import { setTelemetry } from "@clubedge/core";
3
+ import { reportError, setTelemetry } from "@clubedge/core";
4
4
  import type { ObservabilityProvider } from "@clubedge/observability";
5
5
  import { createSentryObservability } from "@clubedge/observability-sentry";
6
6
  import { optional, parseEnv } from "@/env/server";
@@ -25,7 +25,15 @@ const globalForObservability = globalThis as typeof globalThis & {
25
25
  export function startObservability(): ObservabilityProvider | null {
26
26
  if (globalForObservability.clubedgeObservability !== undefined)
27
27
  return globalForObservability.clubedgeObservability;
28
- const env = parseEnv(observabilityEnvSchema, "Observability");
28
+ let env: z.infer<typeof observabilityEnvSchema>;
29
+ try {
30
+ env = parseEnv(observabilityEnvSchema, "Observability");
31
+ } catch (error) {
32
+ // An invalid variable leaves observability off rather than stopping the server.
33
+ reportError(error, { message: "Observability did not start." });
34
+ globalForObservability.clubedgeObservability = null;
35
+ return null;
36
+ }
29
37
  const provider = env.SENTRY_DSN
30
38
  ? createSentryObservability({
31
39
  dsn: env.SENTRY_DSN,
@@ -258,6 +258,7 @@
258
258
  "apps/start/src/routes/jobs",
259
259
  "apps/start/src/server/jobs-forms.ts",
260
260
  "apps/start/src/server/jobs.functions.ts",
261
+ "apps/start/src/server/jobs.test.ts",
261
262
  "apps/start/src/server/jobs.ts",
262
263
  "apps/web/src/app/api/jobs",
263
264
  "apps/web/src/app/dashboard/_components/jobs-actions.ts",
@@ -285,6 +286,7 @@
285
286
  },
286
287
  "files": [
287
288
  "apps/start/src/nitro/plugins/observability.ts",
289
+ "apps/start/src/server/observability.test.ts",
288
290
  "apps/start/src/server/observability.ts",
289
291
  "apps/web/src/instrumentation.ts",
290
292
  "apps/web/src/server/observability.ts",
@@ -300,6 +302,7 @@
300
302
  },
301
303
  "files": [
302
304
  "apps/start/src/nitro/plugins/observability.ts",
305
+ "apps/start/src/server/observability.test.ts",
303
306
  "apps/start/src/server/observability.ts",
304
307
  "apps/web/src/instrumentation.ts",
305
308
  "apps/web/src/server/observability.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clubedge-starter-v1",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "packageManager": "pnpm@10.9.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/auth",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/auth-better-auth",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Better Auth adapter for @clubedge/auth, storing users and sessions in your own PostgreSQL.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/auth-supabase",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Supabase Auth adapter for @clubedge/auth.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/cache",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/cache-redis",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Redis rate limiter and cache for @clubedge/cache.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/core",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/db",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/email",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Email interface, console and in-memory providers, and message templates.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/email-resend",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Resend provider for @clubedge/email, through Resend's HTTP API.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/email-smtp",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "SMTP provider for @clubedge/email, for any SMTP server.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/jobs",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Background jobs interface: job definitions, a queue contract, and an in-memory queue.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/jobs-pg-boss",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "pg-boss provider for @clubedge/jobs: a job queue stored in the project's PostgreSQL.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/local-db",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "PostgreSQL for development without Docker: PGlite served over the PostgreSQL protocol.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/observability",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Observability interface: error reports and spans for @clubedge/core's telemetry hooks, and an in-memory provider for tests.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/observability-otel",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "OpenTelemetry provider for @clubedge/observability: traces and error logs exported over OTLP to any collector.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/observability-sentry",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Sentry provider for @clubedge/observability: server errors and traces sent to Sentry.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/payments",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Provider-neutral payments: the PaymentProvider interface, billing logic, and subscription tables.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/payments-stripe",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Stripe provider for @clubedge/payments: Checkout, the customer portal, and verified webhooks.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/storage",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/storage-s3",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "S3-compatible adapter for @clubedge/storage.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/storage-supabase",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Supabase Storage adapter for @clubedge/storage.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/ui",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "repository": "Clubedge/clubedge-starter",
3
- "ref": "v0.13.0",
4
- "commit": "62fb8d1b1e13ed8e8e134bd1e53f2bfdde2c6a50",
3
+ "ref": "v0.13.1",
4
+ "commit": "28bfbf98c85ae500ace6a062dd10d67746e1a330",
5
5
  "files": [
6
6
  ".dockerignore",
7
7
  ".env.example",
@@ -95,7 +95,9 @@
95
95
  "apps/start/src/server/email.ts",
96
96
  "apps/start/src/server/jobs-forms.ts",
97
97
  "apps/start/src/server/jobs.functions.ts",
98
+ "apps/start/src/server/jobs.test.ts",
98
99
  "apps/start/src/server/jobs.ts",
100
+ "apps/start/src/server/observability.test.ts",
99
101
  "apps/start/src/server/observability.ts",
100
102
  "apps/start/src/server/payments.ts",
101
103
  "apps/start/src/server/storage.ts",