@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.
- package/CHANGELOG.md +8 -0
- package/README.md +11 -10
- package/package.json +3 -3
- package/template/README.md +3 -2
- package/template/SETUP.md +4 -0
- package/template/apps/start/netlify.toml +3 -2
- package/template/apps/start/package.json +3 -2
- package/template/apps/start/scripts/check-env.ts +34 -0
- package/template/apps/start/src/env/production.test.ts +65 -0
- package/template/apps/start/src/env/production.ts +97 -0
- package/template/apps/start/vercel.json +1 -1
- package/template/apps/web/netlify.toml +3 -2
- package/template/apps/web/package.json +3 -2
- package/template/apps/web/scripts/check-env.ts +34 -0
- package/template/apps/web/src/env/production.test.ts +65 -0
- package/template/apps/web/src/env/production.ts +97 -0
- package/template/apps/web/vercel.json +1 -1
- package/template/clubedge.template.json +3 -0
- package/template/package.json +2 -1
- package/template/packages/auth/package.json +1 -1
- package/template/packages/auth-better-auth/package.json +1 -1
- package/template/packages/auth-supabase/package.json +1 -1
- package/template/packages/cache/package.json +1 -1
- package/template/packages/cache-redis/package.json +1 -1
- package/template/packages/core/package.json +1 -1
- package/template/packages/db/package.json +1 -1
- package/template/packages/email/package.json +1 -1
- package/template/packages/email-resend/package.json +1 -1
- package/template/packages/email-smtp/package.json +1 -1
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/jobs-pg-boss/package.json +1 -1
- package/template/packages/local-db/package.json +1 -1
- package/template/packages/observability/package.json +1 -1
- package/template/packages/observability-otel/package.json +1 -1
- package/template/packages/observability-sentry/package.json +1 -1
- package/template/packages/payments/package.json +1 -1
- package/template/packages/payments-stripe/package.json +1 -1
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage-s3/package.json +1 -1
- package/template/packages/storage-supabase/package.json +1 -1
- package/template/packages/ui/package.json +1 -1
- package/template.lock.json +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.
|
|
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.
|
|
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.
|
|
213
|
+
create-clubedge-app v0.27.0
|
|
214
214
|
|
|
|
215
215
|
v
|
|
216
|
-
Clubedge Starter v0.
|
|
217
|
-
commit
|
|
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.
|
|
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.
|
|
229
|
-
`
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
26
|
-
"starterCommit": "
|
|
25
|
+
"starterRef": "v0.18.0",
|
|
26
|
+
"starterCommit": "bcd084d7d2d2aac0600ccb35491f48c251fb3a74"
|
|
27
27
|
},
|
|
28
28
|
"license": "Apache-2.0",
|
|
29
29
|
"repository": {
|
package/template/README.md
CHANGED
|
@@ -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`:
|
|
80
|
-
- Deployment to Netlify with `apps/web/netlify.toml`:
|
|
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
|
-
#
|
|
5
|
-
|
|
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.
|
|
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,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
|
-
#
|
|
5
|
-
|
|
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.
|
|
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
|
+
}
|
|
@@ -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",
|
package/template/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clubedge-starter-v1",
|
|
3
|
-
"version": "0.
|
|
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/observability",
|
|
3
|
-
"version": "0.
|
|
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",
|
package/template.lock.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"repository": "Clubedge/clubedge-starter",
|
|
3
|
-
"ref": "v0.
|
|
4
|
-
"commit": "
|
|
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",
|