@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.
- package/LICENSE +32 -0
- package/README.md +99 -0
- package/bin/cli.js +371 -0
- package/bin/cli.test.js +91 -0
- package/package.json +43 -0
- package/templates/core/CLAUDE.md +36 -0
- package/templates/core/claude/memory/ARCHITECTURE.md +20 -0
- package/templates/core/claude/memory/COMMANDS.md +13 -0
- package/templates/core/claude/memory/DECISIONS.md +5 -0
- package/templates/core/claude/memory/NEXT_STEPS.md +11 -0
- package/templates/core/claude/memory/PROJECT_STATE.md +24 -0
- package/templates/core/claude/skills/kickoff/SKILL.md +84 -0
- package/templates/core/claude/skills/product-owner/SKILL.md +58 -0
- package/templates/core/claude/skills/restore-context/SKILL.md +29 -0
- package/templates/core/claude/skills/save-context/SKILL.md +35 -0
- package/templates/core/docs-architecture/ANTI_PATTERNS.md +180 -0
- package/templates/core/docs-architecture/ARCHITECTURE_PRINCIPLES.md +134 -0
- package/templates/core/docs-architecture/DELIVERY.md +68 -0
- package/templates/core/docs-architecture/DOCS_PLACEMENT.md +151 -0
- package/templates/core/docs-architecture/MULTI_REPO_CONTRACT.md +158 -0
- package/templates/core/docs-architecture/SDK_CONTRACT.md +214 -0
- package/templates/core/docs-architecture/SECURITY_USER_URLS.md +152 -0
- package/templates/core/gitignore +15 -0
- package/templates/core/mcp.json +8 -0
- package/templates/packs/nuxt-web/CLAUDE.md +74 -0
- package/templates/packs/nuxt-web/app/app.vue +5 -0
- package/templates/packs/nuxt-web/app/assets/css/main.css +18 -0
- package/templates/packs/nuxt-web/app/assets/css/tokens.css +41 -0
- package/templates/packs/nuxt-web/app/designSystem/DSButton/components/DSButton.vue +70 -0
- package/templates/packs/nuxt-web/app/designSystem/DSButton/index.ts +4 -0
- package/templates/packs/nuxt-web/app/designSystem/DSButton/tests/DSButton.spec.ts +34 -0
- package/templates/packs/nuxt-web/app/designSystem/DSButton/types/dsButton.ts +5 -0
- package/templates/packs/nuxt-web/app/domain/.gitkeep +0 -0
- package/templates/packs/nuxt-web/app/features/.gitkeep +0 -0
- package/templates/packs/nuxt-web/app/pages/index.vue +36 -0
- package/templates/packs/nuxt-web/app/utils/.gitkeep +0 -0
- package/templates/packs/nuxt-web/claude/memory/COMMANDS.md +21 -0
- package/templates/packs/nuxt-web/docs-architecture/ARCHITECTURE.md +169 -0
- package/templates/packs/nuxt-web/docs-architecture/CONVENTIONS.md +140 -0
- package/templates/packs/nuxt-web/docs-architecture/I18N.md +102 -0
- package/templates/packs/nuxt-web/docs-architecture/OPS_WEB.md +176 -0
- package/templates/packs/nuxt-web/docs-architecture/SEO_AND_ROUTING.md +118 -0
- package/templates/packs/nuxt-web/gitignore +18 -0
- package/templates/packs/nuxt-web/nuxt.config.ts +49 -0
- package/templates/packs/nuxt-web/pack.json +11 -0
- package/templates/packs/nuxt-web/package.json +31 -0
- package/templates/packs/nuxt-web/playwright.config.ts +39 -0
- package/templates/packs/nuxt-web/server/api/health.get.ts +7 -0
- package/templates/packs/nuxt-web/tests/e2e/home.spec.ts +19 -0
- package/templates/packs/nuxt-web/tsconfig.json +4 -0
- package/templates/packs/nuxt-web/vitest.config.ts +23 -0
- package/templates/packs/swift-ios/CLAUDE.md +64 -0
- package/templates/packs/swift-ios/Packages/DataLayer/Package.swift +21 -0
- package/templates/packs/swift-ios/Packages/DataLayer/Sources/DataLayer/DataLayer.swift +11 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Package.swift +20 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Domain/SampleItem.swift +15 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Engine/SampleEngine.swift +14 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Repository/SampleItemRepository.swift +27 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Tests/{{PROJECT_NAME}}CoreTests/SampleEngineTests.swift +32 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Package.swift +17 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/Color+DS.swift +18 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/Components/DSCard.swift +22 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/DS.swift +36 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/DSFont.swift +26 -0
- package/templates/packs/swift-ios/claude/memory/COMMANDS.md +18 -0
- package/templates/packs/swift-ios/docs-architecture/ARCHITECTURE.md +246 -0
- package/templates/packs/swift-ios/docs-architecture/CLOUDKIT_GUIDE.md +224 -0
- package/templates/packs/swift-ios/docs-architecture/CONVENTIONS.md +246 -0
- package/templates/packs/swift-ios/docs-architecture/DESIGN_SYSTEM.md +272 -0
- package/templates/packs/swift-ios/docs-architecture/NAVIGATION.md +241 -0
- package/templates/packs/swift-ios/docs-architecture/TESTING.md +176 -0
- package/templates/packs/swift-ios/docs-architecture/WORKFLOW.md +165 -0
- package/templates/packs/swift-ios/github/workflows/ci.yml +48 -0
- package/templates/packs/swift-ios/gitignore +5 -0
- package/templates/packs/swift-ios/mcp.json +8 -0
- package/templates/packs/swift-ios/pack.json +11 -0
- package/templates/packs/swift-ios/project.yml +33 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/App/App.swift +32 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/App/AppNamespace.swift +4 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/Module/.gitkeep +0 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/Store/.gitkeep +0 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/Tools/.gitkeep +0 -0
- package/templates/packs/ts-sdk/CHANGELOG.md +9 -0
- package/templates/packs/ts-sdk/CLAUDE.md +72 -0
- package/templates/packs/ts-sdk/MIGRATION.md +28 -0
- package/templates/packs/ts-sdk/claude/memory/COMMANDS.md +21 -0
- package/templates/packs/ts-sdk/docs-architecture/ARCHITECTURE.md +132 -0
- package/templates/packs/ts-sdk/docs-architecture/CONVENTIONS_TS.md +152 -0
- package/templates/packs/ts-sdk/gitignore +6 -0
- package/templates/packs/ts-sdk/pack.json +11 -0
- package/templates/packs/ts-sdk/package.json +55 -0
- package/templates/packs/ts-sdk/scripts/verify-dist.mjs +67 -0
- package/templates/packs/ts-sdk/src/clients/AuthClient.ts +168 -0
- package/templates/packs/ts-sdk/src/core/HttpClient.ts +85 -0
- package/templates/packs/ts-sdk/src/core/Logger.ts +27 -0
- package/templates/packs/ts-sdk/src/core/SDKContext.ts +40 -0
- package/templates/packs/ts-sdk/src/core/withTimeout.ts +19 -0
- package/templates/packs/ts-sdk/src/errors/ApiError.ts +93 -0
- package/templates/packs/ts-sdk/src/index.ts +62 -0
- package/templates/packs/ts-sdk/src/types/index.ts +33 -0
- package/templates/packs/ts-sdk/tests/apiError.test.ts +58 -0
- package/templates/packs/ts-sdk/tests/httpClient.test.ts +60 -0
- package/templates/packs/ts-sdk/tests/singleFlight.test.ts +191 -0
- package/templates/packs/ts-sdk/tsconfig.json +15 -0
- package/templates/packs/ts-sdk/tsup.config.ts +22 -0
- package/templates/packs/ts-sdk/vitest.config.ts +8 -0
- package/templates/packs/vapor-api/CLAUDE.md +73 -0
- package/templates/packs/vapor-api/Dockerfile +80 -0
- package/templates/packs/vapor-api/Package.swift +68 -0
- package/templates/packs/vapor-api/Sources/App/App.swift +5 -0
- package/templates/packs/vapor-api/Sources/App/Configure/AppConfig.swift +108 -0
- package/templates/packs/vapor-api/Sources/App/Configure/configure.swift +74 -0
- package/templates/packs/vapor-api/Sources/App/Configure/entrypoint.swift +47 -0
- package/templates/packs/vapor-api/Sources/App/Configure/routes.swift +21 -0
- package/templates/packs/vapor-api/Sources/App/Error/Failed.swift +73 -0
- package/templates/packs/vapor-api/Sources/App/Error/FailedMiddleware.swift +56 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/AppItem.swift +38 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Controllers/ItemControllersCrud.swift +41 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/DTO/ItemDTO.swift +22 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Entities/ItemEntity.swift +30 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Migrations/ItemMigrationCreate.swift +25 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Repositories/ItemRepository.swift +32 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Services/ItemService.swift +57 -0
- package/templates/packs/vapor-api/Sources/App/Registry/ControllersRegister.swift +17 -0
- package/templates/packs/vapor-api/Sources/App/Registry/MiddlewaresRegister.swift +15 -0
- package/templates/packs/vapor-api/Sources/App/Registry/MigrationsRegister.swift +18 -0
- package/templates/packs/vapor-api/Sources/Monitoring/Logging/JSONLogHandler.swift +59 -0
- package/templates/packs/vapor-api/Sources/Monitoring/Middleware/HTTPLoggingMiddleware.swift +50 -0
- package/templates/packs/vapor-api/Sources/Monitoring/Monitoring.swift +110 -0
- package/templates/packs/vapor-api/Sources/{{PROJECT_NAME}}Foundation/String+Trimmed.swift +15 -0
- package/templates/packs/vapor-api/Tests/AppTests/AppTests.swift +155 -0
- package/templates/packs/vapor-api/claude/memory/COMMANDS.md +30 -0
- package/templates/packs/vapor-api/docs-architecture/ARCHITECTURE.md +144 -0
- package/templates/packs/vapor-api/docs-architecture/CONVENTIONS.md +121 -0
- package/templates/packs/vapor-api/docs-architecture/GOTCHAS_LINUX_SWIFT.md +109 -0
- package/templates/packs/vapor-api/docs-architecture/OPS.md +102 -0
- package/templates/packs/vapor-api/env_dist +29 -0
- package/templates/packs/vapor-api/gitignore +7 -0
- package/templates/packs/vapor-api/pack.json +11 -0
- package/templates/packs/vapor-api/scripts/generate-error-codes.sh +73 -0
- 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,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
|