@clubedge/create-clubedge-app 0.38.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 (221) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +13 -11
  3. package/dist/core/customize.js +6 -2
  4. package/package.json +3 -3
  5. package/template/.github/workflows/ci.yml +35 -1
  6. package/template/README.md +18 -8
  7. package/template/SETUP.md +86 -16
  8. package/template/apps/api/.env.example +108 -0
  9. package/template/apps/api/build.mjs +3 -2
  10. package/template/apps/api/clubedge.stack.json +87 -1
  11. package/template/apps/api/package.json +20 -2
  12. package/template/apps/api/scripts/check-env.ts +35 -7
  13. package/template/apps/api/scripts/check-services.ts +204 -9
  14. package/template/apps/api/src/app.test.ts +39 -1
  15. package/template/apps/api/src/app.ts +66 -7
  16. package/template/apps/api/src/config/plans.ts +16 -0
  17. package/template/apps/api/src/env/production.test.ts +88 -0
  18. package/template/apps/api/src/env/production.ts +121 -0
  19. package/template/apps/api/src/http.test.ts +38 -0
  20. package/template/apps/api/src/http.ts +30 -0
  21. package/template/apps/api/src/index.ts +22 -1
  22. package/template/apps/api/src/jobs/index.ts +31 -0
  23. package/template/apps/api/src/jobs/sample.ts +10 -0
  24. package/template/apps/api/src/rate-limit.test.ts +43 -0
  25. package/template/apps/api/src/rate-limit.ts +51 -0
  26. package/template/apps/api/src/routes/account.test.ts +141 -0
  27. package/template/apps/api/src/routes/account.ts +135 -0
  28. package/template/apps/api/src/routes/billing.test.ts +208 -0
  29. package/template/apps/api/src/routes/billing.ts +130 -0
  30. package/template/apps/api/src/routes/client-errors.test.ts +90 -0
  31. package/template/apps/api/src/routes/client-errors.ts +43 -0
  32. package/template/apps/api/src/routes/inngest.test.ts +37 -0
  33. package/template/apps/api/src/routes/inngest.ts +14 -0
  34. package/template/apps/api/src/routes/jobs-run.test.ts +39 -0
  35. package/template/apps/api/src/routes/jobs-run.ts +25 -0
  36. package/template/apps/api/src/routes/jobs.test.ts +85 -0
  37. package/template/apps/api/src/routes/jobs.ts +68 -0
  38. package/template/apps/api/src/server/account.ts +38 -0
  39. package/template/apps/api/src/server/auth.ts +18 -0
  40. package/template/apps/api/src/server/cache.test.ts +65 -0
  41. package/template/apps/api/src/server/cache.ts +57 -0
  42. package/template/apps/api/src/server/email.ts +39 -0
  43. package/template/apps/api/src/server/index.ts +13 -0
  44. package/template/apps/api/src/server/jobs.test.ts +28 -0
  45. package/template/apps/api/src/server/jobs.ts +82 -0
  46. package/template/apps/api/src/server/observability.test.ts +23 -0
  47. package/template/apps/api/src/server/observability.ts +48 -0
  48. package/template/apps/api/src/server/payments.ts +76 -0
  49. package/template/apps/api/src/server/storage.ts +64 -0
  50. package/template/apps/api/src/server/users.ts +8 -0
  51. package/template/apps/api/src/server/variants/auth.appwrite.ts +43 -0
  52. package/template/apps/api/src/server/variants/auth.better-auth.ts +32 -0
  53. package/template/apps/api/src/server/variants/auth.firebase.ts +53 -0
  54. package/template/apps/api/src/server/variants/auth.supabase.ts +31 -1
  55. package/template/apps/api/src/server/variants/cache.memory.ts +25 -0
  56. package/template/apps/api/src/server/variants/email.resend.ts +39 -0
  57. package/template/apps/api/src/server/variants/jobs.bullmq.ts +77 -0
  58. package/template/apps/api/src/server/variants/jobs.inngest.ts +96 -0
  59. package/template/apps/api/src/server/variants/observability.sentry.ts +65 -0
  60. package/template/apps/api/src/server/variants/storage.supabase.ts +59 -0
  61. package/template/apps/api/src/tracing.ts +12 -0
  62. package/template/apps/api-fastapi/.env.example +100 -0
  63. package/template/apps/api-fastapi/Dockerfile +15 -4
  64. package/template/apps/api-fastapi/clubedge.stack.json +125 -1
  65. package/template/apps/api-fastapi/package.json +2 -2
  66. package/template/apps/api-fastapi/pyproject.toml +17 -1
  67. package/template/apps/api-fastapi/src/clubedge_api/account.py +37 -0
  68. package/template/apps/api-fastapi/src/clubedge_api/app.py +113 -18
  69. package/template/apps/api-fastapi/src/clubedge_api/auth.py +18 -1
  70. package/template/apps/api-fastapi/src/clubedge_api/background_jobs.py +115 -0
  71. package/template/apps/api-fastapi/src/clubedge_api/cache.py +178 -0
  72. package/template/apps/api-fastapi/src/clubedge_api/check_env.py +39 -7
  73. package/template/apps/api-fastapi/src/clubedge_api/check_services.py +251 -12
  74. package/template/apps/api-fastapi/src/clubedge_api/contract.py +27 -0
  75. package/template/apps/api-fastapi/src/clubedge_api/contract_check.py +57 -0
  76. package/template/apps/api-fastapi/src/clubedge_api/db.py +32 -0
  77. package/template/apps/api-fastapi/src/clubedge_api/email.py +34 -0
  78. package/template/apps/api-fastapi/src/clubedge_api/email_provider.py +89 -0
  79. package/template/apps/api-fastapi/src/clubedge_api/email_resend.py +55 -0
  80. package/template/apps/api-fastapi/src/clubedge_api/email_smtp.py +83 -0
  81. package/template/apps/api-fastapi/src/clubedge_api/http.py +91 -0
  82. package/template/apps/api-fastapi/src/clubedge_api/job_queue.py +314 -0
  83. package/template/apps/api-fastapi/src/clubedge_api/jobs.py +41 -0
  84. package/template/apps/api-fastapi/src/clubedge_api/jobs_bullmq.py +305 -0
  85. package/template/apps/api-fastapi/src/clubedge_api/jobs_inngest.py +365 -0
  86. package/template/apps/api-fastapi/src/clubedge_api/main.py +25 -0
  87. package/template/apps/api-fastapi/src/clubedge_api/memory_cache.py +54 -0
  88. package/template/apps/api-fastapi/src/clubedge_api/observability.py +50 -0
  89. package/template/apps/api-fastapi/src/clubedge_api/payments.py +333 -0
  90. package/template/apps/api-fastapi/src/clubedge_api/payments_stripe.py +225 -0
  91. package/template/apps/api-fastapi/src/clubedge_api/plans.py +24 -0
  92. package/template/apps/api-fastapi/src/clubedge_api/production_env.py +140 -0
  93. package/template/apps/api-fastapi/src/clubedge_api/rate_limit.py +131 -0
  94. package/template/apps/api-fastapi/src/clubedge_api/routes/__init__.py +1 -0
  95. package/template/apps/api-fastapi/src/clubedge_api/routes/account.py +176 -0
  96. package/template/apps/api-fastapi/src/clubedge_api/routes/billing.py +173 -0
  97. package/template/apps/api-fastapi/src/clubedge_api/routes/client_errors.py +111 -0
  98. package/template/apps/api-fastapi/src/clubedge_api/routes/inngest.py +22 -0
  99. package/template/apps/api-fastapi/src/clubedge_api/routes/jobs.py +112 -0
  100. package/template/apps/api-fastapi/src/clubedge_api/routes/jobs_run.py +32 -0
  101. package/template/apps/api-fastapi/src/clubedge_api/server.py +22 -1
  102. package/template/apps/api-fastapi/src/clubedge_api/settings.py +6 -0
  103. package/template/apps/api-fastapi/src/clubedge_api/storage.py +47 -0
  104. package/template/apps/api-fastapi/src/clubedge_api/storage_local.py +102 -0
  105. package/template/apps/api-fastapi/src/clubedge_api/storage_provider.py +29 -0
  106. package/template/apps/api-fastapi/src/clubedge_api/storage_s3.py +144 -0
  107. package/template/apps/api-fastapi/src/clubedge_api/storage_supabase.py +126 -0
  108. package/template/apps/api-fastapi/src/clubedge_api/telemetry.py +85 -0
  109. package/template/apps/api-fastapi/src/clubedge_api/telemetry_otel.py +137 -0
  110. package/template/apps/api-fastapi/src/clubedge_api/telemetry_sentry.py +99 -0
  111. package/template/apps/api-fastapi/src/clubedge_api/tracing.py +38 -0
  112. package/template/apps/api-fastapi/src/clubedge_api/users.py +53 -0
  113. package/template/apps/api-fastapi/src/clubedge_api/variants/auth_appwrite.py +58 -1
  114. package/template/apps/api-fastapi/src/clubedge_api/variants/auth_better_auth.py +67 -1
  115. package/template/apps/api-fastapi/src/clubedge_api/variants/auth_firebase.py +75 -8
  116. package/template/apps/api-fastapi/src/clubedge_api/variants/auth_supabase.py +55 -1
  117. package/template/apps/api-fastapi/src/clubedge_api/variants/background_jobs_inngest.py +125 -0
  118. package/template/apps/api-fastapi/src/clubedge_api/variants/cache_memory.py +22 -0
  119. package/template/apps/api-fastapi/src/clubedge_api/variants/email_resend.py +35 -0
  120. package/template/apps/api-fastapi/src/clubedge_api/variants/observability_sentry.py +90 -0
  121. package/template/apps/api-fastapi/src/clubedge_api/variants/storage_supabase.py +37 -0
  122. package/template/apps/api-fastapi/tests/__init__.py +0 -0
  123. package/template/apps/api-fastapi/tests/fake_payments.py +172 -0
  124. package/template/apps/api-fastapi/tests/fakes.py +80 -0
  125. package/template/apps/api-fastapi/tests/test_account.py +275 -0
  126. package/template/apps/api-fastapi/tests/test_app.py +12 -19
  127. package/template/apps/api-fastapi/tests/test_background_jobs.py +67 -0
  128. package/template/apps/api-fastapi/tests/test_billing.py +205 -0
  129. package/template/apps/api-fastapi/tests/test_cache.py +134 -0
  130. package/template/apps/api-fastapi/tests/test_check_services.py +82 -0
  131. package/template/apps/api-fastapi/tests/test_client_errors.py +175 -0
  132. package/template/apps/api-fastapi/tests/test_contract_check.py +41 -0
  133. package/template/apps/api-fastapi/tests/test_email.py +82 -0
  134. package/template/apps/api-fastapi/tests/test_email_resend.py +87 -0
  135. package/template/apps/api-fastapi/tests/test_email_smtp.py +128 -0
  136. package/template/apps/api-fastapi/tests/test_http.py +84 -0
  137. package/template/apps/api-fastapi/tests/test_inngest.py +39 -0
  138. package/template/apps/api-fastapi/tests/test_job_queue.py +156 -0
  139. package/template/apps/api-fastapi/tests/test_jobs.py +95 -0
  140. package/template/apps/api-fastapi/tests/test_jobs_bullmq.py +283 -0
  141. package/template/apps/api-fastapi/tests/test_jobs_inngest.py +245 -0
  142. package/template/apps/api-fastapi/tests/test_jobs_run.py +44 -0
  143. package/template/apps/api-fastapi/tests/test_memory_cache.py +42 -0
  144. package/template/apps/api-fastapi/tests/test_observability.py +57 -0
  145. package/template/apps/api-fastapi/tests/test_payments.py +131 -0
  146. package/template/apps/api-fastapi/tests/test_payments_stripe.py +262 -0
  147. package/template/apps/api-fastapi/tests/test_production_env.py +95 -0
  148. package/template/apps/api-fastapi/tests/test_rate_limit.py +103 -0
  149. package/template/apps/api-fastapi/tests/test_storage_local.py +82 -0
  150. package/template/apps/api-fastapi/tests/test_storage_s3.py +196 -0
  151. package/template/apps/api-fastapi/tests/test_storage_supabase.py +95 -0
  152. package/template/apps/api-fastapi/tests/test_telemetry_otel.py +119 -0
  153. package/template/apps/api-fastapi/tests/test_telemetry_sentry.py +70 -0
  154. package/template/apps/api-fastapi/tests/test_users.py +48 -0
  155. package/template/apps/api-fastapi/uv.lock +581 -1
  156. package/template/apps/start/package.json +1 -1
  157. package/template/apps/vite/.env.example +27 -3
  158. package/template/apps/vite/auth.server.mjs +86 -30
  159. package/template/apps/vite/clubedge.stack.json +39 -0
  160. package/template/apps/vite/package.json +7 -2
  161. package/template/apps/vite/server.mjs +22 -5
  162. package/template/apps/vite/src/components/billing/billing-card.tsx +143 -0
  163. package/template/apps/vite/src/components/dashboard/variants/account-button.supabase.tsx +32 -21
  164. package/template/apps/vite/src/components/error-reporting.tsx +52 -0
  165. package/template/apps/vite/src/components/jobs/jobs-card.tsx +95 -0
  166. package/template/apps/vite/src/components/route-error.tsx +12 -0
  167. package/template/apps/vite/src/components/variants/error-reporting.sentry.tsx +34 -0
  168. package/template/apps/vite/src/lib/auth-client.ts +18 -0
  169. package/template/apps/vite/src/main.tsx +6 -0
  170. package/template/apps/vite/src/routes/dashboard.tsx +41 -0
  171. package/template/apps/vite/src/routes/login.tsx +21 -14
  172. package/template/apps/vite/src/routes/settings.tsx +383 -0
  173. package/template/apps/vite/src/variants/auth.appwrite.ts +22 -2
  174. package/template/apps/vite/src/variants/auth.better-auth.ts +31 -2
  175. package/template/apps/vite/src/variants/auth.firebase.ts +39 -2
  176. package/template/apps/vite/src/variants/auth.supabase.ts +20 -1
  177. package/template/apps/vite/src/variants/router.supabase.tsx +8 -0
  178. package/template/apps/web/package.json +1 -1
  179. package/template/clubedge.template.json +3 -1
  180. package/template/e2e/telemetry-fake.ts +78 -9
  181. package/template/package.json +3 -2
  182. package/template/packages/auth/package.json +1 -1
  183. package/template/packages/auth-appwrite/package.json +1 -1
  184. package/template/packages/auth-better-auth/package.json +1 -1
  185. package/template/packages/auth-firebase/package.json +1 -1
  186. package/template/packages/auth-supabase/package.json +1 -1
  187. package/template/packages/auth-supabase/src/index.test.ts +25 -3
  188. package/template/packages/auth-supabase/src/index.ts +12 -0
  189. package/template/packages/cache/package.json +1 -1
  190. package/template/packages/cache-redis/package.json +1 -1
  191. package/template/packages/contract/openapi.yaml +633 -0
  192. package/template/packages/contract/package.json +1 -1
  193. package/template/packages/contract/src/conformance.ts +268 -4
  194. package/template/packages/contract/src/index.ts +12 -0
  195. package/template/packages/contract/src/remote.test.ts +24 -0
  196. package/template/packages/contract/src/schema.test.ts +59 -2
  197. package/template/packages/contract/src/schema.ts +824 -1
  198. package/template/packages/core/package.json +1 -1
  199. package/template/packages/db/package.json +1 -1
  200. package/template/packages/email/package.json +1 -1
  201. package/template/packages/email-resend/package.json +1 -1
  202. package/template/packages/email-smtp/package.json +1 -1
  203. package/template/packages/jobs/package.json +1 -1
  204. package/template/packages/jobs-bullmq/package.json +1 -1
  205. package/template/packages/jobs-inngest/package.json +1 -1
  206. package/template/packages/jobs-inngest/src/index.test.ts +26 -0
  207. package/template/packages/jobs-inngest/src/index.ts +7 -1
  208. package/template/packages/jobs-pg-boss/package.json +1 -1
  209. package/template/packages/local-db/package.json +1 -1
  210. package/template/packages/observability/package.json +1 -1
  211. package/template/packages/observability-otel/package.json +1 -1
  212. package/template/packages/observability-sentry/package.json +1 -1
  213. package/template/packages/payments/package.json +1 -1
  214. package/template/packages/payments-stripe/package.json +1 -1
  215. package/template/packages/storage/package.json +1 -1
  216. package/template/packages/storage-s3/package.json +1 -1
  217. package/template/packages/storage-supabase/package.json +1 -1
  218. package/template/packages/ui/package.json +1 -1
  219. package/template/playwright.config.ts +2 -1
  220. package/template/pnpm-lock.yaml +75 -5
  221. package/template.lock.json +121 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,36 @@
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
+
20
+ ## 0.39.0
21
+
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:
23
+ - `--storage s3` (or a local folder with `--infra local`) and `--storage supabase`.
24
+ - `--cache redis`, for the rate limits and cache.
25
+ - `--email smtp` and `--email resend`; Better Auth's emails in the frontend's server use them too.
26
+ - `--observability otel` and `--observability sentry`, with browser error reporting.
27
+ - `--payments stripe`: the billing card, Checkout, the customer portal, and webhooks through the frontend.
28
+ - `--jobs pg-boss`, `--jobs bullmq`, and `--jobs inngest`, with the worker inside the API and a jobs card on the dashboard.
29
+ - New Vite + Hono projects get the same defaults as the full-stack apps: S3 storage, Redis cache, and SMTP email.
30
+ - `deploy` needs no change: Railway and Docker Compose already send each variable to the service that reads it.
31
+ - **Account settings on React + Vite**: projects with authentication get the settings page (display name, password change, account deletion). With FastAPI it offers the password change.
32
+ - FastAPI is unchanged: the CLI still refuses the module options it doesn't implement, with the message that names them.
33
+ - Generated React + Vite projects' own CI builds and checks the API's image too.
34
+
5
35
  ## 0.38.0
6
36
 
7
37
  - **Security: default to Starter `v0.24.2`** (#116). It updates Next.js to 16.4.3: 16.0.0 to 16.4.0 have two critical unauthenticated remote code execution advisories and a high denial of service ([GHSA-p3m5-59mp-jv5r](https://github.com/advisories/GHSA-p3m5-59mp-jv5r) and related). It also updates Zod to 4.6.7 ([GHSA-mrhx-c7xc-m3w5](https://github.com/advisories/GHSA-mrhx-c7xc-m3w5)). **Every Next.js project from an earlier Starter should run `pnpm clubedge upgrade`**, or raise `next` to `^16.4.3` in `apps/web/package.json` and run `pnpm install`. The Starter release also:
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.24.2
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.24.2 # 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.38.0
340
+ create-clubedge-app v0.40.0
341
341
  |
342
342
  v
343
- Clubedge Starter v0.24.2
344
- commit 4450d2f00a89f1a3929a5c9d2c1541e0d65df891
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.38.0` | `clubedge-starter@v0.24.2` | 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.24.2` tag is expected to resolve to commit
356
- `4450d2f00a89f1a3929a5c9d2c1541e0d65df891`. 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.24.2`. 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.24.2`, 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
 
@@ -461,6 +461,8 @@ Each phase ends with a Starter release and a CLI release pinned to it. The full
461
461
  | 0.36.0 | v0.24.1 | Starter v0.24.1: security update of FastAPI's `certifi`; dependency updates |
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
+ | 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) |
464
466
 
465
467
  ## Community
466
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.38.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.24.2",
28
- "starterCommit": "4450d2f00a89f1a3929a5c9d2c1541e0d65df891"
27
+ "starterRef": "v0.26.0",
28
+ "starterCommit": "7c01a0295bea53adc72bda5b843a478ddd04f892"
29
29
  },
30
30
  "license": "Apache-2.0",
31
31
  "repository": {
@@ -97,9 +97,20 @@ jobs:
97
97
  --health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 10
98
98
  steps:
99
99
  - uses: actions/checkout@v7
100
+ # clubedge:if api=none
100
101
  - name: Build the production image
101
102
  # A public URL of its own, to check that it reaches the build.
102
103
  run: docker build --build-arg NEXT_PUBLIC_APP_URL=https://app.ci.invalid --tag app:ci .
104
+ # clubedge:end
105
+ # clubedge:if api!=none
106
+ # The frontend's image, and the API's, which owns the database and carries the migrator.
107
+ # Their own tags: the Starter repository runs every block of this file.
108
+ - name: Build the production images
109
+ run: |
110
+ set -euo pipefail
111
+ docker build --tag web:ci .
112
+ docker build --file apps/api/Dockerfile --tag api:ci .
113
+ # clubedge:end
103
114
  # clubedge:if framework=next
104
115
  - name: Check that the public URL is compiled into the build
105
116
  # Next.js inlines NEXT_PUBLIC_* at build time, server code included; without the build
@@ -110,9 +121,11 @@ jobs:
110
121
  run: |
111
122
  set -euo pipefail
112
123
  for run in 1 2; do
113
- docker run --rm --network host --env DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/app app:ci node /app/migrate.mjs
124
+ docker run --rm --network host --env DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/app app:ci node /app/migrate.mjs # clubedge:only api=none
125
+ docker run --rm --network host --env DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/app api:ci node /app/migrate.mjs # clubedge:only api!=none
114
126
  done
115
127
  PGPASSWORD=postgres psql --host 127.0.0.1 --username postgres --dbname app --command "select count(*) from public.users"
128
+ # clubedge:if api=none
116
129
  - name: Start the image and check health
117
130
  run: |
118
131
  set -euo pipefail
@@ -128,3 +141,24 @@ jobs:
128
141
  done
129
142
  echo "The container did not become healthy."
130
143
  exit 1
144
+ # clubedge:end
145
+ # clubedge:if api!=none
146
+ # On the host's network, like the migrations: the API on port 3001, and the frontend on
147
+ # 3000, which forwards /api to it. Both must answer: the API itself, and through the frontend.
148
+ - name: Start the images and check health through the frontend
149
+ run: |
150
+ set -euo pipefail
151
+ docker run --detach --name api --network host \
152
+ --env DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/app api:ci
153
+ docker run --detach --name web --network host --env API_URL=http://127.0.0.1:3001 web:ci
154
+ trap 'docker logs api; docker logs web; docker rm --force api web' EXIT
155
+ for attempt in $(seq 1 30); do
156
+ if curl --fail --silent http://127.0.0.1:3001/api/health &&
157
+ curl --fail --silent http://127.0.0.1:3000/api/health; then
158
+ exit 0
159
+ fi
160
+ sleep 2
161
+ done
162
+ echo "The containers did not become healthy."
163
+ exit 1
164
+ # clubedge:end
@@ -48,6 +48,8 @@ Every combination in the CI matrix is generated as a real project and runs its f
48
48
 
49
49
  - Next.js App Router with React, TypeScript, Tailwind CSS 4, and shadcn/ui using Base UI primitives. <!-- clubedge:only framework=next -->
50
50
  - TanStack Start with React, TypeScript, Vite, Tailwind CSS 4, and shadcn/ui using Base UI primitives. <!-- clubedge:only framework=tanstack-start -->
51
+ - A React + Vite single-page app with TypeScript, TanStack Router, Tailwind CSS 4, and shadcn/ui using Base UI primitives, and a Hono API in TypeScript in `apps/api`, joined by one OpenAPI contract. <!-- clubedge:only framework=vite && api=hono -->
52
+ - A React + Vite single-page app with TypeScript, TanStack Router, Tailwind CSS 4, and shadcn/ui using Base UI primitives, and a FastAPI API in Python in `apps/api`, joined by one OpenAPI contract. <!-- clubedge:only framework=vite && api=fastapi -->
51
53
  - pnpm workspaces and Turborepo, with the web app in `apps/web` and framework-agnostic packages under `packages/`.
52
54
  - Drizzle ORM and PostgreSQL schema, migrations, and seed commands.
53
55
  - Zod environment validation, security headers, structured errors, and `/api/health`.
@@ -55,13 +57,18 @@ Every combination in the CI matrix is generated as a real project and runs its f
55
57
 
56
58
  **Modules**
57
59
 
58
- - Supabase Auth with server-verified sessions in secure cookies, a protected dashboard, and rate-limited sign-in. <!-- clubedge:only auth=supabase -->
59
- - Better Auth with users and sessions in your own PostgreSQL, secure session cookies, a protected dashboard, and rate-limited sign-in. <!-- clubedge:only auth=better-auth -->
60
- - Firebase Authentication through the app's own pages, with server-side session cookies, a protected dashboard, and rate-limited sign-in; the Auth Emulator in development. <!-- clubedge:only auth=firebase -->
61
- - Appwrite accounts through the app's own pages, on Appwrite Cloud or your own server, with server-side session cookies, a protected dashboard, and rate-limited sign-in. <!-- clubedge:only auth=appwrite -->
60
+ - Supabase Auth with server-verified sessions in secure cookies, a protected dashboard, and rate-limited sign-in. <!-- clubedge:only auth=supabase && framework!=vite -->
61
+ - Better Auth with users and sessions in your own PostgreSQL, secure session cookies, a protected dashboard, and rate-limited sign-in. <!-- clubedge:only auth=better-auth && framework!=vite -->
62
+ - Firebase Authentication through the app's own pages, with server-side session cookies, a protected dashboard, and rate-limited sign-in; the Auth Emulator in development. <!-- clubedge:only auth=firebase && framework!=vite -->
63
+ - Appwrite accounts through the app's own pages, on Appwrite Cloud or your own server, with server-side session cookies, a protected dashboard, and rate-limited sign-in. <!-- clubedge:only auth=appwrite && framework!=vite -->
64
+ - Supabase Auth in the browser, with access tokens that the API verifies on each request, and a protected dashboard. <!-- clubedge:only auth=supabase && framework=vite -->
65
+ - Better Auth in the frontend's server, with users and sessions in your own PostgreSQL, a secure session cookie that the API checks, a protected dashboard, and rate-limited sign-in. <!-- clubedge:only auth=better-auth && framework=vite -->
66
+ - Firebase Authentication in the browser, with ID tokens that the API verifies on each request, and a protected dashboard; the Auth Emulator in development. <!-- clubedge:only auth=firebase && framework=vite -->
67
+ - Appwrite accounts in the browser, on Appwrite Cloud or your own server, with JWTs that the API verifies on each request, and a protected dashboard. <!-- clubedge:only auth=appwrite && framework=vite -->
62
68
  - Password reset by email, and email verification that can be required before sign-in. <!-- clubedge:only auth=better-auth && email!=none -->
63
69
  - Password reset by email, sent by Supabase. <!-- clubedge:only auth=supabase -->
64
- - An account settings page: display name, password change, and account deletion. <!-- clubedge:only auth!=none -->
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|fastapi -->
65
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 -->
66
73
  - Email through Resend; development prints emails to the server log until the API key is set. <!-- clubedge:only email=resend -->
67
74
  - S3-compatible storage adapter for AWS S3, Cloudflare R2, and MinIO. <!-- clubedge:only storage=s3 -->
@@ -69,15 +76,18 @@ Every combination in the CI matrix is generated as a real project and runs its f
69
76
  - Fixed-window rate limiter and cache on Redis, falling back to process memory without `REDIS_URL`. <!-- clubedge:only cache=redis -->
70
77
  - Fixed-window rate limiter and cache in process memory, suited to a single server instance. <!-- clubedge:only cache=memory -->
71
78
  - Subscriptions with Stripe Checkout and the customer portal, kept in sync by verified webhooks, behind a provider-neutral payments interface. <!-- clubedge:only payments=stripe -->
72
- - Background jobs with retries and schedules, stored in PostgreSQL by pg-boss and run by a worker inside the app server, behind a provider-neutral jobs interface. <!-- clubedge:only jobs=pg-boss -->
73
- - Background jobs with retries and schedules, stored in Redis by BullMQ and run by a worker inside the app server, behind a provider-neutral jobs interface. <!-- clubedge:only jobs=bullmq -->
79
+ - 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 && api=none -->
80
+ - Background jobs with retries and schedules, stored in PostgreSQL by pg-boss and run by a worker inside the API, behind a provider-neutral jobs interface. <!-- clubedge:only jobs=pg-boss && api!=none -->
81
+ - Background jobs with retries and schedules, stored in Redis by BullMQ and run by a worker inside the app server, behind a provider-neutral jobs interface. <!-- clubedge:only jobs=bullmq && api=none -->
82
+ - Background jobs with retries and schedules, stored in Redis by BullMQ and run by a worker inside the API, behind a provider-neutral jobs interface. <!-- clubedge:only jobs=bullmq && api!=none -->
74
83
  - Background jobs with retries and schedules, run by Inngest, which calls the app for each one, behind a provider-neutral jobs interface. <!-- clubedge:only jobs=inngest -->
75
84
  - Server error reporting and request tracing with OpenTelemetry, to any OTLP collector, behind a provider-neutral observability interface. <!-- clubedge:only observability=otel -->
76
85
  - Server error reporting and request tracing with Sentry, behind a provider-neutral observability interface. <!-- clubedge:only observability=sentry -->
77
86
 
78
87
  **Production**
79
88
 
80
- - A production image that includes its own migrator (`node /app/migrate.mjs`), for running migrations before each release.
89
+ - A production image that includes its own migrator (`node /app/migrate.mjs`), for running migrations before each release. <!-- clubedge:only api=none -->
90
+ - Production images for the frontend, served by Node, and for the API, which includes the migrator (`node /app/migrate.mjs`) for running migrations before each release. <!-- clubedge:only api!=none -->
81
91
  - A manual deploy workflow for the chosen platform, and a readiness endpoint (`/api/ready`) that checks the database.
82
92
  - Deployment to Fly.io with `fly.toml`: migrations run as the release command. <!-- clubedge:only deploy=fly -->
83
93
  - Deployment to Railway with `.railway/railway.ts`: migrations run before each release, and `pnpm clubedge deploy` sets up the project. <!-- clubedge:only deploy=railway -->
package/template/SETUP.md CHANGED
@@ -72,6 +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|fastapi -->
75
76
 
76
77
  In Supabase Auth URL configuration, add this redirect URL for local email confirmation and password reset links:
77
78
 
@@ -96,6 +97,7 @@ openssl rand -base64 32
96
97
  Put the result in `BETTER_AUTH_SECRET`. Until it is set, the dashboard stays viewable as a demo and the login page explains what is missing; once it is set, the dashboard redirects signed-out visitors to the login page. Signing in again on another device creates another session row, and signing out deletes it.
97
98
 
98
99
  Sign-up signs the user in immediately; add email verification in `packages/auth-better-auth` if your application needs it. <!-- clubedge:only email=none -->
100
+ The frontend's server still sends password reset and verification emails through Resend when `RESEND_API_KEY` and `EMAIL_FROM` are both set in `apps/web/.env.local`. <!-- clubedge:only email=none && api!=none -->
99
101
  Better Auth sends its emails through the [email module](#email): <!-- clubedge:only email!=none -->
100
102
 
101
103
  - **Password reset:** "Forgot password?" on the login page emails a link to `/reset-password`, valid for one hour and usable once. <!-- clubedge:only email!=none -->
@@ -245,6 +247,8 @@ Use `rediss://` for TLS connections such as Upstash and `redis://` for an unencr
245
247
 
246
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.
247
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!=none -->
251
+
248
252
  <!-- clubedge:end -->
249
253
  <!-- clubedge:if cache=memory -->
250
254
 
@@ -257,7 +261,10 @@ The rate limiter and cache keep their state in process memory, which suits a sin
257
261
 
258
262
  ## Email
259
263
 
260
- 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 })`.
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=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 -->
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 -->
261
268
 
262
269
  <!-- clubedge:end -->
263
270
  <!-- clubedge:if email=smtp -->
@@ -279,12 +286,15 @@ Without a provider configured, development prints each email to the server log,
279
286
 
280
287
  ## Payments
281
288
 
282
- 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`.
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 -->
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 -->
283
292
 
284
293
  <!-- clubedge:end -->
285
294
  <!-- clubedge:if payments=stripe -->
286
295
 
287
- [Stripe](https://stripe.com) runs checkout and billing management. In test mode:
296
+ [Stripe](https://stripe.com) runs checkout and billing management. In test mode: <!-- clubedge:only api=none -->
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 -->
288
298
 
289
299
  1. Copy the secret key from the dashboard's API keys into `STRIPE_SECRET_KEY`.
290
300
  2. Create a recurring price for each plan and put its id in `STRIPE_PRICE_<PLAN>`, such as `STRIPE_PRICE_PRO`.
@@ -294,23 +304,35 @@ The plans users can buy are in `apps/web/src/config/plans.ts`, with the app's ow
294
304
  stripe listen --forward-to localhost:3000/api/webhooks/payments
295
305
  ```
296
306
 
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 -->
308
+
297
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.
298
310
 
299
311
  <!-- clubedge:end -->
300
312
  <!-- clubedge:if payments!=none -->
301
313
 
302
- Payments go through the provider-neutral `PaymentProvider` interface in `packages/payments`. To use another provider, implement that interface in a new package and return it from `apps/web/src/server/payments.ts`; billing logic, the tables, the routes, and the billing card stay the same.
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 -->
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 -->
303
317
 
304
318
  <!-- clubedge:end -->
305
319
  <!-- clubedge:if jobs!=none -->
306
320
 
307
321
  ## Background jobs
308
322
 
309
- Jobs run outside requests, with retries, in a worker that starts inside the app server: `pnpm dev` and the production server both run it, so there is no separate process to deploy. [pg-boss](https://github.com/timgit/pg-boss) stores them in your PostgreSQL, and creates and upgrades its own tables in the `pgboss` schema when the app starts. <!-- clubedge:only jobs=pg-boss -->
310
- Jobs run outside requests, with retries, in a worker that starts inside the app server: `pnpm dev` and the production server both run it, so there is no separate process to deploy. [BullMQ](https://docs.bullmq.io) stores them in Redis (`REDIS_URL`, Redis 6.2 or newer), which the cache also uses; keep it on a Redis that doesn't evict keys, since jobs are data. <!-- clubedge:only jobs=bullmq -->
311
- [Inngest](https://www.inngest.com) runs the jobs: the app sends an event for each one, and Inngest calls the app's `/api/inngest` to run it, with retries and schedules, so nothing runs between requests and serverless hosts need no cron. In development, start Inngest's Dev Server next to `pnpm dev` with `pnpm jobs:dev`, and open its dashboard at <http://localhost:8288> to see every run; `INNGEST_DEV=1` points the app at it. In production, create an app in Inngest, set `INNGEST_EVENT_KEY` and `INNGEST_SIGNING_KEY`, remove `INNGEST_DEV`, and sync the deployed `/api/inngest` URL in Inngest's dashboard (Inngest's Vercel and Netlify integrations do it on each deploy). <!-- clubedge:only jobs=inngest -->
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 -->
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 -->
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 -->
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 -->
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 -->
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 -->
329
+
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 -->
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 -->
312
333
 
313
- Define a job in `apps/web/src/jobs` and add it to the list in `apps/web/src/jobs/index.ts`:
334
+ <!-- clubedge:end -->
335
+ <!-- clubedge:if jobs!=none && api!=fastapi -->
314
336
 
315
337
  ```ts
316
338
  import { defineJob } from "@clubedge/jobs";
@@ -324,15 +346,41 @@ export const welcomeEmail = defineJob<{ userId: string }>({
324
346
  });
325
347
  ```
326
348
 
327
- Queue it from server code with `getJobs().enqueue(welcomeEmail, { userId })`, from `@/server/jobs`. To send any email in the background, enqueue `sendEmailJob` from `@/jobs` with the message, as the auth emails do. <!-- clubedge:only email!=none -->
328
- Queue it from server code with `getJobs().enqueue(welcomeEmail, { userId })`, from `@/server/jobs`. <!-- clubedge:only email=none -->
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
+
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 -->
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 -->
365
+ Queue it from server code with `getJobs().enqueue(welcomeEmail, { userId })`, from `@/server/jobs`. <!-- clubedge:only email=none && api=none -->
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 -->
329
369
 
330
- A thrown error fails the attempt and the job is retried (`retryLimit`, `retryDelaySeconds`, `retryBackoff`). An attempt that runs past `timeoutSeconds`, or whose server stops, is retried too, so make jobs safe to run twice. Add `schedule: "0 3 * * *"` (cron syntax, UTC) to run a job on a schedule. Restart `pnpm dev` after adding or changing a job. The dashboard's jobs card queues the sample job from `apps/web/src/jobs/sample.ts` and lists its recent runs; remove both when you add your own.
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 -->
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 -->
331
373
 
332
- Where no server keeps running, as on Vercel and Netlify, there is no worker: `JOBS_WORKER` defaults to `off` there, and you can set it yourself anywhere. Set `CRON_SECRET` to 16 or more random characters and call `GET /api/jobs/run` every minute or so with the header `Authorization: Bearer <CRON_SECRET>`, from Vercel Cron, a Netlify scheduled function, or any scheduler. Each call runs the jobs that are due. <!-- clubedge:only jobs=pg-boss|bullmq -->
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 -->
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 -->
333
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 -->
334
377
 
335
- Jobs go through the provider-neutral `JobQueue` interface in `packages/jobs`, so job definitions, the routes, and the jobs card stay the same whichever provider runs them. Switch providers with `pnpm clubedge add jobs pg-boss`, `bullmq`, or `inngest`. To add another, implement `JobQueue` in a new package, return it from `apps/web/src/server/jobs.ts`, and prove it with the shared conformance suite, as each provider's tests do:
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 -->
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 -->
336
384
 
337
385
  ```ts
338
386
  import { describeJobQueue } from "@clubedge/jobs/conformance";
@@ -352,6 +400,7 @@ Server errors and traces go to the tool you chose. Errors that escape a page, ro
352
400
 
353
401
  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 -->
354
402
  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 -->
403
+ Set `OTEL_EXPORTER_OTLP_ENDPOINT` in `apps/api/.env.local` 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. The API sends traces 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 API's service id. <!-- clubedge:only framework=vite -->
355
404
 
356
405
  To see traces during development, run [Jaeger](https://www.jaegertracing.io), set `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318`, and open http://localhost:16686:
357
406
 
@@ -364,17 +413,22 @@ docker run --rm -p 16686:16686 -p 4318:4318 jaegertracing/jaeger:latest
364
413
 
365
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 -->
366
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 -->
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 -->
367
418
 
368
- 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.
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 -->
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 -->
369
421
 
370
422
  <!-- clubedge:end -->
371
423
  <!-- clubedge:if observability!=none -->
372
424
 
373
425
  Uncaught errors in the browser are reported too, with the page's path and no query string. With OpenTelemetry, the browser sends them to `/api/client-errors`, which reports them like server errors and accepts 30 reports a minute per address. With Sentry, Sentry's browser SDK sends them directly; it is downloaded only when `SENTRY_DSN` is set. In both cases the browser reads its settings from `/api/client-errors` at runtime, so builds never bake them in.
374
426
 
375
- 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.
427
+ 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. <!-- clubedge:only framework!=vite -->
376
428
 
377
- 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.
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 -->
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 -->
378
432
 
379
433
  <!-- clubedge:end -->
380
434
  <!-- clubedge:if storage!=none -->
@@ -391,6 +445,18 @@ The storage adapter works with AWS S3 and S3-compatible providers such as Cloudf
391
445
 
392
446
  The storage adapter uses Supabase Storage through the signed-in user's Supabase session, so bucket policies can rely on that user. Set `SUPABASE_STORAGE_BUCKET` and choose a bucket policy that matches your application's access model.
393
447
 
448
+ <!-- clubedge:end -->
449
+ <!-- clubedge:if storage!=none && api=hono -->
450
+
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 -->
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 -->
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
+
394
460
  <!-- clubedge:end -->
395
461
  <!-- clubedge:if storage!=none -->
396
462
 
@@ -614,6 +680,10 @@ To change the API, edit `openapi.yaml` first, run `pnpm --filter @clubedge/contr
614
680
  API_URL=http://127.0.0.1:3001 pnpm exec vitest run packages/contract/src/remote.test.ts
615
681
  ```
616
682
 
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`).
684
+
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 -->
686
+
617
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.
618
688
 
619
689
  Run `node migrate.mjs` in the API's image before a new version starts. <!-- clubedge:only api=hono -->