@clubedge/create-clubedge-app 0.29.0 → 0.30.1

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 (70) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +50 -13
  3. package/dist/cli/args.js +14 -3
  4. package/dist/cli/ui.js +16 -4
  5. package/dist/core/deploy.js +43 -2
  6. package/dist/core/health.js +51 -0
  7. package/dist/core/neon.js +314 -0
  8. package/dist/core/railway.js +21 -7
  9. package/dist/deploy-preview.js +215 -0
  10. package/dist/deploy-vercel.js +3 -3
  11. package/dist/deploy.js +19 -2
  12. package/dist/index.js +25 -2
  13. package/dist/inspect.js +15 -1
  14. package/dist/modify.js +29 -0
  15. package/dist/neon.js +187 -0
  16. package/dist/try.js +36 -3
  17. package/package.json +3 -3
  18. package/template/.env.example +7 -0
  19. package/template/.github/workflows/preview.yml +170 -0
  20. package/template/README.md +14 -1
  21. package/template/SETUP.md +37 -0
  22. package/template/apps/start/.env.example +7 -0
  23. package/template/apps/start/package.json +1 -1
  24. package/template/apps/start/scripts/check-services.ts +19 -0
  25. package/template/apps/start/src/env/production.test.ts +17 -0
  26. package/template/apps/start/src/env/production.ts +12 -0
  27. package/template/apps/start/src/env/server.ts +2 -0
  28. package/template/apps/start/src/server/jobs.ts +6 -2
  29. package/template/apps/web/package.json +1 -1
  30. package/template/apps/web/scripts/check-services.ts +19 -0
  31. package/template/apps/web/src/env/production.test.ts +17 -0
  32. package/template/apps/web/src/env/production.ts +12 -0
  33. package/template/apps/web/src/env/server.ts +2 -0
  34. package/template/apps/web/src/server/jobs.ts +6 -2
  35. package/template/clubedge.template.json +9 -0
  36. package/template/package.json +1 -1
  37. package/template/packages/auth/package.json +1 -1
  38. package/template/packages/auth-better-auth/drizzle.config.ts +2 -2
  39. package/template/packages/auth-better-auth/package.json +1 -1
  40. package/template/packages/auth-better-auth/scripts/migrate.ts +2 -2
  41. package/template/packages/auth-supabase/package.json +1 -1
  42. package/template/packages/cache/package.json +1 -1
  43. package/template/packages/cache-redis/package.json +1 -1
  44. package/template/packages/core/package.json +1 -1
  45. package/template/packages/db/drizzle.config.ts +2 -2
  46. package/template/packages/db/package.json +2 -1
  47. package/template/packages/db/scripts/load-env.ts +9 -0
  48. package/template/packages/db/scripts/migrate.ts +2 -2
  49. package/template/packages/db/src/index.ts +1 -0
  50. package/template/packages/db/src/urls.test.ts +32 -0
  51. package/template/packages/db/src/urls.ts +32 -0
  52. package/template/packages/email/package.json +1 -1
  53. package/template/packages/email-resend/package.json +1 -1
  54. package/template/packages/email-smtp/package.json +1 -1
  55. package/template/packages/jobs/package.json +1 -1
  56. package/template/packages/jobs-pg-boss/package.json +1 -1
  57. package/template/packages/local-db/package.json +1 -1
  58. package/template/packages/observability/package.json +1 -1
  59. package/template/packages/observability-otel/package.json +1 -1
  60. package/template/packages/observability-sentry/package.json +1 -1
  61. package/template/packages/payments/drizzle.config.ts +2 -2
  62. package/template/packages/payments/package.json +1 -1
  63. package/template/packages/payments/scripts/migrate.ts +2 -2
  64. package/template/packages/payments-stripe/package.json +1 -1
  65. package/template/packages/storage/package.json +1 -1
  66. package/template/packages/storage-s3/package.json +1 -1
  67. package/template/packages/storage-supabase/package.json +1 -1
  68. package/template/packages/ui/package.json +1 -1
  69. package/template/scripts/migrate.mjs +4 -2
  70. package/template.lock.json +5 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  All notable changes to `create-clubedge-app` are documented here.
4
4
 
5
+ ## 0.30.1
6
+
7
+ Railway previews, fixed after running them for real on the Starter's own pull requests.
8
+
9
+ - `deploy --preview` sets `PORT` on the Railway preview environment (#87). The preview's domain sends traffic to port 3000, but a production service without `PORT` gave the copied environment none: Railway assigned its own port, and the app answered 502.
10
+ - Default to Starter `v0.19.1` (#88), which fixes the Railway step of `.github/workflows/preview.yml`:
11
+ - It works with a workspace token as well as an account token: it no longer runs `railway link`, which needs an account token, since the Railway CLI targets the project from `RAILWAY_PROJECT_ID`.
12
+ - It sets `PORT=3000` on the pull request's environment, for the same reason as above.
13
+ - The pull request comment lists the Vercel and Railway apps separately when both deploy.
14
+ - Tested on the Starter's pull requests: a Neon branch with the migrations, a Vercel and a Railway preview each answering `/api/ready`, and cleanup of the branch and the Railway environment on merge.
15
+ - If your project deploys previews to Railway, run `pnpm clubedge upgrade` to get the fixed `preview.yml`.
16
+
17
+ ## 0.30.0
18
+
19
+ - **`--infra neon`** gives a project a hosted Neon database with nothing to install and no account (#84):
20
+ - The CLI creates a free database through Neon's claimable Postgres. It writes the pooled `DATABASE_URL` and the direct `DATABASE_URL_DIRECT` into `apps/web/.env.local`, and the next steps include `pnpm db:migrate`.
21
+ - Neon keeps an unclaimed database for 72 hours. `--no-neon` creates nothing, and a project is still created when Neon can't be reached.
22
+ - `pnpm clubedge add infra neon` switches an existing project and creates a database when `DATABASE_URL` still held the old infrastructure's example value.
23
+ - **`pnpm clubedge neon`** shows the database's status and hours left (#84):
24
+ - `neon claim` opens Neon's page that moves it into your free Neon account. Claiming resets the password, and the CLI says where to copy the new connection strings from.
25
+ - `neon create` makes a new database, for example after one expired.
26
+ - **`doctor`** checks the database URLs (#84): pooled for the app, direct without `-pooler`, both for the same database. It also warns when a free database has less than a day left.
27
+ - **`deploy`** sends `DATABASE_URL_DIRECT` with a `DATABASE_URL` from the env file, and derives it from a pooled Neon URL when the file has none. It never sends one next to the platform's own database, repoints one left from an earlier database, and warns before deploying a database that is still unclaimed (#84).
28
+ - **`deploy --preview`** deploys the current Git branch next to production on Vercel (a preview deployment) or Railway (an environment copied from production) (#84):
29
+ - With `NEON_API_KEY` and `NEON_PROJECT_ID`, the preview gets its own Neon branch, `preview/<git branch>`, with this branch's migrations applied.
30
+ - On Vercel, the branch's URLs are set as Preview variables of the Git branch, which needs the Vercel project connected to its Git repository.
31
+ - **`try --infra neon`** creates the database on your machine and migrates it on the VM. An uploaded Neon project sends its two URLs (#84).
32
+ - Default to Starter `v0.19.0` (#85). It adds `infra neon`, the optional `DATABASE_URL_DIRECT` that migrations and the jobs worker use, and a preview environment with its own Neon branch for each pull request.
33
+
5
34
  ## 0.29.0
6
35
 
7
36
  - `pnpm clubedge deploy` deploys to Netlify and Render (experimental) (#82), so it now covers Railway, Fly.io, Vercel, Netlify, and Render:
package/README.md CHANGED
@@ -80,7 +80,7 @@ Authentication, file storage, caching, email, payments, background jobs, observa
80
80
  | `--jobs` | `pg-boss`, `none` | `none` |
81
81
  | `--observability` | `otel` (OpenTelemetry, to any OTLP collector), `sentry`, `none` | `none` |
82
82
  | `--deploy` | `docker` (any container host), `fly`, `railway`, `render`, `vercel`, `netlify` | `docker` |
83
- | `--infra` | `docker` (Docker Compose), `supabase` (Supabase CLI), `local` (no Docker: PGlite and a local folder), `none` | `docker` |
83
+ | `--infra` | `docker` (Docker Compose), `supabase` (Supabase CLI), `local` (no Docker: PGlite and a local folder), `neon` (a free hosted Neon database), `none` | `docker` |
84
84
 
85
85
  ```sh
86
86
  # Only the monorepo, UI, database, and tooling
@@ -129,7 +129,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git --framework tanstac
129
129
  pnpm dlx @clubedge/create-clubedge-app my-app --ref main
130
130
 
131
131
  # Reproduce a specific Starter release or commit
132
- pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.18.0
132
+ pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.19.1
133
133
  pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
134
134
 
135
135
  # Test local Starter changes before releasing them
@@ -148,7 +148,7 @@ pnpm clubedge add email resend payments stripe # several modules as
148
148
  pnpm clubedge remove storage payments
149
149
  pnpm dlx @clubedge/create-clubedge-app add infra local --dry-run
150
150
  pnpm dlx @clubedge/create-clubedge-app@latest upgrade # to the newest CLI's Starter release
151
- pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.18.0 # or a specific tag, branch, or commit
151
+ pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.19.1 # or a specific tag, branch, or commit
152
152
  ```
153
153
 
154
154
  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.
@@ -163,6 +163,29 @@ Several modules in one command are applied as one change, and their requirements
163
163
 
164
164
  The project must be a Git repository without uncommitted changes, so you can review the result with `git diff` and undo it. Pass `--force` to skip this check. Add `--no-install` to skip `pnpm install`. Afterward, run `pnpm db:migrate` and `pnpm check:services` if the change added tables or services.
165
165
 
166
+ ## A hosted database with Neon
167
+
168
+ `--infra neon` gives the project a [Neon](https://neon.com) PostgreSQL database with nothing to install and no account:
169
+
170
+ ```sh
171
+ pnpm dlx @clubedge/create-clubedge-app my-app --infra neon
172
+ cd my-app
173
+ pnpm db:migrate
174
+ pnpm dev
175
+ ```
176
+
177
+ The CLI creates a free database through Neon's [claimable Postgres](https://neon.com/docs/reference/claimable-postgres) and writes two URLs into `apps/web/.env.local`: `DATABASE_URL`, the pooled connection the app uses, and `DATABASE_URL_DIRECT`, the direct one that migrations and the background jobs worker use, since a transaction pooler can break them. Neon keeps an unclaimed database for 72 hours, with up to 100 MB:
178
+
179
+ ```sh
180
+ pnpm clubedge neon # where it stands and how long it has left
181
+ pnpm clubedge neon claim # opens Neon's page that moves it into your (free) Neon account
182
+ pnpm clubedge neon create # a new database, for example after one expired
183
+ ```
184
+
185
+ Claiming keeps the data but resets the password: copy the pooled and direct connection strings from the Neon console into the two variables afterward. To use a Neon project you already have, paste its URLs instead, or pass `--no-neon` to create nothing. `pnpm clubedge add infra neon` switches an existing project and creates a database when `DATABASE_URL` held the old infrastructure's example value. The CLI keeps what it needs to claim each database in `neon.json` in its config directory (`~/.config/clubedge`, or `%APPDATA%\clubedge` on Windows); treat it as a secret.
186
+
187
+ `doctor` checks the two URLs (pooled for the app, direct without `-pooler`, both from the same database) and warns when a free database has less than a day left. `deploy` sends `DATABASE_URL_DIRECT` with a `DATABASE_URL` from the env file, derives it from a pooled Neon URL when the file has none, and warns before deploying a database that is still unclaimed.
188
+
166
189
  ## Check a project
167
190
 
168
191
  Two read-only commands help you keep a project healthy:
@@ -185,9 +208,9 @@ pnpm dlx @clubedge/create-clubedge-app try --framework tanstack-start --auth non
185
208
 
186
209
  The VM needs no Railway account and lasts 60 minutes; to keep it, open the claim link the CLI prints within 24 hours after, which moves the VM, its files, and its URL into a Railway account. Keep that link private. Until the VM is claimed, the preview opens only from the IP address that created it (not from another device on mobile data, or through a VPN); claiming makes the URL shareable. Railway allows 3 free VMs per network per day.
187
210
 
188
- The VM has no Docker, so the project uses `--infra local` (PGlite) and Better Auth unless you choose `--auth none`, with email off; other module flags apply as usual.
211
+ The VM has no Docker, so the project uses `--infra local` (PGlite) and Better Auth unless you choose `--auth none`, with email off; other module flags apply as usual. With `--infra neon`, the CLI creates a free Neon database first, the VM migrates it, and `neon claim` (run outside a project) keeps it after the VM is gone.
189
212
 
190
- Inside a project, `pnpm clubedge try` uploads that project instead, to share work in progress without deploying it. It sends what Git would commit (tracked files, plus untracked ones that are not ignored) and never `.env` files other than examples, `node_modules`, or build output; on the VM, the env file is made from `.env.example` as `create` does. The project must use `infra local` or `none`, and not Supabase Auth; `pnpm clubedge add infra local` switches a Docker project. Add `--dry-run` to list the files without uploading anything.
213
+ Inside a project, `pnpm clubedge try` uploads that project instead, to share work in progress without deploying it. It sends what Git would commit (tracked files, plus untracked ones that are not ignored) and never `.env` files other than examples, `node_modules`, or build output; on the VM, the env file is made from `.env.example` as `create` does. The project must use `infra local`, `neon`, or `none`, and not Supabase Auth; with `neon`, the VM gets `DATABASE_URL` and `DATABASE_URL_DIRECT` from your env file; `pnpm clubedge add infra local` switches a Docker project. Add `--dry-run` to list the files without uploading anything.
191
214
 
192
215
  When the app is ready, the CLI opens the preview in your browser (`--no-open` turns that off) and asks whether to share it: claiming the VM, a free Railway sign-up, is what makes the URL work for anyone. `--claim` opens the claim page without asking. `try --status` shows the last VM's preview, time left, and claim link; it connects only while the VM is running, so it never creates a new one. To open the app from another network yourself, forward it: `ssh -i <key> -N -L 3000:127.0.0.1:3000 railway.new`, then http://localhost:3000 (the CLI prints the exact command). The CLI asks before creating anything (`--yes` agrees), keeps its own SSH key in your config directory (`~/.config/clubedge`, or `%APPDATA%\clubedge` on Windows) apart from `~/.ssh`, and connects only to Railway's known host key. Railway's [terms](https://railway.com/legal/terms) apply, and Railway can change or pause its free VM.
193
216
 
@@ -219,32 +242,44 @@ In a project with `--deploy netlify`, `pnpm clubedge deploy` uses [Netlify's CLI
219
242
 
220
243
  In a project with `--deploy render`, `pnpm clubedge deploy` uses Render's API. Render builds the Dockerfile from your Git repository, so push the project to GitHub, GitLab, or Bitbucket first, with Render's access to it granted in the dashboard; `deploy` warns about commits that aren't pushed. It signs in with an API key: `RENDER_API_KEY`, or else it opens the dashboard's API keys page and asks you to paste one. It creates or reuses a web service named after the package, on the Docker runtime and a starter instance (pre-deploy commands need a paid type), which runs `node /app/migrate.mjs` before each release, checks `/api/health`, and deploys again on each push, as `render.yaml` describes. `DATABASE_URL` comes from the env file (Render Postgres, Neon, Supabase, ...). It sets the production variables: the env file's values, the service's `*.onrender.com` URL, and a generated `BETTER_AUTH_SECRET` that later runs keep. Then it starts a deploy and waits until it is live. When Render names the app something other than `https://<service>.onrender.com`, `deploy` sets that URL and deploys again, since the build compiles it in. Running it again reuses the service. Render bills the instance on your plan.
221
244
 
245
+ ### Preview deployments
246
+
247
+ In a project that deploys to Vercel or Railway, `pnpm clubedge deploy --preview` deploys the current Git branch next to production, after a first `deploy`:
248
+
249
+ ```sh
250
+ NEON_API_KEY=... NEON_PROJECT_ID=... pnpm clubedge deploy --preview
251
+ ```
252
+
253
+ With `NEON_API_KEY` and `NEON_PROJECT_ID` (in the shell, or in the env file), the preview gets its own [Neon branch](https://neon.com/docs/introduction/branching), `preview/<git branch>`: a copy of the production database made in seconds that shares its storage until it changes. `deploy` creates it, or reuses it on later runs, applies this branch's migrations to it, and points the preview's `DATABASE_URL` and `DATABASE_URL_DIRECT` at it. On Vercel, the preview is a preview deployment with the project's Preview variables, and the branch's URLs are set as Preview variables of your Git branch, which needs the Vercel project connected to its Git repository; without the Neon settings, it uses the Preview `DATABASE_URL`. On Railway, it is an environment named `preview-<git branch>`, copied from production with its own domain; without the Neon settings, it gets its own empty copy of Railway's Postgres, and when production's database comes from elsewhere, `deploy --preview` refuses, since the copy would use production's data. Production is not changed.
254
+
255
+ Projects created with `--infra neon` also have `.github/workflows/preview.yml`, which does this for every pull request in CI: a Neon branch per pull request with its migrations, the schema diff as a comment, a Vercel or Railway preview, and cleanup when the pull request closes. The Starter's SETUP.md lists the secrets it needs.
256
+
222
257
  ## Version relationship
223
258
 
224
259
  Each CLI release is tested against one specific Starter release:
225
260
 
226
261
  ```text
227
- create-clubedge-app v0.29.0
262
+ create-clubedge-app v0.30.1
228
263
  |
229
264
  v
230
- Clubedge Starter v0.18.0
231
- commit bcd084d7d2d2aac0600ccb35491f48c251fb3a74
265
+ Clubedge Starter v0.19.1
266
+ commit b113170a55a6d9990ca418f4968a7f50ebac1bca
232
267
  ```
233
268
 
234
269
  The current relationship is:
235
270
 
236
271
  | CLI release | Default Starter release | Meaning |
237
272
  | --- | --- | --- |
238
- | `create-clubedge-app@0.29.0` | `clubedge-starter@v0.18.0` | The CLI's required, tested default |
273
+ | `create-clubedge-app@0.30.1` | `clubedge-starter@v0.19.1` | The CLI's required, tested default |
239
274
 
240
275
  This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
241
276
 
242
- The `v0.18.0` tag is expected to resolve to commit
243
- `bcd084d7d2d2aac0600ccb35491f48c251fb3a74`. The CLI verifies this before
277
+ The `v0.19.1` tag is expected to resolve to commit
278
+ `b113170a55a6d9990ca418f4968a7f50ebac1bca`. The CLI verifies this before
244
279
  scaffolding with the default reference and fails rather than silently using a
245
280
  moved tag.
246
281
 
247
- 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.
282
+ When the Starter changes, its maintainers first publish a new versioned release, such as `v0.19.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.
248
283
 
249
284
  Every generated project records its CLI version, Starter repository, Starter ref, Starter commit, framework, and modules in `package.json` and `README.md`.
250
285
 
@@ -299,7 +334,7 @@ CI packs the CLI with `npm pack`, scaffolds clean projects from the packed tarba
299
334
  1. Make and validate changes in `clubedge-starter`.
300
335
  2. Run lint, typecheck, unit tests, browser tests, and production build.
301
336
  3. Update the Starter package versions.
302
- 4. Commit `main`, create a release tag such as `v0.18.0`, and publish the Starter release.
337
+ 4. Commit `main`, create a release tag such as `v0.19.1`, and publish the Starter release.
303
338
 
304
339
  ### CLI maintainers
305
340
 
@@ -338,6 +373,8 @@ Each phase ends with a Starter release and a CLI release pinned to it. The full
338
373
  | 0.27.0 | v0.18.0 | Starter v0.18.0: Vercel and Netlify builds stop when a production variable is missing (`pnpm check:env`) |
339
374
  | 0.28.0 | v0.18.0 | `deploy` to Vercel (experimental) |
340
375
  | 0.29.0 | v0.18.0 | `deploy` to Netlify and Render (experimental) |
376
+ | 0.30.0 | v0.19.0 | `--infra neon` with a free Neon database, the `neon` command, `deploy --preview` with a Neon branch per preview |
377
+ | 0.30.1 | v0.19.1 | Railway previews work with a workspace token and answer on the port their domain targets |
341
378
  | next | | Show the Starter's version of a conflicted file next to yours |
342
379
 
343
380
  ## Community
package/dist/cli/args.js CHANGED
@@ -16,6 +16,8 @@ const OPTIONS = {
16
16
  open: { type: "boolean", default: true },
17
17
  claim: { type: "boolean", default: false },
18
18
  status: { type: "boolean", default: false },
19
+ neon: { type: "boolean", default: true },
20
+ preview: { type: "boolean", default: false },
19
21
  "env-file": { type: "string" },
20
22
  help: { type: "boolean", short: "h", default: false },
21
23
  version: { type: "boolean", short: "v", default: false },
@@ -90,8 +92,8 @@ export function parseCliArgs(argv) {
90
92
  if (!creates && command.name !== "upgrade" && values.ref !== undefined) {
91
93
  throw new UsageError("--ref only applies when creating or upgrading a project.");
92
94
  }
93
- if (command.name !== "deploy" && values["env-file"] !== undefined) {
94
- throw new UsageError("--env-file only applies to deploy.");
95
+ if (command.name !== "deploy" && (values["env-file"] !== undefined || values.preview)) {
96
+ throw new UsageError("--env-file and --preview only apply to deploy.");
95
97
  }
96
98
  if (command.name !== "try" && (values.claim || values.status)) {
97
99
  throw new UsageError("--claim and --status only apply to try.");
@@ -124,6 +126,8 @@ export function parseCliArgs(argv) {
124
126
  open: values.open,
125
127
  claim: values.claim,
126
128
  status: values.status,
129
+ neon: values.neon,
130
+ preview: values.preview,
127
131
  envFile: values["env-file"],
128
132
  help: values.help,
129
133
  version: values.version,
@@ -163,9 +167,16 @@ function parseCommand(positionals) {
163
167
  }
164
168
  if (name === "deploy") {
165
169
  if (module !== undefined)
166
- throw new UsageError("Usage: deploy [--env-file <path>], run inside a project.");
170
+ throw new UsageError("Usage: deploy [--preview] [--env-file <path>], run inside a project.");
167
171
  return { name };
168
172
  }
173
+ if (name === "neon") {
174
+ const action = module ?? "status";
175
+ if (rest.length > 1 || (action !== "status" && action !== "claim" && action !== "create")) {
176
+ throw new UsageError("Usage: neon [status|claim|create], run inside a project with --infra neon.");
177
+ }
178
+ return { name, action };
179
+ }
169
180
  if (name === "try") {
170
181
  if (module !== undefined) {
171
182
  throw new UsageError("Usage: try [--framework <id>] [module flags] (a new project), or try inside a project to upload it.");
package/dist/cli/ui.js CHANGED
@@ -93,9 +93,10 @@ Usage:
93
93
  pnpm dlx @clubedge/create-clubedge-app doctor (inside a project)
94
94
  pnpm dlx @clubedge/create-clubedge-app try [options] (experimental)
95
95
  pnpm dlx @clubedge/create-clubedge-app deploy [options] (inside a project)
96
+ pnpm dlx @clubedge/create-clubedge-app neon [claim|create] (--infra neon)
96
97
 
97
98
  Inside a project with the clubedge script, use pnpm clubedge add, remove,
98
- upgrade, status, doctor, try, or deploy. Without a module or option, add and remove ask
99
+ upgrade, status, doctor, try, deploy, or neon. Without a module or option, add and remove ask
99
100
  for them.
100
101
 
101
102
  add and remove change modules of an existing project, several at once as one
@@ -108,11 +109,16 @@ Usage:
108
109
  newer Starter is available. doctor checks Node.js, pnpm, the dependencies, the
109
110
  lockfile, and the env file, then runs pnpm check:services; it changes nothing.
110
111
 
112
+ --infra neon creates a free Neon database (no account; 72 hours unless claimed) and
113
+ writes its pooled and direct URLs into the env file. neon shows how long it has left,
114
+ neon claim opens the page that keeps it in a Neon account, and neon create makes a
115
+ new one.
116
+
111
117
  try creates a new project on a free, temporary Railway VM (no account, 60 minutes)
112
118
  and prints its preview URL and the link to claim the VM. The VM has no Docker, so
113
- it uses --infra local (PGlite) and Better Auth, with email off. Inside a project,
114
- try uploads that project instead (what Git would commit, never .env files or
115
- node_modules); it needs infra local or none. It asks first, keeps its own SSH key
119
+ it uses --infra local (PGlite) or --infra neon, and Better Auth, with email off.
120
+ Inside a project, try uploads that project instead (what Git would commit, never .env
121
+ files or node_modules); it needs infra local, neon, or none. It asks first, keeps its own SSH key
116
122
  apart from ~/.ssh, and opens the preview when ready. Experimental: Railway can
117
123
  change or pause its free VM.
118
124
 
@@ -127,6 +133,9 @@ ${selectionHelp(manifest)}
127
133
  --no-open try, deploy (Vercel, Netlify, Render): do not open the URL in the browser
128
134
  --claim try: open the page that claims the VM, to share the preview
129
135
  --status try: the last VM's preview, time left, and claim link
136
+ --no-neon --infra neon: don't create a Neon database (set DATABASE_URL yourself)
137
+ --preview deploy (Vercel, Railway): a preview of the current Git branch, with its
138
+ own Neon branch when NEON_API_KEY and NEON_PROJECT_ID are set
130
139
  --env-file <path> deploy: production values (default: .env.production when there is one)
131
140
  -y, --yes Accept defaults and never prompt (default directory: my-app)
132
141
  --no-install Skip dependency installation
@@ -147,6 +156,9 @@ Examples:
147
156
  pnpm dlx @clubedge/create-clubedge-app@latest upgrade
148
157
  pnpm clubedge status
149
158
  pnpm clubedge doctor --no-services
159
+ pnpm dlx @clubedge/create-clubedge-app my-app --infra neon
160
+ pnpm clubedge neon claim
161
+ pnpm clubedge deploy --preview
150
162
  pnpm dlx @clubedge/create-clubedge-app try --framework tanstack-start
151
163
 
152
164
  Learn more:
@@ -1,5 +1,6 @@
1
1
  // What `deploy` sets up on Railway for a project, as pure functions: the service name, the
2
2
  // production variables, and reading the Railway CLI's output. src/deploy.ts runs the CLI.
3
+ import { isNeonPooledUrl, neonDirectUrl } from "./neon.js";
3
4
  /** The port the production image listens on (the Starter's Dockerfile sets PORT=3000). */
4
5
  export const appPort = 3000;
5
6
  /** Railway's names for the databases `railway add --database` creates, used in references. */
@@ -83,7 +84,7 @@ export function planVariables(input) {
83
84
  const set = { ...provided, APP_URL: url, NEXT_PUBLIC_APP_URL: url };
84
85
  if (modules.auth === "better-auth" && !existing.BETTER_AUTH_SECRET)
85
86
  set.BETTER_AUTH_SECRET = secret();
86
- const databaseKeys = Object.values(databaseVariables);
87
+ const databaseKeys = [...Object.values(databaseVariables), "DATABASE_URL_DIRECT"];
87
88
  for (const [key, value] of Object.entries(envFile)) {
88
89
  if (value === "" || databaseKeys.includes(key))
89
90
  continue;
@@ -93,9 +94,49 @@ export function planVariables(input) {
93
94
  set[key] = value;
94
95
  }
95
96
  Object.assign(set, external);
96
- const unset = keys.filter((key) => set[key] === undefined && !existing[key]);
97
+ const direct = directDatabaseUrl(external.DATABASE_URL, envFile.DATABASE_URL_DIRECT);
98
+ if (direct)
99
+ set.DATABASE_URL_DIRECT = direct;
100
+ // A direct URL left from an earlier database would send migrations there: point it at the
101
+ // database the app now uses, or blank it (blank counts as unset).
102
+ else if (existing.DATABASE_URL_DIRECT)
103
+ set.DATABASE_URL_DIRECT = set.DATABASE_URL ?? "";
104
+ // Optional: without it, migrations and the jobs worker use DATABASE_URL.
105
+ const unset = keys.filter((key) => set[key] === undefined && !existing[key] && key !== "DATABASE_URL_DIRECT");
97
106
  return { set, unset };
98
107
  }
108
+ /**
109
+ * DATABASE_URL_DIRECT for a database hosted elsewhere: the env file's, or for a pooled Neon URL the
110
+ * same URL without -pooler. Never next to the platform's own database (no external URL), where it
111
+ * would send migrations to another database.
112
+ */
113
+ export function directDatabaseUrl(external, direct) {
114
+ if (!external)
115
+ return null;
116
+ if (direct?.trim() && !isLocalUrl(direct))
117
+ return direct.trim();
118
+ return isNeonPooledUrl(external) ? neonDirectUrl(external) : null;
119
+ }
120
+ /** `railway environment list --json`: an array of environments, or an object holding them. */
121
+ export function railwayEnvironmentNames(output) {
122
+ let value;
123
+ try {
124
+ value = JSON.parse(output);
125
+ }
126
+ catch {
127
+ throw new Error("Railway listed its environments in a form deploy doesn't read. Check `railway environment list --json`.");
128
+ }
129
+ const list = Array.isArray(value) ? value : (value?.environments ?? []);
130
+ const items = Array.isArray(list) ? list : (list?.edges ?? []);
131
+ return (Array.isArray(items) ? items : [])
132
+ .map((item) => item?.node ?? item)
133
+ .map((item) => item?.name)
134
+ .filter((name) => typeof name === "string");
135
+ }
136
+ /** The body of `POST /v10/projects/:id/env?upsert=true`: encrypted Preview variables of one Git branch. */
137
+ export function vercelPreviewEnvBody(values, gitBranch) {
138
+ return Object.entries(values).map(([key, value]) => ({ key, value, type: "encrypted", target: ["preview"], gitBranch }));
139
+ }
99
140
  /** The public domain in `railway domain` output (JSON or text). */
100
141
  export function parseRailwayDomain(output) {
101
142
  const domain = /([a-z0-9-]+(?:\.[a-z0-9-]+)*\.up\.railway\.app)/i.exec(output)?.[1];
@@ -1,6 +1,7 @@
1
1
  // Pure checks behind `status` and `doctor`: they take what the commands read from the project,
2
2
  // the system, and npm, and return what to show, so each can be tested without a real project.
3
3
  import { envKeys } from "./env.js";
4
+ import { isNeonPooledUrl, isNeonUrl, neonEndpoint } from "./neon.js";
4
5
  /** Compares dotted versions such as "v0.13.1" and "0.12.0"; null when either is not one. */
5
6
  export function compareVersions(a, b) {
6
7
  const parse = (value) => {
@@ -159,3 +160,53 @@ export function checkLockfile(error) {
159
160
  detail: `could not check: ${error.split("\n").find((line) => line.trim()) ?? error}`,
160
161
  };
161
162
  }
163
+ /** The database a connection URL names (its path). */
164
+ function databaseName(url) {
165
+ try {
166
+ return decodeURIComponent(new URL(url.trim()).pathname.slice(1));
167
+ }
168
+ catch {
169
+ return "";
170
+ }
171
+ }
172
+ /**
173
+ * The database URLs in the local env file: Neon's pooled URL for the app and its direct URL for
174
+ * migrations and the jobs worker, and how long a free Neon database the CLI created has left.
175
+ * Null when there is nothing to say (no Neon, no direct URL, and infra isn't neon).
176
+ */
177
+ export function checkDatabaseUrls(input) {
178
+ const { env, infra, claimable, cli, envPath } = input;
179
+ const name = "Database URLs";
180
+ const url = env.DATABASE_URL ?? "";
181
+ const direct = env.DATABASE_URL_DIRECT ?? "";
182
+ if (!url) {
183
+ return infra === "neon"
184
+ ? { name, status: "fail", detail: `DATABASE_URL in ${envPath} is empty`, fix: `Run \`${cli} neon create\` for a free Neon database, or paste your Neon project's pooled URL.` }
185
+ : null;
186
+ }
187
+ if (direct && isNeonPooledUrl(direct)) {
188
+ return { name, status: "fail", detail: "DATABASE_URL_DIRECT points at Neon's pooler", fix: "Use the same URL without -pooler in the host: migrations and the jobs worker need a direct session." };
189
+ }
190
+ if (claimable?.kind === "expired") {
191
+ return { name, status: "fail", detail: "the free Neon database in DATABASE_URL expired, and Neon deleted it", fix: `Run \`${cli} neon create\`, then \`pnpm db:migrate\`.` };
192
+ }
193
+ if (!isNeonUrl(url)) {
194
+ return direct ? { name, status: "ok", detail: "DATABASE_URL for the app, DATABASE_URL_DIRECT for migrations and the jobs worker" } : null;
195
+ }
196
+ if (!isNeonPooledUrl(url)) {
197
+ return { name, status: "warn", detail: "DATABASE_URL is Neon's direct host, which has fewer connections", fix: "Put the pooled URL (the host with -pooler) in DATABASE_URL, and this one in DATABASE_URL_DIRECT." };
198
+ }
199
+ if (!direct) {
200
+ return { name, status: "warn", detail: "DATABASE_URL is Neon's pooler, and DATABASE_URL_DIRECT is not set", fix: "Set DATABASE_URL_DIRECT to the same URL without -pooler in the host, for migrations and the jobs worker." };
201
+ }
202
+ if (neonEndpoint(direct) !== neonEndpoint(url) || databaseName(direct) !== databaseName(url)) {
203
+ return { name, status: "warn", detail: "DATABASE_URL and DATABASE_URL_DIRECT point at different Neon databases", fix: "Copy both from the same branch in the Neon console: pooled and direct." };
204
+ }
205
+ if (claimable?.kind === "unclaimed") {
206
+ const detail = `a free Neon database, unclaimed: ${claimable.hoursLeft} hours left`;
207
+ return claimable.hoursLeft < 24
208
+ ? { name, status: "warn", detail, fix: `Keep it with \`${cli} neon claim\`, or Neon deletes it with its data.` }
209
+ : { name, status: "ok", detail: `${detail} (\`${cli} neon claim\` keeps it)` };
210
+ }
211
+ return { name, status: "ok", detail: "Neon, pooled for the app and direct for migrations and the jobs worker" };
212
+ }