# ADR 035: Application Security Baseline

- HTML version: https://robbiepalmer.me/projects/recipe-site/adrs/035-application-security-baseline
- Project: Recipe Site (https://robbiepalmer.me/projects/recipe-site.md)
- Status: Accepted
- Date: 2026-05-27

# Summary

The recipe site does **not** need a large standalone security program before shipping
authenticated features.

It does need a small, explicit baseline for the new risks introduced by login, sessions,
household sharing, and user-specific writes.

# Context

Several platform-level controls already exist elsewhere in the site architecture, including:

* repository and CI protections
* GitHub/secret-management decisions
* Cloudflare-managed edge controls

What is new here is **application security**:

* browser sessions
* OAuth callback handling
* CSRF and cookie safety
* rate limiting on auth and write paths
* server-side authorization enforcement

The chosen backend —
[Cloudflare Workers](/projects/recipe-site/adrs/033-backend-platform-for-authenticated-features) — provides
several security properties by default that reduce the baseline we need to implement ourselves.

# Decision

Before authenticated features go live, the application must provide this baseline:

1. **Server-owned sessions.** Session cookies must be `HttpOnly`, `Secure`, and appropriately
   scoped.
2. **CSRF protection for browser mutations.** If the app uses cookie-based auth, write endpoints
   must not rely on same-origin assumptions alone.
3. **OAuth integrity checks.** Provider flows must use state validation and any provider-specific
   protections Better Auth expects.
4. **Server-side authorization on every privileged route.** Authz belongs in the runtime, not only
   in client UI.
5. **Rate limiting on auth and write-heavy endpoints.** Sign-in, invite, and mutation paths need
   basic abuse protection from day one.
6. **Input validation at the API boundary.** Typed frontend code is not a substitute for runtime
   validation. Zod schemas at the Hono handler level are consistent with the existing codebase
   pattern.
7. **Least-privilege secrets and database access.** Reuse the broader platform approach; do not
   give every runtime token broad administrative scope if narrower credentials are possible.

## Workers Security Properties

The Cloudflare Workers runtime provides several security properties for free:

| Property                           | What it means                                                                                                                                                                                  |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Managed isolate sandboxing**     | Worker code runs inside Cloudflare-managed V8 isolates rather than a user-managed server process. Global scope may be reused across requests, so request-scoped data must not be stored there. |
| **Automatic HTTPS**                | All traffic is TLS-terminated at Cloudflare's edge by default. No certificate management.                                                                                                      |
| **No user-managed server process** | There is no VM or container for us to patch, harden, or expose directly. Application state still belongs in explicit stores, not process memory.                                               |
| **DDoS protection**                | Cloudflare's edge network provides baseline DDoS mitigation for all Workers. No configuration needed.                                                                                          |
| **No exposed ports**               | Workers are not addressable by IP. All traffic flows through Cloudflare's reverse proxy. There is no server to port-scan.                                                                      |

These do not replace the application-level controls above, but they reduce the surface area that
needs explicit security engineering.

## Rate Limiting Implementation

Rate limiting uses a layered approach:

* **Cloudflare rate limiting rules** (configured via Terraform in
  `infra/main.tf`) for broad endpoint protection — e.g. limiting requests per IP to
  auth endpoints. These run at the edge before the Worker is invoked.
* **Durable Objects or native Cloudflare rate limiting** for application-specific limits — e.g. auth
  attempts per user account, household invite frequency. Durable Objects are included in Workers
  Paid and provide the strong consistency needed for high-frequency counters. Cloudflare's native
  rate limiting is preferable where an edge-level rule can express the policy.
* **Do not use Cloudflare KV for write-heavy rate limiting.** KV is optimized for high-read,
  low-write workloads, is eventually consistent across edge locations, and has a 1 write/second
  limit per key. Those properties make it a poor fit for brute-force counters.
* **Better Auth's built-in rate limiter** should be backed by Durable Objects, native Cloudflare
  rate limiting, or another strongly consistent store rather than KV.

### Tiers and thresholds

The implementation uses three tiers. Both application tiers count in Postgres (via Hyperdrive/Neon),
which is already in the request path and gives the strong consistency these counters need: the
check-and-increment runs as a single `INSERT ... ON CONFLICT` statement, so concurrent requests
cannot race a stale counter. Durable Objects are the natural alternative — they would keep counter
writes off Postgres and closer to the edge — but that is more moving parts than a personal-scale MVP
warrants while the database is already there.

| Tier                 | Mechanism                                     | Scope                     | Default threshold  | Over-limit response        |
| -------------------- | --------------------------------------------- | ------------------------- | ------------------ | -------------------------- |
| Edge (broad)         | Cloudflare rate limiting rule (Terraform)     | Per IP, `/api/auth/*`     | 20 requests / 10s  | `429` at the edge          |
| App auth (default)   | Better Auth limiter, Postgres `customStorage` | Per IP, `/api/auth/*`     | 100 requests / 60s | `429` with `X-Retry-After` |
| App auth (sign-in)   | Better Auth limiter, Postgres `customStorage` | Per IP, `/sign-in/social` | 20 requests / 60s  | `429` with `X-Retry-After` |
| Per-account (writes) | Worker limiter, Postgres `app_rate_limit`     | Per user, invites         | 10 invites / hour  | `429` with `Retry-After`   |

The edge tier is intentionally the loosest: it absorbs volumetric floods before they reach the
Worker while leaving normal multi-tab session polling alone. The application tiers tighten
progressively toward the sensitive sign-in and write paths. Thresholds are configurable — the edge
values are Terraform variables (`auth_rate_limit_*`) and the application values live in the Worker
(`workers/recipe-api/src/auth.ts` and `src/http/rate-limit.ts`).

## Deferred

The following can stay deferred to keep the system lean:

* first-party MFA
* custom fraud/anomaly systems
* dedicated SIEM / security analytics
* public API key management
* elaborate WAF tuning beyond sensible Cloudflare defaults

This is intentionally a **baseline ADR**, not a promise to build security-heavy product features
before they are justified.

# Consequences

## Positive

* The real minimum bar is explicit.
* Security scope stays proportional to a personal-project MVP.
* Workers' built-in isolation properties reduce the surface area that needs manual security work.

## Negative

* "Lean" still means some engineering work: rate limiting, session config, and validation cannot be
  hand-waved away.
* Durable Objects or equivalent strongly consistent rate limiting add implementation work.
* If public signup or friend sharing expands quickly, this baseline may stop being sufficient.

# When To Revisit

Revisit if any of the following become true:

* signup becomes broadly public
* the site adds public APIs, API keys, or machine-to-machine access
* moderation, abuse, or account-recovery burden becomes material
* a security review or penetration test reveals gaps in the baseline

---

Markdown index of this site: https://robbiepalmer.me/llms.txt
