You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Elijah Voigt db71399b2f feat(quotesdb): add Cloudflare WAF rate limiting rules via OpenTofu
Adds infra/rate-limits.tf with a cloudflare_ruleset (phase: http_ratelimit)
implementing per-IP rate limits on all mutating API endpoints:
- PUT /api/quotes: 5 requests per 10 minutes (quote creation)
- POST /api/quotes/:id/report: 3 requests per hour (abuse reports)
- POST /api/quotes/🆔 10 requests per minute (quote updates)
- DELETE /api/quotes/🆔 10 requests per minute (quote deletes)

The report rule is ordered before the general update rule to ensure the
more-specific /report path matches before the broader /api/quotes/:id
pattern. Documents the approach, plan requirements, and layered protection
rationale in docs/ARCHITECTURE.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
..
.gitignore feat(quotesdb): implement API DB layer and all HTTP handlers 3 months ago
README.md chore(quotesdb): commit tickets, TODO, and infra README update 3 months ago
d1.tf feat(quotesdb): implement API DB layer and all HTTP handlers 3 months ago
dns.tf feat(quotesdb): implement API DB layer and all HTTP handlers 3 months ago
main.tf feat(quotesdb): implement API DB layer and all HTTP handlers 3 months ago
pages.tf feat(quotesdb): implement API DB layer and all HTTP handlers 3 months ago
providers.tf feat(quotesdb): implement API DB layer and all HTTP handlers 3 months ago
rate-limits.tf feat(quotesdb): add Cloudflare WAF rate limiting rules via OpenTofu 3 months ago
schema.sql feat(quotesdb): admin super auth code for quote moderation 3 months ago
turnstile.tf feat(quotesdb): Cloudflare Turnstile CAPTCHA on submit 3 months ago
variables.tf feat(quotesdb): Cloudflare Turnstile CAPTCHA on submit 3 months ago
worker.tf feat(quotesdb): implement API DB layer and all HTTP handlers 3 months ago

README.md

quotesdb Infrastructure

OpenTofu configuration for deploying quotesdb to Cloudflare.

Resources provisioned

Resource Description
cloudflare_d1_database.quotesdb D1 SQLite database backing the API
cloudflare_workers_script.api Compiled Wasm Worker serving /api/*
cloudflare_worker_route.api Routes quotes.elijah.run/api/* to the Worker
cloudflare_pages_project.ui Pages project hosting the Yew SPA
cloudflare_record.ui CNAME quotes.elijah.run → Pages
cloudflare_pages_domain.ui Custom domain binding on Pages

Required credentials

Variable Description
cloudflare_api_token Cloudflare API token (Workers, D1, Pages, DNS edit)
cloudflare_account_id Cloudflare account ID
cloudflare_zone_id Zone ID for elijah.run

These are set in terraform.tfvars (gitignored)

cloudflare_api_token="..."
cloudflare_account_id="..."
cloudflare_zone_id="..."

First-time setup (chicken-and-egg)

D1 must exist before the Worker can bind to it. On the very first deploy:

cd infra/
tofu init
tofu apply -target=cloudflare_d1_database.quotesdb
wrangler d1 execute quotesdb --file schema.sql --remote
tofu apply

Subsequent deploys: CI/CD handles everything automatically.

Local apply

cd quotesdb/infra/
tofu init
tofu plan
tofu apply

State

State is stored locally in terraform.tfstate (gitignored). For a team setup, migrate to a remote backend (S3-compatible bucket, Terraform Cloud, etc.).

Files

File Purpose
main.tf Terraform block and provider version constraints
providers.tf Cloudflare provider configuration
variables.tf Input variable declarations
d1.tf Cloudflare D1 database resource
worker.tf Cloudflare Workers script + route
pages.tf Cloudflare Pages project
dns.tf DNS record and custom domain binding
schema.sql Idempotent D1 schema (applied via wrangler, not tofu)
.gitignore Ignores state, lock, and credential files