@horka/app-forge 0.1.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 (141) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +99 -0
  3. package/bin/cli.js +371 -0
  4. package/bin/cli.test.js +91 -0
  5. package/package.json +43 -0
  6. package/templates/core/CLAUDE.md +36 -0
  7. package/templates/core/claude/memory/ARCHITECTURE.md +20 -0
  8. package/templates/core/claude/memory/COMMANDS.md +13 -0
  9. package/templates/core/claude/memory/DECISIONS.md +5 -0
  10. package/templates/core/claude/memory/NEXT_STEPS.md +11 -0
  11. package/templates/core/claude/memory/PROJECT_STATE.md +24 -0
  12. package/templates/core/claude/skills/kickoff/SKILL.md +84 -0
  13. package/templates/core/claude/skills/product-owner/SKILL.md +58 -0
  14. package/templates/core/claude/skills/restore-context/SKILL.md +29 -0
  15. package/templates/core/claude/skills/save-context/SKILL.md +35 -0
  16. package/templates/core/docs-architecture/ANTI_PATTERNS.md +180 -0
  17. package/templates/core/docs-architecture/ARCHITECTURE_PRINCIPLES.md +134 -0
  18. package/templates/core/docs-architecture/DELIVERY.md +68 -0
  19. package/templates/core/docs-architecture/DOCS_PLACEMENT.md +151 -0
  20. package/templates/core/docs-architecture/MULTI_REPO_CONTRACT.md +158 -0
  21. package/templates/core/docs-architecture/SDK_CONTRACT.md +214 -0
  22. package/templates/core/docs-architecture/SECURITY_USER_URLS.md +152 -0
  23. package/templates/core/gitignore +15 -0
  24. package/templates/core/mcp.json +8 -0
  25. package/templates/packs/nuxt-web/CLAUDE.md +74 -0
  26. package/templates/packs/nuxt-web/app/app.vue +5 -0
  27. package/templates/packs/nuxt-web/app/assets/css/main.css +18 -0
  28. package/templates/packs/nuxt-web/app/assets/css/tokens.css +41 -0
  29. package/templates/packs/nuxt-web/app/designSystem/DSButton/components/DSButton.vue +70 -0
  30. package/templates/packs/nuxt-web/app/designSystem/DSButton/index.ts +4 -0
  31. package/templates/packs/nuxt-web/app/designSystem/DSButton/tests/DSButton.spec.ts +34 -0
  32. package/templates/packs/nuxt-web/app/designSystem/DSButton/types/dsButton.ts +5 -0
  33. package/templates/packs/nuxt-web/app/domain/.gitkeep +0 -0
  34. package/templates/packs/nuxt-web/app/features/.gitkeep +0 -0
  35. package/templates/packs/nuxt-web/app/pages/index.vue +36 -0
  36. package/templates/packs/nuxt-web/app/utils/.gitkeep +0 -0
  37. package/templates/packs/nuxt-web/claude/memory/COMMANDS.md +21 -0
  38. package/templates/packs/nuxt-web/docs-architecture/ARCHITECTURE.md +169 -0
  39. package/templates/packs/nuxt-web/docs-architecture/CONVENTIONS.md +140 -0
  40. package/templates/packs/nuxt-web/docs-architecture/I18N.md +102 -0
  41. package/templates/packs/nuxt-web/docs-architecture/OPS_WEB.md +176 -0
  42. package/templates/packs/nuxt-web/docs-architecture/SEO_AND_ROUTING.md +118 -0
  43. package/templates/packs/nuxt-web/gitignore +18 -0
  44. package/templates/packs/nuxt-web/nuxt.config.ts +49 -0
  45. package/templates/packs/nuxt-web/pack.json +11 -0
  46. package/templates/packs/nuxt-web/package.json +31 -0
  47. package/templates/packs/nuxt-web/playwright.config.ts +39 -0
  48. package/templates/packs/nuxt-web/server/api/health.get.ts +7 -0
  49. package/templates/packs/nuxt-web/tests/e2e/home.spec.ts +19 -0
  50. package/templates/packs/nuxt-web/tsconfig.json +4 -0
  51. package/templates/packs/nuxt-web/vitest.config.ts +23 -0
  52. package/templates/packs/swift-ios/CLAUDE.md +64 -0
  53. package/templates/packs/swift-ios/Packages/DataLayer/Package.swift +21 -0
  54. package/templates/packs/swift-ios/Packages/DataLayer/Sources/DataLayer/DataLayer.swift +11 -0
  55. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Package.swift +20 -0
  56. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Domain/SampleItem.swift +15 -0
  57. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Engine/SampleEngine.swift +14 -0
  58. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Repository/SampleItemRepository.swift +27 -0
  59. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Tests/{{PROJECT_NAME}}CoreTests/SampleEngineTests.swift +32 -0
  60. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Package.swift +17 -0
  61. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/Color+DS.swift +18 -0
  62. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/Components/DSCard.swift +22 -0
  63. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/DS.swift +36 -0
  64. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/DSFont.swift +26 -0
  65. package/templates/packs/swift-ios/claude/memory/COMMANDS.md +18 -0
  66. package/templates/packs/swift-ios/docs-architecture/ARCHITECTURE.md +246 -0
  67. package/templates/packs/swift-ios/docs-architecture/CLOUDKIT_GUIDE.md +224 -0
  68. package/templates/packs/swift-ios/docs-architecture/CONVENTIONS.md +246 -0
  69. package/templates/packs/swift-ios/docs-architecture/DESIGN_SYSTEM.md +272 -0
  70. package/templates/packs/swift-ios/docs-architecture/NAVIGATION.md +241 -0
  71. package/templates/packs/swift-ios/docs-architecture/TESTING.md +176 -0
  72. package/templates/packs/swift-ios/docs-architecture/WORKFLOW.md +165 -0
  73. package/templates/packs/swift-ios/github/workflows/ci.yml +48 -0
  74. package/templates/packs/swift-ios/gitignore +5 -0
  75. package/templates/packs/swift-ios/mcp.json +8 -0
  76. package/templates/packs/swift-ios/pack.json +11 -0
  77. package/templates/packs/swift-ios/project.yml +33 -0
  78. package/templates/packs/swift-ios/{{PROJECT_NAME}}/App/App.swift +32 -0
  79. package/templates/packs/swift-ios/{{PROJECT_NAME}}/App/AppNamespace.swift +4 -0
  80. package/templates/packs/swift-ios/{{PROJECT_NAME}}/Module/.gitkeep +0 -0
  81. package/templates/packs/swift-ios/{{PROJECT_NAME}}/Store/.gitkeep +0 -0
  82. package/templates/packs/swift-ios/{{PROJECT_NAME}}/Tools/.gitkeep +0 -0
  83. package/templates/packs/ts-sdk/CHANGELOG.md +9 -0
  84. package/templates/packs/ts-sdk/CLAUDE.md +72 -0
  85. package/templates/packs/ts-sdk/MIGRATION.md +28 -0
  86. package/templates/packs/ts-sdk/claude/memory/COMMANDS.md +21 -0
  87. package/templates/packs/ts-sdk/docs-architecture/ARCHITECTURE.md +132 -0
  88. package/templates/packs/ts-sdk/docs-architecture/CONVENTIONS_TS.md +152 -0
  89. package/templates/packs/ts-sdk/gitignore +6 -0
  90. package/templates/packs/ts-sdk/pack.json +11 -0
  91. package/templates/packs/ts-sdk/package.json +55 -0
  92. package/templates/packs/ts-sdk/scripts/verify-dist.mjs +67 -0
  93. package/templates/packs/ts-sdk/src/clients/AuthClient.ts +168 -0
  94. package/templates/packs/ts-sdk/src/core/HttpClient.ts +85 -0
  95. package/templates/packs/ts-sdk/src/core/Logger.ts +27 -0
  96. package/templates/packs/ts-sdk/src/core/SDKContext.ts +40 -0
  97. package/templates/packs/ts-sdk/src/core/withTimeout.ts +19 -0
  98. package/templates/packs/ts-sdk/src/errors/ApiError.ts +93 -0
  99. package/templates/packs/ts-sdk/src/index.ts +62 -0
  100. package/templates/packs/ts-sdk/src/types/index.ts +33 -0
  101. package/templates/packs/ts-sdk/tests/apiError.test.ts +58 -0
  102. package/templates/packs/ts-sdk/tests/httpClient.test.ts +60 -0
  103. package/templates/packs/ts-sdk/tests/singleFlight.test.ts +191 -0
  104. package/templates/packs/ts-sdk/tsconfig.json +15 -0
  105. package/templates/packs/ts-sdk/tsup.config.ts +22 -0
  106. package/templates/packs/ts-sdk/vitest.config.ts +8 -0
  107. package/templates/packs/vapor-api/CLAUDE.md +73 -0
  108. package/templates/packs/vapor-api/Dockerfile +80 -0
  109. package/templates/packs/vapor-api/Package.swift +68 -0
  110. package/templates/packs/vapor-api/Sources/App/App.swift +5 -0
  111. package/templates/packs/vapor-api/Sources/App/Configure/AppConfig.swift +108 -0
  112. package/templates/packs/vapor-api/Sources/App/Configure/configure.swift +74 -0
  113. package/templates/packs/vapor-api/Sources/App/Configure/entrypoint.swift +47 -0
  114. package/templates/packs/vapor-api/Sources/App/Configure/routes.swift +21 -0
  115. package/templates/packs/vapor-api/Sources/App/Error/Failed.swift +73 -0
  116. package/templates/packs/vapor-api/Sources/App/Error/FailedMiddleware.swift +56 -0
  117. package/templates/packs/vapor-api/Sources/App/Features/Item/AppItem.swift +38 -0
  118. package/templates/packs/vapor-api/Sources/App/Features/Item/Controllers/ItemControllersCrud.swift +41 -0
  119. package/templates/packs/vapor-api/Sources/App/Features/Item/DTO/ItemDTO.swift +22 -0
  120. package/templates/packs/vapor-api/Sources/App/Features/Item/Entities/ItemEntity.swift +30 -0
  121. package/templates/packs/vapor-api/Sources/App/Features/Item/Migrations/ItemMigrationCreate.swift +25 -0
  122. package/templates/packs/vapor-api/Sources/App/Features/Item/Repositories/ItemRepository.swift +32 -0
  123. package/templates/packs/vapor-api/Sources/App/Features/Item/Services/ItemService.swift +57 -0
  124. package/templates/packs/vapor-api/Sources/App/Registry/ControllersRegister.swift +17 -0
  125. package/templates/packs/vapor-api/Sources/App/Registry/MiddlewaresRegister.swift +15 -0
  126. package/templates/packs/vapor-api/Sources/App/Registry/MigrationsRegister.swift +18 -0
  127. package/templates/packs/vapor-api/Sources/Monitoring/Logging/JSONLogHandler.swift +59 -0
  128. package/templates/packs/vapor-api/Sources/Monitoring/Middleware/HTTPLoggingMiddleware.swift +50 -0
  129. package/templates/packs/vapor-api/Sources/Monitoring/Monitoring.swift +110 -0
  130. package/templates/packs/vapor-api/Sources/{{PROJECT_NAME}}Foundation/String+Trimmed.swift +15 -0
  131. package/templates/packs/vapor-api/Tests/AppTests/AppTests.swift +155 -0
  132. package/templates/packs/vapor-api/claude/memory/COMMANDS.md +30 -0
  133. package/templates/packs/vapor-api/docs-architecture/ARCHITECTURE.md +144 -0
  134. package/templates/packs/vapor-api/docs-architecture/CONVENTIONS.md +121 -0
  135. package/templates/packs/vapor-api/docs-architecture/GOTCHAS_LINUX_SWIFT.md +109 -0
  136. package/templates/packs/vapor-api/docs-architecture/OPS.md +102 -0
  137. package/templates/packs/vapor-api/env_dist +29 -0
  138. package/templates/packs/vapor-api/gitignore +7 -0
  139. package/templates/packs/vapor-api/pack.json +11 -0
  140. package/templates/packs/vapor-api/scripts/generate-error-codes.sh +73 -0
  141. package/templates/packs/vapor-api/scripts/validate-env-vars.sh +72 -0
@@ -0,0 +1,109 @@
1
+ # GOTCHAS — Swift on Linux (the deployment target)
2
+
3
+ Server-side Swift develops on macOS but RUNS on Linux, and the two are not the same
4
+ platform. Every entry below was paid for in production by a real team. Format:
5
+ **Symptom → Cause → Fix.** Add new entries with evidence only.
6
+
7
+ ## 1. URLSession crashes on Linux — use AsyncHTTPClient, always
8
+
9
+ - **Symptom:** intermittent hard crashes in production: `Illegal instruction`, libcurl
10
+ error 43 (`CURLE_BAD_FUNCTION_ARGUMENT`), memory corruption under concurrent load.
11
+ Everything green on macOS.
12
+ - **Cause:** on Linux, `URLSession` comes from `FoundationNetworking` — a libcurl-based
13
+ reimplementation with known race conditions in handle configuration. It is NOT the
14
+ battle-tested Darwin stack you developed against.
15
+ - **Fix:** ONE HTTP client for the whole service: **AsyncHTTPClient**, reached through
16
+ Vapor's managed instance `app.http.client.shared`. Inject it (init parameter or a
17
+ config actor) into every client target — never construct ad-hoc `HTTPClient()`
18
+ instances per call (each spawns an event loop group; you leak threads and FDs and must
19
+ manage shutdown yourself).
20
+ ```swift
21
+ var request = HTTPClientRequest(url: url)
22
+ request.method = .GET
23
+ let response = try await httpClient.execute(request, timeout: .seconds(30))
24
+ ```
25
+ Enforcement is a grep: `grep -rn "URLSession" Sources/` must return nothing.
26
+
27
+ > 📖 **War story:** a production team migrated 18 call sites from URLSession to injected
28
+ > AsyncHTTPClient after chasing "random" illegal-instruction crashes for weeks. The
29
+ > crashes stopped the day the migration shipped.
30
+
31
+ ## 2. Non-Sendable containment — Fluent entities stay in the request scope
32
+
33
+ - **Symptom:** Swift 6 data-race diagnostics around model classes; or, with checking
34
+ silenced, corrupted field values under load.
35
+ - **Cause:** Fluent models are mutable `final class`es marked `@unchecked Sendable` —
36
+ the annotation silences the compiler but provides zero runtime safety. An entity that
37
+ crosses a task boundary (cached in a singleton, captured by a background task, passed
38
+ between requests) is a shared-mutable-state bug.
39
+ - **Fix:** containment. Entities are born in a repository call and die at the controller
40
+ edge, mapped to a `Sendable` DTO struct (`DTO.Output(entity:)`). Nothing reference-typed
41
+ leaves the request. Caches and queues hold value types only.
42
+
43
+ ## 3. Migration ordering only fails on FRESH databases
44
+
45
+ - **Symptom:** first deploy to a new environment crashes at boot:
46
+ `relation "teams" does not exist`. Every developer machine and the long-lived staging
47
+ DB boot fine.
48
+ - **Cause:** Fluent executes migrations in REGISTRATION order. Incrementally-migrated
49
+ databases already have every table, so a wrong order hides for months until a
50
+ from-scratch run (new region, new developer, disaster recovery — the worst possible
51
+ moments).
52
+ - **Fix:** central, explicit, commented registration in `configure.swift`: foreign-key
53
+ targets first. Any change to migrations gets verified against a scratch database
54
+ (drop + full migrate) before merge.
55
+
56
+ ## 4. Background work must not borrow request-scoped resources
57
+
58
+ - **Symptom:** intermittent `connection closed` / pool errors in work spawned from a
59
+ handler with `Task { … req.db … }` or `Task.detached`; failures cluster under load and
60
+ vanish locally.
61
+ - **Cause:** `req.db`, `req.client`, `req.logger` live in the request's lifecycle. The
62
+ response returns, the request's resources are released, the orphaned task keeps using
63
+ them.
64
+ - **Fix:** work that outlives the response goes through a persistent queue (Vapor Queues
65
+ with a database/Redis driver) — survives restarts, retries on failure, uses its own
66
+ connections. For app-lifetime (not request-lifetime) work only, use `app.db`/`app.logger`
67
+ explicitly and deliberately.
68
+
69
+ ## 5. `hashValue` is per-process — never persist or compare it across runs
70
+
71
+ - **Symptom:** cache keys, ETags, or dedupe markers computed with `hashValue` never match
72
+ after a restart or between replicas; conditional requests never return 304.
73
+ - **Cause:** Swift's `Hashable` seeds its hash per process BY DESIGN.
74
+ - **Fix:** stable identity only: SHA-256 of the bytes, or a `version`/`updatedAt` column.
75
+ See the ETag entry in `ANTI_PATTERNS.md` — this exact bug shipped to production.
76
+
77
+ ## 6. macOS-green is not done — gate on Linux
78
+
79
+ - **Symptom:** CI on macOS passes; the Docker deploy crashes or behaves differently
80
+ (Foundation gaps, file-path case sensitivity, locale/encoding differences).
81
+ - **Cause:** Foundation on Linux is a different implementation with a different surface;
82
+ the filesystem is case-sensitive; the C library differs.
83
+ - **Fix:** the Linux gate is one command and runs before every ship:
84
+ ```bash
85
+ docker run --rm -v "$PWD:/src" -w /src swift:6.2-noble swift test
86
+ ```
87
+ CI runs the test job in a Swift Linux container, matching the Dockerfile's Swift version.
88
+
89
+ ## 7. `MetricsSystem.bootstrap` may only run once per process
90
+
91
+ - **Symptom:** the app runs fine; the test suite crashes with
92
+ `metrics system can only be initialized once per process` on the second booted app.
93
+ - **Cause:** swift-metrics enforces single bootstrap, but tests create many
94
+ `Application`s, and bootstrap was wired per-application.
95
+ - **Fix:** anchor the registry + bootstrap to the process with a lazy `static let`
96
+ (`MetricsRegistry.shared` in the Monitoring target) — thread-safe by language
97
+ guarantee, runs exactly once, every app shares the registry.
98
+
99
+ ## 8. `.env` is loaded from the WORKING DIRECTORY
100
+
101
+ - **Symptom:** `Missing environment variable …` fatal at boot although `.env` exists and
102
+ is correct — typically when launching from an IDE, a systemd unit, or a script that
103
+ `cd`s elsewhere.
104
+ - **Cause:** Vapor's `Environment.detect()` reads `.env` relative to the process working
105
+ directory, not the binary location or the repo root.
106
+ - **Fix:** always launch from the project root locally; in production don't use `.env`
107
+ files at all — the orchestrator injects real environment variables. The fail-fast
108
+ `AppConfig.Key.get` turns this mistake into an immediate, named boot error instead of
109
+ weird half-configured behavior.
@@ -0,0 +1,102 @@
1
+ # OPS — Logging, Metrics, Docker, Runtime Configuration
2
+
3
+ How this service behaves in production. Everything here is already wired in the skeleton;
4
+ this doc explains the contract so changes don't silently break operations.
5
+
6
+ ## 1. Logging
7
+
8
+ Two modes, decided once in `entrypoint.swift`:
9
+ - **Development**: Vapor's human-readable text handler.
10
+ - **Release**: `JSONLogHandler` (Monitoring target) — ONE JSON object per line on stdout,
11
+ metadata flattened to top-level keys. Loki/Grafana/CloudWatch parse it without config.
12
+
13
+ Level precedence: `--log` CLI flag → `LOG_LEVEL` env var → default (`notice` in
14
+ production, `info` elsewhere). **The level is never hardcoded** — a hardcoded `.debug`
15
+ in configure once shipped to a production fleet and flooded the log store; runtime config
16
+ exists precisely so ops can turn verbosity up DURING an incident without a redeploy.
17
+
18
+ `HTTPLoggingMiddleware` (Monitoring) replaces Vapor's default route logger:
19
+ - adds `duration_ms` to every request log (monotonic clock — NTP-jump safe),
20
+ - skips `/health` and `/metrics` (probes fire every few seconds; logging them buries
21
+ real traffic and inflates storage for zero information).
22
+
23
+ > ⚠️ **Gotcha:** Symptom — log volume explodes after adding an uptime monitor. Cause —
24
+ > probe endpoints logged like user traffic. Fix — exclusion list in the middleware init,
25
+ > not grep-filtering in the log store (you pay for ingestion before filtering).
26
+
27
+ ## 2. Metrics
28
+
29
+ - Backend: swift-metrics API + Prometheus exporter. Feature code NEVER touches the
30
+ metrics API directly — it calls Monitoring helpers (e.g. `recordHTTPError`), so the
31
+ backend stays swappable and metric names stay consistent.
32
+ - The registry and `MetricsSystem.bootstrap` are **process-global** (`MetricsRegistry.shared`,
33
+ a lazy `static let`). Bootstrap may only run once per process; tests boot many apps.
34
+ - `/metrics` endpoint contract:
35
+ - Bearer token from `METRICS_TOKEN`, compared with `constantTimeCompare` — a plain
36
+ `==` leaks token length and prefix timing to an attacker who can measure latency.
37
+ - Monitoring disabled → 503 (scrapers alarm loudly instead of charting zeros).
38
+ - `/health` stays unauthenticated (load balancers can't do auth); it returns liveness
39
+ ONLY — no version, no dependency status, nothing enumerable.
40
+
41
+ ## 3. Docker — the production image
42
+
43
+ `Dockerfile` is multi-stage; each choice is load-bearing:
44
+
45
+ | Choice | Why |
46
+ |---|---|
47
+ | Dependencies resolved in their own layer | `Package.*` unchanged → deps layer cached → rebuilds in seconds |
48
+ | `swift build -c release --static-swift-stdlib` | run image needs no Swift runtime — smaller, fewer CVEs to track |
49
+ | `-Xlinker -ljemalloc` (dynamic) | server-grade allocator; the STATIC jemalloc is incompatible with the static Swift runtime |
50
+ | `swift-backtrace-static` copied + `SWIFT_BACKTRACE` env | crashes print symbolized backtraces in the container — without it a production crash is one unusable line |
51
+ | dedicated `vapor` user, `USER vapor:vapor` | the API never runs as root; a container escape lands in an unprivileged account |
52
+ | `Public/`/`Resources/` copied `chmod -R a-w` | runtime can't mutate its own assets |
53
+ | `ARG/ENV LOG_LEVEL` | verbosity is deploy-time AND runtime configurable |
54
+ | `CMD serve --env production --hostname 0.0.0.0 --port 8080` | binds all interfaces INSIDE the container; the host decides exposure |
55
+
56
+ Build & run:
57
+ ```bash
58
+ docker build -t my-service .
59
+ docker run --rm -p 8080:8080 --env-file .env my-service
60
+ ```
61
+
62
+ > ⚠️ **Gotcha:** Symptom — TLS calls to external APIs fail only in the container
63
+ > (`unable to get local issuer certificate`). Cause — minimal run images ship without CA
64
+ > roots. Fix — `ca-certificates` stays in the run-image package list; never "fix" this by
65
+ > disabling certificate verification.
66
+
67
+ ## 4. Runtime configuration
68
+
69
+ - All config flows through `AppConfig` (typed, fail-fast — see CONVENTIONS.md §3).
70
+ A misconfigured service must die at boot with the variable's name in the crash log;
71
+ orchestrators surface boot crashes immediately, while a lazy nil surfaces as a 3 a.m.
72
+ 500 storm.
73
+ - Secrets (DB password, metrics token, API keys) come from the orchestrator's secret
74
+ store; `env_dist` documents the variable, never the value.
75
+ - Deploy checklist for any config change:
76
+ 1. `AppConfig.Key` case + typed property
77
+ 2. `env_dist` line
78
+ 3. every compose/CI manifest
79
+ 4. `bash scripts/validate-env-vars.sh` green
80
+ 5. secret created in the deploy environment BEFORE the deploy
81
+
82
+ ## 5. Database operations
83
+
84
+ - Migrations run at boot (`app.autoMigrate()` in configure). Registration order is
85
+ load-bearing — see `ARCHITECTURE.md` §5 and GOTCHAS.
86
+ - Connection pool: `maxConnectionsPerEventLoop` × event loops (≈ CPU cores) = total
87
+ connections per replica. Size against the database's `max_connections` BEFORE scaling
88
+ replicas, not after the pool exhaustion incident.
89
+ - Tests run on in-memory SQLite (zero infra), but SQLite is not PostgreSQL: anything
90
+ using PostgreSQL-specific SQL needs a CI job against a real PostgreSQL service
91
+ container before release.
92
+
93
+ ## 6. Shipping checklist
94
+
95
+ ```bash
96
+ swift test # host gate
97
+ docker run --rm -v "$PWD:/src" -w /src swift:6.2-noble swift test # Linux gate (the real target)
98
+ bash scripts/validate-env-vars.sh # config drift
99
+ bash scripts/generate-error-codes.sh && git diff --exit-code docs/ERROR_CODES.md # error doc current
100
+ docker build -t my-service . && docker run --rm -p 8080:8080 --env-file .env my-service
101
+ curl -s localhost:8080/health # eyes-on proof
102
+ ```
@@ -0,0 +1,29 @@
1
+ # {{PROJECT_NAME}} — environment template
2
+ #
3
+ # Copy to .env for local development (Vapor loads .env automatically from the working
4
+ # directory). NEVER commit .env or put real production values here.
5
+ #
6
+ # CONTRACT: every variable below has a matching case in AppConfig.Key
7
+ # (Sources/App/Configure/AppConfig.swift) and vice versa — except LOG_LEVEL, which is
8
+ # read in entrypoint.swift BEFORE config exists. scripts/validate-env-vars.sh enforces
9
+ # the cross-check; run it whenever you touch configuration.
10
+
11
+ # Log level: trace|debug|info|notice|warning|error|critical (never hardcoded in code)
12
+ LOG_LEVEL=debug
13
+
14
+ # Database (PostgreSQL)
15
+ DATABASE_HOST=localhost
16
+ DATABASE_PORT=5432
17
+ DATABASE_USERNAME=vapor_username
18
+ DATABASE_PASSWORD=vapor_password
19
+ DATABASE_NAME=vapor_database
20
+
21
+ # CORS — exact origin of the frontend allowed to call this API
22
+ ALLOWED_ORIGIN=http://localhost:3000
23
+
24
+ # Monitoring (see docs-architecture/OPS.md)
25
+ # When MONITORING_ENABLED=true, METRICS_TOKEN MUST be non-empty — boot fails fast otherwise
26
+ # (an empty token would leave /metrics returning 401 forever). Leave it blank only while
27
+ # MONITORING_ENABLED stays false.
28
+ MONITORING_ENABLED=false
29
+ METRICS_TOKEN=
@@ -0,0 +1,7 @@
1
+ .build/
2
+ .swiftpm/
3
+ .env
4
+ .env.*
5
+ db.sqlite*
6
+ xcuserdata/
7
+ DerivedData/
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "vapor-api",
3
+ "label": "Swift API — Vapor 4 / Fluent / PostgreSQL",
4
+ "languages": ["swift"],
5
+ "idPrompt": "Service identifier",
6
+ "requirements": [
7
+ "Swift 6 toolchain (Xcode 16+ or swift.org toolchain)",
8
+ "Docker (local PostgreSQL, production image builds, Linux test runs)"
9
+ ],
10
+ "notes": "Boots day one: `swift test` runs the full stack on in-memory SQLite — no database needed. First real run: `cp env_dist .env`, start a local PostgreSQL, `swift run App serve`."
11
+ }
@@ -0,0 +1,73 @@
1
+ #!/bin/bash
2
+ # Generates docs/ERROR_CODES.md from the typed error enums in Sources/App/Error/Failed.swift.
3
+ # THE DOC IS GENERATED — never edit it by hand; edit the enum and re-run this script.
4
+ #
5
+ # Depends on two CONVENTIONS.md rules:
6
+ # - one enum per HTTP status, conforming to CustomError
7
+ # - convert() stays on ONE line: `func convert() -> HTTPStatus { .badRequest }`
8
+
9
+ set -e
10
+
11
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
13
+
14
+ FAILED_SWIFT="$PROJECT_ROOT/Sources/App/Error/Failed.swift"
15
+ OUTPUT="$PROJECT_ROOT/docs/ERROR_CODES.md"
16
+
17
+ mkdir -p "$PROJECT_ROOT/docs"
18
+
19
+ status_code() {
20
+ case "$1" in
21
+ badRequest) echo 400 ;;
22
+ unauthorized) echo 401 ;;
23
+ paymentRequired) echo 402 ;;
24
+ forbidden) echo 403 ;;
25
+ notFound) echo 404 ;;
26
+ conflict) echo 409 ;;
27
+ gone) echo 410 ;;
28
+ unprocessableEntity) echo 422 ;;
29
+ tooManyRequests) echo 429 ;;
30
+ internalServerError) echo 500 ;;
31
+ notImplemented) echo 501 ;;
32
+ serviceUnavailable) echo 503 ;;
33
+ *) echo "?" ;;
34
+ esac
35
+ }
36
+
37
+ {
38
+ echo "# Error Codes — GENERATED FILE, DO NOT EDIT"
39
+ echo ""
40
+ echo "> Source of truth: \`Sources/App/Error/Failed.swift\`."
41
+ echo "> Regenerate with \`./scripts/generate-error-codes.sh\`."
42
+ echo ""
43
+ echo "Response body shape: \`{\"code\": <http status>, \"name\": \"<case>\", \"description\": \"<reason>\"}\`"
44
+ echo ""
45
+ echo "| name | HTTP | category |"
46
+ echo "|---|---|---|"
47
+ } > "$OUTPUT"
48
+
49
+ current_enum=""
50
+ cases=""
51
+
52
+ flush() {
53
+ [ -z "$current_enum" ] && return
54
+ code=$(status_code "$1")
55
+ for c in $cases; do
56
+ echo "| \`$c\` | $code | $current_enum |" >> "$OUTPUT"
57
+ done
58
+ cases=""
59
+ }
60
+
61
+ while IFS= read -r line; do
62
+ if [[ "$line" =~ enum[[:space:]]+([A-Za-z]+):[[:space:]]*CustomError ]]; then
63
+ current_enum="${BASH_REMATCH[1]}"
64
+ cases=""
65
+ elif [[ "$line" =~ ^[[:space:]]+case[[:space:]]+([a-zA-Z]+) ]]; then
66
+ cases="$cases ${BASH_REMATCH[1]}"
67
+ elif [[ "$line" =~ convert\(\)[[:space:]]*-\>[[:space:]]*HTTPStatus[[:space:]]*\{[[:space:]]*\.([a-zA-Z]+) ]]; then
68
+ flush "${BASH_REMATCH[1]}"
69
+ current_enum=""
70
+ fi
71
+ done < "$FAILED_SWIFT"
72
+
73
+ echo "Generated $OUTPUT"
@@ -0,0 +1,72 @@
1
+ #!/bin/bash
2
+ # Cross-checks the typed config (AppConfig.Key) against env_dist and every deploy
3
+ # manifest that exists. A variable that exists in code but not in a manifest is a
4
+ # production crash waiting for the next deploy — fail loudly here instead.
5
+ #
6
+ # Usage: ./scripts/validate-env-vars.sh (from anywhere; exits 1 on any mismatch)
7
+
8
+ set -e
9
+
10
+ RED='\033[0;31m'
11
+ GREEN='\033[0;32m'
12
+ YELLOW='\033[1;33m'
13
+ NC='\033[0m'
14
+
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
17
+
18
+ APP_CONFIG="$PROJECT_ROOT/Sources/App/Configure/AppConfig.swift"
19
+ ENV_DIST="$PROJECT_ROOT/env_dist"
20
+
21
+ # Manifests are optional — checked only if present. Add yours here as the project grows.
22
+ MANIFESTS=(
23
+ "$PROJECT_ROOT/docker-compose.yml"
24
+ "$PROJECT_ROOT/deploy/docker-compose.yml"
25
+ "$PROJECT_ROOT/.github/workflows/deploy.yml"
26
+ "$PROJECT_ROOT/.github/workflows/test.yml"
27
+ )
28
+
29
+ echo "Validating environment variables against AppConfig.Key..."
30
+ echo ""
31
+
32
+ # Extract case names from the AppConfig.Key enum ("case VARIABLE_NAME").
33
+ ENV_VARS=$(grep -E '^\s*case [A-Z_]+' "$APP_CONFIG" | sed 's/.*case //' | sed 's/[^A-Z_].*//' | sort -u)
34
+
35
+ if [ -z "$ENV_VARS" ]; then
36
+ echo -e "${RED}No Key cases found in $APP_CONFIG — wrong path?${NC}"
37
+ exit 1
38
+ fi
39
+
40
+ ALL_VALID=true
41
+
42
+ for var in $ENV_VARS; do
43
+ if ! grep -q "^${var}=" "$ENV_DIST" 2>/dev/null; then
44
+ echo -e "${RED}MISSING in env_dist: ${var}${NC}"
45
+ ALL_VALID=false
46
+ fi
47
+ for manifest in "${MANIFESTS[@]}"; do
48
+ if [ -f "$manifest" ]; then
49
+ if ! grep -Eq "${var}[:=]" "$manifest" 2>/dev/null; then
50
+ echo -e "${RED}MISSING in ${manifest#"$PROJECT_ROOT"/}: ${var}${NC}"
51
+ ALL_VALID=false
52
+ fi
53
+ fi
54
+ done
55
+ done
56
+
57
+ # Reverse check: env_dist entries that no longer exist in code (drift the other way).
58
+ # LOG_LEVEL is exempt — read in entrypoint.swift before AppConfig exists.
59
+ for var in $(grep -E '^[A-Z_]+=' "$ENV_DIST" | cut -d= -f1 | sort -u); do
60
+ [ "$var" = "LOG_LEVEL" ] && continue
61
+ if ! echo "$ENV_VARS" | grep -q "^${var}$"; then
62
+ echo -e "${YELLOW}STALE in env_dist (no AppConfig.Key case): ${var}${NC}"
63
+ fi
64
+ done
65
+
66
+ echo ""
67
+ if [ "$ALL_VALID" = true ]; then
68
+ echo -e "${GREEN}All environment variables are consistent.${NC}"
69
+ else
70
+ echo -e "${RED}Fix the mismatches above before committing.${NC}"
71
+ exit 1
72
+ fi