Prerequisites & Architecture
Decision Register
Technology choices, service dependencies, cost targets, and portability constraints for OneSummer — the Common App for summer camps. Every decision is traced to a capability need and sized against the MVP dependency budget of 4–5 services maximum.
Overview & Guiding Principles
OneSummer is a two-sided marketplace where parents discover, compare, and apply to summer camps while camps manage listings and accept applications. A Profile Vault stores child information once, reusable across all applications. The Calendar Planner provides scheduling and conflict detection.
The architecture decisions below follow a capability-first approach: every service added must answer a question that the existing stack cannot answer. The default question before adding anything new is "Can Postgres do this?"
docker-compose.yml. Any developer can clone the repo and have a running local environment in one command. Production runs the same image. No environment-specific code paths.
Scale & Growth Targets
Architecture decisions are anchored to concrete scale targets. Technology that is sufficient for Year 1 is preferred unless it creates a known migration cliff at Year 2 or Year 3.
MVP Dependency Budget
Five service slots. Each slot must be justified by a capability that no existing slot can provide.
Slot 5 is intentionally held open for the first real constraint that Postgres and the app runtime cannot solve. Current candidates when the trigger is hit: object storage (documents exceed 1 GB), PgBouncer (connection exhaustion), or a transactional SMS gateway (if camp operators require SMS alerts). Only one can occupy this slot at MVP scale.
Architecture Decision Records
fly.toml is the only platform-specific artifact. Local dev runs docker-compose up.DATABASE_URL env var. To migrate: change DATABASE_URL, rebuild image, deploy elsewhere. Zero code changes required.
pg_cron. Managed via Fly.io Postgres at MVP. Migrate to Neon or self-managed at Year 2 if cost or connection limits demand it.parent, camp_admin, platform_admin. BetterAuth's organization plugin handles the camp_admin multi-seat use case. Role stored in Postgres, enforced server-side on every request. Row-level security in Postgres provides defense in depth.
bytea or large objects can handle on a $6 VPS. Adding S3 at MVP introduces IAM credentials, pre-signed URL complexity, CORS configuration, and a service dependency for a problem that does not yet exist.
StorageAdapter interface from day one. The MVP implementation writes to Postgres. The Year 3 implementation writes to Cloudflare R2 (S3-compatible, zero egress fees). No application code changes outside the adapter — only the implementation swaps. Design this interface before writing the first file upload handler.
job_queue table polled by the app server every 30 seconds, combined with pg_cron for scheduled triggers. This requires zero additional infrastructure.
FOR UPDATE SKIP LOCKED for safe concurrent dequeue. pg_cron inserts scheduled jobs. App server polls every 30 seconds with a single worker thread.CREATE TABLE job_queue (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
job_type text NOT NULL,
payload jsonb NOT NULL,
status text NOT NULL DEFAULT 'pending',
run_after timestamptz NOT NULL DEFAULT now(),
attempts int NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX ON job_queue (status, run_after)
WHERE status = 'pending';
EmailProvider interface for portability./emails directory using React Email.
Postgres Capability Map
Every capability listed below was evaluated with the question: "Can Postgres do this well enough that adding a dedicated service is unjustified at MVP scale?" Green rows are handled natively. Amber rows use a Postgres extension. Grey rows are deferred.
| Capability | Postgres Feature | Status | Replaces |
|---|---|---|---|
| Full-text camp search Name, activity tags, location |
tsvector, GIN index, ts_rank |
Native | Algolia, Typesense, Elasticsearch |
| Structured document storage Profile vault, form schemas, camp metadata |
JSONB with GIN index |
Native | MongoDB, DynamoDB |
| File storage (medical forms) PDFs, permission slips — MVP only |
bytea column |
Native | S3, Cloudflare R2 (deferred to Y3) |
| Background job queue Email dispatch, reminders, reports |
job_queue table + FOR UPDATE SKIP LOCKED |
Native | Redis + BullMQ, RabbitMQ |
| Scheduled triggers Daily deadline reminders, weekly digests |
pg_cron extension |
Extension | Cron containers, Temporal, Inngest |
| Real-time application events Status changes, new application notifications |
LISTEN / NOTIFY |
Native | Redis pub/sub, WebSocket service |
| Data isolation between camps Camp operators see only their own applications |
Row-level security (RLS) policies | Native | Application-layer tenant filtering |
| Geospatial camp discovery Sort camps by distance from family ZIP code |
PostGIS or earthdistance extension |
Extension | Google Maps distance matrix, dedicated geo service |
| Session storage BetterAuth session table |
Standard table with TTL via pg_cron cleanup |
Native | Redis sessions |
| Rate limiting Login attempts, form submissions |
rate_limit table + timestamp window query |
Native | Redis sliding window, Upstash |
| Vector / semantic search AI-powered camp recommendation matching |
pgvector extension |
Deferred — Year 2+ | Pinecone, Weaviate |
| Analytics / reporting Camp operator dashboards, platform metrics |
Materialized views, window functions | Native | ClickHouse, BigQuery, Redshift |
Cost Projection
Target: $0–25/month at MVP scale (Year 1, 2,000 families, single city). The following projections are based on published free tier limits as of April 2026.
Fly.io Hobby plan covers Postgres + app container. All other services within free tier.
| Service | Year 1 (MVP) | Year 2 (25K) | Year 3 (200K) |
|---|---|---|---|
|
Fly.io — App + Postgres
Hobby: 3 shared-CPU VMs + 256 MB Postgres included free. Upgrade to dedicated at Y2.
|
$0 | $25–50 | $100–200 |
|
Resend — Transactional Email
Free: 3,000 emails/mo. Paid: $20/mo for 50K emails.
|
$0 | $20 | $20–40 |
|
Sentry — Error Tracking
Free: 5,000 errors/mo, 1 user. Team plan $26/mo for >1 developer.
|
$0 | $0 | $26 |
|
Domain + TLS
Cloudflare DNS (free) + Fly.io auto-TLS (free). Domain registration ~$12/yr.
|
$1 avg | $1 avg | $1 avg |
|
Cloudflare R2 — Object Storage
Deferred. Free: 10 GB/mo. $0.015/GB/mo thereafter. Zero egress fees.
|
$0 (deferred) | $0 | $5–15 |
|
PgBouncer — Connection Pooling
Deferred. Run as sidecar container on same Fly.io VM — no additional cost.
|
$0 (deferred) | $0 (sidecar) | $0 |
|
Uptime Monitoring
Better Uptime / UptimeRobot free tier — 50 monitors, 3 min intervals.
|
$0 | $0 | $0 |
| Total Monthly Estimate | $1–5 | $46–71 | $152–282 |
Portability Assessment
The primary portability goal is: any developer can run the full stack locally with docker-compose up in under three minutes. Production can be migrated away from Fly.io in under one day. No vendor-specific APIs in application code.
fly.toml. Migration: change one config file and redeploy.DATABASE_URL env var. pg_cron available on all major managed providers.EmailProvider interface. Swap to SES, Postmark, or SMTP by replacing the provider implementation. React Email templates are provider-agnostic HTML.StorageAdapter interface from day one. MVP writes to Postgres. Year 3 swaps implementation to any S3-compatible provider. Zero application changes required.docker-compose up starts Postgres 16 + app in one command. No local dependencies beyond Docker. Seed script populates test data including camp listings and parent profiles.Accounts & Credentials Checklist
Every account that must be created before the first production deployment. Sorted by dependency order — items marked Day 0 are blockers for initial deploy.
flyctl CLI and authenticatebrew install flyctl or download from fly.io/docs/hands-on/install-flyctl. Run fly auth login.fly apps create onesummer-prodonesummer-staging. Use the same organization.fly postgres create --name onesummer-dbfly secrets set DATABASE_URL=.... Never commit this value.fly.toml with health check, port, and regionprimary_region = "ord" (Chicago) for Year 1 single-city focus. Add [checks] block pointing to /health endpoint.onesummer.com (or chosen domain)app.onesummer.com → Fly.io hostnamefly certs add app.onesummer.com to initiate TLS certificate provisioning via Let's Encrypt.p=none initially, upgrade to p=quarantine after 30 days of monitoring.fly secrets set RESEND_API_KEY=re_.... Never commit the key. Use a separate key for staging vs. production.onesummer.com as a sending domain. Copy the DNS records Resend provides and add them to Cloudflare (DKIM TXT records).from address: hello@onesummer.com for transactional, noreply@onesummer.com for automatedhello@ for emails that parents might reply to. Use noreply@ for system notifications.onesummer. Free tier: 5,000 errors/mo, 1 developer seat.onesummer-web (SvelteKit) and onesummer-server (Node/Bun)SENTRY_DSN_CLIENT and SENTRY_DSN_SERVER.@sentry/sveltekit. Add SENTRY_AUTH_TOKEN to Fly.io and GitHub secrets. Source maps enable readable stack traces in production.onesummer-apponesummer-app/platform as private/app (SvelteKit), /emails (React Email templates), /db (migrations), /docker.FLY_API_TOKEN, SENTRY_AUTH_TOKENFLY_API_TOKEN from fly tokens create deploy. This enables CD: push to main → auto-deploy to Fly.io.main: require PR review + passing CIhttps://app.onesummer.com/health and https://app.onesummer.com/health endpoint should return 200 and check Postgres connectivity. Alert via email + PagerDuty (or phone call) for >3 minute downtime.status.onesummer.comstatus.onesummer.com. Camp operators can check this during incidents.DATABASE_URLfly secrets set. Never in .env committed to git.BETTER_AUTH_SECRETopenssl rand -base64 32.BETTER_AUTH_URLhttps://app.onesummer.com in production, http://localhost:5173 in local dev.RESEND_API_KEYre_. Separate keys for prod and staging.SENTRY_DSNNODE_ENV / PUBLIC_APP_ENVproduction in Fly.io app config. Controls logging verbosity, error details, and dev-only routes.STORAGE_ADAPTERpostgres at MVP. Future values: r2, s3. Controls which StorageAdapter implementation is injected at startup.