E-ink "door sign" for hotel rooms at furry conventions. Roommates show their fursonas, handles, and rough whereabouts; passersby see a sanitized view; QR + per-roommate passcode unlocks more.
Production: cons.social.
- Frontend — SvelteKit 2 + Svelte 5 (runes), TypeScript, Vite,
@sveltejs/adapter-cloudflare. Deploys to Cloudflare Pages on the apex; Pencil mockups source the design. - Backend — Cloudflare Worker (Hono, TypeScript), routed at
cons.social/api/*. - Database — Cloudflare D1 (SQLite at the edge).
- Sessions — Cloudflare KV (for revocation).
- Rate limiting — Workers Rate Limiting binding + Cloudflare zone rule + Turnstile after repeated failures. No IP-based blocking (hotel NAT).
- E-ink device — dumb HTTP client, fetches a server-rendered image with a bearer token. Hardware-agnostic.
- Cons list — daily cron sync of the furrycons.com ICS feed.
apps/
worker/ Cloudflare Worker — API, visitor flow, device sign render,
ICS cron. See apps/worker/README.md for setup.
web/ Pencil.dev frontend (placeholder; toolchain TBD).
packages/
shared/ Zod schemas + the privacy projection (projectRoommate).
The contract the UI imports.
docs/
mockups/ Pencil.dev .pen files (open via the Pencil app + MCP).
PLAN.md Canonical design doc — schema, auth, API surface, scope.
Read this before re-deriving any architectural decision.
Mockups live in docs/mockups/pencil-new.pen (open via the Pencil app). The
design system frame defines a monochrome ink-on-paper palette shared between
the e-ink panel and the web UI.
- E-Ink panel (800×480) — Sign Render, Unpaired (shows pairing code), Token Revoked.
- Visitor (mobile) — Locked list, Passcode Sheet, Unlocked detail.
- Login — Desktop and Mobile, each with a BSky-disabled launch variant (Telegram-only until BSky OAuth lands).
- Setup Wizard — Pick a Con → Name Room → Invite Roommates → All Set.
- Admin — Dashboard, Roommate Editor (Desktop + Mobile), Device Pairing flow, Paired Devices settings.
Each field on a roommate has a minimum tier required to see it:
- guest — anyone who scanned the room QR (proof-of-presence at the door).
- personal — entered that roommate's per-roommate passcode.
- private — admin / the roommate themselves. Default for new fields.
Unlocks are additive across roommates and rotation-aware (rotating a passcode invalidates only that roommate's unlock).
pnpm install
pnpm -r test # 54+ tests, Miniflare pool
pnpm -r typecheck
pnpm -r buildPer-package scripts:
apps/worker—pnpm --filter @con-sign/worker dev(Wrangler local). Setup, secrets, and D1/KV provisioning live inapps/worker/README.md.apps/web— SvelteKit Pages app (pnpm --filter @con-sign/web dev). Dev server proxies/api/*to a locally-run Worker.packages/shared— pure TS, no build step (consumed via workspacemain).
- Worker deploys via the GitHub Actions workflow under
.github/. - Pages project
con-signowns the apex; the Worker is bound tocons.social/api/*(seeapps/worker/wrangler.toml).
Live at https://cons.social — Coming Soon splash is the first page; the rest of the UI lands incrementally.
Backend is end-to-end working: BlueSky + Telegram login, per-roommate
passcodes, room/roommate CRUD, visitor unlock + cookie projection, daily
ICS sync (151 cons live), pair-code device bootstrap (firmware UUID is
the bearer; admin types a 6-char OTP shown on the panel; one endpoint
dispatches unpaired/paired/revoked render), audit log of admin actions,
and a security pass covering rate limiting, TOCTOU race in claim, CSRF
Origin enforcement, JWKS rotation, and an audit trail (see
docs/security-todo.md for the resolved items + remaining
designed-as-accepted notes).
Frontend (SvelteKit) is scaffolded and shipping pages: splash is live;
/login is next. First hardware target picked: TRMNL — see
docs/devices/trmnl/PLAN.md. Server-side PNG render is now live
(?fmt=png on the device endpoint, rasterized via resvg-wasm and
edge-cached for 60 s); the TRMNL adapter endpoints (Mode B BYOS) are
the next backend step.
See PLAN.md, docs/api.md, and git log for the latest.