Architecture
How the demo is organized, from file layout to database schema.
Project structure
src/
+-- hooks.ws.js -- WebSocket lifecycle (identity, presence, cursors)
+-- hooks.server.js -- DB migration, stress board, error handler
+-- app.html -- HTML shell with Svelte favicon
+-- app.css -- Tailwind + DaisyUI setup
+-- routes/
| +-- +layout.svelte -- Navbar: identity, online count, colors, GitHub, theme
| +-- +layout.server.js -- Identity cookie: read or generate
| +-- +page.svelte -- Home: board list + create form + TTL hint
| +-- board/[slug]/
| +-- +page.svelte -- Board: canvas, notes, FAB, undo/redo, rate limit toast
| +-- +page.server.js -- Resolve slug -> board_id
+-- lib/
| +-- names.js -- Random name/color/slug generator
| +-- server/
| | +-- db.js -- Postgres + in-memory (touch, delete, stale cleanup)
| | +-- validate.js -- Input validation (UUID, bounds, allowlist)
| | +-- redis.js -- Redis client, pub/sub, rate limiter, presence, cursors, breaker
| +-- components/
| +-- StickyNote.svelte -- Draggable note: edit, color, delete, z-order, touch
| +-- Canvas.svelte -- Board area: pointer tracking with rAF throttle
| +-- CursorOverlay.svelte -- Canvas 2D cursor rendering with bitmap label cache
| +-- PresenceBar.svelte -- Avatars with maxAge (8 desktop, 1 mobile, +N overflow)
| +-- ActivityTicker.svelte -- Bottom bar: 5 most recent actions
| +-- BoardHeader.svelte -- Title edit + background picker + TTL countdown
| +-- BoardCard.svelte -- Board list item with presence badge + countdown
| +-- CountdownTimer.svelte -- DaisyUI countdown with color urgency
+-- live/
+-- boards.js -- Board CRUD + stream + cleanup cron (1h TTL)
+-- boards/
+-- notes.js -- Note CRUD + batch arrangements + board touch
+-- activity.js -- Activity feed (ephemeral, latest merge)
+-- settings.js -- Board settings (set merge)
+-- cursors.js -- Presence join/leave + cursor movement Database schema
Two tables. No users, no sessions. Identity lives in a cookie. Activity is ephemeral.
CREATE TABLE board (
board_id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
title text NOT NULL,
slug text NOT NULL UNIQUE,
background text DEFAULT '#f5f5f4' NOT NULL,
last_activity timestamptz DEFAULT now() NOT NULL,
created_at timestamptz DEFAULT now() NOT NULL
);
CREATE TABLE note (
note_id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
board_id uuid NOT NULL REFERENCES board (board_id) ON DELETE CASCADE,
content text DEFAULT '' NOT NULL,
x integer DEFAULT 200 NOT NULL,
y integer DEFAULT 200 NOT NULL,
color text DEFAULT '#fef08a' NOT NULL,
creator_name text NOT NULL,
z_index integer DEFAULT 0 NOT NULL,
is_archived boolean DEFAULT FALSE NOT NULL,
created_at timestamptz DEFAULT now() NOT NULL
); Full schema including indexes and auto-archive trigger in schema.sql.
Identity system
No login. Every visitor gets a random two-word name (like “Cosmic Penguin”) and a random color, stored in a cookie. Same tab, new tab, page reload - same identity. New incognito window - fresh identity.
The cookie is validated on both the WebSocket upgrade path (hooks.ws.js) and the HTTP layout load (+layout.server.js). Only cookies with a valid UUID, a name between 1-40 characters, and a valid hex color are accepted.
900 possible name combinations (30 adjectives x 30 nouns). Collisions are harmless - names are for display only, the UUID is what matters.
Board lifecycle and TTL
Boards are ephemeral by design. Every board starts with a 1-hour countdown. Any meaningful action (create/edit/delete a note, change settings, run an arrangement) resets the timer. Boards with no activity for 1 hour are deleted automatically by a server-side cron job.
The stress-me-out board is exempt - it’s auto-created on startup and never expires. The E2E stress tests use it.
Countdown timers are visible on every board card (home page) and in the board header. They use the DaisyUI countdown component and change color as the deadline approaches: neutral over 10 min, warning 5-10 min, error under 5 min.
Was this page helpful?