Skip to main content

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:

  1. Security headers (@fastify/helmet) — must register first to apply to all routes
  2. CORS (@fastify/cors) — cross-origin policy
  3. Rate limiting (@fastify/rate-limit) — in-memory store, per-IP (auth) and per-user (CRUD)
  4. Swagger/OpenAPI (@fastify/swagger + @fastify/swagger-ui) — auto-generated from Zod schemas
  5. Error handler (src/middleware/error-handler.ts) — centralized error mapping
  6. Routes (auth, children, feedings, etc.) — application endpoints
  7. 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
  • AppError instances → 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:

ModelPurpose
UserAccounts with email/password auth
SessionLogin sessions (token-based)
ApiTokenProgrammatic API access (SHA-256 hashed)
ChildBaby profiles
ChildAccessMulti-caregiver permissions (links users to children)
FeedingBreast/bottle feeding logs
DiaperDiaper change logs
SleepSleep session logs (start/end times)
NoteFree-form notes with categories
ShareLinkInvite links for sharing access
AuditLogAppend-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, .xcodeproj gitignored)
  • 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 default
  • actor RetryQueue — mutable queue state protected by actor isolation
  • @Observable @MainActor final class ViewModels — class-level @MainActor ensures all mutations happen on the main thread
  • No @ObservableObject or @StateObject — uses Swift 6's native @Observable macro

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 /health before auth flow
  • Server URL saved to UserDefaults, pre-filled from build config (Info.plist key)
  • 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 develop deploys to dev, push to main deploys to prod
  • Backups: Daily pg_dump with 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 main and develop branches
  • 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_call for deployment workflows

Deploy to Dev (deploy-dev.yml):

  • Trigger: Push to develop branch
  • 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 dev remotely
  • Verifies deployment with health check to https://dev.baby.bretzfam.com/api/v1/health
  • Uses DEV_SSH_KEY, DEV_HOST, and DEPLOY_USER secrets

Deploy to Prod (deploy-prod.yml):

  • Trigger: Push to main branch
  • 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 prod remotely
  • Verifies deployment with health check to https://baby.bretzfam.com/api/v1/health
  • Uses PROD_SSH_KEY, PROD_HOST, and DEPLOY_USER secrets

Deployment Script (infra/deploy.sh):

  • Takes environment argument: dev or prod
  • 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​

FilePurpose
infra/docker-compose.ymlShared base (API + Postgres services)
infra/docker-compose.dev.ymlDev overrides (exposed Postgres port, debug logs)
infra/docker-compose.prod.ymlProd overrides (resource limits, info logs)
infra/docker-compose.local.ymlLocal dev (Postgres only, no API container)
infra/deploy.shDeploy with auto-rollback on health check failure
infra/backup.shDaily pg_dump with 30-day retention
infra/rollback.shRevert to previous deployment
.github/workflows/api-ci.ymlCI workflow (typecheck, lint, test)
.github/workflows/deploy-dev.ymlAuto-deploy to dev on push to develop
.github/workflows/deploy-prod.ymlAuto-deploy to prod on push to main

Key Design Decisions​

DecisionRationale
UUIDs everywherePrevents ID enumeration
All times in UTCClient handles timezone conversion
No soft deletesHard deletes with AuditLog for history
Configurable day boundaryday_start_time (default 07:00) defines when "today" starts
Feeding amounts in mLStored as float, displayed as oz on iOS
Exclusive since filter?since=timestamp returns events after that time (>)
Details array always presentError responses always include details: [], never omit it

See the Specs section for detailed rationale on each feature.