Project Now Korea
Warning
Over a year old. May be outdated
1. High-level architecture
flowchart TD
%% === Clusters ===
subgraph Client["Browser / Next.js frontend"]
browser["Browser"]
end
subgraph EdgeFns["Vercel Edge Functions"]
edgefn["Edge Function"]
end
subgraph Cache["Upstash Redis (cache + pub/sub)"]
redis["Redis"]
end
subgraph Durable["Neon Postgres (event log)"]
neon["Postgres"]
end
%% === Flows ===
browser -- "GET /canvas" --> edgefn
edgefn -- "Read tile" --> redis
edgefn -- "Return image" --> browser
browser -- "POST /api/place" --> edgefn
edgefn -- "SETRANGE + PUBLISH" --> redis
edgefn -- "INSERT pixel event" --> neon
browser -- "SSE /api/updates" --> edgefn
redis -- "Publish updates" --> edgefn
edgefn -- "Stream deltas" --> browser
- Vercel hosts the static Next.js UI, Edge Functions, and scheduled (cron) jobs.
- Upstash Redis is the hot data-plane:
- single-bit/byte writes (
SETBIT/SETRANGE) keep the canvas in memory with O(1) latency(upstash.com) - native Pub/Sub plus Server-Sent Events (SSE) push updates globally(upstash.com)
- atomic Lua scripts and the Upstash Rate-Limit helper enforce "1 pixel /Nsec" per user.
- single-bit/byte writes (
- Neon Postgres is the durable system-of-record. The edge-compatible serverless driver lets Edge Functions upsert rows over HTTPS/WebSockets with 10ms cold-start overhead(neon.com). Keeping history here enables time-lapse, analytics, and roll-back with Neon's cheap "branching" snapshots(neon.com).
2. Data layout in Upstash Redis
Atomic write + broadcast script
SETRANGE modifies ≤1byte so requests stay <1MB(upstash.com) and are atomic at the Redis level.
3. Edge Function: POST /api/place
- Auth - header cookie/JWT or fallback to IP.
- Rate-limit - Upstash helper (
@upstash/ratelimit) → 429 if user has written in the last N seconds. - Tile calculation - map
x,ytotileKeyand byte offset. - Lua script EVALSHA - atomic write + publish.
- Async durability -
await sqlvia@neondatabase/serverlessto insert intopixel_events(id, ts, x, y, color, user_id). Errors are logged but don't block the user path.
Edge Functions are short-lived, so they fit Vercel's normal limits (max 30s run, 25MB bundle(vercel.com)).
4. Real-time fan-out (/api/updates)
Vercel doesn't keep raw WebSockets alive end-to-end(vercel.com), but long-running SSE streams are supported because they're just HTTP responses.
The route:
The browser's EventSource patches the <canvas> in ~50ms worldwide (Upstash's multi-region POPs).
5. Cold-start & initial page load
- Path 1, static snapshot. hourly Vercel Cron downloads all tiles, flattens to a PNG, uploads to Vercel Blob Storage or the Next.js public folder (served by Vercel CDN). First paint shows a compressed image.
- Path 2, progressive tiles. client lazily
GET /api/tile?x=3&y=7(Edge Function readsGETRANGE4096B and responds with Uint8Array).
6. Background jobs (Vercel Cron)
Cron functions are entirely serverless, so they cost nothing when idle.
7. Scaling & limits
Cost example (1keps, 2M MAU):
8. Security & abuse protection
- HTTPS only, with Vercel auto-TLS.
- Origin shielding - strict CORS and
Referrer-Policy. - Rate-limit at API layer + Cloudflare Turnstile to deter scripts.
- Auditable history - every pixel stored immutably in Postgres, write-blocked by RLS so only Edge Functions can insert.
9. Extensibility
- Multiple boards - prefix keys
board:<id>:<tileX>:<tileY>, plus a Postgres foreign-keyboard_id. - Undo / disputes - keep last colour in Lua script return, so moderators can "rewind" by replaying events.
- Time-lapse video - Neon Branch → generate frames in a Vercel Job, push to Cloudflare Stream.
- Gamification - Postgres materialized view for streaks, leaderboards.
10. Why this stays "serverless"
- Scale-to-zero. no container or DB CPU when idle (Neon & Upstash pause).
- Global latency. Vercel Edge + Upstash POPs replicate closer to users, with no region pinning.
- Ops burden. backups, fail-over, TLS, and upgrades are delegated to the three SaaS providers.
Backlinks2
250704Project