@clubedge/create-clubedge-app 0.39.0 → 0.40.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.
Files changed (136) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +12 -11
  3. package/dist/core/customize.js +6 -2
  4. package/package.json +3 -3
  5. package/template/README.md +1 -2
  6. package/template/SETUP.md +46 -11
  7. package/template/apps/api/package.json +1 -1
  8. package/template/apps/api-fastapi/.env.example +100 -0
  9. package/template/apps/api-fastapi/Dockerfile +15 -4
  10. package/template/apps/api-fastapi/clubedge.stack.json +125 -1
  11. package/template/apps/api-fastapi/package.json +2 -2
  12. package/template/apps/api-fastapi/pyproject.toml +17 -1
  13. package/template/apps/api-fastapi/src/clubedge_api/account.py +37 -0
  14. package/template/apps/api-fastapi/src/clubedge_api/app.py +113 -18
  15. package/template/apps/api-fastapi/src/clubedge_api/auth.py +18 -1
  16. package/template/apps/api-fastapi/src/clubedge_api/background_jobs.py +115 -0
  17. package/template/apps/api-fastapi/src/clubedge_api/cache.py +178 -0
  18. package/template/apps/api-fastapi/src/clubedge_api/check_env.py +39 -7
  19. package/template/apps/api-fastapi/src/clubedge_api/check_services.py +251 -12
  20. package/template/apps/api-fastapi/src/clubedge_api/contract.py +27 -0
  21. package/template/apps/api-fastapi/src/clubedge_api/contract_check.py +57 -0
  22. package/template/apps/api-fastapi/src/clubedge_api/db.py +32 -0
  23. package/template/apps/api-fastapi/src/clubedge_api/email.py +34 -0
  24. package/template/apps/api-fastapi/src/clubedge_api/email_provider.py +89 -0
  25. package/template/apps/api-fastapi/src/clubedge_api/email_resend.py +55 -0
  26. package/template/apps/api-fastapi/src/clubedge_api/email_smtp.py +83 -0
  27. package/template/apps/api-fastapi/src/clubedge_api/http.py +91 -0
  28. package/template/apps/api-fastapi/src/clubedge_api/job_queue.py +314 -0
  29. package/template/apps/api-fastapi/src/clubedge_api/jobs.py +41 -0
  30. package/template/apps/api-fastapi/src/clubedge_api/jobs_bullmq.py +305 -0
  31. package/template/apps/api-fastapi/src/clubedge_api/jobs_inngest.py +365 -0
  32. package/template/apps/api-fastapi/src/clubedge_api/main.py +25 -0
  33. package/template/apps/api-fastapi/src/clubedge_api/memory_cache.py +54 -0
  34. package/template/apps/api-fastapi/src/clubedge_api/observability.py +50 -0
  35. package/template/apps/api-fastapi/src/clubedge_api/payments.py +333 -0
  36. package/template/apps/api-fastapi/src/clubedge_api/payments_stripe.py +225 -0
  37. package/template/apps/api-fastapi/src/clubedge_api/plans.py +24 -0
  38. package/template/apps/api-fastapi/src/clubedge_api/production_env.py +140 -0
  39. package/template/apps/api-fastapi/src/clubedge_api/rate_limit.py +131 -0
  40. package/template/apps/api-fastapi/src/clubedge_api/routes/__init__.py +1 -0
  41. package/template/apps/api-fastapi/src/clubedge_api/routes/account.py +176 -0
  42. package/template/apps/api-fastapi/src/clubedge_api/routes/billing.py +173 -0
  43. package/template/apps/api-fastapi/src/clubedge_api/routes/client_errors.py +111 -0
  44. package/template/apps/api-fastapi/src/clubedge_api/routes/inngest.py +22 -0
  45. package/template/apps/api-fastapi/src/clubedge_api/routes/jobs.py +112 -0
  46. package/template/apps/api-fastapi/src/clubedge_api/routes/jobs_run.py +32 -0
  47. package/template/apps/api-fastapi/src/clubedge_api/server.py +22 -1
  48. package/template/apps/api-fastapi/src/clubedge_api/settings.py +6 -0
  49. package/template/apps/api-fastapi/src/clubedge_api/storage.py +47 -0
  50. package/template/apps/api-fastapi/src/clubedge_api/storage_local.py +102 -0
  51. package/template/apps/api-fastapi/src/clubedge_api/storage_provider.py +29 -0
  52. package/template/apps/api-fastapi/src/clubedge_api/storage_s3.py +144 -0
  53. package/template/apps/api-fastapi/src/clubedge_api/storage_supabase.py +126 -0
  54. package/template/apps/api-fastapi/src/clubedge_api/telemetry.py +85 -0
  55. package/template/apps/api-fastapi/src/clubedge_api/telemetry_otel.py +137 -0
  56. package/template/apps/api-fastapi/src/clubedge_api/telemetry_sentry.py +99 -0
  57. package/template/apps/api-fastapi/src/clubedge_api/tracing.py +38 -0
  58. package/template/apps/api-fastapi/src/clubedge_api/users.py +53 -0
  59. package/template/apps/api-fastapi/src/clubedge_api/variants/auth_appwrite.py +58 -1
  60. package/template/apps/api-fastapi/src/clubedge_api/variants/auth_better_auth.py +67 -1
  61. package/template/apps/api-fastapi/src/clubedge_api/variants/auth_firebase.py +75 -8
  62. package/template/apps/api-fastapi/src/clubedge_api/variants/auth_supabase.py +55 -1
  63. package/template/apps/api-fastapi/src/clubedge_api/variants/background_jobs_inngest.py +125 -0
  64. package/template/apps/api-fastapi/src/clubedge_api/variants/cache_memory.py +22 -0
  65. package/template/apps/api-fastapi/src/clubedge_api/variants/email_resend.py +35 -0
  66. package/template/apps/api-fastapi/src/clubedge_api/variants/observability_sentry.py +90 -0
  67. package/template/apps/api-fastapi/src/clubedge_api/variants/storage_supabase.py +37 -0
  68. package/template/apps/api-fastapi/tests/__init__.py +0 -0
  69. package/template/apps/api-fastapi/tests/fake_payments.py +172 -0
  70. package/template/apps/api-fastapi/tests/fakes.py +80 -0
  71. package/template/apps/api-fastapi/tests/test_account.py +275 -0
  72. package/template/apps/api-fastapi/tests/test_app.py +12 -19
  73. package/template/apps/api-fastapi/tests/test_background_jobs.py +67 -0
  74. package/template/apps/api-fastapi/tests/test_billing.py +205 -0
  75. package/template/apps/api-fastapi/tests/test_cache.py +134 -0
  76. package/template/apps/api-fastapi/tests/test_check_services.py +82 -0
  77. package/template/apps/api-fastapi/tests/test_client_errors.py +175 -0
  78. package/template/apps/api-fastapi/tests/test_contract_check.py +41 -0
  79. package/template/apps/api-fastapi/tests/test_email.py +82 -0
  80. package/template/apps/api-fastapi/tests/test_email_resend.py +87 -0
  81. package/template/apps/api-fastapi/tests/test_email_smtp.py +128 -0
  82. package/template/apps/api-fastapi/tests/test_http.py +84 -0
  83. package/template/apps/api-fastapi/tests/test_inngest.py +39 -0
  84. package/template/apps/api-fastapi/tests/test_job_queue.py +156 -0
  85. package/template/apps/api-fastapi/tests/test_jobs.py +95 -0
  86. package/template/apps/api-fastapi/tests/test_jobs_bullmq.py +283 -0
  87. package/template/apps/api-fastapi/tests/test_jobs_inngest.py +245 -0
  88. package/template/apps/api-fastapi/tests/test_jobs_run.py +44 -0
  89. package/template/apps/api-fastapi/tests/test_memory_cache.py +42 -0
  90. package/template/apps/api-fastapi/tests/test_observability.py +57 -0
  91. package/template/apps/api-fastapi/tests/test_payments.py +131 -0
  92. package/template/apps/api-fastapi/tests/test_payments_stripe.py +262 -0
  93. package/template/apps/api-fastapi/tests/test_production_env.py +95 -0
  94. package/template/apps/api-fastapi/tests/test_rate_limit.py +103 -0
  95. package/template/apps/api-fastapi/tests/test_storage_local.py +82 -0
  96. package/template/apps/api-fastapi/tests/test_storage_s3.py +196 -0
  97. package/template/apps/api-fastapi/tests/test_storage_supabase.py +95 -0
  98. package/template/apps/api-fastapi/tests/test_telemetry_otel.py +119 -0
  99. package/template/apps/api-fastapi/tests/test_telemetry_sentry.py +70 -0
  100. package/template/apps/api-fastapi/tests/test_users.py +48 -0
  101. package/template/apps/api-fastapi/uv.lock +581 -1
  102. package/template/apps/start/package.json +1 -1
  103. package/template/apps/vite/package.json +1 -1
  104. package/template/apps/web/package.json +1 -1
  105. package/template/clubedge.template.json +1 -0
  106. package/template/e2e/telemetry-fake.ts +78 -9
  107. package/template/package.json +1 -1
  108. package/template/packages/auth/package.json +1 -1
  109. package/template/packages/auth-appwrite/package.json +1 -1
  110. package/template/packages/auth-better-auth/package.json +1 -1
  111. package/template/packages/auth-firebase/package.json +1 -1
  112. package/template/packages/auth-supabase/package.json +1 -1
  113. package/template/packages/cache/package.json +1 -1
  114. package/template/packages/cache-redis/package.json +1 -1
  115. package/template/packages/contract/openapi.yaml +2 -2
  116. package/template/packages/contract/package.json +1 -1
  117. package/template/packages/core/package.json +1 -1
  118. package/template/packages/db/package.json +1 -1
  119. package/template/packages/email/package.json +1 -1
  120. package/template/packages/email-resend/package.json +1 -1
  121. package/template/packages/email-smtp/package.json +1 -1
  122. package/template/packages/jobs/package.json +1 -1
  123. package/template/packages/jobs-bullmq/package.json +1 -1
  124. package/template/packages/jobs-inngest/package.json +1 -1
  125. package/template/packages/jobs-pg-boss/package.json +1 -1
  126. package/template/packages/local-db/package.json +1 -1
  127. package/template/packages/observability/package.json +1 -1
  128. package/template/packages/observability-otel/package.json +1 -1
  129. package/template/packages/observability-sentry/package.json +1 -1
  130. package/template/packages/payments/package.json +1 -1
  131. package/template/packages/payments-stripe/package.json +1 -1
  132. package/template/packages/storage/package.json +1 -1
  133. package/template/packages/storage-s3/package.json +1 -1
  134. package/template/packages/storage-supabase/package.json +1 -1
  135. package/template/packages/ui/package.json +1 -1
  136. package/template.lock.json +77 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to `create-clubedge-app` are documented here.
4
4
 
5
+ ## 0.40.0
6
+
7
+ - **Every module on React + Vite with FastAPI**: default to Starter `v0.26.0` (#126). `--framework vite --api fastapi` now offers the module options Hono got in 0.39.0, each written in Python:
8
+ - `--storage s3` (or a local folder with `--infra local`) and `--storage supabase`.
9
+ - `--cache redis`, for the rate limits and cache.
10
+ - `--email smtp` and `--email resend`.
11
+ - `--observability otel` and `--observability sentry`, with browser error reporting.
12
+ - `--payments stripe`, on the same tables as the Hono API.
13
+ - `--jobs bullmq` and `--jobs inngest`. `--jobs pg-boss` is still refused for FastAPI: pg-boss has no Python version.
14
+ - The account settings page in full: display name, password change, and account deletion.
15
+ - New Vite + FastAPI projects get the same defaults as the other stacks: S3 storage, Redis cache, and SMTP email.
16
+ - Each module has its own uv dependency group: every project keeps the same `uv.lock`, and its production image installs only the selected modules' libraries.
17
+ - Rendered Python files keep the two blank lines PEP 8 and `ruff format` want between top-level definitions when a conditional block is removed (#121). Generating FastAPI projects from Starter `v0.26.0` needs this release.
18
+ - The README says the decoupled stacks with Hono support every module (#120).
19
+
5
20
  ## 0.39.0
6
21
 
7
22
  - **Every module on React + Vite with Hono**: default to Starter `v0.25.0` (#118). `--framework vite --api hono` now offers every module option the full-stack apps have, each running in the API with the matching UI in the frontend:
package/README.md CHANGED
@@ -141,7 +141,7 @@ They meet at one HTTP contract, `packages/contract/openapi.yaml`: the frontend c
141
141
 
142
142
  With `--auth supabase`, the Vite app owns the browser session and attaches its access token to contract requests. Hono or FastAPI verifies that token with Supabase Auth and returns the contract's shared user shape. Configure `VITE_SUPABASE_URL`, `VITE_SUPABASE_PUBLISHABLE_KEY`, `SUPABASE_URL`, and `SUPABASE_PUBLISHABLE_KEY` in the generated env examples; no Supabase service-role key goes to the browser.
143
143
 
144
- The decoupled stacks support Supabase Auth, Better Auth, Firebase, Appwrite, or `none`, plus memory cache, any local infrastructure (the API's `apps/api/.env.local` holds `DATABASE_URL`, since the API owns the database), and Docker or Railway deployment. With Docker, `pnpm clubedge deploy` writes a `compose.yaml` that builds the frontend and API as separate services; copy the variables of `.env.example` and `apps/api/.env.example` to `.env.production`, fill in their values, and run `docker compose --env-file .env.production up --build -d`. The frontend's `VITE_*` values are build arguments, compiled into its bundle; with Better Auth, its Node server also gets the auth variables (`DATABASE_URL`, `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`, and the email settings). With Railway, deploy provisions a public frontend and a private API, wires the frontend proxy to the API, and runs migrations before releasing the API. See [docs/stack-packs.md](docs/stack-packs.md) for how stacks and deployment facts are described.
144
+ With the Hono API, the decoupled stacks support every module option the full-stack frameworks do (since Starter v0.26.0), each running in the API with the matching UI in the frontend; with FastAPI, they support Supabase Auth, Better Auth, Firebase, Appwrite, or `none`, plus memory cache. Both work with any local infrastructure (the API's `apps/api/.env.local` holds `DATABASE_URL`, since the API owns the database), and Docker or Railway deployment. With Docker, `pnpm clubedge deploy` writes a `compose.yaml` that builds the frontend and API as separate services; copy the variables of `.env.example` and `apps/api/.env.example` to `.env.production`, fill in their values, and run `docker compose --env-file .env.production up --build -d`. The frontend's `VITE_*` values are build arguments, compiled into its bundle; with Better Auth, its Node server also gets the auth variables (`DATABASE_URL`, `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`, and the email settings). With Railway, deploy provisions a public frontend and a private API, wires the frontend proxy to the API, and runs migrations before releasing the API. See [docs/stack-packs.md](docs/stack-packs.md) for how stacks and deployment facts are described.
145
145
 
146
146
  ## Choosing modules
147
147
 
@@ -207,7 +207,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git --framework tanstac
207
207
  pnpm dlx @clubedge/create-clubedge-app my-app --ref main
208
208
 
209
209
  # Reproduce a specific Starter release or commit
210
- pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.25.0
210
+ pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.26.0
211
211
  pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
212
212
 
213
213
  # Test local Starter changes before releasing them
@@ -226,7 +226,7 @@ pnpm clubedge add email resend payments stripe # several modules as
226
226
  pnpm clubedge remove storage payments
227
227
  pnpm dlx @clubedge/create-clubedge-app add infra local --dry-run
228
228
  pnpm dlx @clubedge/create-clubedge-app@latest upgrade # to the newest CLI's Starter release
229
- pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.25.0 # or a specific tag, branch, or commit
229
+ pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.26.0 # or a specific tag, branch, or commit
230
230
  ```
231
231
 
232
232
  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.
@@ -337,27 +337,27 @@ Projects created with `--infra neon` also have `.github/workflows/preview.yml`,
337
337
  Each CLI release is tested against one specific Starter release:
338
338
 
339
339
  ```text
340
- create-clubedge-app v0.39.0
340
+ create-clubedge-app v0.40.0
341
341
  |
342
342
  v
343
- Clubedge Starter v0.25.0
344
- commit 17f4e82dca42884e7bfc3c66991110d00c7428a8
343
+ Clubedge Starter v0.26.0
344
+ commit 7c01a0295bea53adc72bda5b843a478ddd04f892
345
345
  ```
346
346
 
347
347
  The current relationship is:
348
348
 
349
349
  | CLI release | Default Starter release | Meaning |
350
350
  | --- | --- | --- |
351
- | `create-clubedge-app@0.39.0` | `clubedge-starter@v0.25.0` | The CLI's required, tested default |
351
+ | `create-clubedge-app@0.40.0` | `clubedge-starter@v0.26.0` | The CLI's required, tested default |
352
352
 
353
353
  This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
354
354
 
355
- The `v0.25.0` tag is expected to resolve to commit
356
- `17f4e82dca42884e7bfc3c66991110d00c7428a8`. The CLI verifies this before
355
+ The `v0.26.0` tag is expected to resolve to commit
356
+ `7c01a0295bea53adc72bda5b843a478ddd04f892`. The CLI verifies this before
357
357
  scaffolding with the default reference and fails rather than silently using a
358
358
  moved tag.
359
359
 
360
- When the Starter changes, its maintainers first publish a new versioned release, such as `v0.25.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.
360
+ When the Starter changes, its maintainers first publish a new versioned release, such as `v0.26.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.
361
361
 
362
362
  Every generated project records its CLI version, Starter repository, Starter ref, Starter commit, framework, and modules in `package.json` and `README.md`.
363
363
 
@@ -412,7 +412,7 @@ CI packs the CLI with `npm pack`, scaffolds clean projects from the packed tarba
412
412
  1. Make and validate changes in `clubedge-starter`.
413
413
  2. Run lint, typecheck, unit tests, browser tests, and production build.
414
414
  3. Update the Starter package versions.
415
- 4. Commit `main`, create a release tag such as `v0.25.0`, and publish the Starter release.
415
+ 4. Commit `main`, create a release tag such as `v0.26.0`, and publish the Starter release.
416
416
 
417
417
  ### CLI maintainers
418
418
 
@@ -462,6 +462,7 @@ Each phase ends with a Starter release and a CLI release pinned to it. The full
462
462
  | 0.37.0 | v0.24.1 | An MCP server (capabilities, validate, plan, create) and an agent skill for coding agents |
463
463
  | 0.38.0 | v0.24.2 | Starter v0.24.2: Next.js security update (critical RCE advisories); database URLs, Neon, and deploy variables for a frontend and its API |
464
464
  | 0.39.0 | v0.25.0 | Starter v0.25.0: every module on React + Vite with Hono, and account settings on React + Vite |
465
+ | 0.40.0 | v0.26.0 | Starter v0.26.0: every module on React + Vite with FastAPI (pg-boss excepted) |
465
466
 
466
467
  ## Community
467
468
 
@@ -229,8 +229,12 @@ export function applyConditionals(source, selection, path = "file") {
229
229
  if (!removed)
230
230
  return text;
231
231
  // A removed block can leave two blank lines in a row, or blank lines at the start or end of
232
- // the file; formatted sources never have them.
233
- const collapsed = text.replace(/\n{3,}/g, "\n\n").replace(/^\n+/, "");
232
+ // the file; formatted sources never have them. Python is the exception: two blank lines come
233
+ // before a top-level definition (PEP 8, which ruff format enforces), one inside indented code.
234
+ const squeezed = path.endsWith(".py")
235
+ ? text.replace(/\n{4,}/g, "\n\n\n").replace(/\n{3,}(?=[ \t])/g, "\n\n")
236
+ : text.replace(/\n{3,}/g, "\n\n");
237
+ const collapsed = squeezed.replace(/^\n+/, "");
234
238
  return source.endsWith("\n") ? collapsed.replace(/\n+$/, "\n") : collapsed;
235
239
  }
236
240
  /** Removes dependencies on workspace packages that were left out of the project. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/create-clubedge-app",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "description": "Create React apps from Clubedge Starter with a CLI or local MCP server.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.9.0",
@@ -24,8 +24,8 @@
24
24
  },
25
25
  "clubedge": {
26
26
  "starterRepository": "Clubedge/clubedge-starter",
27
- "starterRef": "v0.25.0",
28
- "starterCommit": "17f4e82dca42884e7bfc3c66991110d00c7428a8"
27
+ "starterRef": "v0.26.0",
28
+ "starterCommit": "7c01a0295bea53adc72bda5b843a478ddd04f892"
29
29
  },
30
30
  "license": "Apache-2.0",
31
31
  "repository": {
@@ -68,8 +68,7 @@ Every combination in the CI matrix is generated as a real project and runs its f
68
68
  - Password reset by email, and email verification that can be required before sign-in. <!-- clubedge:only auth=better-auth && email!=none -->
69
69
  - Password reset by email, sent by Supabase. <!-- clubedge:only auth=supabase -->
70
70
  - An account settings page: display name, password change, and account deletion. <!-- clubedge:only auth!=none && framework!=vite -->
71
- - An account settings page: display name, password change, and account deletion. <!-- clubedge:only auth!=none && framework=vite && api=hono -->
72
- - An account settings page with password change; the FastAPI API does not keep display names or delete accounts yet. <!-- clubedge:only auth!=none && framework=vite && api=fastapi -->
71
+ - An account settings page: display name, password change, and account deletion. <!-- clubedge:only auth!=none && framework=vite && api=hono|fastapi -->
73
72
  - 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 -->
74
73
  - Email through Resend; development prints emails to the server log until the API key is set. <!-- clubedge:only email=resend -->
75
74
  - S3-compatible storage adapter for AWS S3, Cloudflare R2, and MinIO. <!-- clubedge:only storage=s3 -->
package/template/SETUP.md CHANGED
@@ -72,7 +72,7 @@ The app is available at <http://localhost:3000>, the liveness endpoint at <http:
72
72
 
73
73
  Create or select a Supabase project and copy its URL and publishable key from the project's API settings into the Supabase variables above. Authentication runs on the server, which validates every user with Supabase, and the publishable key is all sign-in needs. To let users delete their account from the settings page, also set `SUPABASE_SECRET_KEY` to a secret key from the same settings; it stays on the server, so never give it a `NEXT_PUBLIC_` prefix. <!-- clubedge:only framework=next -->
74
74
  Create or select a Supabase project and copy its URL and publishable key from the project's API settings into the Supabase variables above. Authentication runs on the server, which validates every user with Supabase, and the publishable key is all sign-in needs. To let users delete their account from the settings page, also set `SUPABASE_SECRET_KEY` to a secret key from the same settings; it stays on the server. <!-- clubedge:only framework=tanstack-start -->
75
- To let users delete their account from the settings page, set `SUPABASE_SECRET_KEY` in `apps/api/.env.local` to a secret key from the project's API settings; only the API uses it. <!-- clubedge:only framework=vite && api=hono -->
75
+ To let users delete their account from the settings page, set `SUPABASE_SECRET_KEY` in `apps/api/.env.local` to a secret key from the project's API settings; only the API uses it. <!-- clubedge:only framework=vite && api=hono|fastapi -->
76
76
 
77
77
  In Supabase Auth URL configuration, add this redirect URL for local email confirmation and password reset links:
78
78
 
@@ -247,7 +247,7 @@ Use `rediss://` for TLS connections such as Upstash and `redis://` for an unencr
247
247
 
248
248
  Without `REDIS_URL`, the rate limiter and cache use process memory. Memory limits apply per server process, so configure Redis when you run several instances or serverless functions. Never commit a real Redis URL or password.
249
249
 
250
- The API's rate limits use Redis, so `REDIS_URL` belongs in `apps/api/.env.local` and on the API's service. A limit answers `429` with `Retry-After`; when Redis is unreachable, requests go through and the failure is reported. <!-- clubedge:only api=hono -->
250
+ The API's rate limits use Redis, so `REDIS_URL` belongs in `apps/api/.env.local` and on the API's service. A limit answers `429` with `Retry-After`; when Redis is unreachable, requests go through and the failure is reported. <!-- clubedge:only api!=none -->
251
251
 
252
252
  <!-- clubedge:end -->
253
253
  <!-- clubedge:if cache=memory -->
@@ -262,7 +262,8 @@ The rate limiter and cache keep their state in process memory, which suits a sin
262
262
  ## Email
263
263
 
264
264
  The app sends email through the `EmailProvider` in `apps/web/src/server/email.ts`. Server code imports `email` from `@/server/email` and calls `email.send({ to, subject, text, html })`. <!-- clubedge:only api=none -->
265
- The API sends email through the `EmailProvider` in `apps/api/src/server/email.ts`: its code imports `email` from `@/server/email` and calls `email.send({ to, subject, text, html })`. <!-- clubedge:only api!=none -->
265
+ The API sends email through the `EmailProvider` in `apps/api/src/server/email.ts`: its code imports `email` from `@/server/email` and calls `email.send({ to, subject, text, html })`. <!-- clubedge:only api=hono -->
266
+ The API sends email through the `EmailProvider` in `apps/api/src/clubedge_api/email.py`: its code imports `email` from `clubedge_api.email` and awaits `email.send({"to": ..., "subject": ..., "text": ..., "html": ...})`. <!-- clubedge:only api=fastapi -->
266
267
  Better Auth runs in the frontend's server (`apps/web/auth.server.mjs`), which sends its emails with the same provider, so set the email variables in both `apps/web/.env.local` and `apps/api/.env.local`. <!-- clubedge:only api!=none && auth=better-auth -->
267
268
 
268
269
  <!-- clubedge:end -->
@@ -287,12 +288,13 @@ Without a provider configured, development prints each email to the server log,
287
288
 
288
289
  The plans users can buy are in `apps/web/src/config/plans.ts`, with the app's own ids. The dashboard's billing card offers checkout for each plan, then shows the subscription and opens billing management. Server code reads a user's plan with `getBilling().subscriptionFor(userId)` and `isEntitled` from `@/server/payments`, and the `subscriptions` table keeps it in sync through the provider's webhook at `/api/webhooks/payments`. <!-- clubedge:only api=none -->
289
290
  The plans users can buy are in `apps/api/src/config/plans.ts`, with the app's own ids, and the API serves them at `/api/billing`. The dashboard's billing card offers checkout for each plan, then shows the subscription and opens billing management; checkout and the portal send the user back to `FRONTEND_URL`. API code reads a user's plan with `getBilling().subscriptionFor(await getUserProfileId(user))` and `isEntitled` from `@/server/payments` and `@/server/users`, and the `subscriptions` table keeps it in sync through the provider's webhook at `/api/webhooks/payments`, which the frontend forwards to the API. <!-- clubedge:only api=hono -->
291
+ The plans users can buy are in `apps/api/src/clubedge_api/plans.py`, with the app's own ids, and the API serves them at `/api/billing`. The dashboard's billing card offers checkout for each plan, then shows the subscription and opens billing management; checkout and the portal send the user back to `FRONTEND_URL`. API code checks a user's plan with `has_active_subscription(await ensure_user_id(user))` from `clubedge_api.payments` and `clubedge_api.users`, or reads it with `find_subscription` and `is_entitled`. The `subscriptions` table keeps it in sync through the provider's webhook at `/api/webhooks/payments`, which the frontend forwards to the API; its tables and migrations are those of `packages/payments`, applied by `pnpm db:migrate` and by the image's `node migrate.mjs`. <!-- clubedge:only api=fastapi -->
290
292
 
291
293
  <!-- clubedge:end -->
292
294
  <!-- clubedge:if payments=stripe -->
293
295
 
294
296
  [Stripe](https://stripe.com) runs checkout and billing management. In test mode: <!-- clubedge:only api=none -->
295
- [Stripe](https://stripe.com) runs checkout and billing management. The Stripe variables go in `apps/api/.env.local`. In test mode: <!-- clubedge:only api=hono -->
297
+ [Stripe](https://stripe.com) runs checkout and billing management. The Stripe variables go in `apps/api/.env.local`. In test mode: <!-- clubedge:only api!=none -->
296
298
 
297
299
  1. Copy the secret key from the dashboard's API keys into `STRIPE_SECRET_KEY`.
298
300
  2. Create a recurring price for each plan and put its id in `STRIPE_PRICE_<PLAN>`, such as `STRIPE_PRICE_PRO`.
@@ -302,7 +304,7 @@ The plans users can buy are in `apps/api/src/config/plans.ts`, with the app's ow
302
304
  stripe listen --forward-to localhost:3000/api/webhooks/payments
303
305
  ```
304
306
 
305
- The Stripe CLI posts to the frontend on port 3000, which forwards the webhook, body untouched, to the API; the API itself stays private. <!-- clubedge:only api=hono -->
307
+ The Stripe CLI posts to the frontend on port 3000, which forwards the webhook, body untouched, to the API; the API itself stays private. <!-- clubedge:only api!=none -->
306
308
 
307
309
  Turn on the customer portal in the dashboard's billing settings, so "Manage billing" works. In production, add a webhook endpoint for `https://<your-domain>/api/webhooks/payments` with the events `checkout.session.completed` and `customer.subscription.created`, `.updated`, `.deleted`, `.paused`, and `.resumed`, and use its signing secret. Every webhook's signature is verified, each event is applied once, and subscription events read the subscription's current state from Stripe, so retried or out-of-order deliveries are safe.
308
310
 
@@ -311,6 +313,7 @@ Turn on the customer portal in the dashboard's billing settings, so "Manage bill
311
313
 
312
314
  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. <!-- clubedge:only api=none -->
313
315
  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/api/src/server/payments.ts`; billing logic, the tables, the routes, and the billing card stay the same. <!-- clubedge:only api=hono -->
316
+ Payments go through the provider-neutral `PaymentProvider` protocol in `apps/api/src/clubedge_api/payments.py`, which follows `packages/payments` and reads and writes the same tables. To use another provider, implement that protocol in a new module and build its `Billing` in place of `stripe_billing` (`apps/api/src/clubedge_api/payments_stripe.py`); billing logic, the tables, the routes, and the billing card stay the same. <!-- clubedge:only api=fastapi -->
314
317
 
315
318
  <!-- clubedge:end -->
316
319
  <!-- clubedge:if jobs!=none -->
@@ -320,12 +323,16 @@ Payments go through the provider-neutral `PaymentProvider` interface in `package
320
323
  Jobs run outside requests, with retries, in a worker that starts inside the app server: `pnpm dev` and the production server both run it, so there is no separate process to deploy. [pg-boss](https://github.com/timgit/pg-boss) stores them in your PostgreSQL, and creates and upgrades its own tables in the `pgboss` schema when the app starts. <!-- clubedge:only jobs=pg-boss && api=none -->
321
324
  Jobs run outside requests, with retries, in a worker that starts inside the API: `pnpm dev` and the API's 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 API starts. <!-- clubedge:only jobs=pg-boss && api=hono -->
322
325
  Jobs run outside requests, with retries, in a worker that starts inside the app server: `pnpm dev` and the production server both run it, so there is no separate process to deploy. [BullMQ](https://docs.bullmq.io) stores them in Redis (`REDIS_URL`, Redis 6.2 or newer), which the cache also uses; keep it on a Redis that doesn't evict keys, since jobs are data. <!-- clubedge:only jobs=bullmq && api=none -->
323
- Jobs run outside requests, with retries, in a worker that starts inside the API: `pnpm dev` and the API's production server both run it, so there is no separate process to deploy. [BullMQ](https://docs.bullmq.io) stores them in Redis (`REDIS_URL` in `apps/api/.env.local`, Redis 6.2 or newer), which the cache also uses; keep it on a Redis that doesn't evict keys, since jobs are data. <!-- clubedge:only jobs=bullmq && api=hono -->
326
+ Jobs run outside requests, with retries, in a worker that starts inside the API: `pnpm dev` and the API's production server both run it, so there is no separate process to deploy. [BullMQ](https://docs.bullmq.io) stores them in Redis (`REDIS_URL` in `apps/api/.env.local`, Redis 6.2 or newer), which the cache also uses; keep it on a Redis that doesn't evict keys, since jobs are data. <!-- clubedge:only jobs=bullmq && api=hono|fastapi -->
324
327
  [Inngest](https://www.inngest.com) runs the jobs: the app sends an event for each one, and Inngest calls the app's `/api/inngest` to run it, with retries and schedules, so nothing runs between requests and serverless hosts need no cron. In development, start Inngest's Dev Server next to `pnpm dev` with `pnpm jobs:dev`, and open its dashboard at <http://localhost:8288> to see every run; `INNGEST_DEV=1` points the app at it. In production, create an app in Inngest, set `INNGEST_EVENT_KEY` and `INNGEST_SIGNING_KEY`, remove `INNGEST_DEV`, and sync the deployed `/api/inngest` URL in Inngest's dashboard (Inngest's Vercel and Netlify integrations do it on each deploy). <!-- clubedge:only jobs=inngest && api=none -->
325
- [Inngest](https://www.inngest.com) runs the jobs: the API sends an event for each one, and Inngest calls `/api/inngest` to run it, with retries and schedules. Inngest reaches the API through the frontend, which forwards `/api/inngest` to it, so the API stays private; the API tells Inngest its address from `FRONTEND_URL`. In development, start Inngest's Dev Server next to `pnpm dev` with `pnpm jobs:dev`, and open its dashboard at <http://localhost:8288> to see every run; `INNGEST_DEV=1` in `apps/api/.env.local` points the API at it, and the API syncs its jobs once the frontend answers. In production, create an app in Inngest, set `INNGEST_EVENT_KEY` and `INNGEST_SIGNING_KEY` on the API, remove `INNGEST_DEV`, and sync `https://<your-frontend>/api/inngest` in Inngest's dashboard. <!-- clubedge:only jobs=inngest && api=hono -->
328
+ [Inngest](https://www.inngest.com) runs the jobs: the API sends an event for each one, and Inngest calls `/api/inngest` to run it, with retries and schedules. Inngest reaches the API through the frontend, which forwards `/api/inngest` to it, so the API stays private; the API tells Inngest its address from `FRONTEND_URL`. In development, start Inngest's Dev Server next to `pnpm dev` with `pnpm jobs:dev`, and open its dashboard at <http://localhost:8288> to see every run; `INNGEST_DEV=1` in `apps/api/.env.local` points the API at it, and the API syncs its jobs once the frontend answers. In production, create an app in Inngest, set `INNGEST_EVENT_KEY` and `INNGEST_SIGNING_KEY` on the API, remove `INNGEST_DEV`, and sync `https://<your-frontend>/api/inngest` in Inngest's dashboard. <!-- clubedge:only jobs=inngest && api=hono|fastapi -->
326
329
 
327
330
  Define a job in `apps/web/src/jobs` and add it to the list in `apps/web/src/jobs/index.ts`: <!-- clubedge:only api=none -->
328
331
  Define a job in `apps/api/src/jobs` and add it to the list in `apps/api/src/jobs/index.ts`: <!-- clubedge:only api=hono -->
332
+ Define a job in `apps/api/src/clubedge_api/jobs.py` and add it to `JOBS` there: <!-- clubedge:only api=fastapi -->
333
+
334
+ <!-- clubedge:end -->
335
+ <!-- clubedge:if jobs!=none && api!=fastapi -->
329
336
 
330
337
  ```ts
331
338
  import { defineJob } from "@clubedge/jobs";
@@ -339,20 +346,41 @@ export const welcomeEmail = defineJob<{ userId: string }>({
339
346
  });
340
347
  ```
341
348
 
349
+ <!-- clubedge:end -->
350
+ <!-- clubedge:if jobs!=none && api=fastapi -->
351
+
352
+ ```python
353
+ async def _welcome_email(data: JobData, context: JobContext) -> None:
354
+ # Look the user up by data["userId"] and send the email.
355
+ ...
356
+
357
+ welcome_email = JobDefinition(name="welcome-email", run=_welcome_email, retry_limit=3)
358
+ ```
359
+
360
+ <!-- clubedge:end -->
361
+ <!-- clubedge:if jobs!=none -->
362
+
342
363
  Queue it from server code with `getJobs().enqueue(welcomeEmail, { userId })`, from `@/server/jobs`. To send any email in the background, enqueue `sendEmailJob` from `@/jobs` with the message, as the auth emails do. <!-- clubedge:only email!=none && api=none -->
343
364
  Queue it from the API's code with `getJobs().enqueue(welcomeEmail, { userId })`, from `@/server/jobs`. To send any email in the background, enqueue `sendEmailJob` from `@/jobs` with the message. <!-- clubedge:only email!=none && api=hono -->
344
365
  Queue it from server code with `getJobs().enqueue(welcomeEmail, { userId })`, from `@/server/jobs`. <!-- clubedge:only email=none && api=none -->
345
366
  Queue it from the API's code with `getJobs().enqueue(welcomeEmail, { userId })`, from `@/server/jobs`. <!-- clubedge:only email=none && api=hono -->
367
+ Queue it from the API's code with `await get_jobs().enqueue(welcome_email, {"userId": user_id})`, from `clubedge_api.background_jobs`. To send any email in the background, enqueue `send_email_job` from `clubedge_api.jobs` with the message. <!-- clubedge:only email!=none && api=fastapi -->
368
+ Queue it from the API's code with `await get_jobs().enqueue(welcome_email, {"userId": user_id})`, from `clubedge_api.background_jobs`. <!-- clubedge:only email=none && api=fastapi -->
346
369
 
347
370
  A thrown error fails the attempt and the job is retried (`retryLimit`, `retryDelaySeconds`, `retryBackoff`). An attempt that runs past `timeoutSeconds`, or whose server stops, is retried too, so make jobs safe to run twice. Add `schedule: "0 3 * * *"` (cron syntax, UTC) to run a job on a schedule. Restart `pnpm dev` after adding or changing a job. The dashboard's jobs card queues the sample job from `apps/web/src/jobs/sample.ts` and lists its recent runs; remove both when you add your own. <!-- clubedge:only api=none -->
348
371
  A thrown error fails the attempt and the job is retried (`retryLimit`, `retryDelaySeconds`, `retryBackoff`). An attempt that runs past `timeoutSeconds`, or whose server stops, is retried too, so make jobs safe to run twice. Add `schedule: "0 3 * * *"` (cron syntax, UTC) to run a job on a schedule. Restart the API after adding or changing a job. The dashboard's jobs card queues the sample job from `apps/api/src/jobs/sample.ts` through `/api/jobs` and lists its recent runs; remove both when you add your own. <!-- clubedge:only api=hono -->
372
+ A raised exception fails the attempt and the job is retried (`retry_limit`, `retry_delay_seconds`, `retry_backoff`). An attempt that runs past `timeout_seconds`, or whose server stops, is retried too, so make jobs safe to run twice. Add `schedule="0 3 * * *"` (cron syntax, UTC) to run a job on a schedule. Restart the API after adding or changing a job. The dashboard's jobs card queues `sample_job` from `apps/api/src/clubedge_api/jobs.py` through `/api/jobs` and lists its recent runs; remove both when you add your own. <!-- clubedge:only api=fastapi -->
349
373
 
350
374
  Where no server keeps running, as on Vercel and Netlify, there is no worker: `JOBS_WORKER` defaults to `off` there, and you can set it yourself anywhere. Set `CRON_SECRET` to 16 or more random characters and call `GET /api/jobs/run` every minute or so with the header `Authorization: Bearer <CRON_SECRET>`, from Vercel Cron, a Netlify scheduled function, or any scheduler. Each call runs the jobs that are due. <!-- clubedge:only jobs=pg-boss|bullmq && api=none -->
351
- To run the jobs elsewhere, set `JOBS_WORKER=off` on the API: it then runs no worker. Set `CRON_SECRET` to 16 or more random characters and call `GET /api/jobs/run` on the frontend, which forwards it to the API, every minute or so with the header `Authorization: Bearer <CRON_SECRET>`. Each call runs the jobs that are due. The API stops its worker on `SIGTERM`, letting running jobs finish. <!-- clubedge:only jobs=pg-boss|bullmq && api=hono -->
375
+ To run the jobs elsewhere, set `JOBS_WORKER=off` on the API: it then runs no worker. Set `CRON_SECRET` to 16 or more random characters and call `GET /api/jobs/run` on the frontend, which forwards it to the API, every minute or so with the header `Authorization: Bearer <CRON_SECRET>`. Each call runs the jobs that are due. The API stops its worker on `SIGTERM`, letting running jobs finish. <!-- clubedge:only jobs=pg-boss|bullmq && api=hono|fastapi -->
352
376
  Inngest keeps the job history: the jobs card reads it from Inngest's API (the Dev Server's locally), and Inngest's dashboard shows each run's attempts, output, and errors. <!-- clubedge:only jobs=inngest -->
353
377
 
354
378
  Jobs go through the provider-neutral `JobQueue` interface in `packages/jobs`, so job definitions, the routes, and the jobs card stay the same whichever provider runs them. Switch providers with `pnpm clubedge add jobs pg-boss`, `bullmq`, or `inngest`. To add another, implement `JobQueue` in a new package, return it from `apps/web/src/server/jobs.ts`, and prove it with the shared conformance suite, as each provider's tests do: <!-- clubedge:only api=none -->
355
379
  Jobs go through the provider-neutral `JobQueue` interface in `packages/jobs`, so job definitions, the routes, and the jobs card stay the same whichever provider runs them. Switch providers with `pnpm clubedge add jobs pg-boss`, `bullmq`, or `inngest`. To add another, implement `JobQueue` in a new package, return it from `apps/api/src/server/jobs.ts`, and prove it with the shared conformance suite, as each provider's tests do: <!-- clubedge:only api=hono -->
380
+ Jobs go through the provider-neutral `JobQueue` protocol in `apps/api/src/clubedge_api/job_queue.py`, so job definitions, the routes, and the jobs card stay the same whichever provider runs them. Switch providers with `pnpm clubedge add jobs bullmq` or `inngest`; pg-boss, which the TypeScript stacks offer, is not available with the FastAPI API. To add another, implement `JobQueue` in a new module and return it from `apps/api/src/clubedge_api/background_jobs.py`. <!-- clubedge:only api=fastapi -->
381
+
382
+ <!-- clubedge:end -->
383
+ <!-- clubedge:if jobs!=none && api!=fastapi -->
356
384
 
357
385
  ```ts
358
386
  import { describeJobQueue } from "@clubedge/jobs/conformance";
@@ -385,7 +413,8 @@ docker run --rm -p 16686:16686 -p 4318:4318 jaegertracing/jaeger:latest
385
413
 
386
414
  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 -->
387
415
  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 -->
388
- Create a Node.js project in [Sentry](https://sentry.io) and set `SENTRY_DSN` in `apps/api/.env.local` to its DSN; the frontend reads it from the API at runtime. `SENTRY_TRACES_SAMPLE_RATE` is the share of API 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=vite -->
416
+ Create a Node.js project in [Sentry](https://sentry.io) and set `SENTRY_DSN` in `apps/api/.env.local` to its DSN; the frontend reads it from the API at runtime. `SENTRY_TRACES_SAMPLE_RATE` is the share of API 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=vite && api=hono -->
417
+ Create a Python project in [Sentry](https://sentry.io) and set `SENTRY_DSN` in `apps/api/.env.local` to its DSN; the frontend reads it from the API at runtime. `SENTRY_TRACES_SAMPLE_RATE` is the share of API 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, and its integrations trace what FastAPI, httpx, and Redis do in the same traces. <!-- clubedge:only api=fastapi -->
389
418
 
390
419
  To see your own code in browser stack traces, give the build `SENTRY_AUTH_TOKEN` (an auth token with the `project:releases` scope), `SENTRY_ORG`, and `SENTRY_PROJECT`. The build then makes source maps for the browser bundle, uploads them to Sentry, and deletes them, so they are never served. Without the token the build makes no source maps. Set the three variables where the build runs: in CI, or as build-time variables on your platform. <!-- clubedge:only framework!=vite -->
391
420
  The React + Vite build does not upload source maps to Sentry yet, so browser stack traces point into the minified bundle. <!-- clubedge:only framework=vite -->
@@ -399,6 +428,7 @@ On Vercel and Netlify, a function can stop right after it responds, so spans sti
399
428
 
400
429
  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. <!-- clubedge:only api=none -->
401
430
  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/api/src/server/observability.ts`; error reports and spans across the API stay the same. The API stops taking requests on `SIGTERM` and sends what is still buffered before it exits. <!-- clubedge:only api=hono -->
431
+ Observability goes through the `Observability` protocol in `apps/api/src/clubedge_api/telemetry.py`: each API request is traced (`tracing.py`), and every error the API logs at the `ERROR` level, such as with `logger.exception(...)`, reaches the provider, with the `extra` it was logged with as its attributes. To use another tool, implement that protocol and start it from `apps/api/src/clubedge_api/observability.py`; error reports and spans across the API stay the same. The API stops taking requests on `SIGTERM` and sends what is still buffered before it exits. <!-- clubedge:only api=fastapi -->
402
432
 
403
433
  <!-- clubedge:end -->
404
434
  <!-- clubedge:if storage!=none -->
@@ -421,6 +451,12 @@ The storage adapter uses Supabase Storage through the signed-in user's Supabase
421
451
  The API holds the storage adapter: set its variables in `apps/api/.env.local`, and in a route call `getStorage(c.req.raw)` from `apps/api/src/server/storage.ts`. `pnpm check:services` uploads, reads back, and deletes a test file. <!-- clubedge:only storage=s3 -->
422
452
  The API holds the storage adapter: set `SUPABASE_STORAGE_BUCKET` in `apps/api/.env.local`, and in a route call `getStorage(c.req.raw)` from `apps/api/src/server/storage.ts`. It acts as the user whose Supabase access token the request carries (the `Authorization: Bearer` header the frontend sends), and as anonymous without one. <!-- clubedge:only storage=supabase -->
423
453
 
454
+ <!-- clubedge:end -->
455
+ <!-- clubedge:if storage!=none && api=fastapi -->
456
+
457
+ The API holds the storage adapter: set its variables in `apps/api/.env.local`, and in a route call `get_storage(request)` from `apps/api/src/clubedge_api/storage.py`, then await its `upload`, `download`, `delete`, `exists`, or `get_signed_url`. `pnpm check:services` uploads, reads back, and deletes a test file. <!-- clubedge:only storage=s3 -->
458
+ The API holds the storage adapter: set `SUPABASE_STORAGE_BUCKET` in `apps/api/.env.local`, and in a route call `get_storage(request)` from `apps/api/src/clubedge_api/storage.py`, then await its `upload`, `download`, `delete`, `exists`, or `get_signed_url`. It acts as the user whose Supabase access token the request carries (the `Authorization: Bearer` header the frontend sends), and as anonymous without one. <!-- clubedge:only storage=supabase -->
459
+
424
460
  <!-- clubedge:end -->
425
461
  <!-- clubedge:if storage!=none -->
426
462
 
@@ -646,8 +682,7 @@ API_URL=http://127.0.0.1:3001 pnpm exec vitest run packages/contract/src/remote.
646
682
 
647
683
  Some endpoints are an optional part of a module, a capability (`x-clubedge-capability` in `openapi.yaml`): the account settings page's display name and account deletion are the `account` capability of the auth module. An API without a capability answers its endpoints with 404, and the suite checks that unless `API_CAPABILITIES` lists it (such as `API_CAPABILITIES=account`).
648
684
 
649
- The settings page changes passwords with the auth provider, in the browser or in Better Auth's server, which signs out the user's other sessions and applies the provider's rate limits. The display name and account deletion go through the API, which keeps the app's data: deletion checks the password (10 tries a minute per user, refused while the limit cannot be checked), removes the user's app data, then deletes the user from the provider. <!-- clubedge:only auth!=none && api=hono -->
650
- The FastAPI API does not implement the `account` capability yet, so the settings page offers only the password change. <!-- clubedge:only auth!=none && api=fastapi -->
685
+ The settings page changes passwords with the auth provider, in the browser or in Better Auth's server, which signs out the user's other sessions and applies the provider's rate limits. The display name and account deletion go through the API, which keeps the app's data: deletion checks the password (10 tries a minute per user, refused while the limit cannot be checked), removes the user's app data, then deletes the user from the provider. <!-- clubedge:only auth!=none && api=hono|fastapi -->
651
686
 
652
687
  In production, run each app's image: `apps/vite/Dockerfile` (the frontend, served by Node with `API_URL` set to the API's address) and the selected API image (`apps/api/Dockerfile` or `apps/api-fastapi/Dockerfile`). `docker compose up --build` runs both.
653
688
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/api",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -21,6 +21,8 @@ DATABASE_URL_DIRECT=
21
21
  # clubedge:if auth=supabase
22
22
  SUPABASE_URL=
23
23
  SUPABASE_PUBLISHABLE_KEY=
24
+ # Optional: the project's secret key (server-side only), which deleting accounts needs.
25
+ SUPABASE_SECRET_KEY=
24
26
  # clubedge:end
25
27
 
26
28
  # Firebase Authentication. The Firebase API key is public; no service account is needed here.
@@ -34,9 +36,107 @@ FIREBASE_API_KEY=
34
36
  # clubedge:if auth=appwrite
35
37
  APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
36
38
  APPWRITE_PROJECT_ID=
39
+ # Optional: an API key with the sessions.write and users.write scopes, to delete accounts.
40
+ APPWRITE_API_KEY=
37
41
  # clubedge:end
38
42
 
39
43
  # Better Auth service origin (the frontend origin when it hosts /api/auth).
40
44
  # clubedge:if auth=better-auth
41
45
  BETTER_AUTH_URL=http://localhost:3000
42
46
  # clubedge:end
47
+
48
+ # clubedge:if cache=redis
49
+ # Optional rate limits and cache (Redis protocol; rediss:// enables TLS). Without it, both use
50
+ # process memory. Upstash example: rediss://default:YOUR_PASSWORD@your-database.upstash.io:6379
51
+ REDIS_URL= # clubedge:only infra!=docker
52
+ REDIS_URL=redis://localhost:6379 # clubedge:only infra=docker
53
+ # clubedge:end
54
+
55
+ # clubedge:if storage=s3
56
+ # S3-compatible storage (AWS S3, Cloudflare R2, MinIO)
57
+ STORAGE_BUCKET= # clubedge:only infra!=docker
58
+ STORAGE_REGION=auto # clubedge:only infra!=docker
59
+ STORAGE_ENDPOINT= # clubedge:only infra!=docker
60
+ STORAGE_ACCESS_KEY_ID= # clubedge:only infra!=docker
61
+ STORAGE_SECRET_ACCESS_KEY= # clubedge:only infra!=docker
62
+ # Local S3-compatible storage from docker-compose.dev.yml; it accepts any credentials. # clubedge:only infra=docker
63
+ STORAGE_BUCKET=app # clubedge:only infra=docker
64
+ STORAGE_REGION=us-east-1 # clubedge:only infra=docker
65
+ STORAGE_ENDPOINT=http://localhost:9090 # clubedge:only infra=docker
66
+ STORAGE_ACCESS_KEY_ID=local # clubedge:only infra=docker
67
+ STORAGE_SECRET_ACCESS_KEY=local-secret # clubedge:only infra=docker
68
+ STORAGE_PUBLIC_URL=
69
+ # Without an S3 service, files are stored in this folder (relative to the API). # clubedge:only infra=local
70
+ STORAGE_DIRECTORY=../../.data/storage # clubedge:only infra=local
71
+ # clubedge:end
72
+
73
+ # clubedge:if storage=supabase
74
+ # Supabase Storage bucket, accessed as the user whose access token the request carries
75
+ SUPABASE_STORAGE_BUCKET= # clubedge:only infra!=supabase
76
+ SUPABASE_STORAGE_BUCKET=app # clubedge:only infra=supabase
77
+ # clubedge:end
78
+
79
+ # clubedge:if email=smtp
80
+ # Email over SMTP: any provider's SMTP endpoint (smtps:// for TLS). Without it, development
81
+ # prints emails to the server log and production refuses to send.
82
+ SMTP_URL= # clubedge:only infra!=docker|supabase
83
+ # Mailpit from docker-compose.dev.yml; read the emails at http://localhost:8025. # clubedge:only infra=docker
84
+ SMTP_URL=smtp://localhost:1025 # clubedge:only infra=docker
85
+ # The local Supabase inbox; read the emails at http://127.0.0.1:54324. # clubedge:only infra=supabase
86
+ SMTP_URL=smtp://127.0.0.1:54325 # clubedge:only infra=supabase
87
+ EMAIL_FROM=App <no-reply@example.com>
88
+ # clubedge:end
89
+
90
+ # clubedge:if email=resend
91
+ # Resend (https://resend.com). EMAIL_FROM must use a domain verified in Resend. Without the
92
+ # key, development prints emails to the server log and production refuses to send.
93
+ RESEND_API_KEY=
94
+ EMAIL_FROM=App <no-reply@example.com>
95
+ # clubedge:end
96
+
97
+ # clubedge:if payments=stripe
98
+ # Stripe (https://dashboard.stripe.com/apikeys): use test-mode keys during development. The
99
+ # webhook secret comes from `stripe listen` locally, or from the endpoint's settings in production;
100
+ # Stripe calls the webhook through the frontend, at FRONTEND_URL/api/webhooks/payments.
101
+ STRIPE_SECRET_KEY=
102
+ STRIPE_WEBHOOK_SECRET=
103
+ # The Stripe price for each plan in src/clubedge_api/plans.py (STRIPE_PRICE_<PLAN>).
104
+ STRIPE_PRICE_PRO=
105
+ # Tests can send Stripe API requests to a stand-in, such as stripe-mock or the fake in
106
+ # e2e/stripe-fake.ts: STRIPE_API_BASE=http://127.0.0.1:12111. Leave it unset otherwise.
107
+ # clubedge:end
108
+
109
+ # clubedge:if jobs=bullmq
110
+ # Background jobs are stored in Redis (REDIS_URL) through BullMQ and run by a worker inside the
111
+ # API. JOBS_WORKER=off turns the worker off; a scheduler then calls /api/jobs/run with CRON_SECRET
112
+ # (16 or more characters) instead.
113
+ JOBS_WORKER=
114
+ CRON_SECRET=
115
+ # clubedge:end
116
+
117
+ # clubedge:if jobs=inngest
118
+ # Background jobs run by Inngest (https://www.inngest.com), which calls /api/inngest through the
119
+ # frontend (FRONTEND_URL above), so the API stays private. In development, start its Dev Server
120
+ # with pnpm jobs:dev (http://localhost:8288); INNGEST_DEV=1 points the API at it. In production,
121
+ # remove INNGEST_DEV and set the keys of your Inngest app.
122
+ INNGEST_DEV=1
123
+ INNGEST_EVENT_KEY=
124
+ INNGEST_SIGNING_KEY=
125
+ # clubedge:end
126
+
127
+ # clubedge:if observability=otel
128
+ # OpenTelemetry: where traces and error logs go over OTLP/HTTP, such as a collector, Grafana
129
+ # Cloud, or Honeycomb. Empty sends nothing. Headers are key=value pairs separated by commas.
130
+ OTEL_EXPORTER_OTLP_ENDPOINT=
131
+ OTEL_EXPORTER_OTLP_HEADERS=
132
+ OTEL_SERVICE_NAME=
133
+ # clubedge:end
134
+
135
+ # clubedge:if observability=sentry
136
+ # Sentry (https://sentry.io): the DSN from the project's Client Keys settings. Empty sends nothing.
137
+ # The browser reads it from the API at runtime. The traces sample rate is the share of requests
138
+ # traced, from 0 to 1 (default 0.1).
139
+ SENTRY_DSN=
140
+ SENTRY_ENVIRONMENT=
141
+ SENTRY_TRACES_SAMPLE_RATE=
142
+ # clubedge:end
@@ -22,17 +22,28 @@ ARG APP_DIR
22
22
  WORKDIR /app
23
23
  ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=never
24
24
 
25
- # Dependencies first, so their layer stays cached until pyproject.toml (or uv.lock) changes.
25
+ # Dependencies first, so their layers stay cached until pyproject.toml (or uv.lock) changes: the
26
+ # API's own, then the dependency group (pyproject.toml) of each module option the project uses.
27
+ # The development tools stay out of the image.
26
28
  COPY ${APP_DIR}/pyproject.toml ${APP_DIR}/uv.lock ./
27
- RUN uv sync --locked --no-dev --no-install-project
29
+ RUN uv sync --locked --no-default-groups --no-install-project
30
+ RUN uv sync --locked --no-default-groups --group storage-s3 --inexact --no-install-project # clubedge:only storage=s3
31
+ RUN uv sync --locked --no-default-groups --group cache-redis --inexact --no-install-project # clubedge:only cache=redis
32
+ RUN uv sync --locked --no-default-groups --group email-smtp --inexact --no-install-project # clubedge:only email=smtp
33
+ RUN uv sync --locked --no-default-groups --group observability-otel --inexact --no-install-project # clubedge:only observability=otel
34
+ RUN uv sync --locked --no-default-groups --group observability-sentry --inexact --no-install-project # clubedge:only observability=sentry
35
+ RUN uv sync --locked --no-default-groups --group payments-stripe --inexact --no-install-project # clubedge:only payments=stripe
36
+ RUN uv sync --locked --no-default-groups --group jobs-bullmq --inexact --no-install-project # clubedge:only jobs=bullmq
37
+ RUN uv sync --locked --no-default-groups --group jobs-inngest --inexact --no-install-project # clubedge:only jobs=inngest
28
38
  COPY ${APP_DIR}/src ./src
29
- RUN uv sync --locked --no-dev
39
+ # --inexact keeps the groups installed above.
40
+ RUN uv sync --locked --no-default-groups --inexact
30
41
 
31
42
  # Bundle the shared Node migrator so deploy platforms can run it before release.
32
43
  COPY --from=migrator-builder /tmp/.migrator/ ./
33
44
 
34
45
  RUN useradd --system --uid 1001 app
35
46
  USER app
36
- ENV HOST=0.0.0.0 PORT=3001 PATH="/app/.venv/bin:$PATH"
47
+ ENV NODE_ENV=production HOST=0.0.0.0 PORT=3001 PATH="/app/.venv/bin:$PATH"
37
48
  EXPOSE 3001
38
49
  CMD ["python", "-m", "clubedge_api"]