Raw

Architecture

Overview

SlopDrop is a two-package monorepo:

Stack

Component Technology Why
CLI Go Single binary, cross-platform, fast
API Cloudflare Workers (Hono) Edge deployment, low latency, no servers
Metadata D1 (SQLite) Structured queries, tenant isolation
File Storage R2 S3-compatible, no egress fees
Rate Limiting D1 100k writes/day vs KV's 1k
Bot Protection Cloudflare Turnstile Contact form captcha
Access Control Cloudflare Access + WAF Per-tenant auth, subdomain allowlist

Data Model

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Tenant    │────▶│   Project   │────▶│    Drop     │
│             │     │  (implicit) │     │  (versioned)│
│ slug        │     │             │     │ path        │
│ plan        │     │ Derived from│     │ version     │
│ is_public   │     │ drop paths  │     │ content→R2  │
│ api_keys[]  │     │             │     │             │
└─────────────┘     └─────────────┘     └─────────────┘

Projects are implicit — they're derived from the first path segment of drops. No project table in the database.

Tenants are private by default — is_public must be explicitly set to serve content without Cloudflare Access authentication.

Request Flow

Upload (CLI → API → Storage)

1. CLI reads file, computes hash
2. CLI sends POST /api/v1/drops (multipart)
3. API validates: auth → path → secrets → size
4. API stores: content → R2, metadata → D1
5. API returns: version number, URL

Render (Browser → WAF → Access → Worker → HTML)

1. Browser hits {tenant}.slopdrop.dev/{project}/{path}
2. WAF checks subdomain against allowlist → blocks unknown subdomains
3. Cloudflare Access checks auth (per-tenant policy, if app exists)
4. Worker resolves tenant from subdomain via D1 lookup
5. If tenant is private: verify CF Access JWT signature → redirect if invalid
6. Worker fetches content from R2
7. Markdown rendered to HTML with:
   - Table of contents (h2/h3) with scroll spy
   - Wiki-link resolution ([[target]])
   - GitHub-style callouts (NOTE, TIP, WARNING, CAUTION, IMPORTANT)
   - GFM tables with alignment
   - Task lists with checkboxes
8. HTML returned with 60s cache

Admin (admin.slopdrop.dev)

1. Request hits admin.slopdrop.dev
2. CF Access authenticates user (Google OAuth / email)
3. Worker verifies CF Access JWT
4. Server-rendered HTML dashboard:
   - Overview: tenant count, drops, storage, API calls
   - Tenants: list, create, toggle public/private
   - Usage: per-tenant monthly metrics
   - Contacts: form submissions from landing page

Security Model — Three Layers

Layer 1: WAF (Phase 14 — before Worker)

WAF custom rule blocks all *.slopdrop.dev subdomains except an explicit allowlist. Blocked requests never reach the Worker — zero billing, zero exposure.

Layer 2: Cloudflare Access (Phase 19 — before Worker)

Per-tenant Access applications control who can authenticate. Supports Google OAuth, email domains, specific email addresses. Issues a signed JWT on successful auth.

Layer 3: Worker (JWT Verification)

For private tenants (is_public = 0), the Worker verifies the CF-Access-Jwt-Assertion header:

Why all three? WAF handles cost (no billing for junk). Access handles auth UX (login flow). Worker handles the gap: tenant exists in D1 but has no Access app yet.

Additional Security

Versioning

Every upload to the same path increments the version. R2 stores each version at {tenant_id}/{project}/{path}@v{N}. D1 tracks version number, timestamps, content hash, and file size.

The web renderer always serves the latest version. Version history is available via the API but not yet exposed in the rendered view.

Cloudflare Platform Limits

Resource Free Paid ($5/mo)
Worker requests 100k/day 10M/month
D1 reads 5M/day 25B/month
D1 writes 100k/day 50M/month
R2 storage 10 GB $0.015/GB
R2 reads 10M/month $0.36/M
R2 egress Free Free
These are Cloudflare's platform limits, not SlopDrop-imposed. Exceeding = hard block (Error 1027), not throttling. The CLI watcher detects 429/1027 responses and pauses with auto-recovery.

We also enforce a WAF rate limit of 50 requests/10 seconds per IP to catch runaway clients before they burn through the daily quota.