Architecture Overview
Baby Basics is a monorepo with three main components.
System Architecture
┌─────────────┐ HTTPS ┌──────────────┐ SQL ┌──────────────┐
│ iOS App │ ──────────────> │ Fastify API │ ──────────> │ PostgreSQL │
│ (SwiftUI) │ │ (Node.js) │ │ 16 │
└─────────────┘ └──────────────┘ └──────────────┘
│
Nginx Reverse
Proxy
Backend (apps/api/)
- Framework: Fastify 5 with TypeScript
- ORM: Prisma 7 with Driver Adapters (
@prisma/adapter-pg) - Validation: Zod schemas shared with OpenAPI via
fastify-type-provider-zod - Auth: Session tokens (bcrypt +
crypto.randomBytes) + API tokens (SHA-256 hashed) - Testing: vitest with
app.inject()integration tests - Runtime: Node.js 22 LTS
Backend Middleware Pipeline
Plugin registration order is critical in Fastify 5 due to encapsulation:
- Security headers (
@fastify/helmet) — must register first to apply to all routes - CORS (
@fastify/cors) — cross-origin policy - Rate limiting (
@fastify/rate-limit) — in-memory store, per-IP (auth) and per-user (CRUD) - Swagger/OpenAPI (
@fastify/swagger+@fastify/swagger-ui) — auto-generated from Zod schemas - Error handler (
src/middleware/error-handler.ts) — centralized error mapping - Routes (auth, children, feedings, etc.) — application endpoints
- Apple App Site Association (
/.well-known/apple-app-site-association) — iOS password AutoFill support
The error handler automatically maps:
- Zod validation errors → 400 with field-level details
- Prisma errors → P2002 (unique violation) = 409, P2025 (not found) = 404, P2003 (foreign key) = 400
AppErrorinstances → custom status codes- Unknown errors → 500 with sanitized message
All responses include details: [] (always present, empty array for non-validation errors).
Database Schema
The full Prisma schema is at apps/api/prisma/schema.prisma. Key models:
| Model | Purpose |
|---|---|
| User | Accounts with email/password auth |
| Session | Login sessions (token-based) |
| ApiToken | Programmatic API access (SHA-256 hashed) |
| Child | Baby profiles |
| ChildAccess | Multi-caregiver permissions (links users to children) |
| Feeding | Breast/bottle feeding logs |
| Diaper | Diaper change logs |
| Sleep | Sleep session logs (start/end times) |
| Note | Free-form notes with categories |
| ShareLink | Invite links for sharing access |
| AuditLog | Append-only audit trail for all mutations |
See the Database Schema spec for full details.
iOS (apps/ios/)
- Target: iOS 17+ with SwiftUI and Swift 6
- Architecture: MVVM + Repository pattern with
@Observable - Networking: URLSession with async/await (no external HTTP libraries)
- Auth: Keychain token storage with reinstall detection
- Resilience: In-memory retry queue for network failures
- Project management: xcodegen (YAML-based,
.xcodeprojgitignored) - Testing: Swift Testing framework with snapshot tests (Point-Free's swift-snapshot-testing)
iOS Architecture Details
Swift 6 Strict Concurrency:
actor APIClient— all networking operations are isolated, thread-safe by defaultactor RetryQueue— mutable queue state protected by actor isolation@Observable @MainActor final classViewModels — class-level@MainActorensures all mutations happen on the main thread- No
@ObservableObjector@StateObject— uses Swift 6's native@Observablemacro
State Management:
- Global state:
.environment(appState)in app root,@Environment(AppState.self)in views - View-local state:
@State private var viewModel = FeedingViewModel() - NO
@EnvironmentObject— that's for the old ObservableObject pattern
Keychain & Reinstall Detection:
- iOS Keychain persists after app deletion
- On first launch,
UserDefaults.bool(forKey: "hasRunBefore")is false → clear Keychain → set flag - Prevents stale tokens from previous installs
Authentication Flow:
- OnboardingFlow: Three-path auth (Create Account / I Have an Invite / Log In)
- ServerURLView: Validates server URL via
GET /healthbefore auth flow - Server URL saved to UserDefaults, pre-filled from build config (
Info.plistkey) - AuthManager: Session token storage in Keychain, returns full
AuthResponse(session + user) - Password AutoFill enabled via Apple App Site Association (AASA) file served by API
Network Resilience:
- Failed requests queue in
RetryQueue(5 retries with exponential backoff) - UI shows immediate feedback: dismiss sheet + warning toast "Couldn't save · Will retry"
- Permanent failure (after 5 retries) = persistent red toast
Infrastructure (infra/)
- Hosting: Proxmox LXC containers (dev + prod, completely isolated)
- Containers: Docker Compose (API + PostgreSQL per environment)
- Proxy: Nginx Proxy Manager with Let's Encrypt SSL
- CI/CD: GitHub Actions — push to
developdeploys to dev, push tomaindeploys to prod - Backups: Daily
pg_dumpwith 30-day retention - Rollback: One-command rollback with pre-deploy DB snapshots
- Monitoring: Uptime Kuma (planned) pings health endpoint every 30 seconds
CI/CD Pipeline
The project uses GitHub Actions with a split workflow architecture:
CI Workflow (api-ci.yml):
- Runs on all pushes and PRs to
mainanddevelopbranches - Triggers: changes to
apps/api/**,infra/**, or workflow files - Steps: Install dependencies → Generate Prisma client → Run migrations → Typecheck → Lint → Test
- Uses real PostgreSQL 16 (not SQLite) in GitHub Actions services
- Reusable via
workflow_callfor deployment workflows
Deploy to Dev (deploy-dev.yml):
- Trigger: Push to
developbranch - Runs CI workflow first, then deploys if passing
- SSH deployment to dev LXC container (CT 112, 10.0.0.201)
- Executes
/opt/baby-basics/infra/deploy.sh devremotely - Verifies deployment with health check to
https://dev.baby.bretzfam.com/api/v1/health - Uses
DEV_SSH_KEY,DEV_HOST, andDEPLOY_USERsecrets
Deploy to Prod (deploy-prod.yml):
- Trigger: Push to
mainbranch - Runs CI workflow first, then deploys if passing
- SSH deployment to prod LXC container (CT 122, 10.0.0.202)
- Executes
/opt/baby-basics/infra/deploy.sh prodremotely - Verifies deployment with health check to
https://baby.bretzfam.com/api/v1/health - Uses
PROD_SSH_KEY,PROD_HOST, andDEPLOY_USERsecrets
Deployment Script (infra/deploy.sh):
- Takes environment argument:
devorprod - Creates pre-deploy database backup
- Pulls latest code from corresponding branch
- Runs Prisma migrations
- Rebuilds Docker images and restarts containers
- Waits for health check (up to 30 seconds)
- Auto-rollback on health check failure (restores DB + previous code commit)
Key Infrastructure Files
| File | Purpose |
|---|---|
infra/docker-compose.yml | Shared base (API + Postgres services) |
infra/docker-compose.dev.yml | Dev overrides (exposed Postgres port, debug logs) |
infra/docker-compose.prod.yml | Prod overrides (resource limits, info logs) |
infra/docker-compose.local.yml | Local dev (Postgres only, no API container) |
infra/deploy.sh | Deploy with auto-rollback on health check failure |
infra/backup.sh | Daily pg_dump with 30-day retention |
infra/rollback.sh | Revert to previous deployment |
.github/workflows/api-ci.yml | CI workflow (typecheck, lint, test) |
.github/workflows/deploy-dev.yml | Auto-deploy to dev on push to develop |
.github/workflows/deploy-prod.yml | Auto-deploy to prod on push to main |
Key Design Decisions
| Decision | Rationale |
|---|---|
| UUIDs everywhere | Prevents ID enumeration |
| All times in UTC | Client handles timezone conversion |
| No soft deletes | Hard deletes with AuditLog for history |
| Configurable day boundary | day_start_time (default 07:00) defines when "today" starts |
| Feeding amounts in mL | Stored as float, displayed as oz on iOS |
Exclusive since filter | ?since=timestamp returns events after that time (>) |
| Details array always present | Error responses always include details: [], never omit it |
See the Specs section for detailed rationale on each feature.