create-win-project 1.4.0 → 2.0.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 (99) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +54 -147
  3. package/checks/check-compatibility.js +37 -8
  4. package/checks/check-generated-project.js +27 -3
  5. package/checks/check-library.js +13 -3
  6. package/checks/check-package.js +15 -0
  7. package/checks/classify-changes.js +5 -15
  8. package/checks/run-compatibility-shard.js +21 -0
  9. package/docs/README.md +20 -0
  10. package/docs/capabilities.md +13 -0
  11. package/docs/compatibility.md +13 -0
  12. package/docs/generated-project.md +12 -0
  13. package/docs/getting-started.md +26 -0
  14. package/docs/{ARCHITECTURE.md → maintainers/architecture.md} +3 -3
  15. package/docs/maintainers/ci-strategy.md +46 -0
  16. package/docs/{CONTRIBUTING.md → maintainers/contributing.md} +7 -7
  17. package/docs/migration-v2.md +13 -0
  18. package/docs/production-contract.md +24 -0
  19. package/library/INDEX.md +1 -1
  20. package/library/compatibility-impact.json +14 -0
  21. package/library/development-tools/devops/makefile/commands.md +11 -0
  22. package/library/development-tools/devops/makefile/definition.json +2 -2
  23. package/library/development-tools/devops/makefile/docker.md +9 -0
  24. package/library/development-tools/devops/makefile/validation.md +5 -0
  25. package/library/features/sqlalchemy-alembic.md +17 -0
  26. package/library/optional-features/concerns/zod/errors.md +5 -0
  27. package/library/optional-features/concerns/zod/testing.md +5 -0
  28. package/library/optional-features/concerns/zod/validation.md +21 -0
  29. package/library/optional-features/styling/css-modules/definition.json +3 -2
  30. package/library/optional-features/styling/css-modules/responsive.md +5 -0
  31. package/library/optional-features/styling/css-modules/theme.md +7 -0
  32. package/library/stacks/expo/definition.json +2 -2
  33. package/library/stacks/fastapi/architecture.md +40 -0
  34. package/library/stacks/fastapi/definition.json +39 -0
  35. package/library/stacks/fastapi/runtime.md +25 -0
  36. package/library/stacks/fastapi/security.md +26 -0
  37. package/library/stacks/fastapi/structure.md +27 -0
  38. package/library/stacks/fastapi/testing.md +23 -0
  39. package/library/stacks/nextjs/definition.json +2 -2
  40. package/library/stacks/no-frontend/definition.json +1 -1
  41. package/library/stacks/react-vite/definition.json +2 -2
  42. package/library/tested-versions.json +42 -2
  43. package/library/universal/coding-rules/definition.json +3 -3
  44. package/library/universal/coding-rules/hygiene.md +9 -0
  45. package/library/universal/coding-rules/naming.md +17 -0
  46. package/library/universal/git-conventions/branches.md +5 -0
  47. package/library/universal/git-conventions/commits.md +7 -0
  48. package/library/universal/git-conventions/definition.json +4 -2
  49. package/library/universal/git-conventions/workflow.md +5 -0
  50. package/library/universal/typescript/boundaries.md +13 -0
  51. package/library/universal/typescript/definition.json +3 -3
  52. package/library/universal/typescript/errors.md +5 -0
  53. package/library/universal/typescript/patterns.md +7 -0
  54. package/package.json +4 -6
  55. package/src/cli/arguments.js +11 -0
  56. package/src/cli/main.js +18 -0
  57. package/src/cli/questions.js +16 -15
  58. package/src/cli/system-check.js +22 -2
  59. package/src/engine/load-library.js +3 -2
  60. package/src/engine/project-files.js +10 -1
  61. package/src/engine/project-guidance.js +2 -1
  62. package/src/engine/project-shapes.js +4 -4
  63. package/src/engine/render-templates.js +3 -0
  64. package/src/engine/tested-versions.js +21 -2
  65. package/src/engine/upgrade-report.js +20 -0
  66. package/src/stacks/available-stacks.js +2 -0
  67. package/src/stacks/backends/fastapi/ci.js +3 -0
  68. package/src/stacks/backends/fastapi/create-files.js +874 -0
  69. package/src/stacks/backends/fastapi/docker.js +75 -0
  70. package/src/stacks/backends/fastapi/environment.js +3 -0
  71. package/src/stacks/backends/fastapi/index.js +32 -0
  72. package/src/stacks/compose-files.js +19 -1
  73. package/src/stacks/create-project.js +128 -7
  74. package/src/stacks/frontends/nextjs/index.js +1 -1
  75. package/src/stacks/frontends/react-native/create-files.js +4 -1
  76. package/src/stacks/frontends/react-native/environment.js +1 -1
  77. package/src/stacks/frontends/react-native/index.js +1 -1
  78. package/src/stacks/frontends/react-vite/environment.js +1 -1
  79. package/src/stacks/frontends/react-vite/index.js +1 -1
  80. package/src/stacks/shared/capability-packs.js +31 -0
  81. package/src/stacks/shared/environment.js +12 -4
  82. package/src/stacks/shared/javascript-package.js +6 -0
  83. package/templates/ci/fastapi.yml +62 -0
  84. package/templates/docker/compose-prod/fastapi.yml +52 -0
  85. package/templates/docker/compose-prod/springboot.yml +19 -1
  86. package/templates/docker/dockerfile/fastapi.dev.dockerfile +9 -0
  87. package/templates/docker/dockerfile/fastapi.prod.dockerfile +11 -0
  88. package/templates/docker/dockerfile/nextjs.prod.dockerfile +1 -0
  89. package/templates/docker/dockerfile/springboot.prod.dockerfile +4 -1
  90. package/templates/docker/dockerfile/vite.prod.dockerfile +2 -1
  91. package/templates/makefile/fastapi.mk +97 -0
  92. package/library/development-tools/devops/makefile/makefile.md +0 -556
  93. package/library/optional-features/concerns/zod.md +0 -174
  94. package/library/optional-features/styling/css-modules/css-modules-extensions.md +0 -267
  95. package/library/universal/coding-rules/coding-rules.md +0 -281
  96. package/library/universal/git-conventions/git-conventions.md +0 -186
  97. package/library/universal/typescript/typescript.md +0 -272
  98. /package/docs/{CONTENT_MODEL.md → maintainers/content-model.md} +0 -0
  99. /package/docs/{DEPENDENCY_MAINTENANCE.md → maintainers/dependencies.md} +0 -0
@@ -0,0 +1,75 @@
1
+ export function dockerContributions() {
2
+ return [{ template: 'fastapi', developmentPath: 'backend/Dockerfile.dev', productionPath: 'backend/Dockerfile' }]
3
+ }
4
+
5
+ export function fastapiCompose(answers, stack, vars) {
6
+ const apiDir = stack.frontendKey === 'no-frontend' ? '.' : './backend'
7
+ const projectName = vars.PROJECT_NAME
8
+ const frontend = stack.frontendKey === 'react'
9
+ ? ` frontend:\n container_name: ${projectName}-frontend\n build:\n context: ./frontend\n dockerfile: Dockerfile.dev\n ports:\n - "\${FRONTEND_HOST_PORT:-5173}:5173"\n volumes:\n - ./frontend:/app\n - frontend-node-modules:/app/node_modules\n environment:\n VITE_API_URL: http://backend:8000\n depends_on:\n - backend\n networks:\n - app-network\n\n`
10
+ : stack.frontendKey === 'nextjs'
11
+ ? ` frontend:\n container_name: ${projectName}-frontend\n build:\n context: .\n dockerfile: Dockerfile.dev\n ports:\n - "\${FRONTEND_HOST_PORT:-3000}:3000"\n volumes:\n - .:/app\n - /app/backend\n - frontend-node-modules:/app/node_modules\n environment:\n NEXT_PUBLIC_API_URL: http://backend:8000\n depends_on:\n - backend\n networks:\n - app-network\n\n`
12
+ : ''
13
+ const frontendVolume = frontend ? ' frontend-node-modules:\n' : ''
14
+ return `# docker-compose.yml — ${projectName} (${vars.STACK}) — Dev
15
+ services:
16
+ ${frontend} backend:
17
+ container_name: ${projectName}-backend
18
+ build:
19
+ context: ${apiDir}
20
+ dockerfile: Dockerfile.dev
21
+ ports:
22
+ - "\${BACKEND_HOST_PORT:-8000}:8000"
23
+ volumes:
24
+ - ${apiDir}:/app
25
+ - uv-cache:/root/.cache/uv
26
+ environment:
27
+ - DATABASE_URL=postgresql+asyncpg://\${POSTGRES_USER}:\${POSTGRES_PASSWORD}@db:5432/\${POSTGRES_DB}
28
+ - POSTGRES_USER=\${POSTGRES_USER}
29
+ - POSTGRES_PASSWORD=\${POSTGRES_PASSWORD}
30
+ - OIDC_ISSUER=\${OIDC_ISSUER}
31
+ - OIDC_AUDIENCE=\${OIDC_AUDIENCE}
32
+ - OIDC_ALGORITHMS=\${OIDC_ALGORITHMS}
33
+ - OIDC_JWKS_URL=\${OIDC_JWKS_URL}
34
+ - CORS_ALLOWED_ORIGINS=\${CORS_ALLOWED_ORIGINS}
35
+ depends_on:
36
+ db:
37
+ condition: service_healthy
38
+ networks:
39
+ - app-network
40
+ healthcheck:
41
+ test: ["CMD-SHELL", "python -c \\"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')\\""]
42
+ interval: 10s
43
+ timeout: 5s
44
+ retries: 5
45
+
46
+ db:
47
+ container_name: ${projectName}-db
48
+ image: ${vars.POSTGRES_IMAGE}
49
+ ports:
50
+ - "\${POSTGRES_HOST_PORT:-5432}:5432"
51
+ environment:
52
+ - POSTGRES_USER=\${POSTGRES_USER}
53
+ - POSTGRES_PASSWORD=\${POSTGRES_PASSWORD}
54
+ - POSTGRES_DB=\${POSTGRES_DB}
55
+ volumes:
56
+ - postgres-data:/var/lib/postgresql/data
57
+ networks:
58
+ - app-network
59
+ healthcheck:
60
+ test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER} -d \${POSTGRES_DB}"]
61
+ interval: 10s
62
+ timeout: 5s
63
+ retries: 5
64
+
65
+ volumes:
66
+ ${frontendVolume} postgres-data:
67
+ name: ${projectName}-postgres-data
68
+ uv-cache:
69
+ name: ${projectName}-uv-cache
70
+
71
+ networks:
72
+ app-network:
73
+ name: ${projectName}-network
74
+ `
75
+ }
@@ -0,0 +1,3 @@
1
+ export function environmentContributions() {
2
+ return ['API_URL', 'DATABASE_URL', 'POSTGRES_USER', 'POSTGRES_PASSWORD', 'POSTGRES_DB', 'OIDC_ISSUER', 'OIDC_AUDIENCE', 'OIDC_ALGORITHMS', 'OIDC_JWKS_URL', 'CORS_ALLOWED_ORIGINS']
3
+ }
@@ -0,0 +1,32 @@
1
+ import { defineStackAdapter } from '../../rules.js'
2
+ import { buildFastApiFiles } from './create-files.js'
3
+ import { ciContributions } from './ci.js'
4
+ import { dockerContributions } from './docker.js'
5
+ import { environmentContributions } from './environment.js'
6
+
7
+ export const fastapiAdapter = defineStackAdapter({
8
+ id: 'fastapi',
9
+ kind: 'backend',
10
+ label: 'FastAPI',
11
+ compatibleWith: { frontend: ['nextjs', 'react', 'react-native', 'no-frontend'] },
12
+ capabilities: {
13
+ applicationShapes: ['separate', 'api', 'mobile'],
14
+ architectureProfiles: ['small', 'medium', 'large'],
15
+ authenticationModels: ['public', 'undecided', 'oidc'],
16
+ runtime: 'python',
17
+ },
18
+ contributes: {
19
+ files: ({ answers, stack, vars }) => Object.entries(buildFastApiFiles(answers, vars, stack)),
20
+ environment: environmentContributions,
21
+ install: (context = {}) => [{ cwd: context.stack?.frontendKey === 'no-frontend' ? '.' : 'backend', command: 'uv', args: ['sync'] }],
22
+ docker: dockerContributions,
23
+ ci: ciContributions,
24
+ verification: () => [
25
+ { frontend: 'nextjs', architecture: 'small', authentication: 'public' },
26
+ { frontend: 'nextjs', architecture: 'medium', authentication: 'oidc' },
27
+ { frontend: 'react', architecture: 'medium', authentication: 'oidc' },
28
+ { frontend: 'react-native', architecture: 'large', authentication: 'oidc' },
29
+ { frontend: 'no-frontend', architecture: 'medium', authentication: 'undecided' },
30
+ ],
31
+ },
32
+ })
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path'
2
2
  import { packageVersion } from './shared/javascript-package.js'
3
3
  import { stackRegistry } from './available-stacks.js'
4
+ import { capabilityPackFiles } from './shared/capability-packs.js'
4
5
  import { collectContributions } from './shared/contributions.js'
5
6
  import { buildSharedTestFiles, buildSupabaseWebFiles } from './backends/supabase/create-files.js'
6
7
  import { augmentSupabaseNativeFiles } from './backends/supabase/native.js'
@@ -30,6 +31,9 @@ function envFiles(answers, stack) {
30
31
 
31
32
  function projectReadme(answers, stack) {
32
33
  if (stack.frontendKey === 'no-frontend') {
34
+ if (stack.backendKey === 'fastapi') {
35
+ return `# ${answers.projectName}\n\n> ${answers.projectDescription}\n\nGenerated backend-only ${stack.backendLabel} application.\n\n## Start\n\n\`\`\`bash\ncp .env.example .env\nuv sync\nalembic upgrade head\nuv run uvicorn app.main:app --reload\n\`\`\`\n\n## Validate\n\n\`\`\`bash\nuv run ruff check . && uv run ruff format --check . && uv run mypy . && uv run pytest\n\`\`\`\n`
36
+ }
33
37
  return `# ${answers.projectName}\n\n> ${answers.projectDescription}\n\nGenerated backend-only ${stack.backendLabel} application.\n\n## Start\n\n\`\`\`bash\ncp .env.example .env\ncd backend\n./mvnw spring-boot:run # use mvnw.cmd on Windows\n\`\`\`\n\n## Validate\n\n\`\`\`bash\ncd backend\n./mvnw --batch-mode test\n./mvnw --batch-mode package -DskipTests\n\`\`\`\n`
34
38
  }
35
39
  const root = stack.frontendKey === 'react' ? 'frontend/' : ''
@@ -215,8 +219,9 @@ export function buildRunnableFiles(answers, stack, vars) {
215
219
  let files = stack.isMobile ? nativeFiles(answers, stack) : frontendFiles(answers, stack)
216
220
  if (stack.frontendKey === 'react') files = augmentViteFiles(files, stack)
217
221
  Object.assign(files, envFiles(answers, stack))
222
+ Object.assign(files, capabilityPackFiles(answers, stack))
218
223
  files['create-win-project.profile.json'] = json({
219
- schemaVersion: 3,
224
+ schemaVersion: 2,
220
225
  applicationShape: stack.applicationShape,
221
226
  compatibilityProfile: {
222
227
  id: stack.profile.id,
@@ -230,6 +235,19 @@ export function buildRunnableFiles(answers, stack, vars) {
230
235
  audience: stack.authAudience,
231
236
  },
232
237
  stack: stack.key,
238
+ productionBaseline: {
239
+ tests: true,
240
+ continuousIntegration: true,
241
+ productionBuild: !stack.isMobile,
242
+ securityRules: true,
243
+ operationsDocumentation: true,
244
+ deployment: stack.isMobile ? 'eas' : 'cloud-neutral-docker',
245
+ },
246
+ capabilities: {
247
+ uploads: answers.uploads || 'none',
248
+ backgroundJobs: answers.backgroundJobs || 'none',
249
+ offline: answers.offline || 'none',
250
+ },
233
251
  runtimes: stack.profile.runtimes,
234
252
  })
235
253
  files['README.md'] = projectReadme(answers, stack)
@@ -16,6 +16,7 @@ import {
16
16
  } from '../engine/write-files.js'
17
17
  import { writeRenderedFile as writeTemplate } from '../engine/render-templates.js'
18
18
  import { laravelCompose } from './backends/laravel/docker.js'
19
+ import { fastapiCompose } from './backends/fastapi/docker.js'
19
20
 
20
21
  /**
21
22
  * Main entry point — generates the full project
@@ -84,9 +85,18 @@ function validateAnswers(answers) {
84
85
  if (typeof answers.projectDescription !== 'string' || !answers.projectDescription.trim()) {
85
86
  throw new Error('Project description is required')
86
87
  }
87
- if (answers.testing && !['none', 'basic', 'full'].includes(answers.testing)) {
88
+ if (answers.testing && !['basic', 'full'].includes(answers.testing)) {
88
89
  throw new Error(`Unknown testing setup: ${answers.testing}`)
89
90
  }
91
+ const uploads = answers.uploads || 'none'
92
+ const jobs = answers.backgroundJobs || 'none'
93
+ const offline = answers.offline || 'none'
94
+ if (!['none', 'object-storage'].includes(uploads)) throw new Error(`Unknown uploads requirement: ${uploads}`)
95
+ if (!['none', 'queue'].includes(jobs)) throw new Error(`Unknown background-jobs requirement: ${jobs}`)
96
+ if (!['none', 'cache', 'sync'].includes(offline)) throw new Error(`Unknown offline requirement: ${offline}`)
97
+ if (offline !== 'none' && answers.frontend !== 'react-native') throw new Error('Offline capabilities are supported only for mobile applications')
98
+ if (jobs === 'queue' && !['springboot', 'laravel'].includes(answers.backend)) throw new Error('Queues require a Spring Boot or Laravel backend')
99
+ if (uploads === 'object-storage' && answers.backend === 'none') throw new Error('Object storage uploads require a backend or managed data service')
90
100
  if (answers.architecture && !['small', 'medium', 'large'].includes(answers.architecture)) {
91
101
  throw new Error(`Unknown architecture profile: ${answers.architecture}`)
92
102
  }
@@ -101,11 +111,11 @@ function validateAnswers(answers) {
101
111
  // ─── Root files ───────────────────────────────────────────────────────────────
102
112
 
103
113
  async function generateRootFiles(dest, answers, vars, stack, templatesDir) {
104
- await writeTemplate(dest, 'CONTEXT.md', contextMd(vars, answers.expectedConcerns), vars)
114
+ await writeTemplate(dest, 'CONTEXT.md', contextMd(vars, answers.expectedConcerns, answers), vars)
105
115
  // AGENTS.md — template-driven
106
116
  {
107
117
  const tpl = await readTemplate(templatesDir, 'agents', stack.agentsTemplate, '.md')
108
- if (tpl) await writeTemplate(dest, 'AGENTS.md', tpl, vars)
118
+ if (tpl) await writeTemplate(dest, 'AGENTS.md', `${tpl}\n## Deviation policy\n\nAgents may recommend alternatives, but must propose the change and receive explicit approval before changing the selected architecture, provider, authentication model, data boundary, production baseline, or major dependency. Record approved deviations and their rationale in \`CONTEXT.md\`.\n`, vars)
109
119
  else await write(dest, 'AGENTS.md', `# AGENTS.md\nStack: ${stack.label}\n`)
110
120
  }
111
121
  await write(dest, 'PROGRESS.md', progressMd())
@@ -123,6 +133,10 @@ async function generateRootFiles(dest, answers, vars, stack, templatesDir) {
123
133
  await write(dest, '.editorconfig', editorconfig())
124
134
  await write(dest, '.prettierrc', prettierrc())
125
135
  if (stack.backendKey === 'springboot') await write(dest, 'backend/.java-version', `${stack.profile.runtimes.java}\n`)
136
+ if (stack.backendKey === 'fastapi') {
137
+ const apiRoot = stack.frontendKey === 'no-frontend' ? '' : 'backend/'
138
+ await write(dest, `${apiRoot}.python-version`, `${stack.profile.runtimes.python}\n`)
139
+ }
126
140
  if (stack.backendKey === 'laravel') {
127
141
  const laravelRoot = stack.frontendKey === 'laravel-ui' || stack.frontendKey === 'no-frontend' ? '' : 'backend/'
128
142
  await write(dest, `${laravelRoot}.php-version`, `${stack.profile.runtimes.php}\n`)
@@ -138,11 +152,13 @@ async function generateRootFiles(dest, answers, vars, stack, templatesDir) {
138
152
  // Mobile frontends run through Expo, but a separate Laravel backend still
139
153
  // needs its backend and PostgreSQL services. laravelCompose intentionally
140
154
  // omits a frontend service for React Native.
141
- if (answers.docker && (!stack.isMobile || stack.backendKey === 'laravel')) {
155
+ if (answers.docker && (!stack.isMobile || ['laravel', 'fastapi'].includes(stack.backendKey))) {
142
156
  // docker-compose.yml
143
157
  let composeTpl = null
144
158
  if (stack.backendKey === 'laravel') {
145
159
  composeTpl = laravelCompose(answers, stack, vars)
160
+ } else if (stack.backendKey === 'fastapi') {
161
+ composeTpl = fastapiCompose(answers, stack, vars)
146
162
  } else if (stack.needsPackage) {
147
163
  composeTpl = await readTemplate(templatesDir, 'docker/compose', 'springboot', '.yml')
148
164
  } else if (stack.backendKey === 'supabase') {
@@ -172,6 +188,22 @@ async function generateRootFiles(dest, answers, vars, stack, templatesDir) {
172
188
  if (beProd) await writeTemplate(dest, 'backend/Dockerfile', beProd, vars)
173
189
  }
174
190
 
191
+ if (stack.backendKey === 'fastapi') {
192
+ const apiRoot = stack.frontendKey === 'no-frontend' ? '' : 'backend/'
193
+ const prodTpl = await readTemplate(templatesDir, 'docker/compose-prod', 'fastapi', '.yml')
194
+ if (prodTpl) {
195
+ const rendered = stack.frontendKey === 'no-frontend'
196
+ ? prodTpl.replaceAll('context: ./backend', 'context: .').replaceAll('./backend:/app', '.:/app')
197
+ : prodTpl
198
+ await writeTemplate(dest, 'docker-compose.prod.yml', rendered, vars)
199
+ }
200
+
201
+ const beDev = await readTemplate(templatesDir, 'docker/dockerfile', 'fastapi.dev', '.dockerfile')
202
+ if (beDev) await writeTemplate(dest, `${apiRoot}Dockerfile.dev`, beDev, vars)
203
+ const beProd = await readTemplate(templatesDir, 'docker/dockerfile', 'fastapi.prod', '.dockerfile')
204
+ if (beProd) await writeTemplate(dest, `${apiRoot}Dockerfile`, beProd, vars)
205
+ }
206
+
175
207
  // frontend dockerfiles — vite vs nextjs
176
208
  if (stack.frontendKey === 'react') {
177
209
  const viteDev = await readTemplate(templatesDir, 'docker/dockerfile', 'vite.dev', '.dockerfile')
@@ -179,7 +211,7 @@ async function generateRootFiles(dest, answers, vars, stack, templatesDir) {
179
211
  const viteProd = await readTemplate(templatesDir, 'docker/dockerfile', 'vite.prod', '.dockerfile')
180
212
  if (viteProd) {
181
213
  await writeTemplate(dest, 'frontend/Dockerfile', viteProd, vars)
182
- const nginx = `server {\n listen 80;\n location / {\n root /usr/share/nginx/html;\n index index.html;\n try_files $uri $uri/ /index.html;\n }\n}\n`
214
+ const nginx = `server {\n listen 8080;\n server_tokens off;\n root /usr/share/nginx/html;\n add_header X-Content-Type-Options nosniff always;\n add_header Referrer-Policy strict-origin-when-cross-origin always;\n add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;\n add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;\n location /assets/ { try_files $uri =404; add_header Cache-Control "public, max-age=31536000, immutable"; }\n location /api/ { add_header Cache-Control "no-store" always; try_files $uri =404; }\n location / { index index.html; try_files $uri $uri/ /index.html; add_header Cache-Control "no-cache"; }\n}\n`
183
215
  await write(dest, 'frontend/nginx.conf', nginx)
184
216
  }
185
217
  } else if (stack.frontendKey === 'nextjs') {
@@ -190,6 +222,45 @@ async function generateRootFiles(dest, answers, vars, stack, templatesDir) {
190
222
  }
191
223
  }
192
224
 
225
+ // Production artifacts are part of the deployable web contract even when
226
+ // the optional development Docker workflow was not selected.
227
+ if (!answers.docker && !stack.isMobile) {
228
+ if (stack.frontendKey === 'nextjs') {
229
+ const nextProd = await readTemplate(templatesDir, 'docker/dockerfile', 'nextjs.prod', '.dockerfile')
230
+ if (nextProd) await writeTemplate(dest, 'Dockerfile', nextProd, vars)
231
+ }
232
+ if (stack.frontendKey === 'react') {
233
+ const viteProd = await readTemplate(templatesDir, 'docker/dockerfile', 'vite.prod', '.dockerfile')
234
+ if (viteProd) {
235
+ await writeTemplate(dest, 'frontend/Dockerfile', viteProd, vars)
236
+ await write(dest, 'frontend/nginx.conf', `server {\n listen 8080;\n server_tokens off;\n root /usr/share/nginx/html;\n add_header X-Content-Type-Options nosniff always;\n add_header Referrer-Policy strict-origin-when-cross-origin always;\n add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;\n add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;\n location /assets/ { try_files $uri =404; add_header Cache-Control "public, max-age=31536000, immutable"; }\n location /api/ { add_header Cache-Control "no-store" always; try_files $uri =404; }\n location / { index index.html; try_files $uri $uri/ /index.html; add_header Cache-Control "no-cache"; }\n}\n`)
237
+ }
238
+ }
239
+ if (stack.backendKey === 'springboot') {
240
+ const backendProd = await readTemplate(templatesDir, 'docker/dockerfile', 'springboot.prod', '.dockerfile')
241
+ if (backendProd) await writeTemplate(dest, 'backend/Dockerfile', backendProd, vars)
242
+ const composeProd = await readTemplate(templatesDir, 'docker/compose-prod', 'springboot', '.yml')
243
+ if (composeProd) await writeTemplate(dest, 'docker-compose.prod.yml', composeProd, vars)
244
+ }
245
+ if (stack.backendKey === 'fastapi') {
246
+ const apiRoot = stack.frontendKey === 'no-frontend' ? '' : 'backend/'
247
+ const backendProd = await readTemplate(templatesDir, 'docker/dockerfile', 'fastapi.prod', '.dockerfile')
248
+ if (backendProd) await writeTemplate(dest, `${apiRoot}Dockerfile`, backendProd, vars)
249
+ const composeProd = await readTemplate(templatesDir, 'docker/compose-prod', 'fastapi', '.yml')
250
+ if (composeProd) {
251
+ const rendered = stack.frontendKey === 'no-frontend'
252
+ ? composeProd.replaceAll('context: ./backend', 'context: .')
253
+ : composeProd
254
+ await writeTemplate(dest, 'docker-compose.prod.yml', rendered, vars)
255
+ }
256
+ }
257
+ if (stack.backendKey === 'laravel') {
258
+ const laravelRoot = ['laravel-ui', 'no-frontend'].includes(stack.frontendKey) ? '' : 'backend/'
259
+ const laravelProd = await readTemplate(templatesDir, 'docker/dockerfile', 'laravel.prod', '.dockerfile')
260
+ if (laravelProd) await writeTemplate(dest, `${laravelRoot}Dockerfile`, laravelProd, vars)
261
+ }
262
+ }
263
+
193
264
  // PR template
194
265
  if (answers.githubActions) {
195
266
  await write(dest, '.github/PULL_REQUEST_TEMPLATE.md', prTemplate())
@@ -216,6 +287,11 @@ function toolchainGuide(answers, stack) {
216
287
  rows.push(`| PHP | ${stack.profile.runtimes.php} | Host-run Laravel backend | \`${root}.php-version\` |`)
217
288
  rows.push(`| Composer | ${stack.profile.runtimes.composer} | Host-run Laravel backend | \`${root}composer.json\` |`)
218
289
  }
290
+ if (stack.backendKey === 'fastapi') {
291
+ const root = stack.frontendKey === 'no-frontend' ? '' : 'backend/'
292
+ rows.push(`| Python | ${stack.profile.runtimes.python} | Host-run FastAPI backend | \`${root}.python-version\` |`)
293
+ rows.push(`| uv | ${stack.profile.runtimes.uv} | Host-run FastAPI backend | \`${root}pyproject.toml\` |`)
294
+ }
219
295
  if (answers.docker || ['supabase', 'postgres'].includes(stack.backendKey)) {
220
296
  rows.push('| Docker with Compose | Current supported release | Generated containers and local managed services | `docker-compose.yml` when selected |')
221
297
  }
@@ -247,6 +323,8 @@ async function generateDocs(dest, answers, stack) {
247
323
  for (const [filePath, title, description] of docs) {
248
324
  await write(dest, filePath, docPlaceholder(title, description))
249
325
  }
326
+ await write(dest, 'docs/guides/deployment.md', `# Production deployment and rollback\n\nBuild immutable images from the committed lockfiles and deploy \`docker-compose.prod.yml\` where generated. Validate required environment variables before starting; secrets belong in the deployment platform, never images or client bundles. Run readiness checks before routing traffic and allow the documented graceful-shutdown window during replacement.\n\n## Database preflight\n\nBack up PostgreSQL with encryption before migrations, check available connections and migration compatibility, then run migrations as a single release task. Application instances use a bounded connection pool; size the total across replicas below the database limit.\n\n## Rollback\n\nRetain the previous image digest and a compatible database restore point. Stop routing to the failed release, restore the prior image, and restore data only when the migration is not backward compatible. Test restores automatically on a separate database and document retention and recovery ownership.\n`)
327
+ await write(dest, 'docs/guides/operations.md', `# Operations\n\nEvery request crossing an HTTP boundary receives or creates an \`X-Request-ID\` and returns it in responses. Errors use a stable JSON shape: \`{ "error": { "code": "stable_code", "message": "safe message", "requestId": "..." } }\`. Never expose stack traces.\n\nList endpoints use cursor pagination with explicit maximum page sizes. Outbound calls have connection and response timeouts; retry only bounded idempotent operations with jitter. Readiness checks include required downstream dependencies while liveness checks remain process-local.\n\nCORS uses an explicit origin allowlist. Cookie-authenticated browser writes require SameSite cookies plus CSRF validation; bearer-token APIs do not use wildcard origins with credentials. Static fingerprinted assets are immutable, HTML revalidates, and API/auth responses default to \`no-store\`.\n`)
250
328
 
251
329
  const frontendRoot = stack.frontendKey === 'react' ? 'frontend/' : ''
252
330
  const validation = stack.isMobile
@@ -274,6 +352,16 @@ async function generateDocs(dest, answers, stack) {
274
352
  : ''
275
353
  setupGuide = `# Local Setup Guide\n\n## Default local setup\n\nUse PHP ${stack.profile.runtimes.php}, Composer ${stack.profile.runtimes.composer}, and PostgreSQL ${stack.profile.runtimes.postgres}.\n\n\`\`\`bash\ncd ${laravelDir || '.'}\ncomposer install\ncp .env.example .env\nphp artisan key:generate\nphp artisan migrate\nphp artisan serve\n\`\`\`\n${frontendSetup}${answers.docker ? `\n## Optional Docker setup\n\nFrom the repository root:\n\n\`\`\`bash\ndocker compose build\ndocker compose up -d\ndocker compose exec backend php artisan key:generate\ndocker compose exec backend php artisan migrate\n\`\`\`\n\nLater runs use \`docker compose up -d\`; rebuilding remains explicit.\n` : ''}\n## Validate\n\n\`\`\`bash\n${laravelDir ? `cd ${laravelDir}\n` : ''}composer check\n\`\`\`\n\nCommit \`composer.lock\`${stack.frontendKey === 'laravel-ui' && stack.laravelUi === 'inertia-react' ? ' and `package-lock.json`' : ''}.\n`
276
354
  }
355
+ if (stack.backendKey === 'fastapi') {
356
+ const apiDir = stack.frontendKey === 'no-frontend' ? '' : 'backend/'
357
+ const frontendSetup = stack.frontendKey === 'react'
358
+ ? '\nIn another terminal:\n\n```bash\ncd frontend\nnpm install\nnpm run dev\n```\n'
359
+ : stack.frontendKey === 'nextjs'
360
+ ? '\nIn another terminal from the repository root:\n\n```bash\nnpm install\nnpm run dev\n```\n'
361
+ : ''
362
+ setupGuide = `# Local Setup Guide\n\n## Default local setup\n\nUse Python ${stack.profile.runtimes.python}, uv ${stack.profile.runtimes.uv}, and PostgreSQL ${stack.profile.runtimes.postgres}.\n\n\`\`\`bash\ncd ${apiDir || '.'}\nuv sync\ncp .env.example .env\nalembic upgrade head\nuv run uvicorn app.main:app --reload\n\`\`\`\n${frontendSetup}${answers.docker ? `\n## Optional Docker setup\n\nFrom the repository root:\n\n\`\`\`bash\ndocker compose build\ndocker compose up -d\ndocker compose exec backend uv run alembic upgrade head\n\`\`\`\n\nLater runs use \`docker compose up -d\`; rebuilding remains explicit.\n` : ''}\n## Validate\n\n\`\`\`bash\n${apiDir ? `cd ${apiDir}\n` : ''}uv run ruff check . && uv run ruff format --check . && uv run mypy . && uv run pytest\n\`\`\`\n\nCommit the generated \`uv.lock\`; CI uses \`uv sync --frozen\`.\n`
363
+ setupGuide = setupGuide.replace('\nalembic upgrade head\n', '\nuv run alembic upgrade head\n')
364
+ }
277
365
  await fs.writeFile(setupPath, setupGuide, 'utf8')
278
366
  await write(dest, 'docs/guides/toolchain.md', toolchainGuide(answers, stack))
279
367
  await write(dest, 'docs/guides/development-environments.md', developmentEnvironmentGuide(answers, stack))
@@ -289,7 +377,9 @@ async function generateDocs(dest, answers, stack) {
289
377
  ? 'GET http://localhost:8080/api/health'
290
378
  : stack.backendKey === 'laravel'
291
379
  ? 'GET http://localhost:8000/api/health'
292
- : stack.frontendKey === 'nextjs' ? 'GET /api/health' : '(No custom HTTP API is generated for this client-only starter.)'
380
+ : stack.backendKey === 'fastapi'
381
+ ? 'GET http://localhost:8000/health'
382
+ : stack.frontendKey === 'nextjs' ? 'GET /api/health' : '(No custom HTTP API is generated for this client-only starter.)'
293
383
  await write(dest, 'docs/api/overview.md', `# API Overview\n\n## Health\n\n\`${health}\`\n\nAuthentication and authorization behavior is documented in \`docs/architecture/auth-flow.md\`. Add endpoints to \`docs/api/endpoints.md\` in the same change that adds their implementation and tests.\n`)
294
384
  }
295
385
 
@@ -297,7 +387,7 @@ function environmentPurpose(name) {
297
387
  if (name.endsWith('SUPABASE_URL')) return 'Supabase project URL.'
298
388
  if (name.endsWith('SUPABASE_PUBLISHABLE_KEY')) return 'Public Supabase key; RLS protects data.'
299
389
  if (name.endsWith('API_URL')) return 'Base URL of the application API.'
300
- if (name === 'DATABASE_URL') return 'Server-side PostgreSQL JDBC connection URL.'
390
+ if (name === 'DATABASE_URL') return 'Server-side PostgreSQL connection URL.'
301
391
  if (name === 'POSTGRES_USER') return 'Local/deployed database user.'
302
392
  if (name === 'POSTGRES_PASSWORD') return 'Database credential; replace the development example.'
303
393
  if (name === 'POSTGRES_DB') return 'Database name.'
@@ -305,7 +395,10 @@ function environmentPurpose(name) {
305
395
  if (name === 'SPRING_SECURITY_USER_NAME') return 'Development-only generated Spring login name; replace with the product identity store.'
306
396
  if (name === 'SPRING_SECURITY_USER_PASSWORD') return 'Development-only Spring login credential; never commit a real value.'
307
397
  if (name === 'OIDC_ISSUER_URI') return 'Trusted OpenID Connect issuer used to validate access tokens.'
398
+ if (name === 'OIDC_ISSUER') return 'Trusted OpenID Connect issuer used to validate access tokens.'
308
399
  if (name === 'OIDC_AUDIENCE') return 'Required audience for tokens accepted by this API.'
400
+ if (name === 'OIDC_ALGORITHMS') return 'Allowed signing algorithms for accepted access tokens.'
401
+ if (name === 'OIDC_JWKS_URL') return 'JWKS endpoint for access-token signature validation; defaults to the issuer well-known location.'
309
402
  if (name === 'SESSION_DOMAIN') return 'Exact domain scope for the secure session cookie.'
310
403
  if (name === 'SANCTUM_STATEFUL_DOMAINS') return 'Comma-separated first-party browser hosts allowed to use Sanctum session authentication.'
311
404
  if (name === 'CORS_ALLOWED_ORIGINS') return 'Exact browser origins allowed to make credentialed API requests.'
@@ -323,11 +416,24 @@ function authDocumentation(stack) {
323
416
  if (stack.authentication === 'laravel-session') return `# Authentication Flow\n\nLaravel owns the website session. Login regenerates the session identifier, logout invalidates the session and rotates the CSRF token, and protected routes use server-side authorization. The browser receives an HttpOnly session cookie; there is no browser refresh token. Keep CSRF protection enabled for every cookie-authenticated mutation.\n`
324
417
  if (stack.authentication === 'sanctum-spa') return `# Authentication Flow\n\nLaravel Sanctum uses Laravel's secure session cookie for this first-party browser application. The SPA first requests \`/sanctum/csrf-cookie\`, then sends credentialed login and API requests. Sanctum does not give this browser a custom bearer/refresh-token system. Configure exact stateful domains and CORS origins, and enforce resource authorization in Laravel.\n`
325
418
  if (stack.authentication === 'laravel-oidc') return `# Authentication Flow\n\nAuth0 owns login, access/refresh-token issuance, rotation, revocation, and recovery. Clients use Authorization Code with PKCE. Laravel uses the pinned Auth0 resource-server adapter to accept access tokens only and validate signature, issuer, audience, and time claims. Refresh tokens never go to this API. Configure \`AUTH0_DOMAIN\` and \`AUTH0_AUDIENCE\`; resource ownership and permission checks remain application responsibilities.\n`
419
+ if (stack.backendKey === 'fastapi') return `# Authentication Flow\n\nAn external OpenID Connect provider owns login, access/refresh token issuance, rotation, revocation, and recovery. Clients use Authorization Code with PKCE. FastAPI validates bearer access tokens: issuer, audience, algorithm, JWKS signature, expiry, and required claims on every protected request. Refresh tokens never go to the FastAPI resource API. Configure \`OIDC_ISSUER\` and \`OIDC_AUDIENCE\`, then test invalid and authorized tokens.\n`
326
420
  return `# Authentication Flow\n\nAn external OpenID Connect provider owns login, access/refresh token issuance, rotation, revocation, and recovery. Clients use Authorization Code with PKCE. Spring is an OAuth2 Resource Server: it accepts bearer access tokens and validates signature, issuer, audience, time, and authorities. Refresh tokens never go to the Spring resource API. Configure \`OIDC_ISSUER_URI\` and \`OIDC_AUDIENCE\`, then test invalid and authorized tokens.\n`
327
421
  }
328
422
 
329
423
  // ─── CI — template-driven ─────────────────────────────────────────────────────
330
424
 
425
+ function generatedSecurityWorkflow(stack) {
426
+ const hasJavaScript = stack.frontendKey !== 'no-frontend' &&
427
+ !(stack.frontendKey === 'laravel-ui' && stack.laravelUi !== 'inertia-react')
428
+ const npmDirectory = stack.frontendKey === 'react' ? 'frontend' : '.'
429
+ const laravelDirectory = ['laravel-ui', 'no-frontend'].includes(stack.frontendKey) ? '.' : 'backend'
430
+ const codeqlLanguages = [hasJavaScript && 'javascript-typescript', stack.backendKey === 'springboot' && 'java-kotlin'].filter(Boolean)
431
+ const npmAudit = hasJavaScript ? `\n npm-audit:\n runs-on: ubuntu-latest\n defaults:\n run:\n working-directory: ${npmDirectory}\n steps:\n - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4\n - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4\n with:\n node-version: "${stack.profile.runtimes.node}"\n cache: npm\n cache-dependency-path: ${npmDirectory === '.' ? 'package-lock.json' : `${npmDirectory}/package-lock.json`}\n - run: npm ci\n - run: npm audit --audit-level=high\n` : ''
432
+ const composerAudit = stack.backendKey === 'laravel' ? `\n composer-audit:\n runs-on: ubuntu-latest\n defaults:\n run:\n working-directory: ${laravelDirectory}\n steps:\n - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4\n - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2\n with:\n php-version: "${stack.profile.runtimes.php}"\n coverage: none\n - run: composer install --no-interaction --prefer-dist\n - run: composer audit --locked\n` : ''
433
+ const codeql = codeqlLanguages.length ? `\n codeql:\n permissions:\n contents: read\n security-events: write\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4\n - uses: github/codeql-action/init@5ba2889ada762081db2c4f32a729827dce632c7b # v3\n with:\n languages: ${codeqlLanguages.join(',')}\n - uses: github/codeql-action/analyze@5ba2889ada762081db2c4f32a729827dce632c7b # v3\n` : ''
434
+ return `name: Security\n\non:\n pull_request:\n branches: [dev, main]\n push:\n branches: [dev, main]\n schedule:\n - cron: '31 4 * * 1'\n\npermissions:\n contents: read\n\njobs:\n dependency-review:\n if: github.event_name == 'pull_request'\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4\n - uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0\n with:\n fail-on-severity: high\n\n secret-scan:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4\n with:\n fetch-depth: 0\n - uses: trufflesecurity/trufflehog@466da5b0bb161144f6afca9afe5d57975828c410 # v3.90.8\n with:\n extra_args: --results=verified,unknown\n${npmAudit}${composerAudit}${codeql}`
435
+ }
436
+
331
437
  async function generateCI(dest, stack, ciDir, answers, vars) {
332
438
  // Frontend CI — read from ci/{ciTemplate}.yml
333
439
  const feTpl = path.join(ciDir, `${stack.ciTemplate}.yml`)
@@ -377,6 +483,21 @@ async function generateCI(dest, stack, ciDir, answers, vars) {
377
483
  await write(dest, `.github/workflows/ci-backend.yml`, render(content, vars))
378
484
  }
379
485
  }
486
+ if (stack.backendKey === 'fastapi') {
487
+ const beTpl = path.join(ciDir, 'fastapi.yml')
488
+ if (await fs.pathExists(beTpl)) {
489
+ let content = await fs.readFile(beTpl, 'utf-8')
490
+ if (stack.frontendKey === 'no-frontend') {
491
+ content = content.replaceAll(' - backend/**', " - '**'")
492
+ .replaceAll('working-directory: backend', 'working-directory: .')
493
+ }
494
+ if (answers.testing === 'none') {
495
+ content = content.replace(' - name: Run tests\n run: uv run pytest\n', '')
496
+ }
497
+ await write(dest, `.github/workflows/ci-backend.yml`, render(content, vars))
498
+ }
499
+ }
500
+ await write(dest, '.github/workflows/security.yml', generatedSecurityWorkflow(stack))
380
501
  }
381
502
 
382
503
  // ─── Runnable application files ─────────────────────────────────────────────────
@@ -16,7 +16,7 @@ export const nextjsAdapter = defineStackAdapter({
16
16
  kind: 'frontend',
17
17
  label: 'Next.js',
18
18
  compatibleWith: {
19
- backend: ['none', 'postgres', 'supabase', 'springboot', 'laravel'],
19
+ backend: ['none', 'postgres', 'supabase', 'springboot', 'laravel', 'fastapi'],
20
20
  },
21
21
  capabilities: {
22
22
  applicationShapes: ['fullstack', 'separate'],
@@ -7,7 +7,10 @@ export function buildReactNativeFiles(answers, stack, shared) {
7
7
  'package.json': packageFile(answers, stack),
8
8
  '.node-version': `${stack.profile.runtimes.node}\n`,
9
9
  '.npmrc': 'engine-strict=true\n',
10
- 'app.json': json({ expo: { name: answers.projectName, slug: answers.projectName, version: '1.0.0', orientation: 'portrait', scheme: answers.projectName, userInterfaceStyle: 'automatic', plugins: stack.authentication === 'supabase' ? ['expo-router', 'expo-secure-store'] : ['expo-router'], experiments: { typedRoutes: true } } }),
10
+ 'app.json': json({ expo: { name: answers.projectName, slug: answers.projectName, version: '1.0.0', orientation: 'portrait', scheme: answers.projectName, userInterfaceStyle: 'automatic', runtimeVersion: { policy: 'appVersion' }, updates: { checkAutomatically: 'ON_LOAD', fallbackToCacheTimeout: 0 }, plugins: stack.authentication === 'supabase' ? ['expo-router', 'expo-secure-store'] : ['expo-router'], experiments: { typedRoutes: true } } }),
11
+ 'eas.json': json({ cli: { version: '>= 16.0.0', appVersionSource: 'remote' }, build: { development: { developmentClient: true, distribution: 'internal', channel: 'development' }, preview: { distribution: 'internal', channel: 'preview' }, production: { autoIncrement: true, channel: 'production' } }, submit: { production: {} } }),
12
+ 'lib/deep-links.ts': `const allowedRoutes = new Set(['/', '/login'])\n\nexport function acceptDeepLink(value: string, scheme = '${answers.projectName}') {\n try {\n const url = new URL(value)\n if (url.protocol !== \`${'${scheme}'}:\`) return false\n const route = \`/${'${url.host}'}${'${url.pathname}'}\`.replace(/\\/$/, '') || '/'\n return allowedRoutes.has(route)\n } catch { return false }\n}\n`,
13
+ 'docs/mobile/production.md': `# Mobile production\n\nEAS development, preview, and production builds use separate channels and environment variables. Keep signing credentials, service-role keys, and privileged operations outside the client. Sessions use platform SecureStore where authentication is enabled. Only allowlisted custom-scheme routes are accepted.\n\nRuntime compatibility follows the app version; publish updates only after testing the matching native runtime. Request the minimum native permissions and keep store privacy disclosures aligned with actual collection. Verify authentication, deep links, notifications, permissions, updates, and offline behavior on physical iOS and Android devices before release.\n`,
11
14
  'tsconfig.json': json({ extends: 'expo/tsconfig.base', compilerOptions: { strict: true, types: ['jest'], paths: { '@/*': ['./*'] } }, include: ['**/*.ts', '**/*.tsx', '.expo/types/**/*.ts', 'expo-env.d.ts'] }),
12
15
  'expo-env.d.ts': "/// <reference types=\"expo/types\" />\n",
13
16
  'app/_layout.tsx': stack.authentication === 'supabase'
@@ -2,6 +2,6 @@ import { partitionEnvironment, renderEnvironment } from '../../shared/environmen
2
2
 
3
3
  export function buildEnvironmentFiles(answers, stack) {
4
4
  const { publicNames, serverNames } = partitionEnvironment(stack)
5
- const names = stack.backendKey === 'springboot' ? [...publicNames, ...serverNames] : publicNames
5
+ const names = ['springboot', 'fastapi'].includes(stack.backendKey) ? [...publicNames, ...serverNames] : publicNames
6
6
  return { '.env.example': renderEnvironment(names, answers) }
7
7
  }
@@ -7,7 +7,7 @@ export const reactNativeAdapter = defineStackAdapter({
7
7
  id: 'react-native',
8
8
  kind: 'frontend',
9
9
  label: 'React Native (Expo)',
10
- compatibleWith: { backend: ['none', 'supabase', 'springboot', 'laravel'] },
10
+ compatibleWith: { backend: ['none', 'supabase', 'springboot', 'laravel', 'fastapi'] },
11
11
  capabilities: {
12
12
  applicationShapes: ['mobile'],
13
13
  architectureProfiles: ['small', 'medium', 'large'],
@@ -3,6 +3,6 @@ import { partitionEnvironment, renderEnvironment } from '../../shared/environmen
3
3
  export function buildEnvironmentFiles(answers, stack) {
4
4
  const { publicNames, serverNames } = partitionEnvironment(stack)
5
5
  const files = { 'frontend/.env.example': renderEnvironment(publicNames, answers) }
6
- if (stack.backendKey === 'springboot') files['.env.example'] = renderEnvironment(serverNames, answers)
6
+ if (['springboot', 'fastapi'].includes(stack.backendKey)) files['.env.example'] = renderEnvironment(serverNames, answers)
7
7
  return files
8
8
  }
@@ -14,7 +14,7 @@ export const reactViteAdapter = defineStackAdapter({
14
14
  id: 'react',
15
15
  kind: 'frontend',
16
16
  label: 'React + Vite',
17
- compatibleWith: { backend: ['none', 'supabase', 'springboot', 'laravel'] },
17
+ compatibleWith: { backend: ['none', 'supabase', 'springboot', 'laravel', 'fastapi'] },
18
18
  capabilities: {
19
19
  applicationShapes: ['separate', 'frontend'],
20
20
  architectureProfiles: ['small', 'medium', 'large'],
@@ -0,0 +1,31 @@
1
+ const json = (value) => `${JSON.stringify(value, null, 2)}\n`
2
+
3
+ export function capabilityPackFiles(answers, stack) {
4
+ const files = {}
5
+ if (answers.uploads === 'object-storage') {
6
+ files['config/capabilities/uploads.json'] = json({
7
+ visibility: 'private', authorizeEveryOperation: true, maximumBytes: 10_485_760,
8
+ acceptedTypes: [], generatedObjectNames: true, verifySignature: true,
9
+ quarantineBeforeUse: true, malwareScanRequired: true, abandonedUploadCleanupHours: 24,
10
+ })
11
+ files['docs/capabilities/uploads.md'] = '# Private uploads\n\nAuthorize initiation, completion, download, and deletion. Validate declared type and file signature, generate object names, enforce size limits, quarantine until scanning succeeds, and remove abandoned or rejected objects. Tests must cover unauthorized access, oversize and signature rejection, quarantine, scan failure, and cleanup.\n'
12
+ }
13
+ if (answers.backgroundJobs === 'queue') {
14
+ files['config/capabilities/queue.json'] = json({
15
+ adapter: stack.backendKey === 'laravel' ? 'laravel-queue' : 'spring-task',
16
+ attempts: 5, timeoutSeconds: 30, exponentialBackoff: true,
17
+ idempotencyRequired: true, failedJobStore: true, correlationIds: true,
18
+ })
19
+ files['docs/capabilities/queue.md'] = '# Durable background jobs\n\nJobs require stable idempotency keys, bounded exponential retry with jitter, explicit timeouts, a failed-job store, correlation IDs, metrics, and an operator replay procedure. Tests cover duplicate delivery, timeout, retry exhaustion, and safe replay.\n'
20
+ }
21
+ if (answers.offline && answers.offline !== 'none') {
22
+ files['config/capabilities/offline.json'] = json({
23
+ mode: answers.offline, owner: 'mobile-client', containsSensitiveData: false,
24
+ keyNamespace: `${answers.projectName}:v1`, ttlSeconds: 3600,
25
+ invalidation: answers.offline === 'sync' ? 'server-version-and-user-signout' : 'ttl-and-user-signout',
26
+ conflictPolicy: answers.offline === 'sync' ? 'server-authoritative-with-explicit-user-resolution' : 'not-applicable',
27
+ })
28
+ files['docs/capabilities/offline.md'] = `# Offline ${answers.offline}\n\nThe mobile client owns this store. Namespace keys by user and schema version, purge on sign-out, exclude secrets, enforce TTL, expose offline/stale/synchronizing/error states, and test reconnect behavior. ${answers.offline === 'sync' ? 'Synchronize through idempotent server operations and surface conflicts; never silently overwrite divergent user data.' : 'This is a measured read cache, not a source of truth.'}\n`
29
+ }
30
+ return files
31
+ }
@@ -1,11 +1,15 @@
1
1
  export const environmentHeader = '# Copy to .env or .env.local. Never commit real credentials.\n\n'
2
2
 
3
3
  export function environmentLine(name, answers) {
4
+ const isFastApi = answers.backend === 'fastapi'
5
+ const apiPort = isFastApi ? '8000' : '8080'
4
6
  const defaults = {
5
- NEXT_PUBLIC_API_URL: 'http://localhost:8080',
6
- VITE_API_URL: 'http://localhost:8080',
7
- EXPO_PUBLIC_API_URL: 'http://localhost:8080',
8
- DATABASE_URL: `jdbc:postgresql://localhost:5432/${answers.projectName.replaceAll('-', '_')}`,
7
+ NEXT_PUBLIC_API_URL: `http://localhost:${apiPort}`,
8
+ VITE_API_URL: `http://localhost:${apiPort}`,
9
+ EXPO_PUBLIC_API_URL: `http://localhost:${apiPort}`,
10
+ DATABASE_URL: isFastApi
11
+ ? `postgresql+asyncpg://localhost:5432/${answers.projectName.replaceAll('-', '_')}`
12
+ : `jdbc:postgresql://localhost:5432/${answers.projectName.replaceAll('-', '_')}`,
9
13
  POSTGRES_USER: 'postgres',
10
14
  POSTGRES_PASSWORD: 'change-me',
11
15
  POSTGRES_DB: answers.projectName.replaceAll('-', '_'),
@@ -13,7 +17,11 @@ export function environmentLine(name, answers) {
13
17
  SPRING_SECURITY_USER_NAME: 'developer',
14
18
  SPRING_SECURITY_USER_PASSWORD: 'change-me-before-production',
15
19
  OIDC_ISSUER_URI: 'http://localhost:9090/realms/app',
20
+ OIDC_ISSUER: '',
16
21
  OIDC_AUDIENCE: 'api',
22
+ OIDC_ALGORITHMS: 'RS256',
23
+ OIDC_JWKS_URL: '',
24
+ CORS_ALLOWED_ORIGINS: 'http://localhost:3000',
17
25
  }
18
26
  return `${name}=${defaults[name] || ''}`
19
27
  }
@@ -13,6 +13,12 @@ export function composerPackageVersion(profile, name, owner = name) {
13
13
  return resolved
14
14
  }
15
15
 
16
+ export function pythonPackageVersion(profile, name, owner = name) {
17
+ const resolved = profile.pythonPackages?.[name]
18
+ if (!resolved) throw new Error(`${owner} requires ${name} in compatibility profile ${profile.id}`)
19
+ return resolved
20
+ }
21
+
16
22
  export function json(value) {
17
23
  return `${JSON.stringify(value, null, 2)}\n`
18
24
  }