Technical Architecture v1.0

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.

Version
1.0 — April 2026
Runtime
SvelteKit + Node
Data Store
PostgreSQL 16
Containers
Docker / Compose
Compliance
COPPA · SOC 2 path
Scale Target
200K families Yr 3
Section 01

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:

Profile Vault
Sensitive data, selective sharing
Children's medical and emergency data is encrypted at rest with per-family keys. Parents explicitly grant per-camp, per-session access. The vault is the trust anchor of the entire product.
Form Mapper
One profile, any camp's form
A declarative mapping engine transforms normalized child profile fields into each camp's idiosyncratic form structure. Camps define their schema; the mapper does the translation without code changes.
Seasonal Scale
80% load in 4 months
Registration peaks February through May. The system uses container-based horizontal scaling tied to a scheduler, keeping infrastructure costs near-zero during the June–September off-season.
i COPPA is a first-class constraint, not a checkbox
Children under 13 never create accounts, never authenticate, and never directly access any system resource. All consent and data access flows through a verified parent or legal guardian. This constraint propagates throughout the authentication model, data schema, and API surface.
Section 02

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.

CLIENT LAYER APPLICATION LAYER DOMAIN SERVICES DATA LAYER EXTERNAL SERVICES Parent Browser SvelteKit SSR + SPA React-free, 0 hydration cost Camp Admin UI SvelteKit SSR + SPA Same app, different routes Mobile App Future: native wrapper Same API surface CDN / Edge Cache Static assets, SSR cache Camp photos, documents SvelteKit Application SSR pages · REST API routes · Auth middleware +page.server.ts · +server.ts · hooks.server.ts Rate limiter · Request validation · Response shaping Reverse Proxy TLS termination Load balancing (N replicas) Health checks · HTTP/2 Async Job Worker Email dispatch · PDF generation Camp data sync · Webhooks Idempotent · Retryable Identity Auth · Sessions COPPA consent RBAC / permissions svc/identity Profile Vault Child data AES-256 Doc storage Access grants svc/vault Camp Registry Listings · Sessions Form schemas Availability svc/camps Application Workflow · State Review pipeline Waitlist mgmt svc/applications Form Mapper Field translation Schema registry Validation rules svc/mapper Notifications Email · In-app Digest rules Preference store svc/notifications PostgreSQL 16 Primary — all writes RLS · pgcrypto · pg_trgm PG Read Replica Search · reports Streaming replication Redis Sessions · Rate limits Job queue · Cache Object Store Documents · Photos S3-compatible, encrypted Email Provider Payment Gateway Google Maps API CampMinder (future) CampDoc (future)

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).

TLS Proxy Rate Limiter Auth / Session Input Validate Svc Handler Repo / Query Shape & Return

Every request passes validation and authentication before reaching domain logic. Handlers never query the database directly — they call service functions which use repositories.

Section 03

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

Durable identity
Create, verify, and persist a parent account. Recover access via email. Tie children to parent accounts with relationship records. All authentication flows through the parent — children have no credentials.
Profile vault
Securely store a child's personal, medical, dietary, emergency contact, and behavioral data. Encrypt fields individually. Allow the parent to update records and version-track changes. Selectively share a read-only projection with specific camps for specific sessions.
Document management
Upload, version, and label binary files (immunization records, IEPs, photo releases). Associate documents with a child. Share individual documents as part of a camp access grant or include them in application packages.
Camp discovery
Search and filter camp listings by location, date range, age range, activity type, price, and special program (financial aid, inclusion support, etc.). Display results on a map. Rank results by relevance and user preference signals.
Summer planning
Maintain a visual calendar of tentative and confirmed sessions for each child. Detect scheduling conflicts. Track aggregate summer costs. Allow plan sharing with co-parents.
One-click application
Pre-fill any camp's application form using stored profile data via the form mapping engine. Display unmapped or required fields that still need input. Track application status across all submitted applications in one dashboard.
Payment and enrollment
Collect registration deposits and full tuition via a PCI-compliant payment abstraction. Issue receipts. Handle refunds per camp policy. Track payment status per enrollment.

Camp-Side Capabilities

Listing management
Create and maintain a camp profile: name, description, photos, location, age ranges, accreditation, and policies. Define sessions with capacity, dates, pricing, and available spots.
Form schema definition
Define the camp's required application fields using a structured schema (JSON). Map those fields to canonical OneSummer profile fields so the form mapper can auto-fill known data. Mark fields as required, optional, or conditional.
Application review
View incoming applications. Accept, decline, or waitlist applicants. Add internal notes. Send status notifications to parents. Track review pipeline state per session.
Roster management
Export accepted applicant data for operational use. View enrolled campers with emergency contact and medical summaries. Receive data in the format the camp's existing systems expect (CSV, PDF, or API webhook — future).

Platform-Level Capabilities

COPPA consent logging
Record the timestamp, IP, and user agent of verifiable parental consent for every child account creation and every data-sharing grant. Immutable audit log. Support right-to-erasure requests.
Notification orchestration
Send transactional emails and in-app notifications for application status changes, deadlines, waitlist movements, and document requests. Respect per-user notification preferences. Support digest batching during peak season.
Observability
Structured request logs, distributed traces, and application metrics. Error aggregation with source context. Uptime and latency alerting. Seasonal traffic dashboards for infrastructure planning.
Section 04

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.

Identity Service svc/identity
Authentication, authorization, session management, COPPA consent

Responsibilities: 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.

Profile Vault Service svc/vault
Encrypted child profiles, document storage, per-camp access grants

Responsibilities: 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.

Camp Registry Service svc/camps
Camp listings, sessions, form schemas, geolocation

Responsibilities: 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).

Application Engine svc/applications
Application lifecycle, state machine, review workflow, waitlist

Responsibilities: 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).

Form Mapper Service svc/mapper
Declarative field translation, schema registry, validation engine

Responsibilities: 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.

Notifications Service svc/notifications
Email, in-app, digest, preference management

Responsibilities: 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.

Section 05

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 Versioning Policy
Breaking changes require a new version prefix (/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
Section 06

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.

! Sensitive Field Encryption
Columns marked [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

parents identity
idulidPRIMARY KEY — format: par_01...
emailcitextUNIQUE NOT NULL — case-insensitive index
password_hashtextbcrypt cost 12 — nullable if passwordless
nametextNOT NULL
timezonetextIANA zone name — default 'UTC'
email_verified_attimestamptzNULL until verified
notification_prefsjsonbchannel/frequency preferences
created_attimestamptzNOT NULL DEFAULT now()
deleted_attimestamptzsoft delete — COPPA erasure
children identity · COPPA-controlled
idulidPRIMARY KEY — format: chd_01...
parent_idulidFK → parents.id NOT NULL
first_nametextNOT NULL [encrypted]
last_nametextNOT NULL [encrypted]
date_of_birthdateNOT NULL [encrypted]
gender_identitytextnullable [encrypted]
consent_log_idulidFK → coppa_consent_log.id NOT NULL
created_attimestamptzNOT NULL DEFAULT now()
deleted_attimestamptzright-to-erasure
coppa_consent_log compliance · immutable
idulidPRIMARY KEY
parent_idulidFK → parents.id NOT NULL
event_typetext'child_account_created' | 'data_share_granted' | 'account_deleted'
ip_addressinetNOT NULL
user_agenttextNOT NULL
consent_text_hashtextSHA-256 of the consent text shown — immutable
recorded_attimestamptzNOT NULL DEFAULT now() — NO UPDATE trigger

Vault Tables

child_profiles vault · encrypted fields
idulidPRIMARY KEY
child_idulidFK → children.id UNIQUE NOT NULL
medical_databytea[encrypted jsonb] — allergies, meds, conditions
emergency_contactsbytea[encrypted jsonb array]
dietary_databytea[encrypted jsonb]
behavioral_notesbytea[encrypted text] — IEP summary, accommodations
swim_abilitytext'non-swimmer'|'beginner'|'intermediate'|'advanced'
photo_urltextobject store path — nullable
completeness_pctnumeric(4,3)computed on write — 0.000 to 1.000
updated_attimestamptzNOT NULL
versionintegeroptimistic locking — increment on each write
vault_access_grants vault · access control
idulidPRIMARY KEY — format: grnt_01...
child_idulidFK → children.id NOT NULL
camp_idulidFK → camps.id NOT NULL
session_idulidFK → sessions.id NOT NULL
granted_byulidFK → parents.id — who issued the grant
scopestext[]allowed field paths e.g. 'medical.allergies'
granted_attimestamptzNOT NULL DEFAULT now()
expires_attimestamptzNOT NULL — auto-set to session end date
revoked_attimestamptznullable — parent revocation
INDEX on (child_id, camp_id, session_id) · INDEX on expires_at for cleanup jobs
child_documents vault · object store reference
idulidPRIMARY KEY
child_idulidFK → children.id NOT NULL
document_typetext'immunization'|'iep'|'photo_release'|'other'
labeltextparent-provided description
storage_keytextpath in object store — server-side encrypted at rest
mime_typetextvalidated on upload
size_bytesbigintvalidated ≤ 25MB
uploaded_attimestamptzNOT NULL DEFAULT now()
deleted_attimestamptzsoft delete before purge job

Camp Tables

camps registry
idulidPRIMARY KEY — format: cmp_01...
nametextNOT NULL — indexed with tsvector
slugtextUNIQUE — URL-safe name
descriptiontextindexed with tsvector
addressjsonbstructured: street, city, state, zip
locationgeography(Point)PostGIS — GIST index for proximity
age_minsmallintinclusive
age_maxsmallintinclusive
tagstext[]activity categories — GIN index
statustext'draft'|'active'|'suspended'
onboarding_statetext'invited'|'profile_complete'|'schema_set'|'live'
search_vectortsvectorGIN index — maintained by trigger
created_attimestamptzNOT NULL DEFAULT now()
sessions registry · capacity-tracked
idulidPRIMARY KEY — format: ses_01...
camp_idulidFK → camps.id NOT NULL
nametext'Week 1 — Ages 8-10', etc.
start_datedateNOT NULL
end_datedateNOT NULL
capacitysmallinttotal spots
enrolled_countsmallintmaintained by trigger — avoids COUNT(*) queries
price_centsintegerin USD cents — never floats
deposit_centsintegerdue at application submission
application_deadlinedatenullable
statustext'open'|'full'|'waitlist_only'|'closed'
camp_form_schemas mapper · versioned
idulidPRIMARY KEY
camp_idulidFK → camps.id NOT NULL
versionintegermonotonically increasing
schemajsonbarray of FieldDef: {campKey, canonicalKey?, type, required, label}
is_activebooleanonly one active per camp_id — partial unique index
created_attimestamptzNOT NULL DEFAULT now()

Application Tables

applications state machine
idulidPRIMARY KEY — format: app_01...
child_idulidFK → children.id NOT NULL
session_idulidFK → sessions.id NOT NULL
grant_idulidFK → vault_access_grants.id NOT NULL
statustext'draft'|'submitted'|'under_review'|'accepted'|'waitlisted'|'declined'|'enrolled'|'withdrawn'
form_snapshotjsonbsubmitted field values — immutable after submit
waitlist_positionintegernullable — set when status = 'waitlisted'
submitted_attimestamptznullable
reviewed_attimestamptznullable
created_attimestamptzNOT NULL DEFAULT now()
UNIQUE INDEX on (child_id, session_id) WHERE status NOT IN ('withdrawn','declined') — prevents duplicate applications
application_transitions audit log · append-only
idulidPRIMARY KEY
application_idulidFK → applications.id NOT NULL
from_statustextNOT NULL
to_statustextNOT NULL
actor_idulidparent or camp admin who triggered the transition
actor_roletext'parent'|'camp_admin'|'system'
notetextoptional reviewer note — [encrypted] for camp internal notes
occurred_attimestamptzNOT NULL DEFAULT now() — NO UPDATE trigger
Section 07

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

Parent Account Authenticated · Verified email Role: parent creates & manages Child Record No credentials · No session · No API access Data accessed only via parent session grants access Camp Admin Authenticated · Camp-scoped Role: camp_admin Platform Admin Internal — full access Role: platform_admin · MFA required Every child record creation, data share, and account deletion writes an immutable row to coppa_consent_log Timestamp · IP · User-agent · Consent text hash — used for regulatory audits and right-to-erasure compliance

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 CRUDNoneRead
child_profiles.*Full CRUDRead (if granted)None
vault_access_grantsCreate + RevokeRead own grantsRead
applicationsCreate + SubmitReview + TransitionRead
camps.*Read (public)Full CRUDFull CRUD
camp_form_schemasNoneFull CRUDFull CRUD
coppa_consent_logRead own recordsNoneRead
Section 08

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

T1 — Data Exfiltration
Vector: Database compromise (breach, SQL injection, misconfigured backup).
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.
T2 — Unauthorized Child Data Access
Vector: A camp or attacker accessing a child's medical data without a valid grant.
Mitigation: All vault reads enforced through the service layer. Database RLS policies as a secondary layer. Access grants are scoped, time-limited, and audited.
T3 — Account Takeover
Vector: Credential stuffing, phishing, or session hijacking against a parent account.
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.
T4 — Injection / Input Attacks
Vector: SQL injection, XSS via camp-defined form schemas, path traversal in document uploads.
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.
T5 — Insider Threat
Vector: Platform admin accessing child medical records without authorization.
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.
T6 — Third-Party Integration Risk
Vector: Future CampMinder/CampDoc integrations receiving unscoped data payloads.
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

LayerControlImplementation
NetworkTLS 1.3 everywhereReverse proxy terminates TLS; internal service communication over private network only
NetworkRate limitingPer-IP and per-account limits in Redis; auth endpoints throttled to 5 req/min
ApplicationInput validationZod schemas at every API entry point; reject on first validation failure
ApplicationCSRF protectionSameSite=Strict cookies + SvelteKit CSRF protection for all mutation routes
ApplicationContent securityStrict CSP headers; no inline scripts; no eval; external resources allowlisted
ApplicationSecret managementEnvironment variables injected at runtime; no secrets in source control; rotation capability
DataField encryptionAES-256-GCM; per-family derived keys; IV stored alongside ciphertext
DataRow-Level SecurityPostgreSQL RLS policies enforce parent-owns-child at the database level
DataAudit loggingImmutable application_transitions and coppa_consent_log — no UPDATE/DELETE permitted via triggers
InfrastructureLeast privilegeApplication DB user has no DDL rights; separate migration user; no superuser in production
InfrastructureDependency scanningAutomated CVE scanning in CI pipeline; dependabot alerts
Section 09

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.
Section 10

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

Traffic Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec Peak Season — 80% of annual load Near-zero — scale to minimum Early interest

Scaling Approach by Phase

PhaseFamiliesApp InstancesDB ConfigurationKey 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.

i Connection Pool Discipline
The most common scaling failure mode for Node.js applications against Postgres is connection exhaustion. Each SvelteKit replica maintains a pool of at most 10 connections. With 20 replicas at Year 3 peak, that is 200 connections against a Postgres instance that handles 400 max — well within budget. PgBouncer in transaction pooling mode will be introduced when replica count exceeds 10 to keep this ratio safe.
Section 11

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

Structured Logs
Every request produces a single structured JSON log line with: 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
Prometheus-compatible metrics exposed at /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.
Traces
OpenTelemetry instrumentation throughout the service layer. Every request generates a trace with spans for: auth middleware, service function calls, database queries (parameterized — no PII in span attributes), cache lookups, and outbound HTTP calls. Traces connect a parent's "apply" click to the exact Postgres query that was slow.

Key Alerts

AlertConditionSeverityWhy It Matters
High error rate5xx rate > 1% over 5 minutesP1Indicates a broken deploy or downstream failure
Vault latencyp99 vault read > 500msP2Auto-fill feels broken to parent during application
Job queue lagPending jobs > 500 or lag > 5 minP2Email notifications significantly delayed
DB connection poolPool utilization > 80%P2Leading indicator of connection exhaustion
Application submit failuresSubmit 4xx rate > 5% during peak hoursP1Parents blocked from applying — core value prop failure
Expired grants still servedAny vault read on an expired grantP0Data 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).

+ Camp Fair Incident Protocol
The week before and after major camp fair events (typically mid-February and mid-March) is designated a freeze period: no deploys, no dependency updates, no schema migrations. The on-call rotation is staffed. A runbook for the five most likely failure modes (connection pool exhaustion, Redis eviction under memory pressure, slow vault reads, job queue backup, payment gateway timeout) lives in the team wiki and is reviewed at the start of each registration season.
Section 12

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.

ADR-001 Start as a modular monolith, not microservices Accepted

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.

Build a single SvelteKit application with domain services as bounded TypeScript modules. No inter-service network calls in Year 1. Services communicate via direct function call within the same process.
Microservices multiply operational complexity (service discovery, network partitions, distributed tracing, independent deploy pipelines) before the team or codebase justifies it. A modular monolith delivers the architectural discipline without the operational cost.
Service boundaries enforced by code review and linting rules (no cross-domain DB imports), not runtime. Vault Service is the first candidate for extraction at Year 2–3, as its security surface and scaling profile will diverge from the rest.
  • Microservices from day one — too much operational overhead
  • Classic Rails-style layered monolith — no service boundaries, harder to extract later
ADR-002 PostgreSQL as the single source of truth for all primary data Accepted
One PostgreSQL primary instance stores all application data. PostGIS and pg_trgm extensions handle geospatial and full-text search within the same database. No polyglot persistence in Year 1.
PostgreSQL's feature set (JSONB, PostGIS, RLS, tsvector, pgcrypto) is sufficient for all Year 1 and Year 2 requirements. Operating one database is dramatically simpler than operating two. The COPPA consent audit model benefits from ACID transactions that span the consent log and the child record creation atomically.
At Year 3 scale (200K families), the search workload will likely require extraction to a dedicated search engine. The 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
ADR-003 Field-level encryption with per-family derived keys for the Profile Vault Accepted
Sensitive child profile fields (PII, medical data, emergency contacts) are encrypted with AES-256-GCM at the application layer before storage. Encryption keys are derived per family using a master key stored in a secrets manager. The database stores only ciphertext.
Database-level encryption (TDE) protects against stolen disk media but not against a compromised database credential. Application-layer encryption with separately-managed keys ensures that a database credential alone is insufficient to read child medical data. Per-family keys mean a compromised key exposes one family's data, not the entire dataset.
Encrypted fields cannot be searched, sorted, or filtered in SQL. All queries that filter on child profile data must operate on non-encrypted metadata (age range, swim ability level) or fetch the child ID via a permitted path and then decrypt the profile. This is an acceptable constraint given the access pattern (always lookup-by-ID, never search-by-medical-condition).
  • Transparent Data Encryption only — insufficient for the threat model
  • Vault-as-a-service (HashiCorp Vault) — adds operational complexity before justified
ADR-004 Form mapping is data, not code — declarative JSON schema registry Accepted
Each camp's form schema is a JSON document stored in the database. A generic mapping engine interprets the schema at runtime to translate canonical profile fields into camp-specific field names and formats. Adding a new camp requires a data operation, not a code deploy.
OneSummer's moat is the breadth of camp coverage. If adding each new camp requires a code change and deploy, growth is gated by engineering capacity. A declarative schema allows camp onboarding staff (or eventually, self-service camp admins) to define mappings without developer involvement. This directly implements the Open/Closed Principle: the mapping engine is closed to modification, open to extension via new schema records.
The mapping schema language must be expressive enough to handle conditional fields, format transformations (e.g., date format conversion), and required/optional flags. An admin UI for schema building is a Year 1 internal tool, self-service for camp admins in Year 2.
  • Per-camp code adapters — gated by developer time, doesn't scale
  • LLM-based field mapping — non-deterministic, cannot be unit-tested
ADR-005 COPPA: children have no credentials and are never directly addressable via API Accepted
Children are data entities, not actors. No child under 13 may create an account, receive a login, or make a direct API call. All data about a child is accessed through a verified parent session. This is enforced at the API authentication layer, not just by convention.
COPPA requires verifiable parental consent before collecting personal information from children under 13. The simplest and most defensible compliance posture is to remove the child as an authentication principal entirely. There is no meaningful product use case that requires a child to directly authenticate — camps communicate with parents, not children.
All consent events are recorded in 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
ADR-006 Seasonal scaling via calendar-driven scheduled scaling, not purely reactive autoscaling Accepted
Infrastructure is scaled up on a predetermined calendar schedule (e.g., scale to peak configuration on February 1st, scale down on June 15th) rather than relying solely on reactive CPU/request-based autoscaling.
The camp registration season is highly predictable — the same window every year. Reactive scaling reacts to traffic that has already arrived; for a consumer-facing registration system where the first impression during a camp fair weekend matters, scaling up before demand arrives is the correct posture. Reactive scaling remains as a secondary safety net.
Slightly higher infrastructure cost during the pre-season ramp-up (January). Requires an annual review of the scaling schedule. The seasonal configuration should be defined in code (infrastructure as code), not set manually in a console.
  • Purely reactive autoscaling — risk of cold start / warm-up lag during viral traffic spikes
  • Always-on peak configuration — 4–6x higher annual infrastructure cost
ADR-007 Use PostgreSQL full-text search in Year 1; design for extraction Accepted
Camp discovery search is powered by PostgreSQL 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.
At Year 1 scale (hundreds of camp listings), PostgreSQL full-text search provides sub-100ms query performance and requires no additional infrastructure. The operational simplicity of staying within a single database is significant for an early-stage team.
When the camp catalog exceeds approximately 5,000 listings, or when product requires relevance tuning, typo tolerance, or real-time faceting at high concurrency, the search backend should be migrated to Typesense or Elasticsearch. The 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
ADR-008 Document storage in S3-compatible object store, not the database Accepted
Binary documents (immunization records, IEPs) are stored in an S3-compatible object store. The database stores only the storage key, metadata, and access control records. Document URLs are pre-signed with short TTLs (15 minutes) and scoped to the requesting parent or granted camp.
Storing binary documents in Postgres (as bytea or large objects) bloats the database, complicates backups, and prevents the use of CDN distribution for document delivery. Object stores are purpose-built for this access pattern. Server-side encryption at rest in the object store plus pre-signed URL TTLs prevent unauthorized document access without requiring the application to proxy every download.
Document deletion (right-to-erasure) requires a two-step process: soft-delete the database record, then schedule a background job to delete from the object store. The soft-delete window (30 days) provides an accidental-deletion recovery buffer.
  • Database BLOB storage — poor performance, complicates backup strategy
  • Filesystem on the application server — not compatible with horizontal scaling
ADR-009 Abstract payment processing behind a service interface; never store raw card data Accepted
All payment collection is handled by a PCI-compliant payment provider via their hosted fields or client-side tokenization SDK. The OneSummer backend never receives or stores raw card numbers. The payments service stores only the provider's payment method token and transaction IDs.
PCI DSS compliance for a system that handles raw card data requires a Qualified Security Assessor audit and significant ongoing compliance work. Using a payment provider's tokenization SDK reduces the PCI scope to SAQ A (the simplest tier), which is achievable without a QSA engagement. The abstract service interface allows the provider to be swapped without application changes.
Refund logic must call the provider API rather than reversing a local record. Provider-specific webhooks must be handled for asynchronous payment state changes (ACH, bank transfer). All monetary amounts stored in integer cents — never floats.
  • Build payment processing in-house — PCI scope and liability not appropriate
ADR-010 External camp management integrations (CampMinder, CampDoc) are deferred to Year 2 behind an adapter interface Deferred
CampMinder and CampDoc integrations are not built in Year 1. The application is designed so that camp data imports and applicant data exports can be handled by adapters plugged into the Camp Registry and Application services when the integrations are built. The integration interface is defined now; the implementation is deferred.
External integrations require API access agreements, authentication flows, and handling the instability of third-party APIs. Year 1 camps will onboard via direct data entry. The form schema and applicant export already capture the data that integrations would push/pull — the plumbing work is minimal when the time comes. Defining the interface now prevents the integration being bolted on in a way that violates service boundaries.
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.
Integration priority should be driven by camp adoption data: if 30% of onboarded camps use CampMinder, build that adapter first. Do not build integrations speculatively.