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?