@next-friday/create-app 9.6.0 → 9.7.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/README.md CHANGED
@@ -31,7 +31,7 @@ Every stack is built from [`templates/base`](templates/base), which holds the sh
31
31
  - **Code quality:** TypeScript, ESLint and Prettier with the Next Friday configs, Vitest with coverage, fast-check, Knip, dependency-cruiser, editorconfig-checker, and sort-package-json.
32
32
  - **Git hooks:** Husky runs gitleaks, `pnpm rules:check`, and lint-staged before each commit, commitlint on each message, and `pnpm check` before each push. After the first commit, commits to `main` are blocked, and local merges into `main` always are; pushes to `main` are blocked once it exists on the remote.
33
33
  - **CI:** GitHub Actions run the static checks, tests with patch coverage, the build, end-to-end tests for Next.js and Payload, a container build and vulnerability scan, Semgrep, OSV-Scanner, CodeQL, dependency review, OpenSSF Scorecard, gitleaks, TruffleHog, zizmor, and pull request title validation. Dependabot keeps dependencies current.
34
- - **Deployment:** a `Dockerfile` that runs as the `node` user, and a Railway workflow that promotes one commit through `dev`, `uat`, and `prod`.
34
+ - **Deployment:** a `Dockerfile` that runs as the `node` user. With `--deploy railway`, also a Railway workflow that promotes one commit through `dev`, `uat`, and `prod`.
35
35
  - **Agent rules:** `CLAUDE.md` imports `AGENTS.md`. Shared rules live in `.agents/rules`, with hooks that enforce them in Claude Code and Cursor.
36
36
 
37
37
  ## Requirements
@@ -44,17 +44,18 @@ Every stack is built from [`templates/base`](templates/base), which holds the sh
44
44
  ## Options
45
45
 
46
46
  ```text
47
- pnpm create @next-friday/app [name] [--stack nodejs|nestjs|nextjs|payload|turborepo]
47
+ pnpm create @next-friday/app [name] [--stack nodejs|nestjs|nextjs|payload|turborepo] [--deploy none|railway]
48
48
  ```
49
49
 
50
- | Option | Description |
51
- | :--------------- | :---------------------------------------------------------------------------- |
52
- | `name` | Project directory and package name. Asked when left out. |
53
- | `--stack` | `nodejs`, `nestjs`, `nextjs`, `payload`, or `turborepo`. Asked when left out. |
54
- | `--skip-git` | Do not run `git init`. |
55
- | `--skip-install` | Do not run `pnpm install`. |
56
- | `-v, --version` | Print the version. |
57
- | `-h, --help` | Print the usage. |
50
+ | Option | Description |
51
+ | :--------------- | :---------------------------------------------------------------------------------------- |
52
+ | `name` | Project directory and package name. Asked when left out. |
53
+ | `--stack` | `nodejs`, `nestjs`, `nextjs`, `payload`, or `turborepo`. Asked when left out. |
54
+ | `--deploy` | Deploy target: `none` or `railway`. The default is `none`, which adds no deploy workflow. |
55
+ | `--skip-git` | Do not run `git init`. |
56
+ | `--skip-install` | Do not run `pnpm install`. |
57
+ | `-v, --version` | Print the version. |
58
+ | `-h, --help` | Print the usage. |
58
59
 
59
60
  The name must:
60
61
 
package/dist/main.mjs CHANGED
@@ -117,6 +117,16 @@ function validateProjectName(name, cwd) {
117
117
  function ownEntries(directory) {
118
118
  return readdirSync(directory).filter((entry) => entry !== ".git");
119
119
  }
120
+ // src/deploy-targets.ts
121
+ const deployTargets = ["none", "railway"];
122
+ /**
123
+ * Check whether a string names a supported deploy target.
124
+ * @param value - Deploy target from the command line.
125
+ * @returns Whether `value` is a supported deploy target.
126
+ */
127
+ function isDeployTarget(value) {
128
+ return deployTargets.some((target) => target === value);
129
+ }
120
130
  // src/stacks.ts
121
131
  const stacks = [
122
132
  {
@@ -173,10 +183,11 @@ function templateName(stack) {
173
183
  return `next-friday-template-${stack}`;
174
184
  }
175
185
  // src/arguments.ts
176
- const usage = `Usage: pnpm create @next-friday/app [name] [--stack ${stacks.map((stack) => stack.id).join("|")}]
186
+ const usage = `Usage: pnpm create @next-friday/app [name] [--stack ${stacks.map((stack) => stack.id).join("|")}] [--deploy ${deployTargets.join("|")}]
177
187
 
178
188
  name Project directory and package name, or . for the current directory. Asked when left out.
179
189
  --stack Template stack. Asked when left out.
190
+ --deploy Deploy target: ${deployTargets.join(" or ")}. The default is none.
180
191
  --skip-git Do not run git init.
181
192
  --skip-install Do not run pnpm install.
182
193
  -v, --version Show the version.
@@ -192,6 +203,10 @@ function parseCliArguments(cliArguments) {
192
203
  allowPositionals: true,
193
204
  options: {
194
205
  stack: { type: "string" },
206
+ deploy: {
207
+ type: "string",
208
+ default: "none"
209
+ },
195
210
  "skip-git": {
196
211
  type: "boolean",
197
212
  default: false
@@ -212,23 +227,33 @@ function parseCliArguments(cliArguments) {
212
227
  }
213
228
  }
214
229
  });
215
- const { help, stack, version } = values;
230
+ const { deploy, help, stack, version } = values;
216
231
  const isInformational = help || version;
217
232
  if (!isInformational && positionals.length > 1) throw new Error(`Pass one project name, not ${positionals.length}: ${positionals.join(" ")}.`);
218
233
  const [name] = positionals;
219
- if (!isInformational && stack !== void 0 && !isStackId(stack)) {
220
- const ids = stacks.map((supported) => supported.id).join(", ");
221
- throw new Error(`Unknown stack "${stack}". Use one of: ${ids}.`);
222
- }
234
+ if (!isInformational) assertKnownChoices(stack, deploy);
223
235
  return {
224
236
  help,
225
237
  name,
226
238
  version,
239
+ deploy: isDeployTarget(deploy) ? deploy : "none",
227
240
  stack: stack !== void 0 && isStackId(stack) ? stack : void 0,
228
241
  skipGit: values["skip-git"],
229
242
  skipInstall: values["skip-install"]
230
243
  };
231
244
  }
245
+ /**
246
+ * Throw when the stack or the deploy target is not one the CLI supports.
247
+ * @param stack - Stack from the command line, or `undefined` when left out.
248
+ * @param deploy - Deploy target from the command line.
249
+ */
250
+ function assertKnownChoices(stack, deploy) {
251
+ if (stack !== void 0 && !isStackId(stack)) {
252
+ const ids = stacks.map((supported) => supported.id).join(", ");
253
+ throw new Error(`Unknown stack "${stack}". Use one of: ${ids}.`);
254
+ }
255
+ if (!isDeployTarget(deploy)) throw new Error(`Unknown deploy target "${deploy}". Use one of: ${deployTargets.join(", ")}.`);
256
+ }
232
257
  // src/rename.ts
233
258
  /**
234
259
  * Replace the template name with the project name in every text file.
@@ -245,6 +270,45 @@ async function renameProject(directory, from, to) {
245
270
  await writeFile(file, text.replaceAll(from, () => to));
246
271
  }));
247
272
  }
273
+ // src/deploy-sections.ts
274
+ const marker = /^\s*(?:#|\/\/|<!--)\s*deploy:(?<name>[a-z]+)\s*(?:-->\s*)?$/u;
275
+ const endName = "end";
276
+ /**
277
+ * Keep the sections of a template file marked for the chosen deploy target and remove the others.
278
+ * A section starts at a comment line that holds `deploy:<target>` and ends at one that holds `deploy:end`.
279
+ * Every marker line is removed, with a blank line that directly follows it.
280
+ * @param content - Text of a copied template file.
281
+ * @param target - Chosen deploy target.
282
+ * @returns The text for the chosen deploy target.
283
+ */
284
+ function resolveDeploySections(content, target) {
285
+ const kept = [];
286
+ let section;
287
+ let isAfterMarker = false;
288
+ for (const line of content.split("\n")) {
289
+ const name = marker.exec(line)?.groups?.name;
290
+ if (name === void 0) {
291
+ if (!(isAfterMarker && line.trim() === "") && (section ?? target) === target) kept.push(line);
292
+ } else section = sectionAfterMarker(name, section);
293
+ isAfterMarker = name !== void 0;
294
+ }
295
+ if (section !== void 0) throw new Error(`The deploy:${section} section has no deploy:end marker.`);
296
+ return kept.join("\n");
297
+ }
298
+ /**
299
+ * Give the section a marker line leaves open, or throw when the marker is out of place.
300
+ * @param name - Deploy target the marker names, or `end`.
301
+ * @param section - Deploy target of the section the marker sits in, or `undefined` outside a section.
302
+ * @returns The deploy target of the section now open, or `undefined` after an end marker.
303
+ */
304
+ function sectionAfterMarker(name, section) {
305
+ if (name === endName) {
306
+ if (section === void 0) throw new Error("A deploy:end marker has no deploy target section to close.");
307
+ return;
308
+ }
309
+ if (section !== void 0) throw new Error(`A deploy:${name} marker starts inside the deploy:${section} section.`);
310
+ return name;
311
+ }
248
312
  // src/create.ts
249
313
  const templatesDirectory = fileURLToPath(new URL("../templates", import.meta.url));
250
314
  const interruptSignals = [
@@ -276,7 +340,7 @@ async function create(cliArguments) {
276
340
  const target = path.resolve(cwd, name);
277
341
  const directory = existsSync(target) ? realpathSync(target) : target;
278
342
  log.step(`Copying the ${stack} template.`);
279
- await writeProject(stack, directory, manifestName);
343
+ await writeProject(stack, options.deploy, directory, manifestName);
280
344
  log.success(`Created ${manifestName} from the ${stack} template.`);
281
345
  const { git, isInstalled } = setUpProject(directory, options);
282
346
  outro(nextSteps(name, stack, git, isInstalled));
@@ -294,10 +358,11 @@ async function create(cliArguments) {
294
358
  * Copy, rename, and write `.env`, removing what was written when a step fails or Ctrl+C, SIGTERM, or SIGHUP arrives.
295
359
  * An interrupt waits for the running step, so the removal does not race the copy.
296
360
  * @param stack - Supported stack id.
361
+ * @param deploy - Supported deploy target.
297
362
  * @param directory - Project directory.
298
363
  * @param manifestName - Package name for `package.json`.
299
364
  */
300
- async function writeProject(stack, directory, manifestName) {
365
+ async function writeProject(stack, deploy, directory, manifestName) {
301
366
  const kept = existsSync(directory) ? readdirSync(directory) : void 0;
302
367
  let isInterrupted = false;
303
368
  const interrupt = () => {
@@ -305,7 +370,7 @@ async function writeProject(stack, directory, manifestName) {
305
370
  };
306
371
  for (const signal of interruptSignals) process.on(signal, interrupt);
307
372
  try {
308
- await copyTemplate(stack, directory);
373
+ await copyTemplate(stack, deploy, directory);
309
374
  await renameProject(directory, templateName(stack), manifestName);
310
375
  await writeEnvironmentFile(directory);
311
376
  if (isInterrupted) throw new CancelledError();
@@ -382,19 +447,49 @@ function existingGit(directory) {
382
447
  return existsSync(path.join(directory, ".git")) ? "existing" : void 0;
383
448
  }
384
449
  /**
385
- * Copy the shared template files, then the stack's files, into the project directory.
450
+ * Copy the shared template files, the stack's files, and the deploy target's files into the project directory.
386
451
  * npm leaves `.gitignore` out of published packages, so the template keeps it as `_gitignore`.
387
452
  * @param stack - Supported stack id.
453
+ * @param deploy - Supported deploy target.
388
454
  * @param directory - Project directory.
389
455
  */
390
- async function copyTemplate(stack, directory) {
391
- for (const template of ["base", stack]) if (!existsSync(path.join(templatesDirectory, template))) throw new Error(`The ${template} template is missing from ${templatesDirectory}. Reinstall @next-friday/create-app.`);
392
- await cp(path.join(templatesDirectory, "base"), directory, { recursive: true });
393
- await cp(path.join(templatesDirectory, stack), directory, { recursive: true });
456
+ async function copyTemplate(stack, deploy, directory) {
457
+ const deployLayer = deploy === "none" ? void 0 : `deploy-${deploy}`;
458
+ const required = [
459
+ "base",
460
+ stack,
461
+ ...deployLayer === void 0 ? [] : [`${deployLayer}/base`]
462
+ ];
463
+ for (const template of required) if (!existsSync(path.join(templatesDirectory, template))) throw new Error(`The ${template} template is missing from ${templatesDirectory}. Reinstall @next-friday/create-app.`);
464
+ const copyLayer = async (layer) => {
465
+ await cp(path.join(templatesDirectory, layer), directory, { recursive: true });
466
+ };
467
+ await copyLayer("base");
468
+ await copyLayer(stack);
469
+ if (deployLayer !== void 0) {
470
+ await copyLayer(`${deployLayer}/base`);
471
+ if (existsSync(path.join(templatesDirectory, deployLayer, stack))) await copyLayer(`${deployLayer}/${stack}`);
472
+ }
394
473
  await rename(path.join(directory, "_gitignore"), path.join(directory, ".gitignore"));
474
+ await resolveDeployTarget(directory, deploy);
395
475
  await markScriptsExecutable(directory);
396
476
  }
397
477
  /**
478
+ * Resolve the deploy target sections of every copied file that holds a marker.
479
+ * @param directory - Project directory.
480
+ * @param deploy - Supported deploy target.
481
+ */
482
+ async function resolveDeployTarget(directory, deploy) {
483
+ const files = await listProjectFiles(directory);
484
+ await Promise.all(files.map(async (file) => {
485
+ const content = await readFile(file);
486
+ if (!content.includes("deploy:")) return;
487
+ const template = content.toString();
488
+ const resolved = resolveDeploySections(template, deploy);
489
+ if (resolved !== template) await writeFile(file, resolved);
490
+ }));
491
+ }
492
+ /**
398
493
  * Make every file that starts with a shebang executable. `pnpm pack` drops the executable bit,
399
494
  * and the Git and agent hooks run these scripts directly.
400
495
  * @param directory - Project directory.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next-friday/create-app",
3
- "version": "9.6.0",
3
+ "version": "9.7.0",
4
4
  "description": "Create a Next Friday project from a template.",
5
5
  "keywords": [
6
6
  "create",
@@ -64,7 +64,10 @@ Replace `security@example.com` in `SECURITY.md` with the contact the security po
64
64
 
65
65
  Rulesets in a private repository need GitHub Pro, Team, or Enterprise; on GitHub Free, make the repository public or upgrade.
66
66
 
67
- - **Secrets and variables → Actions → Variables:** in a private repository, set `CODE_SCANNING` to `true` to run CodeQL, Scorecard, and Dependency Review, which need GitHub Code Security there; public repositories run them without it. Set `DEPLOY_ENABLED` to `true` when each push to `main` that passes CI should deploy to `dev`; the README's deploy section sets up the environments.
67
+ - **Secrets and variables → Actions → Variables:** in a private repository, set `CODE_SCANNING` to `true` to run CodeQL, Scorecard, and Dependency Review, which need GitHub Code Security there; public repositories run them without it. Set `CONTAINER_IMAGE` to `false` to skip the job that builds and scans the container image, for a project that ships no image.
68
+ <!-- deploy:railway -->
69
+ - **Secrets and variables → Actions → Variables:** set `DEPLOY_ENABLED` to `true` when each push to `main` that passes CI should deploy to `dev`; the README's deploy section sets up the environments.
70
+ <!-- deploy:end -->
68
71
  - Create the `needs triage` label, which the issue forms apply: `gh label create "needs triage"`.
69
72
 
70
73
  **Check:** a test pull request shows the two required checks and asks for a code owner's review.
@@ -94,7 +97,12 @@ Replace `security@example.com` in `SECURITY.md` with the contact the security po
94
97
  - **Sources of truth (engineering lead):** for each artifact, name one system as the source of truth: this repository, an existing tool such as Jira, or, as the minimum, a link both ways (the record id in the file and the commit SHA in the record).
95
98
  - **Higher risk (engineering lead):** decide which changes count as higher risk. A tech lead or architect approves their specs and plans.
96
99
  - **Policies (each policy owner, with an engineer):** the plugin ships a default for each area, `plugins/sdlc/claude-code/skills/<area>-policy/SKILL.md` (security: OWASP ASVS 5.0.0 Level 1; UX: WCAG 2.2 Level AA; compliance: the OECD privacy principles; brand: one declared source for every brand value). Review it. To replace a default, add the organization's own rules in `.agents/skills/<area>-policy/SKILL.md` in the project: that file wins over the plugin's default, and the `CODEOWNERS` line of step 2 for that area makes the policy owner its code owner. Do not edit the plugin's copy. Then run `pnpm rules:sync`, which renders the project's own policy skill for Claude Code and Cursor. Test that the skill triggers: ask for the relevant task in different ways and confirm the skill loads each time.
100
+ <!-- deploy:railway -->
97
101
  - **Production gate (engineering leadership, with change management and compliance):** name the release managers and add them as required reviewers on the `prod` environment, as the README's deploy section describes. `.agents/hooks/agent-production-gate.sh` blocks an agent's shell commands that dispatch a `prod` deploy, run a Railway command with `prod` as its environment, or push a release tag. It does not read MCP tools. A release manager whose own agent session must do one of these starts the agent with `RELEASE_APPROVAL` set to the approval record, for example `RELEASE_APPROVAL=CHG-123 claude`. The value lifts the gate only when it matches the extended regular expression on the first line of `.agents/release-approval-form`, shipped as `^[A-Z]+-[0-9]+$`; set that line to the form of your organization's approval records. Each gate hook appends its decisions to `.agents/logs/gate-decisions.jsonl`, which git ignores: the time, the hook, the decision, the session, and the reason for a block, and, when the production gate is lifted, the approval value and the Git user name. A record holds no command text and no path.
102
+ <!-- deploy:end -->
103
+ <!-- deploy:none -->
104
+ - **Production gate (engineering leadership, with change management and compliance):** name the release managers and add them as required reviewers on the `prod` environment, as the README's deploy section describes. `.agents/hooks/agent-production-gate.sh` blocks an agent's shell commands that dispatch a `prod` deploy or push a release tag. It does not read MCP tools. A release manager whose own agent session must do one of these starts the agent with `RELEASE_APPROVAL` set to the approval record, for example `RELEASE_APPROVAL=CHG-123 claude`. The value lifts the gate only when it matches the extended regular expression on the first line of `.agents/release-approval-form`, shipped as `^[A-Z]+-[0-9]+$`; set that line to the form of your organization's approval records. Each gate hook appends its decisions to `.agents/logs/gate-decisions.jsonl`, which git ignores: the time, the hook, the decision, the session, and the reason for a block, and, when the production gate is lifted, the approval value and the Git user name. A record holds no command text and no path.
105
+ <!-- deploy:end -->
98
106
  - **Review (tech lead):** `.agents/review.md` names this stack's generated files to skip; add a path when the project gains a generated file, then run `pnpm rules:sync`, which renders it to `REVIEW.md` for Claude Code Review. Once a month, rate the review findings and tune `.agents/review.md`.
99
107
  - **People without Git experience:** connect GitHub to Claude (claude.ai or Cowork), so Claude commits `intent.md` for them.
100
108
 
@@ -0,0 +1,66 @@
1
+ {
2
+ "$schema": "https://turborepo.dev/schema.json",
3
+ "futureFlags": {
4
+ "affectedUsingTaskInputs": true
5
+ },
6
+ "globalDependencies": [
7
+ ".dockerignore",
8
+ ".env",
9
+ "package.json",
10
+ "pnpm-workspace.yaml",
11
+ "railway.json"
12
+ ],
13
+ "globalPassThroughEnv": [
14
+ "APP_URL",
15
+ "CI",
16
+ "DATABASE_URL",
17
+ "NEXT_TELEMETRY_DISABLED",
18
+ "PAYLOAD_SECRET"
19
+ ],
20
+ "tasks": {
21
+ "build": {
22
+ "dependsOn": ["^build"],
23
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"],
24
+ "outputs": [".next/**", "!.next/cache/**", "!.next/dev/**"]
25
+ },
26
+ "depcruise": {
27
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"]
28
+ },
29
+ "dev": {
30
+ "cache": false,
31
+ "persistent": true,
32
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"]
33
+ },
34
+ "start": {
35
+ "cache": false,
36
+ "persistent": true,
37
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"]
38
+ },
39
+ "test": {
40
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"]
41
+ },
42
+ "test:changed": {
43
+ "cache": false,
44
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"]
45
+ },
46
+ "test:coverage": {
47
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"],
48
+ "outputs": ["coverage/**"]
49
+ },
50
+ "test:e2e": {
51
+ "cache": false,
52
+ "dependsOn": ["build"],
53
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"]
54
+ },
55
+ "test:watch": {
56
+ "cache": false,
57
+ "persistent": true,
58
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"]
59
+ },
60
+ "typecheck": {
61
+ "dependsOn": ["^typecheck"],
62
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"],
63
+ "outputs": [".next/types/**", "next-env.d.ts"]
64
+ }
65
+ }
66
+ }
@@ -45,7 +45,12 @@ The `verifier` subagent starts the app with `pnpm start:dev`. It exercises the c
45
45
 
46
46
  When Claude makes a mistake twice, add the correction here.
47
47
 
48
+ <!-- deploy:railway -->
48
49
  - **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod`, runs a Railway CLI command that names `prod`, or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool or a Railway command without `-e` or `--environment` passes it.
50
+ <!-- deploy:end -->
51
+ <!-- deploy:none -->
52
+ - **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod` or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool passes it.
53
+ <!-- deploy:end -->
49
54
  - **Issues**: open an issue only to hand work to another person, never to give a pull request a number to close. A pull request closes an issue that already exists with `Closes #<issue>`.
50
55
  - **What belongs in the repository**: files, commit messages, and pull requests hold the change, its reason, and the checks. Write no date, log, history, mistake count, token figure, or session note into a file.
51
56
  - **Sourced requirements**: add a requirement to an intent, spec, or plan only when a person decided it or a named source says it. This includes a requirement a review bot suggests. When no person decided it and no source says it, leave it out and answer the thread with that.
@@ -1,4 +1,4 @@
1
- # Runs on any OCI platform (Railway, Fly.io, Cloud Run). The platform sets `PORT`.
1
+ # Runs on any OCI container platform. The platform sets `PORT`.
2
2
  FROM \
3
3
  node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 \
4
4
  AS base
@@ -34,13 +34,15 @@ CI also runs the build, the end-to-end tests, the container image scan, and the
34
34
 
35
35
  ## Container
36
36
 
37
- The `Dockerfile` builds a production image that runs as a non-root user (UID 1000). The platform must set `PORT`; Railway sets it automatically. The build needs `pnpm-lock.yaml`, so run `pnpm install` once before the first `docker build`.
37
+ The `Dockerfile` builds a production image that runs as a non-root user (UID 1000). The platform must set `PORT`. The build needs `pnpm-lock.yaml`, so run `pnpm install` once before the first `docker build`.
38
38
 
39
39
  ```sh
40
40
  docker build -t app .
41
41
  docker run --env PORT=3000 --publish 3000:3000 app
42
42
  ```
43
43
 
44
+ <!-- deploy:railway -->
45
+
44
46
  ## Deploy
45
47
 
46
48
  `.github/workflows/deploy.yml` deploys the `Dockerfile` to Railway and promotes one commit through `dev`, `uat`, and `prod`. `railway.json` sets the Railway health check to `/health`. The workflow stays off until you configure it:
@@ -56,6 +58,18 @@ docker run --env PORT=3000 --publish 3000:3000 app
56
58
  3. Set the repository variable `DEPLOY_ENABLED` to `true`. Each push to `main` that passes CI then deploys to `dev`.
57
59
  4. Promote with **Actions → Deploy → Run workflow**, giving the environment and the full commit SHA. The workflow requires a successful CI run on `main` for that commit and a successful deployment to the previous environment, then waits for `/health`.
58
60
 
59
- Remove the workflow, `railway.json`, and the `deploy-dev` job in `ci.yml` if the project deploys elsewhere.
61
+ <!-- deploy:end -->
62
+
63
+ <!-- deploy:none -->
64
+
65
+ ## Deploy
66
+
67
+ The project has no deploy step yet. A deploy step needs:
68
+
69
+ - a workflow under `.github/workflows/` that deploys;
70
+ - the `prod` environment under **Settings → Environments**, with the release managers as its required reviewers;
71
+ - the image the `Dockerfile` builds.
72
+
73
+ <!-- deploy:end -->
60
74
 
61
75
  See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution workflow.
@@ -46,7 +46,12 @@ The `verifier` subagent starts the app with `pnpm dev`. It exercises the changed
46
46
  When Claude makes a mistake twice, add the correction here.
47
47
 
48
48
  - **First end-to-end run**: run `pnpm exec playwright install chromium` once. `pnpm test:e2e` starts the development server unless one is already running.
49
+ <!-- deploy:railway -->
49
50
  - **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod`, runs a Railway CLI command that names `prod`, or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool or a Railway command without `-e` or `--environment` passes it.
51
+ <!-- deploy:end -->
52
+ <!-- deploy:none -->
53
+ - **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod` or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool passes it.
54
+ <!-- deploy:end -->
50
55
  - **Issues**: open an issue only to hand work to another person, never to give a pull request a number to close. A pull request closes an issue that already exists with `Closes #<issue>`.
51
56
  - **What belongs in the repository**: files, commit messages, and pull requests hold the change, its reason, and the checks. Write no date, log, history, mistake count, token figure, or session note into a file.
52
57
  - **Sourced requirements**: add a requirement to an intent, spec, or plan only when a person decided it or a named source says it. This includes a requirement a review bot suggests. When no person decided it and no source says it, leave it out and answer the thread with that.
@@ -1,4 +1,4 @@
1
- # Runs on any OCI platform (Railway, Fly.io, Cloud Run). On Vercel, deploy without it. The platform sets `PORT`.
1
+ # Runs on any OCI container platform. A platform that builds Next.js itself deploys without it. The platform sets `PORT`.
2
2
  FROM \
3
3
  node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 \
4
4
  AS build
@@ -32,7 +32,7 @@ CI also runs the build, the end-to-end tests, the container image scan, and the
32
32
 
33
33
  ## Container
34
34
 
35
- The `Dockerfile` builds a standalone production image that runs as the `node` user, for platforms such as Railway. The server listens on `PORT`, or 3000 when it is unset.
35
+ The `Dockerfile` builds a standalone production image that runs as the `node` user for a container platform. The server listens on `PORT`, or 3000 when it is unset.
36
36
 
37
37
  ```sh
38
38
  docker build -t app .
@@ -41,6 +41,8 @@ docker run --publish 3000:3000 app
41
41
 
42
42
  On Vercel, deploy without the image; the Next.js preset takes precedence over the `Dockerfile`.
43
43
 
44
+ <!-- deploy:railway -->
45
+
44
46
  ## Deploy
45
47
 
46
48
  `.github/workflows/deploy.yml` deploys the `Dockerfile` to Railway and promotes one commit through `dev`, `uat`, and `prod`. `railway.json` sets the Railway health check to `/health`. The workflow stays off until you configure it:
@@ -56,6 +58,18 @@ On Vercel, deploy without the image; the Next.js preset takes precedence over th
56
58
  3. Set the repository variable `DEPLOY_ENABLED` to `true`. Each push to `main` that passes CI then deploys to `dev`.
57
59
  4. Promote with **Actions → Deploy → Run workflow**, giving the environment and the full commit SHA. The workflow requires a successful CI run on `main` for that commit and a successful deployment to the previous environment, then waits for `/health`.
58
60
 
59
- Remove the workflow, `railway.json`, and the `deploy-dev` job in `ci.yml` if the project deploys elsewhere.
61
+ <!-- deploy:end -->
62
+
63
+ <!-- deploy:none -->
64
+
65
+ ## Deploy
66
+
67
+ The project has no deploy step yet. A deploy step needs:
68
+
69
+ - a workflow under `.github/workflows/` that deploys;
70
+ - the `prod` environment under **Settings → Environments**, with the release managers as its required reviewers;
71
+ - the image the `Dockerfile` builds.
72
+
73
+ <!-- deploy:end -->
60
74
 
61
75
  See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution workflow.
@@ -44,7 +44,12 @@ The `verifier` subagent starts the app with `pnpm start:dev`. It exercises the c
44
44
 
45
45
  When Claude makes a mistake twice, add the correction here.
46
46
 
47
+ <!-- deploy:railway -->
47
48
  - **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod`, runs a Railway CLI command that names `prod`, or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool or a Railway command without `-e` or `--environment` passes it.
49
+ <!-- deploy:end -->
50
+ <!-- deploy:none -->
51
+ - **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod` or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool passes it.
52
+ <!-- deploy:end -->
48
53
  - **Issues**: open an issue only to hand work to another person, never to give a pull request a number to close. A pull request closes an issue that already exists with `Closes #<issue>`.
49
54
  - **What belongs in the repository**: files, commit messages, and pull requests hold the change, its reason, and the checks. Write no date, log, history, mistake count, token figure, or session note into a file.
50
55
  - **Sourced requirements**: add a requirement to an intent, spec, or plan only when a person decided it or a named source says it. This includes a requirement a review bot suggests. When no person decided it and no source says it, leave it out and answer the thread with that.
@@ -1,4 +1,4 @@
1
- # Runs on any OCI platform (Railway, Fly.io, Cloud Run). The platform sets `PORT`.
1
+ # Runs on any OCI container platform. The platform sets `PORT`.
2
2
  FROM \
3
3
  node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 \
4
4
  AS base
@@ -32,13 +32,15 @@ CI also runs the build, the container image scan, and the security scans, includ
32
32
 
33
33
  ## Container
34
34
 
35
- The `Dockerfile` builds a production image that runs as a non-root user (UID 1000). The platform must set `PORT`; Railway sets it automatically. The build needs `pnpm-lock.yaml`, so run `pnpm install` once before the first `docker build`.
35
+ The `Dockerfile` builds a production image that runs as a non-root user (UID 1000). The platform must set `PORT`. The build needs `pnpm-lock.yaml`, so run `pnpm install` once before the first `docker build`.
36
36
 
37
37
  ```sh
38
38
  docker build -t app .
39
39
  docker run --env PORT=3000 --publish 3000:3000 app
40
40
  ```
41
41
 
42
+ <!-- deploy:railway -->
43
+
42
44
  ## Deploy
43
45
 
44
46
  `.github/workflows/deploy.yml` deploys the `Dockerfile` to Railway and promotes one commit through `dev`, `uat`, and `prod`. `railway.json` sets the Railway health check to `/health`. The workflow stays off until you configure it:
@@ -54,6 +56,18 @@ docker run --env PORT=3000 --publish 3000:3000 app
54
56
  3. Set the repository variable `DEPLOY_ENABLED` to `true`. Each push to `main` that passes CI then deploys to `dev`.
55
57
  4. Promote with **Actions → Deploy → Run workflow**, giving the environment and the full commit SHA. The workflow requires a successful CI run on `main` for that commit and a successful deployment to the previous environment, then waits for `/health`.
56
58
 
57
- Remove the workflow, `railway.json`, and the `deploy-dev` job in `ci.yml` if the project deploys elsewhere.
59
+ <!-- deploy:end -->
60
+
61
+ <!-- deploy:none -->
62
+
63
+ ## Deploy
64
+
65
+ The project has no deploy step yet. A deploy step needs:
66
+
67
+ - a workflow under `.github/workflows/` that deploys;
68
+ - the `prod` environment under **Settings → Environments**, with the release managers as its required reviewers;
69
+ - the image the `Dockerfile` builds.
70
+
71
+ <!-- deploy:end -->
58
72
 
59
73
  See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution workflow.
@@ -49,7 +49,12 @@ The `verifier` subagent starts the app with `pnpm dev`, which needs a PostgreSQL
49
49
  When Claude makes a mistake twice, add the correction here.
50
50
 
51
51
  - **Database for `pnpm dev` and end-to-end tests**: both need a PostgreSQL database in `DATABASE_URL` and `PAYLOAD_SECRET` in `.env`; the README lists every variable. Run `pnpm exec playwright install chromium` once before the first `pnpm test:e2e`.
52
+ <!-- deploy:railway -->
52
53
  - **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod`, runs a Railway CLI command that names `prod`, or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool or a Railway command without `-e` or `--environment` passes it.
54
+ <!-- deploy:end -->
55
+ <!-- deploy:none -->
56
+ - **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod` or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool passes it.
57
+ <!-- deploy:end -->
53
58
  - **Issues**: open an issue only to hand work to another person, never to give a pull request a number to close. A pull request closes an issue that already exists with `Closes #<issue>`.
54
59
  - **What belongs in the repository**: files, commit messages, and pull requests hold the change, its reason, and the checks. Write no date, log, history, mistake count, token figure, or session note into a file.
55
60
  - **Sourced requirements**: add a requirement to an intent, spec, or plan only when a person decided it or a named source says it. This includes a requirement a review bot suggests. When no person decided it and no source says it, leave it out and answer the thread with that.
@@ -1,4 +1,4 @@
1
- # Runs on any OCI platform (Railway, Fly.io, Cloud Run). On Vercel, deploy without it. The platform sets `PORT`.
1
+ # Runs on any OCI container platform. A platform that builds Next.js itself deploys without it. The platform sets `PORT`.
2
2
  FROM \
3
3
  node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 \
4
4
  AS build
@@ -48,7 +48,7 @@ CI also runs the build, the end-to-end tests, the container image scan, and the
48
48
 
49
49
  ## Container
50
50
 
51
- The `Dockerfile` builds a standalone production image that runs as the `node` user, for platforms such as Railway. The server listens on `PORT`, or 3000 when it is unset, and needs `DATABASE_URL`, `PAYLOAD_SECRET`, and `APP_URL`, the public URL that Payload accepts cookie-authenticated and cross-origin requests from.
51
+ The `Dockerfile` builds a standalone production image that runs as the `node` user for a container platform. The server listens on `PORT`, or 3000 when it is unset, and needs `DATABASE_URL`, `PAYLOAD_SECRET`, and `APP_URL`, the public URL that Payload accepts cookie-authenticated and cross-origin requests from.
52
52
 
53
53
  ```sh
54
54
  docker build -t app .
@@ -61,6 +61,8 @@ The config sets no `email` adapter, so Payload writes every email, including pas
61
61
 
62
62
  On Vercel, deploy without the image; the Next.js preset takes precedence over the `Dockerfile`.
63
63
 
64
+ <!-- deploy:railway -->
65
+
64
66
  ## Deploy
65
67
 
66
68
  `.github/workflows/deploy.yml` deploys the `Dockerfile` to Railway and promotes one commit through `dev`, `uat`, and `prod`. `railway.json` sets the Railway health check to `/health`. The workflow stays off until you configure it:
@@ -82,6 +84,18 @@ On Vercel, deploy without the image; the Next.js preset takes precedence over th
82
84
  3. Set the repository variable `DEPLOY_ENABLED` to `true`. Each push to `main` that passes CI then deploys to `dev`.
83
85
  4. Promote with **Actions → Deploy → Run workflow**, giving the environment and the full commit SHA. The workflow requires a successful CI run on `main` for that commit and a successful deployment to the previous environment, then waits for `/health`.
84
86
 
85
- Remove the workflow, `railway.json`, and the `deploy-dev` job in `ci.yml` if the project deploys elsewhere.
87
+ <!-- deploy:end -->
88
+
89
+ <!-- deploy:none -->
90
+
91
+ ## Deploy
92
+
93
+ The project has no deploy step yet. A deploy step needs:
94
+
95
+ - a workflow under `.github/workflows/` that deploys;
96
+ - the `prod` environment under **Settings → Environments**, with the release managers as its required reviewers;
97
+ - the image the `Dockerfile` builds.
98
+
99
+ <!-- deploy:end -->
86
100
 
87
101
  See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution workflow.
@@ -52,7 +52,12 @@ When Claude makes a mistake twice, add the correction here.
52
52
 
53
53
  - **Next.js docs live per app**: this Next.js version differs from your training data. Read the guide in `apps/app/node_modules/next/dist/docs/` or `apps/backoffice/node_modules/next/dist/docs/` before writing Next.js code; the workspace root has no `next` package.
54
54
  - **Database for `pnpm dev` and end-to-end tests**: the backoffice needs a PostgreSQL database in `DATABASE_URL` and `PAYLOAD_SECRET` in `.env`; the README lists every variable. Run `pnpm --filter ./apps/app exec playwright install chromium` once before the first `pnpm test:e2e`.
55
+ <!-- deploy:railway -->
55
56
  - **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod`, runs a Railway CLI command that names `prod`, or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool or a Railway command without `-e` or `--environment` passes it.
57
+ <!-- deploy:end -->
58
+ <!-- deploy:none -->
59
+ - **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod` or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool passes it.
60
+ <!-- deploy:end -->
56
61
  - **Issues**: open an issue only to hand work to another person, never to give a pull request a number to close. A pull request closes an issue that already exists with `Closes #<issue>`.
57
62
  - **What belongs in the repository**: files, commit messages, and pull requests hold the change, its reason, and the checks. Write no date, log, history, mistake count, token figure, or session note into a file.
58
63
  - **Sourced requirements**: add a requirement to an intent, spec, or plan only when a person decided it or a named source says it. This includes a requirement a review bot suggests. When no person decided it and no source says it, leave it out and answer the thread with that.
@@ -61,6 +61,8 @@ Uploaded images, up to 10 MB each, are written to the backoffice container files
61
61
 
62
62
  On Vercel, deploy an app without its image. The Next.js preset takes precedence over the Dockerfile.
63
63
 
64
+ <!-- deploy:railway -->
65
+
64
66
  ## Deploy
65
67
 
66
68
  `.github/workflows/deploy.yml` deploys both apps and promotes one commit through `dev`, `uat`, and `prod`. `railway.json` sets the Railway health check to `/health`. Create one Railway service per app, with that service's Dockerfile path set to `apps/app/Dockerfile` or `apps/backoffice/Dockerfile`. The workflow stays off until you configure it:
@@ -84,7 +86,19 @@ On Vercel, deploy an app without its image. The Next.js preset takes precedence
84
86
  3. Set the repository variable `DEPLOY_ENABLED` to `true`. Each push to `main` that passes CI then deploys to `dev`. The workflow compares the commit with the commit of the last successful deploy to that environment, found from the successful `deploy.yml` and `ci.yml` runs on `main`, and runs `railway up` only for services whose packages changed. A promotion through `uat` or `prod` uses the same comparison against the commit last deployed there, so several commits still ship every changed service. The first deploy to an environment ships both services.
85
87
  4. Promote with **Actions → Deploy → Run workflow**, giving the environment and the full commit SHA. The workflow requires a successful CI run on `main` for that commit and a successful deployment to the previous environment, then waits for `/health` on each service it uploaded.
86
88
 
87
- Remove the workflow, `railway.json`, and the `deploy-dev` job in `ci.yml` if the project deploys elsewhere.
89
+ <!-- deploy:end -->
90
+
91
+ <!-- deploy:none -->
92
+
93
+ ## Deploy
94
+
95
+ The project has no deploy step yet. A deploy step needs:
96
+
97
+ - a workflow under `.github/workflows/` that deploys;
98
+ - the `prod` environment under **Settings → Environments**, with the release managers as its required reviewers;
99
+ - the image the `Dockerfile` builds.
100
+
101
+ <!-- deploy:end -->
88
102
 
89
103
  ## Mobile app
90
104
 
@@ -94,6 +108,6 @@ To add an Expo app, create it under `apps/mobile`:
94
108
  2. Use `@expo/fingerprint` with `expo-github-action/fingerprint` so a pull request requests a native build only when the native layer changed ([Expo fingerprint](https://docs.expo.dev/versions/latest/sdk/fingerprint/)).
95
109
  3. Keep a single `react-native` version in the workspace. Duplicate React Native versions in one monorepo are not supported ([Expo: Monorepos](https://docs.expo.dev/guides/monorepos/)).
96
110
 
97
- EAS Build and Submit replace Railway for that app. CI `--affected` already includes `apps/*` and `packages/*`, so an Expo app is skipped when it did not change.
111
+ EAS Build and Submit deploy that app, in place of the project's deploy step. CI `--affected` already includes `apps/*` and `packages/*`, so an Expo app is skipped when it did not change.
98
112
 
99
113
  See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution workflow.
@@ -1,4 +1,4 @@
1
- # Runs on any OCI platform (Railway, Fly.io, Cloud Run). On Vercel, deploy without it. The platform sets `PORT`.
1
+ # Runs on any OCI container platform. A platform that builds Next.js itself deploys without it. The platform sets `PORT`.
2
2
  FROM \
3
3
  node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 \
4
4
  AS build
@@ -1,4 +1,4 @@
1
- # Runs on any OCI platform (Railway, Fly.io, Cloud Run). On Vercel, deploy without it. The platform sets `PORT`.
1
+ # Runs on any OCI container platform. A platform that builds Next.js itself deploys without it. The platform sets `PORT`.
2
2
  FROM \
3
3
  node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 \
4
4
  AS build
@@ -3,13 +3,7 @@
3
3
  "futureFlags": {
4
4
  "affectedUsingTaskInputs": true
5
5
  },
6
- "globalDependencies": [
7
- ".dockerignore",
8
- ".env",
9
- "package.json",
10
- "pnpm-workspace.yaml",
11
- "railway.json"
12
- ],
6
+ "globalDependencies": [".dockerignore", ".env", "package.json", "pnpm-workspace.yaml"],
13
7
  "globalPassThroughEnv": [
14
8
  "APP_URL",
15
9
  "CI",