Project Now Korea
1.High‑level architecture
- 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
Key | Type | Purpose | Est. size |
---|---|---|---|
board:<tileX>:<tileY> | String (4096B) | 64×64 chunk; one byte per pixel (256 colours) | ≤1MB total for 1024×1024 board – under Upstash's 100MB record limit(upstash.com) |
last:<userId> | String | Unix ms of last placement | 8B |
updates | Pub/Sub channel | JSON {x,y,color} deltas | transient |
Atomic write + broadcast script
-- EVALSHA in Edge Function
-- KEYS[1] = tile key, ARGV[1] = offset, ARGV[2] = colour byte, ARGV[3] = userId
redis.call('SETRANGE', KEYS[1], ARGV[1], ARGV[2])
redis.call('PUBLISH', 'updates', cjson.encode{ x=..., y=..., c=ARGV[2], u=ARGV[3] })
return true
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,y
totileKey
and byte offset. - Lua script EVALSHA – atomic write + publish.
- Async durability –
await sql
via@neondatabase/serverless
to 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; 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:
export const runtime = 'edge'
export async function GET() {
const { subscribe } = Redis.fromEnv() // Upstash edge SDK
const stream = new ReadableStream({
async start(controller) {
for await (const m of subscribe('updates')) {
controller.enqueue(`data:${m}\n\n`)
}
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-store',
Connection: 'keep-alive',
},
})
}
The browser's EventSource
patches the <canvas>
in ~50ms worldwide (Upstash's multi‑region POPs).
5.Cold‑start & initial page load
- Path1 – 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.
- Path2 – Progressive tiles — client lazily
GET /api/tile?x=3&y=7
(Edge Function readsGETRANGE
4096B and responds with Uint8Array).
6.Background jobs (Vercel Cron)
Schedule | Task | Details |
---|---|---|
Every 5min | Snapshot → Neon | SELECT publish_snapshot() stores board hash + diff as JSONB. |
Hourly | Analytics | Aggregate pixel_events into leaderboard per user/colour. |
Nightly | Cleanup | DEL or EXPIRE old last:<user> keys to reclaim memory. |
Cron functions are entirely serverless; zero cost when idle.
7.Scaling & limits
Layer | Bottleneck | Mitigation |
---|---|---|
Upstash commands | 100req/s on $50 fixed‑price DB(community.fly.io) | Batch writes (tiles) + local client diffing reduces command rate ~20×. |
Traffic | SSE is unidirectional; outbound only | Browsers auto‑reconnect; no extra Upstash ops. |
Neon connections | HTTP multiplexed driver | Each Edge Function uses a short‑lived fetch, no pool required. |
Storage | Board ≈1MB RAM, pixelevents ≈6GB/day @ 10keps | Neon branch pruning + ZSTD compression. |
Cost example (1keps, 2M MAU):
Service | Tier | Est. cost |
---|---|---|
Vercel | Hobby → Pro | 55 |
Upstash Redis | Fixed‑price 100req/s + 3GB | $50 |
Neon Postgres | 1GB storage, auto‑scale‑to‑0 | $30 |
Total (≈) | 135/mo |
8.Security & abuse protection
- HTTPS only; 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>
; Postgres foreign‑keyboard_id
. - Undo / disputes – keep last colour in Lua script return; 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; no region pinning.
- Ops burden — backups, fail‑over, TLS, and upgrades are delegated to the three SaaS providers.