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.