OneSummer
Technical Architecture
A capability-first architecture for the Common App for summer camps. Two-sided marketplace. COPPA-compliant profile vault. Seasonal-elastic infrastructure. Designed to serve 2,000 families in Year 1 and 200,000 by Year 3.
Executive Summary
OneSummer is a two-sided marketplace that eliminates the annual ordeal of summer camp registration. Parents fill out a child's profile once; that profile powers applications to any participating camp. Camps receive structured, consistently-formatted applicant data instead of bespoke PDFs. This document describes the technical system that makes that exchange possible.
The architecture begins as a well-structured monolith — a single SvelteKit application backed by PostgreSQL — organized into clearly-bounded service modules from day one. This approach gives the team a single deployment unit during the critical early traction phase while preserving clear service boundaries that allow extraction into independent services when load or team structure demands it. The SOLID principles, particularly Single Responsibility and the Dependency Inversion Principle, govern module boundaries throughout.
Three technical challenges define the design above all others:
Architecture Overview
The system is a modular monolith deployed in Docker containers. The SvelteKit application serves both the server-side rendered frontend and the REST API. A single PostgreSQL instance is the system of record. An object store handles binary documents. A background job queue processes asynchronous work. Redis provides a session store and short-lived cache layer.
Dashed boxes and lines indicate future capabilities not in Year 1 scope.
Request Lifecycle
Every inbound request travels the same path regardless of surface (parent UI, camp admin UI, or future API consumer).
Every request passes validation and authentication before reaching domain logic. Handlers never query the database directly — they call service functions which use repositories.
Capability Inventory
Before selecting technologies or drawing service boundaries, the system's required capabilities are listed independently of any implementation. This prevents architecture driven by tooling familiarity rather than functional requirements.
Parent-Side Capabilities
Camp-Side Capabilities
Platform-Level Capabilities
Service Definitions
Each service is a bounded module within the SvelteKit application. Services communicate by calling each other's public TypeScript interfaces, never by reaching into each other's database queries. This enforces the Dependency Inversion Principle and makes extraction to independent processes straightforward.
svc/identityResponsibilities: Parent account creation with email verification. Passwordless magic-link login and password-based login. Session token issuance (opaque token stored in Redis, never a JWT with sensitive payload). Role-based access control with roles: parent, camp_admin, platform_admin. Child record creation — parents create child accounts, not children themselves. Verifiable parental consent (VPC) recording per COPPA requirements. Permission scope enforcement: which parent may access which child, which camp admin may access which camp's data.
Key interfaces: createAccount(), verifyEmail(), createSession(), destroySession(), addChildToAccount(), recordConsent(), hasPermission(actorId, resource, action).
Boundaries: This service issues and validates session tokens. All other services call hasPermission() before performing any action. No other service ever bypasses this check by reading the user table directly.
svc/vaultResponsibilities: Store and retrieve structured child profile data across domains: personal info, medical conditions, allergies, medications, dietary restrictions, emergency contacts, behavioral notes, and swim ability. Encrypt sensitive medical fields using AES-256-GCM with per-family derived keys. Manage binary document uploads (immunization records, IEPs) in object storage. Manage access grants: a parent explicitly authorizes a specific camp to read a specific child's data for a specific session. Produce read-only profile projections — only the fields the camp needs — when fulfilling a grant.
Key interfaces: getProfile(childId, viewerScope), updateProfile(childId, patch, parentId), grantAccess(childId, campId, sessionId, fieldScopes), revokeAccess(grantId), uploadDocument(childId, file), getDocumentUrl(docId, grantId).
Critical constraint: No other service reads the child_profiles table directly. All reads go through getProfile() which enforces the caller's grant scope before decrypting and returning data.
svc/campsResponsibilities: CRUD for camp entities and their sessions. Manage session capacity and available spots. Store camp-specific application form schemas (see Form Mapper). Geocode camp addresses via the Maps API and store coordinates for proximity search. Manage camp media (photos, video links). Publish and unpublish listings. Track camp onboarding state.
Key interfaces: getCamp(campId), listCamps(filter), createSession(campId, sessionData), getAvailability(sessionId), getFormSchema(campId), setFormSchema(campId, schema).
svc/applicationsResponsibilities: Manage the full application lifecycle as an explicit state machine: draft → submitted → under_review → accepted | waitlisted | declined → enrolled | withdrawn. Enforce business rules at each transition (e.g., cannot submit without required documents, cannot enroll without payment). Track application history with timestamps and actor attribution. Manage waitlist ordering and automated promotion when spots open. Coordinate with the vault to ensure the camp's access grant is issued when an application is accepted.
Key interfaces: createApplication(childId, sessionId), submitApplication(appId), transition(appId, newState, actorId), getApplicationsForParent(parentId), getApplicationsForCamp(campId, sessionId).
svc/mapperResponsibilities: Maintain the canonical OneSummer field registry — the authoritative set of child profile field keys and their types. Translate a camp's form schema (which uses camp-specific field names) into a mapping against canonical fields. At application time, resolve a pre-filled form payload by: fetching the child's vault projection, applying the field mappings, and returning a form object with filled and missing fields distinguished. Validate that required fields are present and values conform to type constraints. Surface unmapped fields that the parent must manually fill.
Key interfaces: resolveForm(campId, childId, parentGrantId) returns {prefilled: Record, missing: FieldSpec[], unmapped: FieldSpec[]}. validateFormPayload(campId, payload). getFieldRegistry().
Design note: The mapping definition lives in the database (not code). Camp admins can update their mapping without a deploy. This is the Open/Closed Principle applied to data: the engine is closed for modification but open for extension via new mapping records.
svc/notificationsResponsibilities: Accept notification events from other services via an internal event interface (not a message bus in Year 1 — direct function call that enqueues a job). Render email templates for each event type. Respect per-user notification preferences (frequency, channel). Batch low-priority notifications into daily digests during peak season to reduce email fatigue. Store notification history for the in-app notification center. Never send notifications about a child's data to anyone other than the verified parent.
Key interfaces: send(event: NotificationEvent). Event types: application.submitted, application.status_changed, waitlist.promoted, document.requested, session.deadline_approaching.
svc/searchResponsibilities: Power the camp discovery experience. Full-text search on camp names and descriptions using PostgreSQL's pg_trgm extension and tsvector indexes (sufficient for Year 1 scale; extractable to a dedicated search engine at 50K+ listings). Geospatial proximity search using PostGIS. Filter by age range, date overlap, activity type, price band, and availability. Aggregate facet counts for filter UI. Log search queries and click-through for relevance tuning.
Key interfaces: search(query: SearchQuery) returns paginated, ranked CampSummary[]. Reads exclusively from the read replica to avoid contention with write traffic.
API Contracts
All API routes live under /api/v1/. Authentication is via a session token in the Authorization: Bearer <token> header. All responses follow the envelope: {"data": ..., "meta": ..., "error": null} on success and {"data": null, "error": {"code": "...", "message": "..."}} on failure. All timestamps are ISO 8601 UTC.
/api/v2/). Additive changes (new optional fields, new endpoints) are non-breaking and do not require a version bump. The v1 surface will be maintained until all consumers have migrated. Deprecation notices are communicated via the Deprecation response header.Auth Endpoints
POST /api/v1/auth/register
Creates a parent account and sends a verification email. Does not issue a session until email is verified.
// Request
{
"email": "alex@example.com",
"password": "minimum-12-chars",
"name": "Alex Rivera",
"timezone": "America/New_York"
}
// 201 Created
{
"data": {
"parentId": "par_01J8K...",
"emailVerification": "pending"
}
}
// Errors
// 409 CONFLICT — email already registered
// 422 VALIDATION — invalid email or weak password
POST /api/v1/auth/children
Creates a child record under the authenticated parent. Records verifiable parental consent at this moment.
// Request — requires authenticated parent session
{
"firstName": "Sam",
"lastName": "Rivera",
"dateOfBirth": "2015-07-12",
"consentAcknowledged": true // must be true; COPPA consent recorded server-side
}
// 201 Created
{
"data": {
"childId": "chd_01J8L...",
"firstName": "Sam",
"age": 10,
"consentLogId": "clog_01J8M..."
}
}
// Errors
// 400 BAD_REQUEST — consentAcknowledged !== true
// 422 VALIDATION — invalid date of birth
Vault Endpoints
PUT /api/v1/children/{childId}/profile
Upserts a child's profile fields. Parent must own the child. Fields are validated against the canonical field registry before storage.
// Request — partial update accepted (PATCH semantics via PUT)
{
"medical": {
"allergies": ["peanuts", "tree nuts"],
"epiPenRequired": true,
"conditions": ["asthma"],
"medications": [{"name": "Albuterol", "dose": "90mcg PRN"}],
"physicianName": "Dr. Chen",
"physicianPhone": "+12125551234"
},
"emergency": {
"contacts": [
{"name": "Alex Rivera", "relationship": "parent", "phone": "+19175551234", "priority": 1}
]
},
"swimAbility": "intermediate"
}
// 200 OK
{
"data": {
"childId": "chd_01J8L...",
"updatedAt": "2026-01-15T14:30:00Z",
"completeness": 0.72 // fraction of canonical fields populated
}
}
// Errors
// 403 FORBIDDEN — authenticated parent does not own childId
// 422 VALIDATION — field type mismatch against canonical registry
POST /api/v1/children/{childId}/grants
Grants a camp explicit read access to a child's data for a specific session. This must happen before the camp can view applicant data or before the form mapper can fetch vault data for auto-fill.
// Request
{
"campId": "cmp_01J9A...",
"sessionId": "ses_01J9B...",
"scopes": [
"medical.allergies", "medical.epiPenRequired",
"emergency.contacts", "swimAbility", "documents.immunization"
]
}
// 201 Created
{
"data": {
"grantId": "grnt_01J9C...",
"expiresAt": "2026-09-01T00:00:00Z", // auto-expires after session end
"scopes": ["medical.allergies", "..."]
}
}
Application Endpoints
POST /api/v1/applications
Initiates an application. Triggers the form mapper to resolve pre-filled data and returns the form payload with filled and missing fields identified.
// Request
{
"childId": "chd_01J8L...",
"sessionId": "ses_01J9B..."
}
// 201 Created
{
"data": {
"applicationId": "app_01JAA...",
"status": "draft",
"form": {
"prefilled": {
"camperFirstName": "Sam",
"allergyList": "Peanuts, tree nuts",
"emergencyContact1": "Alex Rivera — +19175551234"
},
"missing": [
{"field": "tShirtSize", "required": false},
{"field": "cabinPreference", "required": false}
],
"unmapped": [
{"field": "specialOlympicsId", "required": false}
]
},
"grantId": "grnt_01J9C..." // auto-created grant for this session
}
}
// Errors
// 409 CONFLICT — active application already exists for this child + session
// 410 GONE — session is full; offer waitlist option
POST /api/v1/applications/{applicationId}/submit
// Request — final field values (merged prefilled + parent overrides)
{
"fields": {
"camperFirstName": "Sam",
"allergyList": "Peanuts, tree nuts",
"tShirtSize": "YM"
}
}
// 200 OK
{
"data": {
"applicationId": "app_01JAA...",
"status": "submitted",
"submittedAt": "2026-02-14T10:05:33Z"
}
}
// Errors
// 422 VALIDATION — required camp fields missing
// 409 CONFLICT — application not in draft state
Data Schema
All tables live in a single PostgreSQL 16 database. Row-Level Security (RLS) policies enforce access at the database level as a defense-in-depth measure. Migrations are managed with a versioned migration tool. The schema uses ULIDs as primary keys — lexicographically sortable, globally unique, URL-safe.
[encrypted] store AES-256-GCM ciphertext. The application decrypts them using a per-family key derived from a master key stored in a secrets manager (never in the database). The database stores only ciphertext — a database dump alone is insufficient to expose PII. Key rotation is a planned Year 2 capability.Identity Tables
Vault Tables
Camp Tables
Application Tables
Auth Model & COPPA
The authentication model is structured entirely around the COPPA constraint: children under 13 may never directly access the system. All data flows are mediated by a verified parent. This is not merely a UI limitation — it is enforced at the API, service, and database layers.
Actor Hierarchy
Session Mechanism
Sessions use opaque tokens — a cryptographically random 32-byte value stored in Redis with a 7-day TTL (sliding). The token is sent as an HttpOnly; Secure; SameSite=Strict cookie. The payload stored in Redis contains: parent ID, role, a list of child IDs the parent owns, and the session's IP and user agent for anomaly detection. There are no JWTs in the session path — this eliminates the risk of signed-but-expired-token attacks and allows instant revocation by deleting the Redis key.
Permission Model
| Resource | Parent (own children) | Camp Admin (own camp) | Platform Admin |
|---|---|---|---|
children.* | Full CRUD | None | Read |
child_profiles.* | Full CRUD | Read (if granted) | None |
vault_access_grants | Create + Revoke | Read own grants | Read |
applications | Create + Submit | Review + Transition | Read |
camps.* | Read (public) | Full CRUD | Full CRUD |
camp_form_schemas | None | Full CRUD | Full CRUD |
coppa_consent_log | Read own records | None | Read |
Security Architecture
Security for OneSummer is not an afterthought — it is structurally embedded. The primary threat model is the exposure of children's medical and identity data to unauthorized parties. Every architectural decision is evaluated against this threat.
Threat Model Summary
Mitigation: Field-level encryption of all PII. AES-256-GCM per-family derived keys. Keys never stored in the database. Encrypted database backups with separate key management.
Mitigation: All vault reads enforced through the service layer. Database RLS policies as a secondary layer. Access grants are scoped, time-limited, and audited.
Mitigation: bcrypt cost 12 for passwords. Magic-link login as default (no password stored). HttpOnly session cookies. Re-authentication required for sensitive actions (adding a child, issuing a grant). Anomaly detection on session IP/UA.
Mitigation: Parameterized queries only (no raw SQL in application code). Input validation via Zod schemas at API boundary. Camp form schemas are data, never executed. Document uploads validated for MIME type and scanned before storage.
Mitigation: Platform admins cannot access encrypted vault data (no vault decryption keys in admin scope). All admin actions are logged. Principle of least privilege enforced at the role level.
Mitigation: External integrations receive only the minimum data payload for the specific transaction. No bulk export APIs. All integration data flows through the grant/scope system.
Security Controls by Layer
| Layer | Control | Implementation |
|---|---|---|
| Network | TLS 1.3 everywhere | Reverse proxy terminates TLS; internal service communication over private network only |
| Network | Rate limiting | Per-IP and per-account limits in Redis; auth endpoints throttled to 5 req/min |
| Application | Input validation | Zod schemas at every API entry point; reject on first validation failure |
| Application | CSRF protection | SameSite=Strict cookies + SvelteKit CSRF protection for all mutation routes |
| Application | Content security | Strict CSP headers; no inline scripts; no eval; external resources allowlisted |
| Application | Secret management | Environment variables injected at runtime; no secrets in source control; rotation capability |
| Data | Field encryption | AES-256-GCM; per-family derived keys; IV stored alongside ciphertext |
| Data | Row-Level Security | PostgreSQL RLS policies enforce parent-owns-child at the database level |
| Data | Audit logging | Immutable application_transitions and coppa_consent_log — no UPDATE/DELETE permitted via triggers |
| Infrastructure | Least privilege | Application DB user has no DDL rights; separate migration user; no superuser in production |
| Infrastructure | Dependency scanning | Automated CVE scanning in CI pipeline; dependabot alerts |
Technology Stack Rationale
Each technology choice below is justified against project requirements and contrasted with at least one viable alternative. The guiding question for every choice: does this reduce complexity today without creating irreversible decisions tomorrow?
| Layer | Choice | Justification | Trade-off vs. Alternative |
|---|---|---|---|
| Frontend | SvelteKit v2, Node adapter |
Co-locates server-side data fetching (+page.server.ts) with UI, eliminating a separate API-for-frontend layer. Svelte's compiled output is significantly smaller than React, reducing Time to Interactive — important for mobile parents on slow connections during camp fair season. The file-based routing maps cleanly to the two-sided UX (parent routes vs. camp admin routes). |
vs. Next.js: Next.js has a larger ecosystem, but React's runtime overhead and React Server Components complexity is unnecessary for this application's size. Next.js lock-in to Vercel's edge infrastructure conflicts with the Docker-first deployment model. SvelteKit can be self-hosted without platform-specific primitives. |
| Database | PostgreSQL 16 + PostGIS + pgcrypto |
PostgreSQL's native pg_trgm and tsvector provide sufficient full-text search for Year 1 (sub-10K camp listings). PostGIS provides production-grade geospatial proximity search without an additional service. Row-Level Security is a built-in mechanism for the COPPA grant enforcement model. pgcrypto enables field-level encryption within the database. JSONB allows the flexible form schema and profile fields without a schema migration for each new field type. |
vs. MongoDB: MongoDB's document model could simplify the flexible profile fields, but its lack of multi-document transactions and weaker RLS story makes the COPPA consent model harder to enforce reliably. PostgreSQL's ACID guarantees are essential for application state machine transitions that must be atomic with audit log writes. vs. PlanetScale/Neon: Serverless Postgres is attractive for seasonal scale, but adds latency and complexity during the early phase when a single managed Postgres instance is sufficient and cheaper. |
| Container | Docker + Compose Swarm / K8s-ready |
Docker Compose in development ensures parity between local and production environments. In production, a single Compose file (or lightweight Swarm configuration) provides horizontal scaling of the SvelteKit app replicas without the Kubernetes operational overhead inappropriate for a seed-stage team. The compose setup is directly exportable to Kubernetes YAML when the team and load justify it. | vs. Kubernetes (Year 1): K8s provides superior auto-scaling and self-healing, but requires significant operational investment. At 2K families and one engineering team, the operational cost exceeds the benefit. Compose-based deployment on a managed VM or PaaS (Railway, Render, Fly.io) is the right Year 1 choice. vs. serverless functions: Serverless would handle seasonal elasticity natively but introduces cold starts (bad UX during camp-fair traffic spikes), connection pool limitations with Postgres, and a fundamentally different mental model for the team. |
| Cache / Queue | Redis 7 session + BullMQ jobs |
Redis serves three purposes: session store (fast lookup, TTL-based expiry), rate limiting (token bucket via Redis counters), and job queue (BullMQ for background email dispatch, PDF generation, and camp sync jobs). Using Redis for all three avoids introducing a dedicated message broker (RabbitMQ, SQS) before the volume justifies it. | vs. PostgreSQL for queues: Polling a Postgres jobs table is a viable simple alternative (SKIP LOCKED) and eliminates one dependency. The trade-off is that high-frequency polling adds write load during peak season. Redis is introduced primarily because the session store requires it anyway — the queue is a secondary benefit. vs. SQS/SNS: Cloud-native message queues would be more durable but introduce AWS lock-in and additional operational surface for a team not yet at that scale. |
| Language | TypeScript 5 strict mode |
TypeScript's type system enforces the contract between the form mapper's canonical field registry, the vault's profile types, and the API response shapes at compile time rather than at runtime. Given that the form mapper must correctly translate typed fields, runtime type errors would be particularly damaging. Strict mode with noUncheckedIndexedAccess eliminates a class of null dereference bugs in the mapping engine. |
vs. Go: Go's performance characteristics are superior for high-concurrency API servers, but the team productivity cost of splitting frontend (TypeScript) and backend (Go) languages at seed stage is not justified. SvelteKit's isomorphic TypeScript allows the same type definitions to be used in the form rendering and the server-side mapper. This is a pragmatic choice that should be revisited at the Y3 scale target. |
| Search | pg_trgm + tsvector + PostGIS |
In-database full-text search is sufficient and operationally simpler for the Year 1 catalog size. pg_trgm trigram indexes provide fuzzy matching (handling typos in camp names). PostGIS handles all geospatial queries. No additional service to operate or pay for. |
vs. Algolia / Typesense (Year 2+): At 10K+ listings or when relevance tuning, faceted navigation, and instant search become table stakes, a dedicated search engine should replace this layer. The svc/search interface isolates this behind an abstraction, making the swap non-breaking. The data model (keeping a search_vector column) is forward-compatible with an external indexing pipeline. |
| ORM / Queries | Drizzle ORM + raw SQL for complex queries |
Drizzle provides TypeScript-first schema definitions that serve as the source of truth for both migrations and type inference. Unlike Prisma, Drizzle does not use a runtime query engine — it compiles to raw SQL, giving predictable performance and the ability to inspect every query. Complex queries (search with PostGIS, RLS-aware aggregations) are written in raw SQL within repository functions rather than fighting an ORM abstraction. | vs. Prisma: Prisma's developer experience is excellent, but its engine-based runtime (especially the Rust query engine) adds memory overhead and has historically caused issues with connection pooling in containerized environments. Drizzle's zero-runtime approach is preferable in Docker-based deployments. |
| Validation | Zod | Zod schemas at the API boundary provide runtime validation with TypeScript type inference. The same Zod schema is reused for form validation on the client and request validation on the server, eliminating schema drift. The form mapper's canonical field registry is a Zod schema, meaning type mismatches between vault data and camp form fields are caught at schema parse time. | vs. Yup / class-validator: Yup is the primary alternative; Zod's TypeScript inference is tighter and its error messages are more actionable. Class-validator requires decorator metadata and couples validation to class definitions, which does not fit the functional patterns used throughout the service layer. |
Scalability Strategy
OneSummer has an unusual traffic profile: approximately 80% of all usage occurs during a 14-week window from February through May. The architecture must handle this efficiently without paying for Year 3 infrastructure during Year 1, and without requiring a re-architecture to handle the Year 3 load.
The Seasonal Load Model
Scaling Approach by Phase
| Phase | Families | App Instances | DB Configuration | Key Action |
|---|---|---|---|---|
| Year 1 | 2,000 | 1–2 replicas | Single Postgres (4 vCPU, 16 GB) | Docker Compose on a single managed VM. Redis on the same host. Monitor for bottlenecks. |
| Year 2 | 20,000 | 2–6 replicas | Primary + 1 read replica. PgBouncer for connection pooling. | Externalize Redis to managed service. Route search/read queries to replica. Add CDN for camp media. Consider managed Postgres. |
| Year 3 | 200,000 | 6–20 replicas | Primary + 2 read replicas. Logical replication to search database. | Migrate to Kubernetes with HPA (Horizontal Pod Autoscaler) tied to request queue depth. Extract search to Typesense or Elasticsearch. Consider extracting the Vault Service as an independent microservice for independent scaling and compliance surface isolation. |
Seasonal Scaling Automation
A scheduler (cron job in Year 1, a cloud scheduler in Year 2+) scales the application replica count on a calendar schedule aligned with the registration season: scale up to peak configuration by February 1st, scale down to minimum by June 15th. This is deterministic and predictable — the traffic pattern is known a year in advance, making reactive auto-scaling a secondary mechanism rather than the primary one.
Observability
A system that cannot be debugged in production is not a finished system. Observability is designed in from day one, not bolted on when something goes wrong.
Three Pillars
requestId, parentId (redacted in error logs), route, statusCode, durationMs, service. Errors include a stack trace and the error code. Logs are never printed with console.log — a central logger wrapper enforces structure. Sensitive fields (tokens, passwords, encrypted blobs) are stripped by the logger before output./metrics. Key metrics: request rate by route, p50/p95/p99 latency, error rate by status code, active session count, application submissions per minute (seasonal KPI), vault read latency, Redis hit rate, Postgres query duration histogram, job queue depth and lag.Key Alerts
| Alert | Condition | Severity | Why It Matters |
|---|---|---|---|
| High error rate | 5xx rate > 1% over 5 minutes | P1 | Indicates a broken deploy or downstream failure |
| Vault latency | p99 vault read > 500ms | P2 | Auto-fill feels broken to parent during application |
| Job queue lag | Pending jobs > 500 or lag > 5 min | P2 | Email notifications significantly delayed |
| DB connection pool | Pool utilization > 80% | P2 | Leading indicator of connection exhaustion |
| Application submit failures | Submit 4xx rate > 5% during peak hours | P1 | Parents blocked from applying — core value prop failure |
| Expired grants still served | Any vault read on an expired grant | P0 | Data access control violation |
Deployment & CI/CD
The CI pipeline runs on every pull request: TypeScript type check, ESLint with security rules, unit tests (Vitest), integration tests against a test Postgres instance (Docker), and a Zod schema validation check to ensure no API response shape regressions. No PR merges with a failing check.
Deployments use a blue-green strategy: the new container image is deployed alongside the existing one. The load balancer shifts traffic after the health check passes. Database migrations run before the new container receives traffic. Migrations are backward-compatible by policy (additive changes only in a single migration; destructive changes in a separate migration after the old code is no longer in production).
Architecture Decision Records
The following ADRs document the key decisions made in this architecture, the context that drove them, and the trade-offs accepted. New significant technical decisions should be added here before implementation begins.
OneSummer is a two-sided marketplace that must achieve simultaneous supply (camps) and demand (parents) to have value. The Year 1 objective is proving product-market fit, not demonstrating distributed systems expertise.
- Microservices from day one — too much operational overhead
- Classic Rails-style layered monolith — no service boundaries, harder to extract later
svc/search abstraction boundary is designed to make this swap non-disruptive.- MongoDB — weaker ACID guarantees, no RLS, no PostGIS
- Separate Elasticsearch for search — premature operational complexity
- Transparent Data Encryption only — insufficient for the threat model
- Vault-as-a-service (HashiCorp Vault) — adds operational complexity before justified
- Per-camp code adapters — gated by developer time, doesn't scale
- LLM-based field mapping — non-deterministic, cannot be unit-tested
coppa_consent_log with IP, user agent, and a hash of the consent text displayed. The right to erasure (FTC's COPPA rule) is implemented as a soft delete followed by a background hard-delete job that purges encrypted profile data and documents after 30 days, retaining only the anonymized consent log for regulatory compliance.- Age-gating with self-reported DOB — not legally sufficient under COPPA
- Teen-accessible profiles (13+) — out of scope for Year 1, requires separate legal review
- Purely reactive autoscaling — risk of cold start / warm-up lag during viral traffic spikes
- Always-on peak configuration — 4–6x higher annual infrastructure cost
tsvector and pg_trgm indexes, with PostGIS for geospatial queries. The search service interface abstracts the implementation so the underlying engine can be replaced without changing callers.svc/search abstraction boundary makes this a contained change.- Algolia from day one — cost and operational overhead not justified at Year 1 catalog size
- Elasticsearch — significant operational overhead for early stage
- Database BLOB storage — poor performance, complicates backup strategy
- Filesystem on the application server — not compatible with horizontal scaling
- Build payment processing in-house — PCI scope and liability not appropriate
CampManagementAdapter: importSessions(campId), exportEnrollment(applicationId), syncRoster(campId, sessionId). Each integration implements this interface. The Camp Registry service calls the adapter — it does not know which CMS it is talking to.