@clubedge/create-clubedge-app 0.22.0 → 0.24.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 (37) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +13 -11
  3. package/dist/core/deploy.js +45 -0
  4. package/dist/deploy.js +109 -13
  5. package/package.json +3 -3
  6. package/template/.github/workflows/deploy.yml +3 -2
  7. package/template/.railway/railway.ts +35 -0
  8. package/template/README.md +1 -1
  9. package/template/SETUP.md +3 -1
  10. package/template/apps/start/package.json +1 -1
  11. package/template/apps/web/package.json +1 -1
  12. package/template/clubedge.template.json +1 -1
  13. package/template/package.json +1 -1
  14. package/template/packages/auth/package.json +1 -1
  15. package/template/packages/auth-better-auth/package.json +1 -1
  16. package/template/packages/auth-supabase/package.json +1 -1
  17. package/template/packages/cache/package.json +1 -1
  18. package/template/packages/cache-redis/package.json +1 -1
  19. package/template/packages/core/package.json +1 -1
  20. package/template/packages/db/package.json +1 -1
  21. package/template/packages/email/package.json +1 -1
  22. package/template/packages/email-resend/package.json +1 -1
  23. package/template/packages/email-smtp/package.json +1 -1
  24. package/template/packages/jobs/package.json +1 -1
  25. package/template/packages/jobs-pg-boss/package.json +1 -1
  26. package/template/packages/local-db/package.json +1 -1
  27. package/template/packages/observability/package.json +1 -1
  28. package/template/packages/observability-otel/package.json +1 -1
  29. package/template/packages/observability-sentry/package.json +1 -1
  30. package/template/packages/payments/package.json +1 -1
  31. package/template/packages/payments-stripe/package.json +1 -1
  32. package/template/packages/storage/package.json +1 -1
  33. package/template/packages/storage-s3/package.json +1 -1
  34. package/template/packages/storage-supabase/package.json +1 -1
  35. package/template/packages/ui/package.json +1 -1
  36. package/template.lock.json +3 -3
  37. package/template/railway.json +0 -13
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to `create-clubedge-app` are documented here.
4
4
 
5
+ ## 0.24.0
6
+
7
+ - Default to Starter `v0.17.0` (#61), which adds:
8
+ - Railway configuration as code: projects that deploy to Railway get `.railway/railway.ts` (the migrations before each release, the `/api/health` check) instead of `railway.json`, which Railway stops reading on 2026-12-01. It names the service after the package, and `pnpm clubedge deploy` applies it before each release.
9
+ - The deploy workflow on Railway CLI 5.
10
+ - If you deploy to Railway: run `pnpm clubedge upgrade`, then deploy once with `pnpm clubedge deploy` (or run `railway config apply`) before 2026-12-01.
11
+
12
+ ## 0.23.0
13
+
14
+ - `deploy` moves Railway projects to `.railway/railway.ts`, Railway's configuration as code, which replaces `railway.json`: Railway stops reading `railway.json` on 2026-12-01 (#59).
15
+ - When the project has no Railway configuration file, it writes one with the project's own names: the migrations before each release and the `/api/health` check, as in `railway.json`. Variables stay as `deploy` set them.
16
+ - It adds Railway's TypeScript SDK (`railway`) as a dev dependency, and applies the file with `railway config apply` before each release. Commit `.railway/railway.ts`, `package.json`, and `pnpm-lock.yaml`.
17
+ - It needs Railway CLI 5.42.1 or newer: an installed `railway`, else one npx already downloaded, and only then a download.
18
+ - Still on Starter `v0.16.1`.
19
+
5
20
  ## 0.22.0
6
21
 
7
22
  - Add `deploy`: `pnpm clubedge deploy` does the first deploy of a project that uses `deploy railway`, with Railway's CLI and your Railway login (#55).
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.16.1
130
+ pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.17.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.16.1 # or a specific tag, branch, or commit
149
+ pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.17.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.
@@ -199,34 +199,34 @@ pnpm clubedge deploy --env-file .env.production # with your production provide
199
199
  pnpm clubedge deploy --dry-run # show the plan, create nothing
200
200
  ```
201
201
 
202
- With [Railway's CLI](https://docs.railway.com/cli) (the installed `railway`, or `npx @railway/cli`) and your Railway login, it creates or reuses a project named after the package, Postgres (and Redis with `cache redis`), the app's service, and a `*.up.railway.app` domain. It sets the variables it can: `DATABASE_URL` and `REDIS_URL` as references to those databases, `APP_URL` and `NEXT_PUBLIC_APP_URL`, `PORT`, and a generated `BETTER_AUTH_SECRET` that later runs keep. Values from `--env-file` come on top; the variables `.env.example` lists that nothing sets are reported, for the modules that need them (storage, email, payments). Then `railway up` builds the Dockerfile, Railway runs the migrations before the release and checks `/api/health`, and the URL is printed. Running `deploy` again reuses everything, so it also adds variables and ships a new release. Railway bills usage on your plan.
202
+ With [Railway's CLI](https://docs.railway.com/cli) (the installed `railway`, or `npx @railway/cli`) and your Railway login, it creates or reuses a project named after the package, Postgres (and Redis with `cache redis`), the app's service, and a `*.up.railway.app` domain. It sets the variables it can: `DATABASE_URL` and `REDIS_URL` as references to those databases, `APP_URL` and `NEXT_PUBLIC_APP_URL`, `PORT`, and a generated `BETTER_AUTH_SECRET` that later runs keep. Values from `--env-file` come on top; the variables `.env.example` lists that nothing sets are reported, for the modules that need them (storage, email, payments). The service's settings (the migrations before each release, the `/api/health` check) go in `.railway/railway.ts`, Railway's configuration as code, which replaces `railway.json` (Railway stops reading it on 2026-12-01): `deploy` writes it when the project has none, adds Railway's TypeScript SDK (`railway`) as a dev dependency, and applies it with `railway config apply`; commit them. Then `railway up` builds the Dockerfile, and `deploy` waits for Railway's final status before printing the URL. It needs Railway CLI 5.42.1 or newer: an installed `railway`, else one npx already downloaded, else it downloads one. Running `deploy` again reuses everything, so it also adds variables and ships a new release. Railway bills usage on your plan.
203
203
 
204
204
  ## Version relationship
205
205
 
206
206
  Each CLI release is tested against one specific Starter release:
207
207
 
208
208
  ```text
209
- create-clubedge-app v0.22.0
209
+ create-clubedge-app v0.24.0
210
210
  |
211
211
  v
212
- Clubedge Starter v0.16.1
213
- commit 72ff89e53a55a4e4a6262b364ce73be3b1c7a6b9
212
+ Clubedge Starter v0.17.0
213
+ commit 91be4541de5833527b52a8871a08578f63b89735
214
214
  ```
215
215
 
216
216
  The current relationship is:
217
217
 
218
218
  | CLI release | Default Starter release | Meaning |
219
219
  | --- | --- | --- |
220
- | `create-clubedge-app@0.22.0` | `clubedge-starter@v0.16.1` | The CLI's required, tested default |
220
+ | `create-clubedge-app@0.24.0` | `clubedge-starter@v0.17.0` | The CLI's required, tested default |
221
221
 
222
222
  This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
223
223
 
224
- The `v0.16.1` tag is expected to resolve to commit
225
- `72ff89e53a55a4e4a6262b364ce73be3b1c7a6b9`. The CLI verifies this before
224
+ The `v0.17.0` tag is expected to resolve to commit
225
+ `91be4541de5833527b52a8871a08578f63b89735`. The CLI verifies this before
226
226
  scaffolding with the default reference and fails rather than silently using a
227
227
  moved tag.
228
228
 
229
- When the Starter changes, its maintainers first publish a new versioned release, such as `v0.16.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.
229
+ When the Starter changes, its maintainers first publish a new versioned release, such as `v0.17.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.
230
230
 
231
231
  Every generated project records its CLI version, Starter repository, Starter ref, Starter commit, framework, and modules in `package.json` and `README.md`.
232
232
 
@@ -281,7 +281,7 @@ CI packs the CLI with `npm pack`, scaffolds clean projects from the packed tarba
281
281
  1. Make and validate changes in `clubedge-starter`.
282
282
  2. Run lint, typecheck, unit tests, browser tests, and production build.
283
283
  3. Update the Starter package versions.
284
- 4. Commit `main`, create a release tag such as `v0.16.1`, and publish the Starter release.
284
+ 4. Commit `main`, create a release tag such as `v0.17.0`, and publish the Starter release.
285
285
 
286
286
  ### CLI maintainers
287
287
 
@@ -313,6 +313,8 @@ Each phase ends with a Starter release and a CLI release pinned to it. The full
313
313
  | 0.20.0 | v0.15.0 | The Starter's version of each conflicted file in `.clubedge/incoming/`, several modules at once, sharing a `try` preview by claiming the VM, `try --status` |
314
314
  | 0.21.0 | v0.16.0 | Starter v0.16.0: upgrades delivered as pull requests, Node 24 actions; release tooling for maintainers |
315
315
  | 0.22.0 | v0.16.1 | `pnpm clubedge deploy`: a guided first deploy to Railway; faster Docker builds |
316
+ | 0.23.0 | v0.16.1 | `deploy` moves Railway projects to `.railway/railway.ts` before Railway stops reading `railway.json` |
317
+ | 0.24.0 | v0.17.0 | Starter v0.17.0: Railway configuration as code (`.railway/railway.ts`) instead of `railway.json` |
316
318
  | next | | Show the Starter's version of a conflicted file next to yours |
317
319
 
318
320
  ## Community
@@ -129,3 +129,48 @@ export function deploymentStatus(output, id, since) {
129
129
  : deployments.find((deployment) => deployment.createdAt && new Date(deployment.createdAt).getTime() >= since.getTime() - 5_000);
130
130
  return found ? { id: found.id, status: found.status } : null;
131
131
  }
132
+ /** The oldest Railway CLI that applies `.railway/railway.ts`: its IaC engine ships in the CLI. */
133
+ export const minimumRailwayCli = [5, 42, 1];
134
+ /** The version in `railway --version` output, such as "railway 5.67.1". */
135
+ export function railwayCliVersion(output) {
136
+ const match = /\b(\d+)\.(\d+)\.(\d+)\b/.exec(output);
137
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
138
+ }
139
+ export function meetsMinimumCli(version) {
140
+ for (let i = 0; i < 3; i++) {
141
+ if (version[i] !== minimumRailwayCli[i])
142
+ return version[i] > minimumRailwayCli[i];
143
+ }
144
+ return true;
145
+ }
146
+ export const railwayIacPath = ".railway/railway.ts";
147
+ /**
148
+ * The project's Railway configuration as code (`.railway/railway.ts`), which replaces railway.json:
149
+ * Railway stops reading railway.json on 2026-12-01. Railway builds the root Dockerfile by itself,
150
+ * the variables stay managed by `deploy` and the dashboard, and the region is Railway's default.
151
+ */
152
+ export function railwayIacSource(service) {
153
+ const name = JSON.stringify(service);
154
+ return `// Railway's configuration for this project, applied with \`railway config apply\` (pnpm clubedge
155
+ // deploy does it before each release). It replaces railway.json, which Railway stops reading on
156
+ // 2026-12-01. Railway builds the root Dockerfile; variables are set with \`railway variable set\`.
157
+ import { defineRailway, project, service } from "railway/iac";
158
+
159
+ // This repository manages only its own resources in the Railway project.
160
+ export const partial = ${name};
161
+
162
+ export default defineRailway(() => {
163
+ const app = service(${name}, {
164
+ // Migrations run before each release; a failed migration stops it.
165
+ preDeploy: "node /app/migrate.mjs",
166
+ healthcheck: "/api/health",
167
+ healthcheckTimeout: 60,
168
+ });
169
+
170
+ return project(${name}, {
171
+ variables: { managed: false },
172
+ resources: [app],
173
+ });
174
+ });
175
+ `;
176
+ }
package/dist/deploy.js CHANGED
@@ -4,31 +4,94 @@
4
4
  import { cancel, confirm, isCancel, log, note, outro, spinner } from "@clack/prompts";
5
5
  import { spawn } from "node:child_process";
6
6
  import { randomBytes } from "node:crypto";
7
- import { readFile } from "node:fs/promises";
8
- import { join, resolve } from "node:path";
7
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
8
+ import { homedir } from "node:os";
9
+ import { dirname, join, resolve } from "node:path";
9
10
  import process from "node:process";
10
11
  import { UsageError } from "./cli/args.js";
11
12
  import { bold, dim } from "./cli/ui.js";
12
- import { appPort, databasesFor, deploymentStatus, exampleKeys, finalStatuses, parseDeploymentId, parseEnvFile, parseRailwayDomain, parseVariables, planVariables, railwayDatabases, serviceName, serviceNames, } from "./core/deploy.js";
13
+ import { appPort, databasesFor, deploymentStatus, exampleKeys, finalStatuses, meetsMinimumCli, minimumRailwayCli, parseDeploymentId, parseEnvFile, parseRailwayDomain, parseVariables, planVariables, railwayCliVersion, railwayDatabases, railwayIacPath, railwayIacSource, serviceName, serviceNames, } from "./core/deploy.js";
13
14
  import { readProject } from "./core/project.js";
14
15
  import { cliCommand } from "./modify.js";
15
- import { commandSucceeds } from "./utils/process.js";
16
+ import { resolvePnpm } from "./steps/install.js";
17
+ import { pathExists } from "./utils/fs.js";
18
+ import { run } from "./utils/process.js";
16
19
  const railwayTermsUrl = "https://railway.com/legal/terms";
17
20
  const deployDocsUrl = "https://starter.clubedge.live/docs/starter/deploy";
18
- // Package manager shims (npx.cmd, railway.cmd) need a shell on Windows. Every argument passed
19
- // here is a plain word; values go through stdin, so the shell never sees them.
20
- const useShell = process.platform === "win32";
21
- /** The Railway CLI: the installed `railway`, or the npm package through npx. */
21
+ const windows = process.platform === "win32";
22
+ const binaryName = windows ? "railway.exe" : "railway";
23
+ async function cliVersion(command, shell) {
24
+ return await new Promise((resolveVersion) => {
25
+ const [file, ...args] = command;
26
+ const child = spawn(file, [...args, "--version"], { shell, stdio: ["ignore", "pipe", "ignore"] });
27
+ let out = "";
28
+ child.stdout?.on("data", (chunk) => (out += chunk.toString()));
29
+ child.once("error", () => resolveVersion(null));
30
+ child.once("close", (code) => resolveVersion(code === 0 ? railwayCliVersion(out) : null));
31
+ });
32
+ }
33
+ /** The railway.exe behind `railway` on PATH (npm installs a railway.cmd shim next to its package). */
34
+ async function installedBinary() {
35
+ if (!windows)
36
+ return (await cliVersion(["railway"], false)) ? "railway" : null;
37
+ const found = await new Promise((resolveWhere) => {
38
+ const child = spawn("where", ["railway"], { stdio: ["ignore", "pipe", "ignore"] });
39
+ let out = "";
40
+ child.stdout?.on("data", (chunk) => (out += chunk.toString()));
41
+ child.once("error", () => resolveWhere(""));
42
+ child.once("close", () => resolveWhere(out));
43
+ });
44
+ for (const line of found.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean)) {
45
+ if (line.toLowerCase().endsWith(".exe"))
46
+ return line;
47
+ const shimmed = join(dirname(line), "node_modules", "@railway", "cli", "bin", binaryName);
48
+ if (await pathExists(shimmed))
49
+ return shimmed;
50
+ }
51
+ return null;
52
+ }
53
+ /** Railway CLI binaries npx already downloaded, newest first: no download on a slow connection. */
54
+ async function cachedBinaries() {
55
+ const cache = process.env.npm_config_cache ||
56
+ (windows && process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, "npm-cache") : join(homedir(), ".npm"));
57
+ const found = [];
58
+ for (const entry of await readdir(join(cache, "_npx")).catch(() => [])) {
59
+ const path = join(cache, "_npx", entry, "node_modules", "@railway", "cli", "bin", binaryName);
60
+ const stats = await stat(path).catch(() => null);
61
+ if (stats?.isFile())
62
+ found.push({ path, time: stats.mtimeMs });
63
+ }
64
+ return found.sort((a, b) => b.time - a.time).map((entry) => entry.path);
65
+ }
66
+ /**
67
+ * A Railway CLI new enough for `.railway/railway.ts`: the installed one, then one npx already
68
+ * downloaded, and only then a download through npx.
69
+ */
22
70
  async function railwayCli() {
23
- if (await commandSucceeds("railway", ["--version"]))
24
- return ["railway"];
25
- return ["npx", "--yes", "@railway/cli"];
71
+ const usable = async (binary) => {
72
+ const version = await cliVersion([binary], false);
73
+ return version !== null && meetsMinimumCli(version);
74
+ };
75
+ const installed = await installedBinary();
76
+ if (installed && (await usable(installed)))
77
+ return { command: [installed], executable: installed, shell: false };
78
+ for (const binary of await cachedBinaries()) {
79
+ if (await usable(binary))
80
+ return { command: [binary], executable: binary, shell: false };
81
+ }
82
+ await cliVersion(["npx", "--yes", "@railway/cli@latest"], windows);
83
+ for (const binary of await cachedBinaries()) {
84
+ if (await usable(binary))
85
+ return { command: [binary], executable: binary, shell: false };
86
+ }
87
+ throw new UsageError(`deploy needs Railway CLI ${minimumRailwayCli.join(".")} or newer. Install it with \`npm install -g @railway/cli\`, then run deploy again.`);
26
88
  }
27
89
  async function railway(cli, args, options = {}) {
28
- const [command, ...prefix] = cli;
90
+ const [command, ...prefix] = cli.command;
29
91
  return await new Promise((resolvePromise, reject) => {
30
92
  const child = spawn(command, [...prefix, ...args], {
31
- shell: useShell,
93
+ shell: cli.shell,
94
+ env: { ...process.env, _: cli.executable },
32
95
  stdio: options.inherit ? "inherit" : ["pipe", "pipe", "pipe"],
33
96
  });
34
97
  let stdout = "";
@@ -58,6 +121,7 @@ async function agree(args, service, databases) {
58
121
  `• A project with ${databases.map((db) => railwayDatabases[db]).join(" and ")}, and a service "${service}" with a public *.up.railway.app domain.`,
59
122
  "• The production variables Railway can provide; your provider keys come from --env-file.",
60
123
  "• Railway builds the Dockerfile, runs the migrations before the release, and checks /api/health; the first build takes several minutes.",
124
+ `• The service's settings go in ${railwayIacPath}, which replaces railway.json; the railway package is added to read it.`,
61
125
  "",
62
126
  `Railway bills usage on your plan. Running deploy again reuses all of it. Railway's terms: ${railwayTermsUrl}`,
63
127
  ].join("\n"), "Deploy to Railway");
@@ -85,6 +149,7 @@ export async function deployProject(args) {
85
149
  log.message([
86
150
  `• Railway project "${service}" with ${databases.join(" and ")}, and the service "${service}"`,
87
151
  `• A *.up.railway.app domain for port ${appPort}`,
152
+ `• ${railwayIacPath} (written if missing, with Railway's SDK added), applied with railway config apply`,
88
153
  `• Variables: ${Object.keys(preview.set).join(", ")}`,
89
154
  ...(preview.unset.length ? [`• Not set: ${preview.unset.join(", ")}`] : []),
90
155
  `• railway up: build, migrations, health check`,
@@ -146,7 +211,38 @@ export async function deployProject(args) {
146
211
  continue;
147
212
  await railway(rw, ["variable", "set", key, "--stdin", "--service", service, "--skip-deploys"], { input: value });
148
213
  }
214
+ // The service's settings as code: .railway/railway.ts replaces railway.json, which Railway stops
215
+ // reading on 2026-12-01. It needs Railway's TypeScript SDK, and applies before each release.
216
+ const iacFiles = [railwayIacPath, ".railway/railway.py", ".railway/railway.go"];
217
+ const changed = [];
218
+ let hasIac = false;
219
+ for (const file of iacFiles)
220
+ if (await pathExists(join(project.root, file)))
221
+ hasIac = true;
222
+ if (!hasIac) {
223
+ await mkdir(join(project.root, ".railway"), { recursive: true });
224
+ await writeFile(join(project.root, railwayIacPath), railwayIacSource(service));
225
+ changed.push(railwayIacPath);
226
+ }
227
+ if (await pathExists(join(project.root, railwayIacPath))) {
228
+ const manifest = JSON.parse(await readFile(join(project.root, "package.json"), "utf8"));
229
+ if (!manifest.devDependencies?.railway && !manifest.dependencies?.railway) {
230
+ const pnpm = await resolvePnpm();
231
+ if (!pnpm)
232
+ throw new UsageError("pnpm was not found. Enable it with `corepack enable`, then run deploy again.");
233
+ progress.stop("Adding Railway's TypeScript SDK (railway), which .railway/railway.ts imports");
234
+ const [command, ...prefix] = pnpm;
235
+ await run(command, [...prefix, "add", "--save-dev", "--workspace-root", "railway"], project.root);
236
+ changed.push("package.json", "pnpm-lock.yaml");
237
+ progress.start("Applying .railway/railway.ts");
238
+ }
239
+ }
240
+ progress.message(`Applying ${railwayIacPath}`);
241
+ await railway(rw, ["config", "apply", "--yes"]);
149
242
  progress.stop(`Railway is ready: ${url}`);
243
+ if (changed.length) {
244
+ log.info(`Commit ${changed.join(", ")}: Railway reads the service's settings from ${railwayIacPath} now. railway.json is no longer needed (Railway stops reading it on 2026-12-01).`);
245
+ }
150
246
  // Start the deploy detached, then follow its status: `railway up` can return before the build
151
247
  // ends, and only Railway's final status says whether the release went live.
152
248
  const deploying = spinner();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/create-clubedge-app",
3
- "version": "0.22.0",
3
+ "version": "0.24.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.16.1",
26
- "starterCommit": "72ff89e53a55a4e4a6262b364ce73be3b1c7a6b9"
25
+ "starterRef": "v0.17.0",
26
+ "starterCommit": "91be4541de5833527b52a8871a08578f63b89735"
27
27
  },
28
28
  "license": "Apache-2.0",
29
29
  "repository": {
@@ -54,8 +54,9 @@ jobs:
54
54
  runs-on: ubuntu-24.04
55
55
  steps:
56
56
  - uses: actions/checkout@v7
57
- # Uploads this checkout; Railway builds the image and runs the pre-deploy migrations.
58
- - run: npx --yes @railway/cli@4 up --ci --service "$RAILWAY_SERVICE"
57
+ # Uploads this checkout; Railway builds the image and runs the pre-deploy migrations set by
58
+ # .railway/railway.ts (applied with `pnpm clubedge deploy` or `railway config apply`).
59
+ - run: npx --yes @railway/cli@5 up --ci --service "$RAILWAY_SERVICE"
59
60
  env:
60
61
  RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
61
62
  RAILWAY_SERVICE: ${{ vars.RAILWAY_SERVICE }}
@@ -0,0 +1,35 @@
1
+ // Railway's configuration for this project, as code: the migrations before each release and the
2
+ // health check. `pnpm clubedge deploy` applies it before each release; elsewhere, run
3
+ // `railway config apply` once. Railway builds the root Dockerfile; set variables with
4
+ // `railway variable set` or the dashboard.
5
+ import { readFileSync } from "node:fs";
6
+ import { defineRailway, project, service } from "railway/iac";
7
+
8
+ // The Railway service is named after the package, as `pnpm clubedge deploy` creates it. A service
9
+ // you created yourself under another name: put that name here.
10
+ const packageName: string = JSON.parse(
11
+ readFileSync(new URL("../package.json", import.meta.url), "utf8"),
12
+ ).name;
13
+ const name =
14
+ packageName
15
+ .replace(/^@[^/]+\//, "")
16
+ .toLowerCase()
17
+ .replace(/[^a-z0-9-]+/g, "-")
18
+ .replace(/^-+|-+$/g, "") || "web";
19
+
20
+ // This repository manages only its own resources in the Railway project.
21
+ export const partial = name;
22
+
23
+ export default defineRailway(() => {
24
+ const app = service(name, {
25
+ // Migrations run before each release; a failed migration stops it.
26
+ preDeploy: "node /app/migrate.mjs",
27
+ healthcheck: "/api/health",
28
+ healthcheckTimeout: 60,
29
+ });
30
+
31
+ return project(name, {
32
+ variables: { managed: false },
33
+ resources: [app],
34
+ });
35
+ });
@@ -74,7 +74,7 @@ Every combination in the CI matrix is generated as a real project and runs its f
74
74
  - A production image that includes its own migrator (`node /app/migrate.mjs`), for running migrations before each release.
75
75
  - A manual deploy workflow for the chosen platform, and a readiness endpoint (`/api/ready`) that checks the database.
76
76
  - Deployment to Fly.io with `fly.toml`: migrations run as the release command. <!-- clubedge:only deploy=fly -->
77
- - Deployment to Railway with `railway.json`: migrations run before each deploy. <!-- clubedge:only deploy=railway -->
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
79
  - Deployment to Vercel with `apps/web/vercel.json`: migrations run during the build. <!-- clubedge:only deploy=vercel -->
80
80
  - Deployment to Netlify with `apps/web/netlify.toml`: migrations run during the build. <!-- clubedge:only deploy=netlify -->
package/template/SETUP.md CHANGED
@@ -390,7 +390,9 @@ Set the other production variables listed below with `fly secrets set`. Each dep
390
390
  <!-- clubedge:end -->
391
391
  <!-- clubedge:if deploy=railway -->
392
392
 
393
- `railway.json` deploys the production image to [Railway](https://railway.com): create a project from this repository, add the production variables listed below to the service, and deploy. Railway runs `node /app/migrate.mjs` as the pre-deploy command, so a failed migration stops the release, and checks `/api/health` before switching traffic.
393
+ `.railway/railway.ts` is the service's configuration on [Railway](https://railway.com), as code: Railway builds the root `Dockerfile`, runs `node /app/migrate.mjs` before each release, so a failed migration stops it, and checks `/api/health` before switching traffic.
394
+
395
+ The quickest first deploy is `pnpm clubedge deploy`: it creates the project, Postgres, the service, its domain and variables, applies `.railway/railway.ts`, and deploys. To deploy another way, add Railway's TypeScript SDK with `pnpm add -D -w railway`, run `railway config apply` once (Railway CLI 5.42.1 or newer), and add the production variables listed below to the service. The file names the service after the package; if yours has another name, change it there.
394
396
 
395
397
  <!-- clubedge:end -->
396
398
  <!-- clubedge:if deploy=render -->
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/start",
3
- "version": "0.16.1",
3
+ "version": "0.17.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/web",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "scripts": {
@@ -397,7 +397,7 @@
397
397
  },
398
398
  "railway": {
399
399
  "name": "Railway",
400
- "files": ["railway.json"]
400
+ "files": [".railway/railway.ts"]
401
401
  },
402
402
  "render": {
403
403
  "name": "Render",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clubedge-starter-v1",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "packageManager": "pnpm@10.9.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/auth",
3
- "version": "0.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
3
+ "version": "0.17.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.16.1",
4
- "commit": "72ff89e53a55a4e4a6262b364ce73be3b1c7a6b9",
3
+ "ref": "v0.17.0",
4
+ "commit": "91be4541de5833527b52a8871a08578f63b89735",
5
5
  "files": [
6
6
  ".dockerignore",
7
7
  ".env.example",
@@ -16,6 +16,7 @@
16
16
  "_gitignore",
17
17
  ".prettierignore",
18
18
  ".prettierrc.json",
19
+ ".railway/railway.ts",
19
20
  "AGENTS.md",
20
21
  "CODE_OF_CONDUCT.md",
21
22
  "CONTRIBUTING.md",
@@ -378,7 +379,6 @@
378
379
  "playwright.config.ts",
379
380
  "pnpm-lock.yaml",
380
381
  "pnpm-workspace.yaml",
381
- "railway.json",
382
382
  "render.yaml",
383
383
  "scripts/build-migrator.mjs",
384
384
  "scripts/migrate.mjs",
@@ -1,13 +0,0 @@
1
- {
2
- "$schema": "https://railway.com/railway.schema.json",
3
- "build": {
4
- "builder": "DOCKERFILE",
5
- "dockerfilePath": "Dockerfile"
6
- },
7
- "deploy": {
8
- "preDeployCommand": ["node /app/migrate.mjs"],
9
- "healthcheckPath": "/api/health",
10
- "healthcheckTimeout": 60,
11
- "restartPolicyType": "ON_FAILURE"
12
- }
13
- }