@clubedge/create-clubedge-app 0.26.0 → 0.27.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 (42) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +11 -10
  3. package/package.json +3 -3
  4. package/template/README.md +3 -2
  5. package/template/SETUP.md +4 -0
  6. package/template/apps/start/netlify.toml +3 -2
  7. package/template/apps/start/package.json +3 -2
  8. package/template/apps/start/scripts/check-env.ts +34 -0
  9. package/template/apps/start/src/env/production.test.ts +65 -0
  10. package/template/apps/start/src/env/production.ts +97 -0
  11. package/template/apps/start/vercel.json +1 -1
  12. package/template/apps/web/netlify.toml +3 -2
  13. package/template/apps/web/package.json +3 -2
  14. package/template/apps/web/scripts/check-env.ts +34 -0
  15. package/template/apps/web/src/env/production.test.ts +65 -0
  16. package/template/apps/web/src/env/production.ts +97 -0
  17. package/template/apps/web/vercel.json +1 -1
  18. package/template/clubedge.template.json +3 -0
  19. package/template/package.json +2 -1
  20. package/template/packages/auth/package.json +1 -1
  21. package/template/packages/auth-better-auth/package.json +1 -1
  22. package/template/packages/auth-supabase/package.json +1 -1
  23. package/template/packages/cache/package.json +1 -1
  24. package/template/packages/cache-redis/package.json +1 -1
  25. package/template/packages/core/package.json +1 -1
  26. package/template/packages/db/package.json +1 -1
  27. package/template/packages/email/package.json +1 -1
  28. package/template/packages/email-resend/package.json +1 -1
  29. package/template/packages/email-smtp/package.json +1 -1
  30. package/template/packages/jobs/package.json +1 -1
  31. package/template/packages/jobs-pg-boss/package.json +1 -1
  32. package/template/packages/local-db/package.json +1 -1
  33. package/template/packages/observability/package.json +1 -1
  34. package/template/packages/observability-otel/package.json +1 -1
  35. package/template/packages/observability-sentry/package.json +1 -1
  36. package/template/packages/payments/package.json +1 -1
  37. package/template/packages/payments-stripe/package.json +1 -1
  38. package/template/packages/storage/package.json +1 -1
  39. package/template/packages/storage-s3/package.json +1 -1
  40. package/template/packages/storage-supabase/package.json +1 -1
  41. package/template/packages/ui/package.json +1 -1
  42. package/template.lock.json +8 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to `create-clubedge-app` are documented here.
4
4
 
5
+ ## 0.27.0
6
+
7
+ - Default to Starter `v0.18.0` (#77), which stops Vercel and Netlify builds when a production variable is missing:
8
+ - On those hosts the platform only runs the app, and the database, Redis, storage, and email come from your providers. `pnpm check:env` now runs first in the build command of `vercel.json` and `netlify.toml`, before migrations.
9
+ - It lists each variable the project's modules need that is unset, points at `localhost`, or isn't an absolute URL, and stops the build. That includes `.env.example`'s `example.com` sender in `EMAIL_FROM`.
10
+ - Generated projects check only their own modules. `SKIP_ENV_CHECK=1` builds anyway.
11
+ - If you deploy to Vercel or Netlify: run `pnpm clubedge upgrade`, then set the variables for every environment you build (Production and Preview).
12
+
5
13
  ## 0.26.0
6
14
 
7
15
  - `deploy` uses your own databases when the env file has their URLs (#74). The platform hosts everything as before when it doesn't.
package/README.md CHANGED
@@ -127,7 +127,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git --framework tanstac
127
127
  pnpm dlx @clubedge/create-clubedge-app my-app --ref main
128
128
 
129
129
  # Reproduce a specific Starter release or commit
130
- pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.17.1
130
+ pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.18.0
131
131
  pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
132
132
 
133
133
  # Test local Starter changes before releasing them
@@ -146,7 +146,7 @@ pnpm clubedge add email resend payments stripe # several modules as
146
146
  pnpm clubedge remove storage payments
147
147
  pnpm dlx @clubedge/create-clubedge-app add infra local --dry-run
148
148
  pnpm dlx @clubedge/create-clubedge-app@latest upgrade # to the newest CLI's Starter release
149
- pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.17.1 # or a specific tag, branch, or commit
149
+ pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.18.0 # or a specific tag, branch, or commit
150
150
  ```
151
151
 
152
152
  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.
@@ -210,27 +210,27 @@ In a project with `--deploy fly`, `pnpm clubedge deploy` does the same with [Fly
210
210
  Each CLI release is tested against one specific Starter release:
211
211
 
212
212
  ```text
213
- create-clubedge-app v0.26.0
213
+ create-clubedge-app v0.27.0
214
214
  |
215
215
  v
216
- Clubedge Starter v0.17.1
217
- commit d9d787b6750e862b9de81a3f8f36b7aa07757f7a
216
+ Clubedge Starter v0.18.0
217
+ commit bcd084d7d2d2aac0600ccb35491f48c251fb3a74
218
218
  ```
219
219
 
220
220
  The current relationship is:
221
221
 
222
222
  | CLI release | Default Starter release | Meaning |
223
223
  | --- | --- | --- |
224
- | `create-clubedge-app@0.26.0` | `clubedge-starter@v0.17.1` | The CLI's required, tested default |
224
+ | `create-clubedge-app@0.27.0` | `clubedge-starter@v0.18.0` | The CLI's required, tested default |
225
225
 
226
226
  This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
227
227
 
228
- The `v0.17.1` tag is expected to resolve to commit
229
- `d9d787b6750e862b9de81a3f8f36b7aa07757f7a`. The CLI verifies this before
228
+ The `v0.18.0` tag is expected to resolve to commit
229
+ `bcd084d7d2d2aac0600ccb35491f48c251fb3a74`. The CLI verifies this before
230
230
  scaffolding with the default reference and fails rather than silently using a
231
231
  moved tag.
232
232
 
233
- When the Starter changes, its maintainers first publish a new versioned release, such as `v0.17.1`. The CLI is then updated to use that release, all generated-project checks must pass, and a new CLI version is published. This keeps Starter and CLI releases independently versioned while preserving a tested compatibility relationship.
233
+ When the Starter changes, its maintainers first publish a new versioned release, such as `v0.18.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.
234
234
 
235
235
  Every generated project records its CLI version, Starter repository, Starter ref, Starter commit, framework, and modules in `package.json` and `README.md`.
236
236
 
@@ -285,7 +285,7 @@ CI packs the CLI with `npm pack`, scaffolds clean projects from the packed tarba
285
285
  1. Make and validate changes in `clubedge-starter`.
286
286
  2. Run lint, typecheck, unit tests, browser tests, and production build.
287
287
  3. Update the Starter package versions.
288
- 4. Commit `main`, create a release tag such as `v0.17.1`, and publish the Starter release.
288
+ 4. Commit `main`, create a release tag such as `v0.18.0`, and publish the Starter release.
289
289
 
290
290
  ### CLI maintainers
291
291
 
@@ -321,6 +321,7 @@ Each phase ends with a Starter release and a CLI release pinned to it. The full
321
321
  | 0.24.0 | v0.17.0 | Starter v0.17.0: Railway configuration as code (`.railway/railway.ts`) instead of `railway.json` |
322
322
  | 0.25.0 | v0.17.1 | `deploy` to Fly.io (experimental); weekly Dependabot updates |
323
323
  | 0.26.0 | v0.17.1 | `deploy` uses your own databases (Supabase, Neon, Upstash) when the env file has their URLs, and reads `.env.production` |
324
+ | 0.27.0 | v0.18.0 | Starter v0.18.0: Vercel and Netlify builds stop when a production variable is missing (`pnpm check:env`) |
324
325
  | next | | Show the Starter's version of a conflicted file next to yours |
325
326
 
326
327
  ## Community
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/create-clubedge-app",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Create a Next.js or TanStack Start application from the Clubedge Starter reference implementation.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.9.0",
@@ -22,8 +22,8 @@
22
22
  },
23
23
  "clubedge": {
24
24
  "starterRepository": "Clubedge/clubedge-starter",
25
- "starterRef": "v0.17.1",
26
- "starterCommit": "d9d787b6750e862b9de81a3f8f36b7aa07757f7a"
25
+ "starterRef": "v0.18.0",
26
+ "starterCommit": "bcd084d7d2d2aac0600ccb35491f48c251fb3a74"
27
27
  },
28
28
  "license": "Apache-2.0",
29
29
  "repository": {
@@ -76,8 +76,8 @@ Every combination in the CI matrix is generated as a real project and runs its f
76
76
  - Deployment to Fly.io with `fly.toml`: migrations run as the release command. <!-- clubedge:only deploy=fly -->
77
77
  - Deployment to Railway with `.railway/railway.ts`: migrations run before each release, and `pnpm clubedge deploy` sets up the project. <!-- clubedge:only deploy=railway -->
78
78
  - Deployment to Render with a `render.yaml` Blueprint: migrations run before each deploy. <!-- clubedge:only deploy=render -->
79
- - Deployment to Vercel with `apps/web/vercel.json`: migrations run during the build. <!-- clubedge:only deploy=vercel -->
80
- - Deployment to Netlify with `apps/web/netlify.toml`: migrations run during the build. <!-- clubedge:only deploy=netlify -->
79
+ - Deployment to Vercel with `apps/web/vercel.json`: the build stops when a production variable is missing, then runs migrations. <!-- clubedge:only deploy=vercel -->
80
+ - Deployment to Netlify with `apps/web/netlify.toml`: the build stops when a production variable is missing, then runs migrations. <!-- clubedge:only deploy=netlify -->
81
81
 
82
82
  **Quality**
83
83
 
@@ -270,6 +270,7 @@ Playwright's first run may require installing Chromium with `pnpm exec playwrigh
270
270
  Local services:
271
271
 
272
272
  - `pnpm check:services`: check every configured service with a real round trip.
273
+ - `pnpm check:env`: check that the environment has every production variable the project's modules need. Vercel and Netlify builds run it first. <!-- clubedge:only deploy=vercel|netlify -->
273
274
  - `pnpm infra:up` and `pnpm infra:down`: start and stop the local services from `docker-compose.dev.yml`. <!-- clubedge:only infra=docker -->
274
275
  - `pnpm infra:reset`: stop them and delete their data. `pnpm infra:logs` follows their logs. <!-- clubedge:only infra=docker -->
275
276
  - `pnpm infra:up` and `pnpm infra:down`: start and stop the local Supabase stack. <!-- clubedge:only infra=supabase -->
package/template/SETUP.md CHANGED
@@ -408,6 +408,8 @@ Render runs `node /app/migrate.mjs` as the pre-deploy command before each deploy
408
408
  `apps/web/vercel.json` deploys the app to [Vercel](https://vercel.com). Import the repository, set the project's Root Directory to `apps/web`, add the production variables listed below, and deploy. Vercel installs the whole pnpm workspace and builds the app. <!-- clubedge:only framework=next -->
409
409
  `apps/web/vercel.json` deploys the app to [Vercel](https://vercel.com). Import the repository, set the project's Root Directory to `apps/web`, add the production variables listed below, and deploy. The build detects Vercel and writes Vercel's Build Output, with server routes as functions. <!-- clubedge:only framework=tanstack-start -->
410
410
 
411
+ Vercel only runs the app: the database, Redis, file storage, and email come from your providers (for example Supabase, Upstash, and Cloudflare R2). The build command starts with `pnpm check:env`, which stops the build when a variable the project's modules need is unset, or points at `localhost`, and lists each one. Set them for every environment you deploy (Production and Preview); `SKIP_ENV_CHECK=1` builds anyway.
412
+
411
413
  Vercel has no step that runs before a release, so the build command applies migrations (`pnpm db:migrate`) against the `DATABASE_URL` of the deployment's environment. Give Preview deployments their own database, or remove the migration step from `buildCommand` and run migrations yourself. Serverless functions open a connection per instance: use your provider's pooled connection URL (for example Supabase's transaction pooler or Neon's pooled endpoint).
412
414
 
413
415
  Vercel runs no worker for [background jobs](#background-jobs). Set `CRON_SECRET` and add `"crons": [{ "path": "/api/jobs/run", "schedule": "* * * * *" }]` to `vercel.json`; Vercel Cron sends the secret itself. Cron jobs more frequent than daily need a paid Vercel plan; otherwise call the endpoint from another scheduler. <!-- clubedge:only jobs=pg-boss -->
@@ -418,6 +420,8 @@ Vercel runs no worker for [background jobs](#background-jobs). Set `CRON_SECRET`
418
420
  `apps/web/netlify.toml` deploys the app to [Netlify](https://www.netlify.com). Import the repository, set the site's Base directory to `apps/web`, add the production variables listed below, and deploy. Netlify installs its Next.js runtime automatically. <!-- clubedge:only framework=next -->
419
421
  `apps/web/netlify.toml` deploys the app to [Netlify](https://www.netlify.com). Import the repository, set the site's Base directory to `apps/web`, add the production variables listed below, and deploy. The build detects Netlify and writes its functions and static assets. <!-- clubedge:only framework=tanstack-start -->
420
422
 
423
+ Netlify only runs the app: the database, Redis, file storage, and email come from your providers (for example Supabase, Upstash, and Cloudflare R2). The build command starts with `pnpm check:env`, which stops the build when a variable the project's modules need is unset, or points at `localhost`, and lists each one. Set them for every deploy context you build (production and deploy previews); `SKIP_ENV_CHECK=1` builds anyway.
424
+
421
425
  Netlify has no step that runs before a release, so the build command applies migrations (`pnpm db:migrate`) against the `DATABASE_URL` of the deploy context. Give deploy previews their own database, or remove the migration step from the build command and run migrations yourself. Serverless functions open a connection per instance: use your provider's pooled connection URL (for example Supabase's transaction pooler or Neon's pooled endpoint).
422
426
 
423
427
  Netlify runs no worker for [background jobs](#background-jobs). Set `JOBS_WORKER=off` and `CRON_SECRET`, then call `/api/jobs/run` every minute from a [scheduled function](https://docs.netlify.com/build/functions/scheduled-functions/) or another scheduler, with the header `Authorization: Bearer <CRON_SECRET>`. <!-- clubedge:only jobs=pg-boss -->
@@ -1,6 +1,7 @@
1
1
  # Netlify (https://docs.netlify.com/configure-builds/file-based-configuration/). Set the site's
2
2
  # base directory to apps/web; the build detects Netlify and writes its functions and assets.
3
3
  [build]
4
- # Migrations run during the build, against the DATABASE_URL set for the deploy context.
5
- command = "pnpm --dir ../.. db:migrate && pnpm run build"
4
+ # check:env stops the build when a variable the project needs is unset; then migrations run,
5
+ # against the DATABASE_URL set for the deploy context.
6
+ command = "pnpm run check:env && pnpm --dir ../.. db:migrate && pnpm run build"
6
7
  publish = "dist"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/start",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -9,7 +9,8 @@
9
9
  "build": "vite build",
10
10
  "start": "node --env-file-if-exists=.env.local .output/server/index.mjs",
11
11
  "typecheck": "tsr generate && tsc --noEmit",
12
- "check:services": "tsx scripts/check-services.ts"
12
+ "check:services": "tsx scripts/check-services.ts",
13
+ "check:env": "tsx scripts/check-env.ts"
13
14
  },
14
15
  "dependencies": {
15
16
  "@clubedge/auth": "workspace:*",
@@ -0,0 +1,34 @@
1
+ // Checks the production variables before a deploy build: `pnpm check:env`. Vercel's and Netlify's
2
+ // build commands run it first, so a missing key stops the build. It reads the build's environment,
3
+ // not .env.local; SKIP_ENV_CHECK=1 builds anyway.
4
+ import { checkProductionEnv } from "../src/env/production";
5
+
6
+ function main() {
7
+ if (process.env.SKIP_ENV_CHECK === "1") {
8
+ console.warn(
9
+ "check:env skipped (SKIP_ENV_CHECK=1): missing variables fail at runtime instead.",
10
+ );
11
+ return;
12
+ }
13
+ const { missing, local, invalid } = checkProductionEnv(process.env);
14
+ if (!missing.length && !local.length && !invalid.length) {
15
+ console.log("✓ check:env: every production variable this project needs is set.");
16
+ return;
17
+ }
18
+ const lines = [
19
+ ...missing.map(({ key, use }) => `✗ ${key} is not set (${use})`),
20
+ ...local.map((key) => `✗ ${key} points at localhost, which the deployed app can't reach`),
21
+ ...invalid.map((problem) => `✗ ${problem}`),
22
+ ];
23
+ console.error(
24
+ [
25
+ "check:env: the production environment is incomplete, so the build stops here.",
26
+ ...lines,
27
+ "Fix them in the host's environment variables for this environment (Production or Preview),",
28
+ "then deploy again. SKIP_ENV_CHECK=1 builds anyway.",
29
+ ].join("\n"),
30
+ );
31
+ process.exit(1);
32
+ }
33
+
34
+ main();
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { checkProductionEnv, isLocalUrl, requiredVariables } from "./production";
3
+
4
+ // A value for every variable this project requires, whichever modules it has.
5
+ const complete = (extra: Record<string, string> = {}) => {
6
+ const env: Record<string, string> = { ...extra };
7
+ for (const { key } of requiredVariables(extra)) env[key] ??= "https://service.example.com";
8
+ if ("BETTER_AUTH_SECRET" in env) env.BETTER_AUTH_SECRET = "x".repeat(32);
9
+ return env;
10
+ };
11
+
12
+ describe("production env check", () => {
13
+ it("passes when every required variable is set", () => {
14
+ expect(checkProductionEnv(complete())).toEqual({ missing: [], local: [], invalid: [] });
15
+ expect(checkProductionEnv(complete({ VERCEL: "1" }))).toEqual({
16
+ missing: [],
17
+ local: [],
18
+ invalid: [],
19
+ });
20
+ });
21
+
22
+ it("always requires the database and the public URL", () => {
23
+ const keys = checkProductionEnv({}).missing.map(({ key }) => key);
24
+ expect(keys).toContain("DATABASE_URL");
25
+ expect(keys).toContain("APP_URL");
26
+ });
27
+
28
+ it("counts empty values as unset", () => {
29
+ const env = { ...complete(), DATABASE_URL: " " };
30
+ expect(checkProductionEnv(env).missing.map(({ key }) => key)).toEqual(["DATABASE_URL"]);
31
+ });
32
+
33
+ it("rejects URLs to this machine", () => {
34
+ const env = {
35
+ ...complete(),
36
+ DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/app",
37
+ };
38
+ expect(checkProductionEnv(env).local).toEqual(["DATABASE_URL"]);
39
+ });
40
+
41
+ it("rejects a URL that isn't absolute", () => {
42
+ const env = { ...complete(), APP_URL: "my-app.vercel.app" };
43
+ expect(checkProductionEnv(env).invalid).toEqual(["APP_URL is not an absolute URL"]);
44
+ });
45
+
46
+ const hasEmail = requiredVariables({}).some(({ key }) => key === "EMAIL_FROM");
47
+ it.runIf(hasEmail)("rejects the example.com sender from .env.example", () => {
48
+ const env = { ...complete(), EMAIL_FROM: "App <no-reply@example.com>" };
49
+ expect(checkProductionEnv(env).invalid).toEqual([
50
+ "EMAIL_FROM still uses example.com: set a sender on a domain you verified",
51
+ ]);
52
+ expect(checkProductionEnv({ ...env, EMAIL_FROM: "App <hi@my-app.com>" }).invalid).toEqual([]);
53
+ });
54
+
55
+ it("tells local URLs from hosted ones", () => {
56
+ expect(isLocalUrl("http://localhost:3000")).toBe(true);
57
+ expect(isLocalUrl("redis://127.0.0.1:6379")).toBe(true);
58
+ expect(isLocalUrl("postgres://u:p@host.docker.internal/app")).toBe(true);
59
+ expect(isLocalUrl("postgresql://u:p@aws-0-eu-west-1.pooler.supabase.com:6543/postgres")).toBe(
60
+ false,
61
+ );
62
+ expect(isLocalUrl("rediss://default:pw@eu1-x.upstash.io:6379")).toBe(false);
63
+ expect(isLocalUrl("not a url")).toBe(false);
64
+ });
65
+ });
@@ -0,0 +1,97 @@
1
+ // The variables a production deploy needs, for the modules this project uses. Hosts that only run
2
+ // the app (Vercel, Netlify) check them before the build with `pnpm check:env`, so a missing key
3
+ // stops the deploy instead of shipping an app whose features fail at runtime.
4
+
5
+ type Env = Record<string, string | undefined>;
6
+
7
+ /**
8
+ * `url`: an absolute URL to a hosted service, not this machine. `serverless`: only on hosts that
9
+ * run no long-lived process (Vercel, Netlify).
10
+ */
11
+ export type Requirement = { key: string; use: string; url?: boolean; serverless?: boolean };
12
+
13
+ /** What every production deploy of this project needs. */
14
+ export function requiredVariables(env: Env): Requirement[] {
15
+ const required: Requirement[] = [
16
+ { key: "DATABASE_URL", use: "the database", url: true },
17
+ { key: "APP_URL", use: "links in emails and redirects", url: true },
18
+ // clubedge:if auth=supabase
19
+ { key: "SUPABASE_URL", use: "Supabase Auth", url: true },
20
+ { key: "SUPABASE_PUBLISHABLE_KEY", use: "Supabase Auth" },
21
+ // clubedge:end
22
+ // clubedge:if auth=better-auth
23
+ { key: "BETTER_AUTH_SECRET", use: "Better Auth sessions" },
24
+ // clubedge:end
25
+ // clubedge:if cache=redis
26
+ { key: "REDIS_URL", use: "the cache and rate limits", url: true },
27
+ // clubedge:end
28
+ // clubedge:if storage=s3
29
+ { key: "STORAGE_BUCKET", use: "file storage" },
30
+ { key: "STORAGE_ACCESS_KEY_ID", use: "file storage" },
31
+ { key: "STORAGE_SECRET_ACCESS_KEY", use: "file storage" },
32
+ // clubedge:end
33
+ // clubedge:if email=smtp
34
+ { key: "SMTP_URL", use: "email", url: true },
35
+ // clubedge:end
36
+ // clubedge:if email=resend
37
+ { key: "RESEND_API_KEY", use: "email" },
38
+ // clubedge:end
39
+ // clubedge:if email=smtp|resend
40
+ { key: "EMAIL_FROM", use: "the sender of every email" },
41
+ // clubedge:end
42
+ // clubedge:if payments=stripe
43
+ { key: "STRIPE_SECRET_KEY", use: "payments" },
44
+ { key: "STRIPE_WEBHOOK_SECRET", use: "payments" },
45
+ { key: "STRIPE_PRICE_PRO", use: "payments" },
46
+ // clubedge:end
47
+ // clubedge:if jobs=pg-boss
48
+ // No jobs worker runs there: Vercel Cron or another scheduler calls /api/jobs/run with it.
49
+ { key: "CRON_SECRET", use: "background jobs, run by a cron", serverless: true },
50
+ // clubedge:end
51
+ ];
52
+ const serverless = Boolean(env.VERCEL || env.NETLIFY);
53
+ return required.filter((requirement) => serverless || !requirement.serverless);
54
+ }
55
+
56
+ /** The host of an absolute URL, or null for a value that isn't one. */
57
+ function urlHost(value: string): string | null {
58
+ try {
59
+ return new URL(value.trim()).hostname.toLowerCase();
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ /** A URL to this machine: from a development env file, unreachable from the deployed app. */
66
+ export function isLocalUrl(value: string): boolean {
67
+ const host = urlHost(value);
68
+ if (host === null) return false;
69
+ return (
70
+ ["localhost", "127.0.0.1", "0.0.0.0", "[::1]", "host.docker.internal"].includes(host) ||
71
+ host.endsWith(".localhost")
72
+ );
73
+ }
74
+
75
+ export type ProductionEnvCheck = { missing: Requirement[]; local: string[]; invalid: string[] };
76
+
77
+ /** The required variables that are unset, point at this machine, or have an unusable value. */
78
+ export function checkProductionEnv(env: Env): ProductionEnvCheck {
79
+ const required = requiredVariables(env);
80
+ const missing = required.filter(({ key }) => !env[key]?.trim());
81
+ const urls = required.filter(({ key, url }) => url && env[key]?.trim());
82
+ const local = urls.filter(({ key }) => isLocalUrl(env[key]!)).map(({ key }) => key);
83
+ const invalid = urls
84
+ .filter(({ key }) => urlHost(env[key]!) === null)
85
+ .map(({ key }) => `${key} is not an absolute URL`);
86
+ // clubedge:if auth=better-auth
87
+ const secret = env.BETTER_AUTH_SECRET?.trim();
88
+ if (secret && secret.length < 32) invalid.push("BETTER_AUTH_SECRET needs at least 32 characters");
89
+ // clubedge:end
90
+ // clubedge:if email=smtp|resend
91
+ // .env.example's placeholder sender: providers refuse a domain you haven't verified.
92
+ if (/@example\.com\b/i.test(env.EMAIL_FROM ?? "")) {
93
+ invalid.push("EMAIL_FROM still uses example.com: set a sender on a domain you verified");
94
+ }
95
+ // clubedge:end
96
+ return { missing, local, invalid };
97
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "$schema": "https://openapi.vercel.sh/vercel.json",
3
3
  "framework": null,
4
- "buildCommand": "pnpm --dir ../.. db:migrate && pnpm run build"
4
+ "buildCommand": "pnpm run check:env && pnpm --dir ../.. db:migrate && pnpm run build"
5
5
  }
@@ -1,6 +1,7 @@
1
1
  # Netlify (https://docs.netlify.com/configure-builds/file-based-configuration/). Set the site's
2
2
  # base directory to apps/web; Netlify installs its Next.js runtime automatically.
3
3
  [build]
4
- # Migrations run during the build, against the DATABASE_URL set for the deploy context.
5
- command = "pnpm --dir ../.. db:migrate && pnpm run build"
4
+ # check:env stops the build when a variable the project needs is unset; then migrations run,
5
+ # against the DATABASE_URL set for the deploy context.
6
+ command = "pnpm run check:env && pnpm --dir ../.. db:migrate && pnpm run build"
6
7
  publish = ".next"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/web",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "scripts": {
@@ -8,7 +8,8 @@
8
8
  "build": "next build",
9
9
  "start": "node scripts/start-standalone.mjs",
10
10
  "typecheck": "tsc --noEmit",
11
- "check:services": "tsx scripts/check-services.ts"
11
+ "check:services": "tsx scripts/check-services.ts",
12
+ "check:env": "tsx scripts/check-env.ts"
12
13
  },
13
14
  "dependencies": {
14
15
  "@clubedge/auth": "workspace:*",
@@ -0,0 +1,34 @@
1
+ // Checks the production variables before a deploy build: `pnpm check:env`. Vercel's and Netlify's
2
+ // build commands run it first, so a missing key stops the build. It reads the build's environment,
3
+ // not .env.local; SKIP_ENV_CHECK=1 builds anyway.
4
+ import { checkProductionEnv } from "../src/env/production";
5
+
6
+ function main() {
7
+ if (process.env.SKIP_ENV_CHECK === "1") {
8
+ console.warn(
9
+ "check:env skipped (SKIP_ENV_CHECK=1): missing variables fail at runtime instead.",
10
+ );
11
+ return;
12
+ }
13
+ const { missing, local, invalid } = checkProductionEnv(process.env);
14
+ if (!missing.length && !local.length && !invalid.length) {
15
+ console.log("✓ check:env: every production variable this project needs is set.");
16
+ return;
17
+ }
18
+ const lines = [
19
+ ...missing.map(({ key, use }) => `✗ ${key} is not set (${use})`),
20
+ ...local.map((key) => `✗ ${key} points at localhost, which the deployed app can't reach`),
21
+ ...invalid.map((problem) => `✗ ${problem}`),
22
+ ];
23
+ console.error(
24
+ [
25
+ "check:env: the production environment is incomplete, so the build stops here.",
26
+ ...lines,
27
+ "Fix them in the host's environment variables for this environment (Production or Preview),",
28
+ "then deploy again. SKIP_ENV_CHECK=1 builds anyway.",
29
+ ].join("\n"),
30
+ );
31
+ process.exit(1);
32
+ }
33
+
34
+ main();
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { checkProductionEnv, isLocalUrl, requiredVariables } from "./production";
3
+
4
+ // A value for every variable this project requires, whichever modules it has.
5
+ const complete = (extra: Record<string, string> = {}) => {
6
+ const env: Record<string, string> = { ...extra };
7
+ for (const { key } of requiredVariables(extra)) env[key] ??= "https://service.example.com";
8
+ if ("BETTER_AUTH_SECRET" in env) env.BETTER_AUTH_SECRET = "x".repeat(32);
9
+ return env;
10
+ };
11
+
12
+ describe("production env check", () => {
13
+ it("passes when every required variable is set", () => {
14
+ expect(checkProductionEnv(complete())).toEqual({ missing: [], local: [], invalid: [] });
15
+ expect(checkProductionEnv(complete({ VERCEL: "1" }))).toEqual({
16
+ missing: [],
17
+ local: [],
18
+ invalid: [],
19
+ });
20
+ });
21
+
22
+ it("always requires the database and the public URL", () => {
23
+ const keys = checkProductionEnv({}).missing.map(({ key }) => key);
24
+ expect(keys).toContain("DATABASE_URL");
25
+ expect(keys).toContain("NEXT_PUBLIC_APP_URL");
26
+ });
27
+
28
+ it("counts empty values as unset", () => {
29
+ const env = { ...complete(), DATABASE_URL: " " };
30
+ expect(checkProductionEnv(env).missing.map(({ key }) => key)).toEqual(["DATABASE_URL"]);
31
+ });
32
+
33
+ it("rejects URLs to this machine", () => {
34
+ const env = {
35
+ ...complete(),
36
+ DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/app",
37
+ };
38
+ expect(checkProductionEnv(env).local).toEqual(["DATABASE_URL"]);
39
+ });
40
+
41
+ it("rejects a URL that isn't absolute", () => {
42
+ const env = { ...complete(), NEXT_PUBLIC_APP_URL: "my-app.vercel.app" };
43
+ expect(checkProductionEnv(env).invalid).toEqual(["NEXT_PUBLIC_APP_URL is not an absolute URL"]);
44
+ });
45
+
46
+ const hasEmail = requiredVariables({}).some(({ key }) => key === "EMAIL_FROM");
47
+ it.runIf(hasEmail)("rejects the example.com sender from .env.example", () => {
48
+ const env = { ...complete(), EMAIL_FROM: "App <no-reply@example.com>" };
49
+ expect(checkProductionEnv(env).invalid).toEqual([
50
+ "EMAIL_FROM still uses example.com: set a sender on a domain you verified",
51
+ ]);
52
+ expect(checkProductionEnv({ ...env, EMAIL_FROM: "App <hi@my-app.com>" }).invalid).toEqual([]);
53
+ });
54
+
55
+ it("tells local URLs from hosted ones", () => {
56
+ expect(isLocalUrl("http://localhost:3000")).toBe(true);
57
+ expect(isLocalUrl("redis://127.0.0.1:6379")).toBe(true);
58
+ expect(isLocalUrl("postgres://u:p@host.docker.internal/app")).toBe(true);
59
+ expect(isLocalUrl("postgresql://u:p@aws-0-eu-west-1.pooler.supabase.com:6543/postgres")).toBe(
60
+ false,
61
+ );
62
+ expect(isLocalUrl("rediss://default:pw@eu1-x.upstash.io:6379")).toBe(false);
63
+ expect(isLocalUrl("not a url")).toBe(false);
64
+ });
65
+ });
@@ -0,0 +1,97 @@
1
+ // The variables a production deploy needs, for the modules this project uses. Hosts that only run
2
+ // the app (Vercel, Netlify) check them before the build with `pnpm check:env`, so a missing key
3
+ // stops the deploy instead of shipping an app whose features fail at runtime.
4
+
5
+ type Env = Record<string, string | undefined>;
6
+
7
+ /**
8
+ * `url`: an absolute URL to a hosted service, not this machine. `serverless`: only on hosts that
9
+ * run no long-lived process (Vercel, Netlify).
10
+ */
11
+ export type Requirement = { key: string; use: string; url?: boolean; serverless?: boolean };
12
+
13
+ /** What every production deploy of this project needs. */
14
+ export function requiredVariables(env: Env): Requirement[] {
15
+ const required: Requirement[] = [
16
+ { key: "DATABASE_URL", use: "the database", url: true },
17
+ { key: "NEXT_PUBLIC_APP_URL", use: "links in emails and redirects", url: true },
18
+ // clubedge:if auth=supabase
19
+ { key: "NEXT_PUBLIC_SUPABASE_URL", use: "Supabase Auth", url: true },
20
+ { key: "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY", use: "Supabase Auth" },
21
+ // clubedge:end
22
+ // clubedge:if auth=better-auth
23
+ { key: "BETTER_AUTH_SECRET", use: "Better Auth sessions" },
24
+ // clubedge:end
25
+ // clubedge:if cache=redis
26
+ { key: "REDIS_URL", use: "the cache and rate limits", url: true },
27
+ // clubedge:end
28
+ // clubedge:if storage=s3
29
+ { key: "STORAGE_BUCKET", use: "file storage" },
30
+ { key: "STORAGE_ACCESS_KEY_ID", use: "file storage" },
31
+ { key: "STORAGE_SECRET_ACCESS_KEY", use: "file storage" },
32
+ // clubedge:end
33
+ // clubedge:if email=smtp
34
+ { key: "SMTP_URL", use: "email", url: true },
35
+ // clubedge:end
36
+ // clubedge:if email=resend
37
+ { key: "RESEND_API_KEY", use: "email" },
38
+ // clubedge:end
39
+ // clubedge:if email=smtp|resend
40
+ { key: "EMAIL_FROM", use: "the sender of every email" },
41
+ // clubedge:end
42
+ // clubedge:if payments=stripe
43
+ { key: "STRIPE_SECRET_KEY", use: "payments" },
44
+ { key: "STRIPE_WEBHOOK_SECRET", use: "payments" },
45
+ { key: "STRIPE_PRICE_PRO", use: "payments" },
46
+ // clubedge:end
47
+ // clubedge:if jobs=pg-boss
48
+ // No jobs worker runs there: Vercel Cron or another scheduler calls /api/jobs/run with it.
49
+ { key: "CRON_SECRET", use: "background jobs, run by a cron", serverless: true },
50
+ // clubedge:end
51
+ ];
52
+ const serverless = Boolean(env.VERCEL || env.NETLIFY);
53
+ return required.filter((requirement) => serverless || !requirement.serverless);
54
+ }
55
+
56
+ /** The host of an absolute URL, or null for a value that isn't one. */
57
+ function urlHost(value: string): string | null {
58
+ try {
59
+ return new URL(value.trim()).hostname.toLowerCase();
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ /** A URL to this machine: from a development env file, unreachable from the deployed app. */
66
+ export function isLocalUrl(value: string): boolean {
67
+ const host = urlHost(value);
68
+ if (host === null) return false;
69
+ return (
70
+ ["localhost", "127.0.0.1", "0.0.0.0", "[::1]", "host.docker.internal"].includes(host) ||
71
+ host.endsWith(".localhost")
72
+ );
73
+ }
74
+
75
+ export type ProductionEnvCheck = { missing: Requirement[]; local: string[]; invalid: string[] };
76
+
77
+ /** The required variables that are unset, point at this machine, or have an unusable value. */
78
+ export function checkProductionEnv(env: Env): ProductionEnvCheck {
79
+ const required = requiredVariables(env);
80
+ const missing = required.filter(({ key }) => !env[key]?.trim());
81
+ const urls = required.filter(({ key, url }) => url && env[key]?.trim());
82
+ const local = urls.filter(({ key }) => isLocalUrl(env[key]!)).map(({ key }) => key);
83
+ const invalid = urls
84
+ .filter(({ key }) => urlHost(env[key]!) === null)
85
+ .map(({ key }) => `${key} is not an absolute URL`);
86
+ // clubedge:if auth=better-auth
87
+ const secret = env.BETTER_AUTH_SECRET?.trim();
88
+ if (secret && secret.length < 32) invalid.push("BETTER_AUTH_SECRET needs at least 32 characters");
89
+ // clubedge:end
90
+ // clubedge:if email=smtp|resend
91
+ // .env.example's placeholder sender: providers refuse a domain you haven't verified.
92
+ if (/@example\.com\b/i.test(env.EMAIL_FROM ?? "")) {
93
+ invalid.push("EMAIL_FROM still uses example.com: set a sender on a domain you verified");
94
+ }
95
+ // clubedge:end
96
+ return { missing, local, invalid };
97
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "$schema": "https://openapi.vercel.sh/vercel.json",
3
3
  "framework": "nextjs",
4
- "buildCommand": "pnpm --dir ../.. db:migrate && pnpm run build"
4
+ "buildCommand": "pnpm run check:env && pnpm --dir ../.. db:migrate && pnpm run build"
5
5
  }
@@ -425,6 +425,7 @@
425
425
  "apps/start/scripts/check-services.ts",
426
426
  "apps/start/src/components/route-error.tsx",
427
427
  "apps/start/src/env/auth.ts",
428
+ "apps/start/src/env/production.ts",
428
429
  "apps/start/src/jobs/index.ts",
429
430
  "apps/start/src/routes/__root.tsx",
430
431
  "apps/start/src/routes/dashboard.tsx",
@@ -442,6 +443,7 @@
442
443
  "apps/web/src/app/page.tsx",
443
444
  "apps/web/src/app/settings/actions.ts",
444
445
  "apps/web/src/env/auth.ts",
446
+ "apps/web/src/env/production.ts",
445
447
  "apps/web/src/instrumentation.ts",
446
448
  "apps/web/src/jobs/index.ts",
447
449
  "apps/web/src/server/auth-email.ts",
@@ -451,6 +453,7 @@
451
453
  "supabase/config.toml"
452
454
  ],
453
455
  "exclude": [
456
+ ".github/dependabot.yml",
454
457
  ".github/workflows/starter.yml",
455
458
  "ROADMAP.md",
456
459
  "clubedge.template.json",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clubedge-starter-v1",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "packageManager": "pnpm@10.9.0",
@@ -29,6 +29,7 @@
29
29
  "docker:build": "docker build -t clubedge-starter .",
30
30
  "docker:start": "docker run --rm -p 3000:3000 --env-file apps/web/.env.local clubedge-starter",
31
31
  "check:services": "pnpm --filter @clubedge/web check:services",
32
+ "check:env": "pnpm --filter @clubedge/web check:env",
32
33
  "clubedge": "pnpm dlx @clubedge/create-clubedge-app@latest",
33
34
  "infra:up": "docker compose -f docker-compose.dev.yml up -d --wait",
34
35
  "infra:down": "docker compose -f docker-compose.dev.yml down",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/auth",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/auth-better-auth",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Better Auth adapter for @clubedge/auth, storing users and sessions in your own PostgreSQL.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/auth-supabase",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Supabase Auth adapter for @clubedge/auth.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/cache",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/cache-redis",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Redis rate limiter and cache for @clubedge/cache.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/core",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/db",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/email",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Email interface, console and in-memory providers, and message templates.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/email-resend",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Resend provider for @clubedge/email, through Resend's HTTP API.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/email-smtp",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "SMTP provider for @clubedge/email, for any SMTP server.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/jobs",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Background jobs interface: job definitions, a queue contract, and an in-memory queue.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/jobs-pg-boss",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "pg-boss provider for @clubedge/jobs: a job queue stored in the project's PostgreSQL.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/local-db",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "PostgreSQL for development without Docker: PGlite served over the PostgreSQL protocol.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/observability",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Observability interface: error reports and spans for @clubedge/core's telemetry hooks, and an in-memory provider for tests.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/observability-otel",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "OpenTelemetry provider for @clubedge/observability: traces and error logs exported over OTLP to any collector.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/observability-sentry",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Sentry provider for @clubedge/observability: server and browser errors, traces, and source map uploads.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/payments",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Provider-neutral payments: the PaymentProvider interface, billing logic, and subscription tables.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/payments-stripe",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Stripe provider for @clubedge/payments: Checkout, the customer portal, and verified webhooks.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/storage",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/storage-s3",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "S3-compatible adapter for @clubedge/storage.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/storage-supabase",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Supabase Storage adapter for @clubedge/storage.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/ui",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "repository": "Clubedge/clubedge-starter",
3
- "ref": "v0.17.1",
4
- "commit": "d9d787b6750e862b9de81a3f8f36b7aa07757f7a",
3
+ "ref": "v0.18.0",
4
+ "commit": "bcd084d7d2d2aac0600ccb35491f48c251fb3a74",
5
5
  "files": [
6
6
  ".dockerignore",
7
7
  ".env.example",
@@ -36,6 +36,7 @@
36
36
  "apps/start/package.json",
37
37
  "apps/start/public/.well-known/logo.svg",
38
38
  "apps/start/public/favicon.ico",
39
+ "apps/start/scripts/check-env.ts",
39
40
  "apps/start/scripts/check-services.ts",
40
41
  "apps/start/src/components/auth/auth-form-controls.tsx",
41
42
  "apps/start/src/components/auth/auth-page.tsx",
@@ -57,6 +58,8 @@
57
58
  "apps/start/src/config/site.ts",
58
59
  "apps/start/src/env/auth.test.ts",
59
60
  "apps/start/src/env/auth.ts",
61
+ "apps/start/src/env/production.test.ts",
62
+ "apps/start/src/env/production.ts",
60
63
  "apps/start/src/env/server.test.ts",
61
64
  "apps/start/src/env/server.ts",
62
65
  "apps/start/src/jobs/index.ts",
@@ -136,6 +139,7 @@
136
139
  "apps/web/postcss.config.mjs",
137
140
  "apps/web/public/.gitkeep",
138
141
  "apps/web/public/.well-known/logo.svg",
142
+ "apps/web/scripts/check-env.ts",
139
143
  "apps/web/scripts/check-services.ts",
140
144
  "apps/web/scripts/start-standalone.mjs",
141
145
  "apps/web/src/app/actions/auth.ts",
@@ -177,6 +181,8 @@
177
181
  "apps/web/src/config/site.ts",
178
182
  "apps/web/src/env/auth.ts",
179
183
  "apps/web/src/env/client.ts",
184
+ "apps/web/src/env/production.test.ts",
185
+ "apps/web/src/env/production.ts",
180
186
  "apps/web/src/env/server.test.ts",
181
187
  "apps/web/src/env/server.ts",
182
188
  "apps/web/src/instrumentation.ts",