@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.
- package/CHANGELOG.md +4 -0
- package/README.md +122 -76
- package/package.json +3 -3
- package/template/README.md +179 -51
- package/template/apps/start/package.json +1 -1
- package/template/apps/start/src/nitro/plugins/jobs.ts +2 -2
- package/template/apps/start/src/server/jobs.test.ts +27 -0
- package/template/apps/start/src/server/jobs.ts +20 -8
- package/template/apps/start/src/server/observability.test.ts +23 -0
- package/template/apps/start/src/server/observability.ts +10 -2
- package/template/apps/start/src/server/variants/observability.sentry.ts +10 -2
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/src/server/jobs.ts +20 -8
- package/template/apps/web/src/server/observability.ts +10 -2
- package/template/apps/web/src/server/variants/observability.sentry.ts +10 -2
- package/template/clubedge.template.json +3 -0
- package/template/package.json +1 -1
- package/template/packages/auth/package.json +1 -1
- package/template/packages/auth-better-auth/package.json +1 -1
- package/template/packages/auth-supabase/package.json +1 -1
- package/template/packages/cache/package.json +1 -1
- package/template/packages/cache-redis/package.json +1 -1
- package/template/packages/core/package.json +1 -1
- package/template/packages/db/package.json +1 -1
- 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 +1 -1
- package/template/packages/jobs-pg-boss/package.json +1 -1
- 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.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
|
-
|
|
4
|
-
[](LICENSE)
|
|
5
|
+
**Create a production-ready Next.js or TanStack Start app with one command, and keep it up to date.**
|
|
5
6
|
|
|
6
|
-
|
|
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
|
-
|
|
10
|
+
[](https://www.npmjs.com/package/@clubedge/create-clubedge-app)
|
|
11
|
+
[](https://www.npmjs.com/package/@clubedge/create-clubedge-app)
|
|
12
|
+
[](https://github.com/Clubedge/create-clubedge-app/actions/workflows/ci.yml)
|
|
13
|
+
[](https://github.com/Clubedge/create-clubedge-app/releases)
|
|
14
|
+
[](LICENSE)
|
|
9
15
|
|
|
10
|
-
|
|
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
|
-
|
|
22
|
+
</div>
|
|
13
23
|
|
|
14
|
-
```
|
|
15
|
-
create-clubedge-app
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
91
|
+
Interactive runs ask whether to customize these after the framework question.
|
|
108
92
|
|
|
109
93
|
## Options
|
|
110
94
|
|
|
111
95
|
```text
|
|
112
|
-
Usage:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
--
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
--
|
|
120
|
-
|
|
121
|
-
--
|
|
122
|
-
|
|
123
|
-
--
|
|
124
|
-
--
|
|
125
|
-
|
|
126
|
-
-
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
##
|
|
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
|
-
|
|
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.
|
|
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.
|
|
26
|
-
"starterCommit": "
|
|
25
|
+
"starterRef": "v0.13.1",
|
|
26
|
+
"starterCommit": "28bfbf98c85ae500ace6a062dd10d67746e1a330"
|
|
27
27
|
},
|
|
28
28
|
"license": "Apache-2.0",
|
|
29
29
|
"repository": {
|
package/template/README.md
CHANGED
|
@@ -1,38 +1,75 @@
|
|
|
1
|
+
<!-- clubedge:if starter-repository -->
|
|
2
|
+
<div align="center">
|
|
3
|
+
|
|
1
4
|
# Clubedge Starter
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
[](LICENSE)
|
|
6
|
+
**A production-ready full-stack TypeScript starter that ships only the modules you choose.**
|
|
5
7
|
|
|
6
|
-
|
|
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
|
-
|
|
11
|
+
[](https://github.com/Clubedge/clubedge-starter/actions/workflows/ci.yml)
|
|
12
|
+
[](https://github.com/Clubedge/clubedge-starter/actions/workflows/starter.yml)
|
|
13
|
+
[](https://github.com/Clubedge/clubedge-starter/releases)
|
|
14
|
+
[](https://www.npmjs.com/package/@clubedge/create-clubedge-app)
|
|
15
|
+
[](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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
81
|
+
**Quality**
|
|
45
82
|
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
86
|
+
<!-- clubedge:if starter-repository -->
|
|
80
87
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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,
|
|
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,8 +1,8 @@
|
|
|
1
1
|
import { definePlugin } from "nitro";
|
|
2
|
-
import {
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
package/template/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clubedge/observability",
|
|
3
|
-
"version": "0.13.
|
|
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",
|
package/template.lock.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"repository": "Clubedge/clubedge-starter",
|
|
3
|
-
"ref": "v0.13.
|
|
4
|
-
"commit": "
|
|
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",
|