Architecture

A social media platform built entirely on Cloudflare's edge stack — Workers handle compute, D1 provides shared SQLite shards, R2 stores media, and KV caches profiles and routes.

Cloudflare Workers D1 (SQLite at Edge) R2 Object Storage KV Cache
01 — Overview

System at a Glance

A social media platform built for staying in touch with people you know — no ads, no data sales, no virality mechanics. The entire backend runs on Cloudflare's edge network: Workers handle compute, D1 provides shared SQLite shard databases, R2 stores media uploads, and KV caches profiles and shard routes.

🌍

Edge-First

Every request is served from the nearest Cloudflare data center. No origin server, no cold starts.

🗄️

Shared D1 Shards

Users are assigned to a shared D1 shard via load balancing. All their posts, friends, and inbox live in that shard.

Time-Bounded

Posts expire automatically — free tier media lasts 48 hours, text 30d. Paid tiers get 90–180 days.

02 — Tech Stack

Technology Choices

Every piece of the stack runs on Cloudflare's infrastructure. No external databases, no third-party servers, no AWS dependencies.

Cloudflare Workers
Compute (API + SSR)
D1 (SQLite)
Shared shard databases
R2 Object Storage
Media uploads (photos, videos)
KV (Key-Value)
Profile cache, shard routing
SvelteKit + Svelte 5
Frontend (runes mode)
Drizzle ORM
Type-safe SQL queries
Tailwind v4 + Flowbite-Svelte v2
Styling & components
pnpm
Package manager
Vitest + Playwright
Unit + E2E tests
03 — Request Flow

Edge Architecture

When a user opens the app, their request hits the nearest Cloudflare data center (300+ cities worldwide). The Worker handles routing, auth, and business logic. Data queries go to D1 in the same region — single-digit millisecond latency.

User (browser)
↓ HTTPS request
Cloudflare Edge (nearest city)
├─ Worker handles request (SvelteKit SSR)
├─ JWT verified via HMAC-SHA256 (in-Worker, no external call)
├─ KV lookup: user:{userId} → { databaseId, shardId, tier }
├─ Resolve D1 binding from databaseId
├─ D1 query (SQLite, same-region shard)
├─ Media uploads: Worker → R2 put (no presigned URLs)
└─ SvelteKit SSR → HTML response
User sees the page

Media uploads go through the Worker to R2 directly — the Worker reads the file body and writes to R2. There is no presigned URL flow; the Worker is the intermediary.

04 — Data Layer

Shared D1 Shards

Users are assigned to a shared D1 shard database (e.g. SHARD_FREE_001). Each shard stores data for many users — the findAvailableShard function picks the shard with the lowest user count (load balancing). The Admin DB (separate D1) holds accounts, subscription plans, feature flags, RBAC, and billing.

User Shard Tables

Post
Comment
Reaction
Friend
Inbox
Media
Notification
UserProfile
UserAccount
Conversation
Message
Document (legacy)

Admin DB Tables

User
Account
SubscriptionPlan
FeatureFlag
ShardRegistry
Role
Permission
RolePermission
UserRoleAssignment
RefreshToken
BillingPaymentMethod
ProvisioningQueue
SiteSetting
EmailCampaign

+ several billing/RBAC/feature-flag tables

Shard Routing

1. getUserDatabaseId(userId)
→ KV lookup: user:{userId} → { databaseId, shardId, tier }
→ Falls back to Admin DB on KV miss, then populates KV
2. resolveShardBindingAsync(databaseId)
→ Local dev: hardcoded map (e.g. local-dev-free-001 → SHARD_FREE_001)
→ Production: ShardRegistry lookup by databaseId → bindingName
3. Returns { db: drizzle(d1), userId, d1 }

3 shards per environment (dev/qa/prod): SHARD_FREE_001/002/003. New shards are staged with maxUsers: 999999 — no hard cap on users per shard.

05 — Feed & Fan-Out

Letterbox Pattern

When you post, the system writes a copy to each friend's inbox (write-time fan-out). Reading your feed is a single query against your own Inbox table — no joins, no cross-shard lookups. Each inbox entry contains a denormalized snapshot of the author's name, avatar, post content, and media references, so feed reads never touch the author's D1 shard.

Write (post creation):
1. POST /api/posts → Worker on author's shard
2. Insert into Post table
3. fanOutPost: get friends list from author's shard
4. Get author snapshot (displayName + avatarR2Key)
5. For each friend →
resolve friend's shard via getShardClientForUserId
writeInboxEntry (denormalized snapshot)
6. Best-effort — failures don't abort

Read (feed):
1. GET /api/feed → Worker on viewer's shard
2. SELECT * FROM Inbox
WHERE expiresAt > now
ORDER BY receivedAt DESC
LIMIT 25 (FEED_PAGE_SIZE)
3. Parse authorSnapshot from each inbox row
4. Mark entries consumed=true
5. Return posts (no cross-shard queries)

Fan-Out

Currently synchronous and inline — all friends processed in a loop. MAX_SYNC_FANOUT = 100 is defined but not enforced. No async queue or Durable Object path is wired.

Profile Cache

KV stores { displayName, avatarR2Key, tier } under user:{userId} for friend lists and recommendations. Feed reads use the denormalized authorSnapshot baked into inbox rows — not the KV cache.

06 — Media & Storage

R2 Object Storage

Media uploads (photos, videos) go to Cloudflare R2 — zero egress fees, same-region as the user's D1 shard. The Worker receives the file and writes it to R2 directly (no presigned URL flow).

Max Media Size

50 MB

per upload

Max Avatar Size

5 MB

resized to 256×256 client-side

Storage Quota

500 MB – 20 GB

by tier

R2 Path Convention

Media: media/{userId}/{mediaUuid}.{ext}
Avatar: avatars/{userId}/avatar.{ext}
Cover: covers/{userId}/cover.{ext}

Allowed: JPEG, PNG, GIF, WebP (images) + MP4, WebM (video)

Oldest-first eviction: when a user hits their storage quota, the oldest unlocked media is automatically deleted. Locked media is exempt from eviction. Avatars are center-cropped to 256×256 and converted to JPEG @ 90% quality on the client before upload. Covers are resized to 1200px width.

07 — Plan System

Subscription Tiers

Three tiers with time-based post expiry, storage quotas, and media capabilities. Defined in plan-service.ts and seeded into the Admin DB via seed-core.sql. expiresAt is stamped at creation and is immutable — downgrading does not change existing posts' expiry.

FeatureFree ($0)Basic ($1/mo)Pro ($2/mo)
Text post retention30 days90 days180 days
Media post retention48 hours90 days180 days
Storage quota500 MB5 GB20 GB
Photo uploads
Video uploads
Lock media

Free tier allows photo and video uploads — media posts just expire after 48 hours. Paid tiers get 90–180 day media retention. Locked media survives post expiry (the post reference is set to NULL but the media file remains in R2 and counts toward quota).

08 — Auth & Security

Authentication

Password Hashing

PBKDF2-SHA256 with 100,000 iterations via Web Crypto subtle.deriveBits. 32-byte derived key, 16-byte salt. Hash format: pbkdf2$100000$saltHex$hashHex. Constant-time comparison. No bcrypt (not available in Workers runtime).

JWT Tokens

HS256 (HMAC-SHA256) access tokens: 15-minute TTL. Opaque refresh tokens: 30-day TTL (32 random bytes, stored in RefreshToken table). Refresh token rotation on use — old token revoked, new one issued.

Bearer Header Auth

API uses Authorization: Bearer header. No cookies, no Set-Cookie. CSRF disabled (header-based auth, not form/cookie). All UI uses apiClient (not raw fetch) — auto-attaches Bearer token, 401 → refresh → retry with concurrent refresh prevention.

RBAC & 403 Gating

Admin endpoints double-gated: requireAdmin (checks admin.access permission) then requirePermission (e.g. users.read, roles.create). Both return 403 with audit log on failure. User-facing endpoints use requireAuthenticated (401-only).

09 — API Endpoints

RESTful API

All endpoints are Cloudflare Pages Functions deployed alongside the frontend. JWT-authenticated via Bearer header. Admin endpoints are 403-gated by role/permission. All UI uses apiClient (not raw fetch).

94 endpoint files across auth, posts, feed, friends, media, profile, notifications, account/billing, admin (users, roles, plans, feature flags, shards, settings), and Stripe webhook. Key endpoints below:

MethodPathDescription
POST/api/auth/registerCreate account + assign shard
POST/api/auth/loginLogin → access + refresh tokens
POST/api/auth/refreshRotate refresh token, new access token
POST/api/auth/logoutRevoke refresh token
POST/api/auth/delete-accountDelete account + data
GET/api/postsList own posts
POST/api/postsCreate post (text or media)
GET/api/posts/:uuidGet a single post
DELETE/api/posts/:uuidDelete a post
POST/api/posts/:uuid/commentsComment on a post
POST/api/posts/:uuid/reactReact to a post
GET/api/feedRead inbox (feed) — letterbox pattern
GET/api/friendsList friends
GET/api/friends/:userIdFollow / unfollow a user
GET/api/friends/recommendationsFriend-of-friend suggestions
GET/api/friends/searchSearch for users
GET/api/notificationsList notifications
POST/api/media/upload-urlsUpload media to R2 via Worker
POST/api/media/:uuid/lockLock media (prevent eviction)
GET/api/profile/:userIdGet user profile
PUT/api/profileUpdate own profile
POST/api/profile/avatarUpload avatar (resized 256×256)
POST/api/account/subscribeSubscribe to a plan
GET/api/account/usageGet storage usage
POST/api/stripe/webhookStripe webhook (billing)
GET/api/admin/usersAdmin: list users (403-gated)
GET/api/admin/shardsAdmin: list shards (403-gated)
GET/api/admin/feature-flagsAdmin: list feature flags (403-gated)
GET/api/admin/plansAdmin: list subscription plans (403-gated)

Showing 29 of 94 endpoint files. Full list includes admin CRUD for roles, permissions, email campaigns, audit logs, provisioning, database stats, and more.