@clubedge/create-clubedge-app 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +13 -11
- package/dist/core/lockfile-peers.js +16 -1
- package/package.json +3 -3
- package/template/.env.example +24 -0
- package/template/README.md +8 -0
- package/template/SETUP.md +69 -0
- package/template/apps/start/.env.example +24 -0
- package/template/apps/start/package.json +6 -1
- package/template/apps/start/src/components/jobs/jobs-card.tsx +71 -0
- package/template/apps/start/src/jobs/index.ts +7 -0
- package/template/apps/start/src/jobs/sample.ts +10 -0
- package/template/apps/start/src/nitro/plugins/jobs.ts +8 -0
- package/template/apps/start/src/nitro/plugins/observability.ts +10 -0
- package/template/apps/start/src/routes/api/jobs/run.ts +28 -0
- package/template/apps/start/src/routes/api/webhooks/payments.ts +2 -1
- package/template/apps/start/src/routes/dashboard.tsx +11 -0
- package/template/apps/start/src/routes/jobs/sample.ts +11 -0
- package/template/apps/start/src/server/auth-forms.ts +4 -4
- package/template/apps/start/src/server/auth.ts +2 -2
- package/template/apps/start/src/server/email.ts +3 -2
- package/template/apps/start/src/server/jobs-forms.ts +17 -0
- package/template/apps/start/src/server/jobs.functions.ts +30 -0
- package/template/apps/start/src/server/jobs.ts +67 -0
- package/template/apps/start/src/server/observability.ts +36 -0
- package/template/apps/start/src/server/payments.ts +12 -8
- package/template/apps/start/src/server/storage.ts +3 -2
- package/template/apps/start/src/server/users.ts +2 -1
- package/template/apps/start/src/server/variants/auth.better-auth.ts +2 -2
- package/template/apps/start/src/server/variants/email.resend.ts +3 -2
- package/template/apps/start/src/server/variants/observability.sentry.ts +42 -0
- package/template/apps/start/src/server/variants/storage.supabase.ts +3 -2
- package/template/apps/start/src/start.ts +40 -1
- package/template/apps/start/vite.config.ts +2 -1
- package/template/apps/web/next-env.d.ts +2 -2
- package/template/apps/web/package.json +6 -1
- package/template/apps/web/src/app/actions/auth.ts +4 -4
- package/template/apps/web/src/app/api/jobs/run/route.ts +23 -0
- package/template/apps/web/src/app/api/webhooks/payments/route.ts +2 -1
- package/template/apps/web/src/app/dashboard/_components/jobs-actions.ts +14 -0
- package/template/apps/web/src/app/dashboard/_components/jobs-card.tsx +84 -0
- package/template/apps/web/src/app/dashboard/page.tsx +7 -0
- package/template/apps/web/src/instrumentation.ts +34 -0
- package/template/apps/web/src/jobs/index.ts +7 -0
- package/template/apps/web/src/jobs/sample.ts +10 -0
- package/template/apps/web/src/server/email.ts +3 -2
- package/template/apps/web/src/server/jobs.ts +67 -0
- package/template/apps/web/src/server/observability.ts +36 -0
- package/template/apps/web/src/server/payments.ts +12 -8
- package/template/apps/web/src/server/storage.ts +3 -2
- package/template/apps/web/src/server/users.ts +2 -1
- package/template/apps/web/src/server/variants/email.resend.ts +3 -2
- package/template/apps/web/src/server/variants/observability.sentry.ts +42 -0
- package/template/apps/web/src/server/variants/storage.supabase.ts +3 -2
- package/template/apps/web/src/variants/proxy.better-auth.ts +2 -1
- package/template/clubedge.template.json +78 -0
- package/template/e2e/jobs.spec.ts +33 -0
- package/template/e2e/observability.spec.ts +28 -0
- package/template/e2e/telemetry-fake.ts +91 -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-better-auth/src/index.ts +2 -2
- 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/core/src/errors.ts +3 -1
- package/template/packages/core/src/index.ts +9 -0
- package/template/packages/core/src/telemetry.test.ts +86 -0
- package/template/packages/core/src/telemetry.ts +79 -0
- 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 +18 -0
- package/template/packages/jobs/src/index.test.ts +69 -0
- package/template/packages/jobs/src/index.ts +205 -0
- package/template/packages/jobs/tsconfig.json +4 -0
- package/template/packages/jobs-pg-boss/package.json +23 -0
- package/template/packages/jobs-pg-boss/src/index.test.ts +154 -0
- package/template/packages/jobs-pg-boss/src/index.ts +213 -0
- package/template/packages/jobs-pg-boss/tsconfig.json +4 -0
- package/template/packages/local-db/package.json +1 -1
- package/template/packages/observability/package.json +21 -0
- package/template/packages/observability/src/index.ts +65 -0
- package/template/packages/observability/tsconfig.json +4 -0
- package/template/packages/observability-otel/package.json +30 -0
- package/template/packages/observability-otel/src/index.test.ts +93 -0
- package/template/packages/observability-otel/src/index.ts +176 -0
- package/template/packages/observability-otel/tsconfig.json +4 -0
- package/template/packages/observability-sentry/package.json +22 -0
- package/template/packages/observability-sentry/src/index.test.ts +70 -0
- package/template/packages/observability-sentry/src/index.ts +69 -0
- package/template/packages/observability-sentry/tsconfig.json +4 -0
- package/template/packages/payments/package.json +1 -1
- package/template/packages/payments-stripe/package.json +1 -1
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage-s3/package.json +1 -1
- package/template/packages/storage-supabase/package.json +1 -1
- package/template/packages/ui/package.json +1 -1
- package/template/pnpm-lock.yaml +878 -56
- package/template.lock.json +47 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `create-clubedge-app` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.14.0
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
- Test an observability project in CI and in the release smoke test, and add the observability module in the change-modules job.
|
|
9
|
+
|
|
10
|
+
## 0.13.0
|
|
11
|
+
|
|
12
|
+
- Default to Starter `v0.12.0`, which adds background jobs (`--jobs pg-boss`): jobs stored in the project's PostgreSQL, run by a worker inside the app server, with retries and schedules, and `/api/jobs/run` for serverless hosts. `--jobs` needs no CLI code: it comes from the Starter's manifest.
|
|
13
|
+
- Fix generated lockfiles that pnpm rejected as broken when pruning an optional peer left a package with no dependencies, such as `pg-pool` once `pg` is pruned for `--jobs none`. Such entries are now written as `{}`, the way pnpm writes them.
|
|
14
|
+
- Test a jobs project in CI and in the release smoke test, and add the jobs module in the change-modules job.
|
|
15
|
+
|
|
5
16
|
## 0.12.0
|
|
6
17
|
|
|
7
18
|
- Default to Starter `v0.11.0`, which adds payments (`--payments stripe`): Stripe Checkout, the customer portal, and verified webhooks behind a provider-neutral payments interface. `--payments` needs no CLI code: it comes from the Starter's manifest.
|
package/README.md
CHANGED
|
@@ -12,27 +12,27 @@ The CLI downloads a tested Starter release, names the generated project, creates
|
|
|
12
12
|
Each CLI release is tested against one specific Starter release:
|
|
13
13
|
|
|
14
14
|
```text
|
|
15
|
-
create-clubedge-app v0.
|
|
15
|
+
create-clubedge-app v0.14.0
|
|
16
16
|
|
|
|
17
17
|
v
|
|
18
|
-
Clubedge Starter v0.
|
|
19
|
-
commit
|
|
18
|
+
Clubedge Starter v0.13.0
|
|
19
|
+
commit 62fb8d1b1e13ed8e8e134bd1e53f2bfdde2c6a50
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
The current relationship is:
|
|
23
23
|
|
|
24
24
|
| CLI release | Default Starter release | Meaning |
|
|
25
25
|
| --- | --- | --- |
|
|
26
|
-
| `create-clubedge-app@0.
|
|
26
|
+
| `create-clubedge-app@0.14.0` | `clubedge-starter@v0.13.0` | The CLI's required, tested default |
|
|
27
27
|
|
|
28
28
|
This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
|
|
29
29
|
|
|
30
|
-
The `v0.
|
|
31
|
-
`
|
|
30
|
+
The `v0.13.0` tag is expected to resolve to commit
|
|
31
|
+
`62fb8d1b1e13ed8e8e134bd1e53f2bfdde2c6a50`. The CLI verifies this before
|
|
32
32
|
scaffolding with the default reference and fails rather than silently using a
|
|
33
33
|
moved tag.
|
|
34
34
|
|
|
35
|
-
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.
|
|
35
|
+
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.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.
|
|
36
36
|
|
|
37
37
|
## Requirements
|
|
38
38
|
|
|
@@ -85,7 +85,7 @@ Either way, the app lives in `apps/web` and uses the same shared packages, so th
|
|
|
85
85
|
|
|
86
86
|
### Choosing services
|
|
87
87
|
|
|
88
|
-
Authentication, file storage, caching, email, payments, local infrastructure, and deployment are modules. Each has a default, and anything you leave out is removed completely: its package, its provider SDK, its environment variables, its documentation, and its lockfile entries. The Starter's `clubedge.template.json` defines the modules, so a module a newer Starter adds gets its `--<module>` flag without a CLI change; `--help` lists the modules of the Starter this CLI release creates.
|
|
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.
|
|
89
89
|
|
|
90
90
|
| Flag | Options | Default |
|
|
91
91
|
| ----------- | ------------------------------------------ | ---------- |
|
|
@@ -94,6 +94,8 @@ Authentication, file storage, caching, email, payments, local infrastructure, an
|
|
|
94
94
|
| `--cache` | `redis` (falls back to memory without `REDIS_URL`), `memory` | `redis` |
|
|
95
95
|
| `--email` | `smtp` (any provider, or a local inbox), `resend`, `none` | `smtp` |
|
|
96
96
|
| `--payments` | `stripe`, `none` (needs an auth module) | `none` |
|
|
97
|
+
| `--jobs` | `pg-boss`, `none` | `none` |
|
|
98
|
+
| `--observability` | `otel` (OpenTelemetry, to any OTLP collector), `sentry`, `none` | `none` |
|
|
97
99
|
| `--deploy` | `docker` (any container host), `fly`, `railway`, `render`, `vercel`, `netlify` | `docker` |
|
|
98
100
|
| `--infra` | `docker` (Docker Compose), `supabase` (Supabase CLI), `local` (no Docker: PGlite and a local folder), `none` | `docker` |
|
|
99
101
|
|
|
@@ -139,7 +141,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git --framework tanstac
|
|
|
139
141
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref main
|
|
140
142
|
|
|
141
143
|
# Reproduce a specific Starter release or commit
|
|
142
|
-
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.
|
|
144
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.13.0
|
|
143
145
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
|
|
144
146
|
|
|
145
147
|
# Test local Starter changes before releasing them
|
|
@@ -156,7 +158,7 @@ pnpm dlx @clubedge/create-clubedge-app add cache redis # or switch: add aut
|
|
|
156
158
|
pnpm dlx @clubedge/create-clubedge-app remove storage
|
|
157
159
|
pnpm dlx @clubedge/create-clubedge-app add infra local --dry-run
|
|
158
160
|
pnpm dlx @clubedge/create-clubedge-app@latest upgrade # to the newest CLI's Starter release
|
|
159
|
-
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.
|
|
161
|
+
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.13.0 # or a specific tag, branch, or commit
|
|
160
162
|
```
|
|
161
163
|
|
|
162
164
|
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.
|
|
@@ -207,7 +209,7 @@ CI packs the CLI with `npm pack`, scaffolds a clean project from the packed tarb
|
|
|
207
209
|
1. Make and validate changes in `clubedge-starter`.
|
|
208
210
|
2. Run lint, typecheck, unit tests, browser tests, and production build.
|
|
209
211
|
3. Update the Starter package versions.
|
|
210
|
-
4. Commit `main`, create a release tag such as `v0.
|
|
212
|
+
4. Commit `main`, create a release tag such as `v0.13.0`, and publish the Starter release.
|
|
211
213
|
|
|
212
214
|
### CLI maintainers
|
|
213
215
|
|
|
@@ -170,5 +170,20 @@ export function pruneOptionalPeers(source) {
|
|
|
170
170
|
}
|
|
171
171
|
output.push(line);
|
|
172
172
|
}
|
|
173
|
-
return output.join("\n");
|
|
173
|
+
return closeEmptySnapshots(output).join("\n");
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* A snapshot whose only dependencies were removed links is left without a body, which pnpm reads
|
|
177
|
+
* as null and rejects as a broken lockfile; pnpm itself writes such entries as "key: {}".
|
|
178
|
+
*/
|
|
179
|
+
function closeEmptySnapshots(lines) {
|
|
180
|
+
let section = "";
|
|
181
|
+
return lines.map((line, index) => {
|
|
182
|
+
if (/^\S/.test(line))
|
|
183
|
+
section = line.replace(/:.*$/, "");
|
|
184
|
+
if (section !== "snapshots" || !/^ {2}\S.*:$/.test(line))
|
|
185
|
+
return line;
|
|
186
|
+
const next = lines.slice(index + 1).find((candidate) => candidate.trim() !== "");
|
|
187
|
+
return next === undefined || !/^ {4}/.test(next) ? `${line} {}` : line;
|
|
188
|
+
});
|
|
174
189
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clubedge/create-clubedge-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Create a Next.js or TanStack Start application from the Clubedge Starter reference implementation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.9.0",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
"clubedge": {
|
|
24
24
|
"starterRepository": "Clubedge/clubedge-starter",
|
|
25
|
-
"starterRef": "v0.
|
|
26
|
-
"starterCommit": "
|
|
25
|
+
"starterRef": "v0.13.0",
|
|
26
|
+
"starterCommit": "62fb8d1b1e13ed8e8e134bd1e53f2bfdde2c6a50"
|
|
27
27
|
},
|
|
28
28
|
"license": "Apache-2.0",
|
|
29
29
|
"repository": {
|
package/template/.env.example
CHANGED
|
@@ -60,6 +60,30 @@ STRIPE_WEBHOOK_SECRET=
|
|
|
60
60
|
STRIPE_PRICE_PRO=
|
|
61
61
|
# clubedge:end
|
|
62
62
|
|
|
63
|
+
# clubedge:if jobs=pg-boss
|
|
64
|
+
# Background jobs are stored in DATABASE_URL and run by a worker inside the app server. Set
|
|
65
|
+
# JOBS_WORKER=off where no server keeps running (it is off by default on Vercel and Netlify);
|
|
66
|
+
# a scheduler then calls /api/jobs/run with CRON_SECRET (16 or more characters) instead.
|
|
67
|
+
JOBS_WORKER=
|
|
68
|
+
CRON_SECRET=
|
|
69
|
+
# clubedge:end
|
|
70
|
+
|
|
71
|
+
# clubedge:if observability=otel
|
|
72
|
+
# OpenTelemetry: where traces and error logs go over OTLP/HTTP, such as a collector, Grafana
|
|
73
|
+
# Cloud, or Honeycomb. Empty sends nothing. Headers are key=value pairs separated by commas.
|
|
74
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=
|
|
75
|
+
OTEL_EXPORTER_OTLP_HEADERS=
|
|
76
|
+
OTEL_SERVICE_NAME=
|
|
77
|
+
# clubedge:end
|
|
78
|
+
|
|
79
|
+
# clubedge:if observability=sentry
|
|
80
|
+
# Sentry (https://sentry.io): the DSN from the project's Client Keys settings. Empty sends nothing.
|
|
81
|
+
# The traces sample rate is the share of requests traced, from 0 to 1 (default 0.1).
|
|
82
|
+
SENTRY_DSN=
|
|
83
|
+
SENTRY_ENVIRONMENT=
|
|
84
|
+
SENTRY_TRACES_SAMPLE_RATE=
|
|
85
|
+
# clubedge:end
|
|
86
|
+
|
|
63
87
|
# clubedge:if storage=s3
|
|
64
88
|
# S3-compatible storage (AWS S3, Cloudflare R2, MinIO)
|
|
65
89
|
STORAGE_BUCKET= # clubedge:only infra!=docker
|
package/template/README.md
CHANGED
|
@@ -28,6 +28,9 @@ This repository contains the same application for two frameworks, Next.js (`apps
|
|
|
28
28
|
- Email over SMTP, for any provider or a local inbox; development prints emails to the server log until SMTP is configured. <!-- clubedge:only email=smtp -->
|
|
29
29
|
- Email through Resend; development prints emails to the server log until the API key is set. <!-- clubedge:only email=resend -->
|
|
30
30
|
- Subscriptions with Stripe Checkout and the customer portal, kept in sync by verified webhooks, behind a provider-neutral payments interface. <!-- clubedge:only payments=stripe -->
|
|
31
|
+
- 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
|
+
- Server error reporting and request tracing with OpenTelemetry, to any OTLP collector, behind a provider-neutral observability interface. <!-- clubedge:only observability=otel -->
|
|
33
|
+
- Server error reporting and request tracing with Sentry, behind a provider-neutral observability interface. <!-- clubedge:only observability=sentry -->
|
|
31
34
|
- Zod environment validation, security headers, structured errors, and `/api/health`.
|
|
32
35
|
- Docker support, GitHub Actions CI, Vitest, Playwright, ESLint, and Prettier.
|
|
33
36
|
- A production image that includes its own migrator (`node /app/migrate.mjs`), for running migrations before each release.
|
|
@@ -224,6 +227,11 @@ The TanStack Start app is `apps/start`, run with `pnpm --filter @clubedge/start
|
|
|
224
227
|
- `packages/email-resend/`: Resend email provider <!-- clubedge:only email=resend -->
|
|
225
228
|
- `packages/payments/`: `PaymentProvider` interface, billing logic, and the subscription tables <!-- clubedge:only payments!=none -->
|
|
226
229
|
- `packages/payments-stripe/`: Stripe payment provider <!-- clubedge:only payments=stripe -->
|
|
230
|
+
- `packages/jobs/`: `JobQueue` interface, job definitions, and an in-memory queue for tests <!-- clubedge:only jobs!=none -->
|
|
231
|
+
- `packages/jobs-pg-boss/`: pg-boss job queue <!-- clubedge:only jobs=pg-boss -->
|
|
232
|
+
- `packages/observability/`: `ObservabilityProvider` interface and an in-memory provider for tests <!-- clubedge:only observability!=none -->
|
|
233
|
+
- `packages/observability-otel/`: OpenTelemetry provider (OTLP/HTTP traces and error logs) <!-- clubedge:only observability=otel -->
|
|
234
|
+
- `packages/observability-sentry/`: Sentry provider <!-- clubedge:only observability=sentry -->
|
|
227
235
|
- `packages/ui/`: shared shadcn/ui components, utilities, and global theme styles
|
|
228
236
|
- `.github/`: CI workflow, issue forms, and pull request template
|
|
229
237
|
- `SETUP.md`, `CONTRIBUTING.md`, `SECURITY.md`: setup, contribution, and security guides
|
package/template/SETUP.md
CHANGED
|
@@ -46,6 +46,9 @@ The variables are:
|
|
|
46
46
|
- `SMTP_URL` and `EMAIL_FROM` (for sending email): see [Email](#email). <!-- clubedge:only email=smtp -->
|
|
47
47
|
- `RESEND_API_KEY` and `EMAIL_FROM` (for sending email): see [Email](#email). <!-- clubedge:only email=resend -->
|
|
48
48
|
- `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, and `STRIPE_PRICE_<PLAN>` (for payments): see [Payments](#payments). <!-- clubedge:only payments=stripe -->
|
|
49
|
+
- `JOBS_WORKER` and `CRON_SECRET` (optional): see [Background jobs](#background-jobs). <!-- clubedge:only jobs=pg-boss -->
|
|
50
|
+
- `OTEL_EXPORTER_OTLP_ENDPOINT` (to send traces and errors): see [Observability](#observability). <!-- clubedge:only observability=otel -->
|
|
51
|
+
- `SENTRY_DSN` (to send errors and traces): see [Observability](#observability). <!-- clubedge:only observability=sentry -->
|
|
49
52
|
- `STORAGE_*` (for file storage): see [File storage](#file-storage). <!-- clubedge:only storage=s3 -->
|
|
50
53
|
- `SUPABASE_STORAGE_BUCKET` (for file storage): see [File storage](#file-storage). <!-- clubedge:only storage=supabase -->
|
|
51
54
|
|
|
@@ -236,6 +239,65 @@ Turn on the customer portal in the dashboard's billing settings, so "Manage bill
|
|
|
236
239
|
|
|
237
240
|
Payments go through the provider-neutral `PaymentProvider` interface in `packages/payments`. To use another provider, implement that interface in a new package and return it from `apps/web/src/server/payments.ts`; billing logic, the tables, the routes, and the billing card stay the same.
|
|
238
241
|
|
|
242
|
+
<!-- clubedge:end -->
|
|
243
|
+
<!-- clubedge:if jobs=pg-boss -->
|
|
244
|
+
|
|
245
|
+
## Background jobs
|
|
246
|
+
|
|
247
|
+
Jobs run outside requests, with retries, in a worker that starts inside the app server: `pnpm dev` and the production server both run it, so there is no separate process to deploy. [pg-boss](https://github.com/timgit/pg-boss) stores them in your PostgreSQL, and creates and upgrades its own tables in the `pgboss` schema when the app starts.
|
|
248
|
+
|
|
249
|
+
Define a job in `apps/web/src/jobs` and add it to the list in `apps/web/src/jobs/index.ts`:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
import { defineJob } from "@clubedge/jobs";
|
|
253
|
+
|
|
254
|
+
export const welcomeEmail = defineJob<{ userId: string }>({
|
|
255
|
+
name: "welcome-email",
|
|
256
|
+
retryLimit: 3,
|
|
257
|
+
async run({ userId }) {
|
|
258
|
+
// Look the user up and send the email.
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Queue it from server code with `getJobs().enqueue(welcomeEmail, { userId })`, from `@/server/jobs`. A thrown error fails the attempt and the job is retried (`retryLimit`, `retryDelaySeconds`, `retryBackoff`). An attempt that runs past `timeoutSeconds`, or whose server stops, is retried too, so make jobs safe to run twice. Add `schedule: "0 3 * * *"` (cron syntax) to run a job on a schedule. Restart `pnpm dev` after adding or changing a job. The dashboard's jobs card queues the sample job from `apps/web/src/jobs/sample.ts` and lists its recent runs; remove both when you add your own.
|
|
264
|
+
|
|
265
|
+
Where no server keeps running, as on Vercel and Netlify, there is no worker: `JOBS_WORKER` defaults to `off` there, and you can set it yourself anywhere. Set `CRON_SECRET` to 16 or more random characters and call `GET /api/jobs/run` every minute or so with the header `Authorization: Bearer <CRON_SECRET>`, from Vercel Cron, a Netlify scheduled function, or any scheduler. Each call runs the jobs that are due. Scheduled jobs need a worker.
|
|
266
|
+
|
|
267
|
+
Jobs go through the provider-neutral `JobQueue` interface in `packages/jobs`. To use another queue, implement that interface in a new package and return it from `apps/web/src/server/jobs.ts`; job definitions, the routes, and the jobs card stay the same.
|
|
268
|
+
|
|
269
|
+
<!-- clubedge:end -->
|
|
270
|
+
<!-- clubedge:if observability!=none -->
|
|
271
|
+
|
|
272
|
+
## Observability
|
|
273
|
+
|
|
274
|
+
Server errors and traces go to the tool you chose. Errors that escape a page, route, server action, or server function are sent with the request's method and path, and so are errors the app handles and reports with `reportError` from `@clubedge/core`, such as a failed webhook, email, or background job. Every request is traced, and so are calls through the email, storage, and payments composition roots, as spans such as `email.send` and `storage.upload`. Wrap your own work with `traced("name", () => work())` from `@clubedge/core`. Until the tool is configured nothing is sent, and errors keep going to the server log.
|
|
275
|
+
|
|
276
|
+
<!-- clubedge:end -->
|
|
277
|
+
<!-- clubedge:if observability=otel -->
|
|
278
|
+
|
|
279
|
+
Set `OTEL_EXPORTER_OTLP_ENDPOINT` to an OpenTelemetry collector or to a vendor's OTLP/HTTP endpoint (Grafana Cloud, Honeycomb, Datadog, and others accept it), with any API key in `OTEL_EXPORTER_OTLP_HEADERS` as `key=value` pairs. Traces go to `/v1/traces` and errors to `/v1/logs` as error log records, and each error is also recorded on the span it happened in. `OTEL_SERVICE_NAME` defaults to the project's service id. The spans Next.js and pg-boss create join the same traces. <!-- clubedge:only framework=next -->
|
|
280
|
+
Set `OTEL_EXPORTER_OTLP_ENDPOINT` to an OpenTelemetry collector or to a vendor's OTLP/HTTP endpoint (Grafana Cloud, Honeycomb, Datadog, and others accept it), with any API key in `OTEL_EXPORTER_OTLP_HEADERS` as `key=value` pairs. Traces go to `/v1/traces` and errors to `/v1/logs` as error log records, and each error is also recorded on the span it happened in. `OTEL_SERVICE_NAME` defaults to the project's service id. <!-- clubedge:only framework=tanstack-start -->
|
|
281
|
+
|
|
282
|
+
To see traces during development, run [Jaeger](https://www.jaegertracing.io), set `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318`, and open http://localhost:16686:
|
|
283
|
+
|
|
284
|
+
```sh
|
|
285
|
+
docker run --rm -p 16686:16686 -p 4318:4318 jaegertracing/jaeger:latest
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
<!-- clubedge:end -->
|
|
289
|
+
<!-- clubedge:if observability=sentry -->
|
|
290
|
+
|
|
291
|
+
Create a Node.js project in [Sentry](https://sentry.io) and set `SENTRY_DSN` to its DSN. `SENTRY_TRACES_SAMPLE_RATE` is the share of requests traced, from 0 to 1 (default 0.1; use 1 while trying it out), and `SENTRY_ENVIRONMENT` labels events (default `NODE_ENV`). The SDK sends no user details, cookies, query strings, or request bodies. The spans Next.js and pg-boss create join the same traces. <!-- clubedge:only framework=next -->
|
|
292
|
+
Create a Node.js project in [Sentry](https://sentry.io) and set `SENTRY_DSN` to its DSN. `SENTRY_TRACES_SAMPLE_RATE` is the share of requests traced, from 0 to 1 (default 0.1; use 1 while trying it out), and `SENTRY_ENVIRONMENT` labels events (default `NODE_ENV`). The SDK sends no user details, cookies, query strings, or request bodies. <!-- clubedge:only framework=tanstack-start -->
|
|
293
|
+
|
|
294
|
+
<!-- clubedge:end -->
|
|
295
|
+
<!-- clubedge:if observability!=none -->
|
|
296
|
+
|
|
297
|
+
Only server-side errors are reported; errors in the browser are not. On Vercel and Netlify, a function can stop right after it responds, so spans still waiting in a batch at that moment can be lost.
|
|
298
|
+
|
|
299
|
+
Observability goes through the provider-neutral `ObservabilityProvider` interface in `packages/observability` and the telemetry hooks in `@clubedge/core`. To use another tool, implement that interface in a new package and install it from `apps/web/src/server/observability.ts`; error reports and spans across the app stay the same.
|
|
300
|
+
|
|
239
301
|
<!-- clubedge:end -->
|
|
240
302
|
<!-- clubedge:if storage!=none -->
|
|
241
303
|
|
|
@@ -338,6 +400,8 @@ Render runs `node /app/migrate.mjs` as the pre-deploy command before each deploy
|
|
|
338
400
|
|
|
339
401
|
Vercel has no step that runs before a release, so the build command applies migrations (`pnpm db:migrate`) against the `DATABASE_URL` of the deployment's environment. Give Preview deployments their own database, or remove the migration step from `buildCommand` and run migrations yourself. Serverless functions open a connection per instance: use your provider's pooled connection URL (for example Supabase's transaction pooler or Neon's pooled endpoint).
|
|
340
402
|
|
|
403
|
+
Vercel runs no worker for [background jobs](#background-jobs). Set `CRON_SECRET` and add `"crons": [{ "path": "/api/jobs/run", "schedule": "* * * * *" }]` to `vercel.json`; Vercel Cron sends the secret itself. Cron jobs more frequent than daily need a paid Vercel plan; otherwise call the endpoint from another scheduler. <!-- clubedge:only jobs=pg-boss -->
|
|
404
|
+
|
|
341
405
|
<!-- clubedge:end -->
|
|
342
406
|
<!-- clubedge:if deploy=netlify -->
|
|
343
407
|
|
|
@@ -346,6 +410,8 @@ Vercel has no step that runs before a release, so the build command applies migr
|
|
|
346
410
|
|
|
347
411
|
Netlify has no step that runs before a release, so the build command applies migrations (`pnpm db:migrate`) against the `DATABASE_URL` of the deploy context. Give deploy previews their own database, or remove the migration step from the build command and run migrations yourself. Serverless functions open a connection per instance: use your provider's pooled connection URL (for example Supabase's transaction pooler or Neon's pooled endpoint).
|
|
348
412
|
|
|
413
|
+
Netlify runs no worker for [background jobs](#background-jobs). Set `JOBS_WORKER=off` and `CRON_SECRET`, then call `/api/jobs/run` every minute from a [scheduled function](https://docs.netlify.com/build/functions/scheduled-functions/) or another scheduler, with the header `Authorization: Bearer <CRON_SECRET>`. <!-- clubedge:only jobs=pg-boss -->
|
|
414
|
+
|
|
349
415
|
<!-- clubedge:end -->
|
|
350
416
|
|
|
351
417
|
`.github/workflows/deploy.yml` deploys on demand: in GitHub, open Actions, then Deploy, then Run workflow.
|
|
@@ -371,6 +437,9 @@ Production needs these variables:
|
|
|
371
437
|
- `RESEND_API_KEY` and `EMAIL_FROM`; without them, production refuses to send email. <!-- clubedge:only email=resend -->
|
|
372
438
|
- `STRIPE_SECRET_KEY` (live mode), `STRIPE_WEBHOOK_SECRET` from the production webhook endpoint, and `STRIPE_PRICE_<PLAN>` for each plan. <!-- clubedge:only payments=stripe -->
|
|
373
439
|
- `REDIS_URL`, when you run more than one instance. <!-- clubedge:only cache=redis -->
|
|
440
|
+
- `CRON_SECRET`, where no worker runs and a scheduler calls `/api/jobs/run`. <!-- clubedge:only jobs=pg-boss -->
|
|
441
|
+
- `OTEL_EXPORTER_OTLP_ENDPOINT`, and `OTEL_EXPORTER_OTLP_HEADERS` if your collector needs a key. <!-- clubedge:only observability=otel -->
|
|
442
|
+
- `SENTRY_DSN`, and `SENTRY_ENVIRONMENT` to tell environments apart. <!-- clubedge:only observability=sentry -->
|
|
374
443
|
- The `STORAGE_*` variables for your S3-compatible bucket. <!-- clubedge:only storage=s3 -->
|
|
375
444
|
- `SUPABASE_STORAGE_BUCKET`. <!-- clubedge:only storage=supabase -->
|
|
376
445
|
|
|
@@ -62,6 +62,30 @@ STRIPE_WEBHOOK_SECRET=
|
|
|
62
62
|
STRIPE_PRICE_PRO=
|
|
63
63
|
# clubedge:end
|
|
64
64
|
|
|
65
|
+
# clubedge:if jobs=pg-boss
|
|
66
|
+
# Background jobs are stored in DATABASE_URL and run by a worker inside the app server. Set
|
|
67
|
+
# JOBS_WORKER=off where no server keeps running (it is off by default on Vercel and Netlify);
|
|
68
|
+
# a scheduler then calls /api/jobs/run with CRON_SECRET (16 or more characters) instead.
|
|
69
|
+
JOBS_WORKER=
|
|
70
|
+
CRON_SECRET=
|
|
71
|
+
# clubedge:end
|
|
72
|
+
|
|
73
|
+
# clubedge:if observability=otel
|
|
74
|
+
# OpenTelemetry: where traces and error logs go over OTLP/HTTP, such as a collector, Grafana
|
|
75
|
+
# Cloud, or Honeycomb. Empty sends nothing. Headers are key=value pairs separated by commas.
|
|
76
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=
|
|
77
|
+
OTEL_EXPORTER_OTLP_HEADERS=
|
|
78
|
+
OTEL_SERVICE_NAME=
|
|
79
|
+
# clubedge:end
|
|
80
|
+
|
|
81
|
+
# clubedge:if observability=sentry
|
|
82
|
+
# Sentry (https://sentry.io): the DSN from the project's Client Keys settings. Empty sends nothing.
|
|
83
|
+
# The traces sample rate is the share of requests traced, from 0 to 1 (default 0.1).
|
|
84
|
+
SENTRY_DSN=
|
|
85
|
+
SENTRY_ENVIRONMENT=
|
|
86
|
+
SENTRY_TRACES_SAMPLE_RATE=
|
|
87
|
+
# clubedge:end
|
|
88
|
+
|
|
65
89
|
# clubedge:if storage=s3
|
|
66
90
|
# S3-compatible storage (AWS S3, Cloudflare R2, MinIO)
|
|
67
91
|
STORAGE_BUCKET= # clubedge:only infra!=docker
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clubedge/start",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
"@clubedge/email": "workspace:*",
|
|
23
23
|
"@clubedge/email-resend": "workspace:*",
|
|
24
24
|
"@clubedge/email-smtp": "workspace:*",
|
|
25
|
+
"@clubedge/jobs": "workspace:*",
|
|
26
|
+
"@clubedge/jobs-pg-boss": "workspace:*",
|
|
27
|
+
"@clubedge/observability": "workspace:*",
|
|
28
|
+
"@clubedge/observability-otel": "workspace:*",
|
|
29
|
+
"@clubedge/observability-sentry": "workspace:*",
|
|
25
30
|
"@clubedge/payments": "workspace:*",
|
|
26
31
|
"@clubedge/payments-stripe": "workspace:*",
|
|
27
32
|
"@clubedge/storage": "workspace:*",
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ListChecks } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import type { JobsStatus } from "@/server/jobs.functions";
|
|
4
|
+
import { Badge } from "@clubedge/ui/components/badge";
|
|
5
|
+
import { Button } from "@clubedge/ui/components/button";
|
|
6
|
+
import {
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardDescription,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle,
|
|
12
|
+
} from "@clubedge/ui/components/card";
|
|
13
|
+
|
|
14
|
+
const timeFormat = new Intl.DateTimeFormat("en", { timeStyle: "medium", timeZone: "UTC" });
|
|
15
|
+
|
|
16
|
+
/** Starts the sample job and lists the current user's recent runs. The form posts to /jobs/sample. */
|
|
17
|
+
export function JobsCard({ status }: { status: JobsStatus }) {
|
|
18
|
+
const { jobs } = status;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Card className="gap-0 py-0" id="jobs">
|
|
22
|
+
<CardHeader className="flex-row items-start justify-between gap-3 border-b px-5 py-5 sm:px-6">
|
|
23
|
+
<div className="grid gap-1">
|
|
24
|
+
<CardTitle className="text-base">Background jobs</CardTitle>
|
|
25
|
+
<CardDescription>
|
|
26
|
+
{status.worker
|
|
27
|
+
? "Work runs outside requests in this server's worker, with retries."
|
|
28
|
+
: "This server runs no worker: jobs run when /api/jobs/run is called."}
|
|
29
|
+
</CardDescription>
|
|
30
|
+
</div>
|
|
31
|
+
<span className="grid size-9 place-items-center rounded-lg bg-primary/10 text-primary">
|
|
32
|
+
<ListChecks aria-hidden="true" className="size-4" />
|
|
33
|
+
</span>
|
|
34
|
+
</CardHeader>
|
|
35
|
+
<CardContent className="grid gap-4 px-5 py-5 sm:px-6">
|
|
36
|
+
{jobs === null ? (
|
|
37
|
+
<p className="text-sm text-muted-foreground">
|
|
38
|
+
Jobs are unavailable: the database could not be reached.
|
|
39
|
+
</p>
|
|
40
|
+
) : (
|
|
41
|
+
<>
|
|
42
|
+
<form action="/jobs/sample" method="post">
|
|
43
|
+
<Button type="submit" variant="outline">
|
|
44
|
+
Run a sample job
|
|
45
|
+
</Button>
|
|
46
|
+
</form>
|
|
47
|
+
{jobs.length === 0 ? (
|
|
48
|
+
<p className="text-sm text-muted-foreground">No jobs yet.</p>
|
|
49
|
+
) : (
|
|
50
|
+
<ul aria-label="Recent jobs" className="grid gap-2">
|
|
51
|
+
{jobs.map((job) => (
|
|
52
|
+
<li className="flex items-center justify-between gap-3 text-sm" key={job.id}>
|
|
53
|
+
<span>
|
|
54
|
+
{job.name}{" "}
|
|
55
|
+
<span className="text-muted-foreground">
|
|
56
|
+
{timeFormat.format(new Date(job.createdAt))} UTC
|
|
57
|
+
</span>
|
|
58
|
+
</span>
|
|
59
|
+
<Badge variant={job.state === "completed" ? "default" : "secondary"}>
|
|
60
|
+
{job.state}
|
|
61
|
+
</Badge>
|
|
62
|
+
</li>
|
|
63
|
+
))}
|
|
64
|
+
</ul>
|
|
65
|
+
)}
|
|
66
|
+
</>
|
|
67
|
+
)}
|
|
68
|
+
</CardContent>
|
|
69
|
+
</Card>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineJob } from "@clubedge/jobs";
|
|
2
|
+
|
|
3
|
+
/** An example job that the dashboard can start; replace it with your own. */
|
|
4
|
+
export const sampleJob = defineJob<{ requestedBy: string }>({
|
|
5
|
+
name: "sample",
|
|
6
|
+
async run({ requestedBy }, { attempt }) {
|
|
7
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
8
|
+
return { message: `Hello, ${requestedBy}, from the jobs worker.`, attempt };
|
|
9
|
+
},
|
|
10
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { definePlugin } from "nitro";
|
|
2
|
+
import { getJobs, startJobsWorker } from "@/server/jobs";
|
|
3
|
+
|
|
4
|
+
/** Starts the background jobs worker with the server, and stops it with the server. */
|
|
5
|
+
export default definePlugin((nitroApp) => {
|
|
6
|
+
startJobsWorker();
|
|
7
|
+
nitroApp.hooks.hook("close", () => getJobs().stop());
|
|
8
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { definePlugin } from "nitro";
|
|
2
|
+
import { startObservability } from "@/server/observability";
|
|
3
|
+
|
|
4
|
+
/** Starts error reporting and tracing with the server, and sends what is buffered when it stops. */
|
|
5
|
+
export default definePlugin((nitroApp) => {
|
|
6
|
+
const provider = startObservability();
|
|
7
|
+
nitroApp.hooks.hook("close", async () => {
|
|
8
|
+
await provider?.flush();
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
3
|
+
import { getCronSecret, getJobs } from "@/server/jobs";
|
|
4
|
+
|
|
5
|
+
function isAuthorized(request: Request, secret: string): boolean {
|
|
6
|
+
const given = Buffer.from(request.headers.get("authorization") ?? "");
|
|
7
|
+
const expected = Buffer.from(`Bearer ${secret}`);
|
|
8
|
+
return given.length === expected.length && timingSafeEqual(given, expected);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Runs the jobs that are due, for hosts without a long-running worker: call it on a schedule
|
|
13
|
+
* with `Authorization: Bearer $CRON_SECRET` (Vercel Cron sends that header itself).
|
|
14
|
+
*/
|
|
15
|
+
export const Route = createFileRoute("/api/jobs/run")({
|
|
16
|
+
server: {
|
|
17
|
+
handlers: {
|
|
18
|
+
GET: async ({ request }) => {
|
|
19
|
+
const secret = getCronSecret();
|
|
20
|
+
if (!secret) return Response.json({ error: "Not found" }, { status: 404 });
|
|
21
|
+
if (!isAuthorized(request, secret))
|
|
22
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
23
|
+
const result = await getJobs().runDue();
|
|
24
|
+
return Response.json(result, { headers: { "Cache-Control": "no-store" } });
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
2
|
import { getBilling, isPaymentsConfigured, WebhookVerificationError } from "@/server/payments";
|
|
3
|
+
import { reportError } from "@clubedge/core";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* The payment provider's webhook. The body is read as raw text, because the signature covers
|
|
@@ -22,7 +23,7 @@ export const Route = createFileRoute("/api/webhooks/payments")({
|
|
|
22
23
|
if (error instanceof WebhookVerificationError) {
|
|
23
24
|
return Response.json({ error: "Invalid signature." }, { status: 400 });
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
+
reportError(error, { message: "Payment webhook failed" });
|
|
26
27
|
return Response.json({ error: "Webhook failed." }, { status: 500 });
|
|
27
28
|
}
|
|
28
29
|
},
|
|
@@ -51,6 +51,10 @@ import { getPageUser } from "@/server/auth.functions";
|
|
|
51
51
|
import { BillingCard } from "@/components/billing/billing-card";
|
|
52
52
|
import { getBillingStatus } from "@/server/billing.functions";
|
|
53
53
|
// clubedge:end
|
|
54
|
+
// clubedge:if jobs!=none
|
|
55
|
+
import { JobsCard } from "@/components/jobs/jobs-card";
|
|
56
|
+
import { getJobsStatus } from "@/server/jobs.functions";
|
|
57
|
+
// clubedge:end
|
|
54
58
|
import { AccountButton } from "@/components/dashboard/account-button";
|
|
55
59
|
import { AppSidebar } from "@/components/dashboard/app-sidebar";
|
|
56
60
|
import { ThemeToggle } from "@/components/theme/theme-toggle";
|
|
@@ -116,6 +120,9 @@ export const Route = createFileRoute("/dashboard")({
|
|
|
116
120
|
// clubedge:if payments!=none
|
|
117
121
|
billing: await getBillingStatus(),
|
|
118
122
|
// clubedge:end
|
|
123
|
+
// clubedge:if jobs!=none
|
|
124
|
+
jobs: await getJobsStatus(),
|
|
125
|
+
// clubedge:end
|
|
119
126
|
}),
|
|
120
127
|
component: DashboardPage,
|
|
121
128
|
});
|
|
@@ -257,6 +264,10 @@ function DashboardPage() {
|
|
|
257
264
|
<BillingCard billing={data.billing} />
|
|
258
265
|
{/* clubedge:end */}
|
|
259
266
|
|
|
267
|
+
{/* clubedge:if jobs!=none */}
|
|
268
|
+
<JobsCard status={data.jobs} />
|
|
269
|
+
{/* clubedge:end */}
|
|
270
|
+
|
|
260
271
|
<section className="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]" id="modules">
|
|
261
272
|
<Card className="gap-0 py-0">
|
|
262
273
|
<CardHeader className="flex-row items-start justify-between gap-3 border-b px-5 py-5 sm:px-6">
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { handleSampleJob } from "@/server/jobs-forms";
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute("/jobs/sample")({
|
|
5
|
+
server: {
|
|
6
|
+
handlers: {
|
|
7
|
+
GET: () => new Response(null, { status: 303, headers: { Location: "/dashboard" } }),
|
|
8
|
+
POST: () => handleSampleJob(),
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "@tanstack/react-start/server-only";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { getClientIp, safeRedirectPath } from "@clubedge/core";
|
|
3
|
+
import { getClientIp, safeRedirectPath, reportError } from "@clubedge/core";
|
|
4
4
|
import { getAppUrl } from "@/env/server";
|
|
5
5
|
import { loginUrl } from "@/lib/login-url";
|
|
6
6
|
import { getAuth, isAuthConfigured } from "./auth";
|
|
@@ -49,7 +49,7 @@ async function isRateLimited(request: Request, action: "sign-in" | "sign-up" | "
|
|
|
49
49
|
return !result.success;
|
|
50
50
|
} catch (error) {
|
|
51
51
|
// Fail open: an unavailable limiter backend should not lock every user out.
|
|
52
|
-
|
|
52
|
+
reportError(error, { message: "Rate limiter unavailable" });
|
|
53
53
|
return false;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -142,7 +142,7 @@ export async function handleResetPassword(request: Request): Promise<Response> {
|
|
|
142
142
|
// A Supabase reset link signed the user in; sign out so the new password is used next.
|
|
143
143
|
await auth.signOut();
|
|
144
144
|
} catch (error) {
|
|
145
|
-
|
|
145
|
+
reportError(error, { message: "Sign-out after password reset failed" });
|
|
146
146
|
}
|
|
147
147
|
return seeOther(loginUrl({ notice: "password-updated" }));
|
|
148
148
|
}
|
|
@@ -152,7 +152,7 @@ export async function handleSignOut(): Promise<Response> {
|
|
|
152
152
|
try {
|
|
153
153
|
await getAuth().signOut();
|
|
154
154
|
} catch (error) {
|
|
155
|
-
|
|
155
|
+
reportError(error, { message: "Sign-out failed" });
|
|
156
156
|
}
|
|
157
157
|
return seeOther("/login");
|
|
158
158
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "@tanstack/react-start/server-only";
|
|
2
2
|
import type { AuthProvider, AuthUser } from "@clubedge/auth";
|
|
3
3
|
import { createSupabaseAuth, createSupabaseServerClient } from "@clubedge/auth-supabase";
|
|
4
|
-
import { AppError } from "@clubedge/core";
|
|
4
|
+
import { AppError, reportError } from "@clubedge/core";
|
|
5
5
|
import { getSupabaseAuthEnv } from "@/env/auth";
|
|
6
6
|
import { requestCookieStore } from "./cookies";
|
|
7
7
|
|
|
@@ -34,7 +34,7 @@ export async function refreshAuthSession(): Promise<void> {
|
|
|
34
34
|
await getAuth().refreshSession();
|
|
35
35
|
} catch (error) {
|
|
36
36
|
// Pages still render; protected handlers re-check the user and reject when needed.
|
|
37
|
-
|
|
37
|
+
reportError(error, { message: "Session refresh failed" });
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { createConsoleEmail, type EmailProvider } from "@clubedge/email";
|
|
4
4
|
import { createSmtpEmail } from "@clubedge/email-smtp";
|
|
5
5
|
import { optional, parseEnv } from "@/env/server";
|
|
6
|
+
import { withSpans } from "@clubedge/core";
|
|
6
7
|
|
|
7
8
|
export type { EmailMessage, EmailProvider } from "@clubedge/email";
|
|
8
9
|
|
|
@@ -34,6 +35,6 @@ function resolveProvider(): EmailProvider {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/** The configured email provider, resolved on first use so imports stay side-effect free. */
|
|
37
|
-
export const email: EmailProvider = {
|
|
38
|
+
export const email: EmailProvider = withSpans("email", {
|
|
38
39
|
send: (message) => resolveProvider().send(message),
|
|
39
|
-
};
|
|
40
|
+
});
|