A family wall calendar for the web. Glance turns a wall-mounted tablet, a spare laptop or any touch screen into a shared noticeboard for one household: today's schedule, the weather, family notes and a photo frame, readable from across the room.
What it's for
- Anyone in the room can see what's happening now and next, today's weather and the latest family notes at a glance — and tap to add or edit an event or a note. Nobody signs in at the screen.
- The account holder (usually a parent) owns the family's single account (one email, magic-link sign-in), signs screens in, picks the background and uploads photos.
- It keeps working when things go wrong. Every screen is local-first: it starts and stays usable offline, queues changes until the server is back, and shows each failing zone (calendar, weather, notes, photos) on its own instead of a blank page.
- It rests when nobody's looking. After a period without touch, or during quiet hours, the screen switches to a dimmed Ambient view.
Views: Today, Calendar, Weather, Notes, Photos, Settings (with Background) and Ambient. Layouts are designed for 1920×1080 and 1280×800 wall screens, laptops, iPad portrait and phones, and the colors follow the four seasons.
Stack
| Area | Choice |
|---|---|
| Framework | Next.js 16 (App Router, run as a SPA), React 19, TypeScript (strict) |
| Styling | StyleX with design tokens and season themes; Geist fonts; Lucide icons |
| Client data | Dexie (IndexedDB) as the local store, sync engine in a Web Worker via Comlink |
| Offline | Serwist service worker (PWA) |
| Validation | Valibot schemas shared by client and server |
| Server | Next.js Route Handlers; storage behind a Database port: PostgreSQL via pg (raw SQL + numbered migrations; PGlite in tests) or PGlite in the process (DB=memory) |
| Photos | sharp (display and thumbnail WebP copies), exif-reader |
| Weather | Open-Meteo forecast and geocoding (no API key) |
Sign-in links through Resend, Mailtrap or Mailgun over HTTP, any SMTP server via nodemailer, or a local Mailpit |
|
| Tooling | npm, Node 24, Biome (lint + format), Vitest + Testing Library + fake-indexeddb |
Architecture
Browser (one per screen) Next.js server (one Node process)
┌──────────────────────────────────────────┐ ┌─────────────────────────────────────┐
│ Screens (features/*) │ │ src/app/api/**/route.ts │
│ read ─▶ data hooks (useLiveQuery) │ │ session → Valibot → service │
│ write ─▶ repos ─▶ Dexie (row + outbox) │ │ server/services ─▶ PostgreSQL │
│ ▲ │ HTTPS │ server/bus ─▶ SSE per account │
│ Sync worker ───────────┘ │◀────────▶│ server/jobs (expiry, link cleanup) │
│ push outbox · pull changes · SSE/poll │ cookie │ server/weather · server/mail │
│ Service worker: app shell + photo cache │ │ DATA_DIR: photos │
└──────────────────────────────────────────┘ └─────────────────────────────────────┘
- Local-first. Screens never call the network for app data. They read IndexedDB through hooks and write through repos, which save the row and an outbox entry in one transaction. A sync worker pushes the outbox, pulls versioned changes and listens on an SSE stream (polling every 30 s as a fallback). The last write to reach the server wins.
- Pure business rules. Event status, the Today hero, note expiry, seasons, time formats and so on are pure functions in
src/domain, of(data, now, settings). Derived values are computed, never stored. - Thin server. Route Handlers only check the session (and
Originon writes), validate with Valibot, call a service and map itsResultto HTTP. Services scope every query by account. - Enforced boundaries. Biome import rules keep each module in its lane (e.g.
uican't touch data,servercan't import client code).
src/app/ thin route files (views, API Route Handlers, service worker)
src/domain/ schemas, types and pure business rules
src/data/ Dexie store, repos and hooks, sync engine and worker, transports
src/features/ one folder per view: Screen container + pure components
src/ui/ shared design-system components
src/shell/ app shell, nav bar, providers (now, settings, season, network, idle)
src/theme/ StyleX tokens, season themes, fonts
src/server/ server-only: db, auth, services, weather, mail, jobs, SSE bus
src/fixtures/ shared test data
src/copy.ts every user-facing string
db-migrations/ PostgreSQL schema
docs/ spec, technical design, plan and design handoff
Development
Requirements
- Node 24 (see
.nvmrc) and npm - PostgreSQL (any recent version) running locally, with a
glancedatabase:createdb glance. Tests don't need it (they run on PGlite). Without it, setDB=memoryin.env.development.local: an in-process PGlite holds the data, gone on restart - Mailpit (
brew install mailpit, thenmailpit): the dev server sends sign-in emails to it over SMTP on port 1025; read them at http://localhost:8025
Get started
npm ci
npm run dev
Open http://localhost:3000. The dev server reads .env.development (committed: DATABASE_URL=postgres://localhost:5432/glance, photos in .data/, git-ignored, mail to Mailpit with MAIL_PROVIDER=mailpit), then .env.development.local (git-ignored) for a different DATABASE_URL or mail provider (.env.example lists every variable). Migrations run on boot; without a reachable database the server logs the error and stops.
The dev server runs the real app: sign in on /signin and read the code in Mailpit at http://localhost:8025. /dev/gallery shows every component in all four seasons. The server still checks its config at boot; a missing or bad variable stops it with Invalid server config: and the variable names.
Real sign-in and offline
The service worker and the offline cold start only run in a production build. Run npm run build, then npm start with DATABASE_URL, DATA_DIR (an empty folder), APP_URL=http://localhost:3000, MAIL_FROM and a mail provider: MAIL_PROVIDER=mailpit sends to Mailpit on localhost:1025, or RESEND_API_KEY alone uses Resend (with MAIL_FROM on a domain verified in Resend, or Glance <onboarding@resend.dev>, which only delivers to your Resend account's own email). Request a link on /signin and open it from the email. Browsers treat localhost as secure, so the service worker and the offline cold start work there too.
Commands
| Command | What it does |
|---|---|
npm run dev |
Dev server (real sign-in and sync, no service worker) |
npm run check |
Biome lint and format check |
npm run typecheck |
tsc --noEmit |
npm test |
Vitest (unit, component, data and sync tests) |
npm run build |
Production build |
npm start |
Serve the production build (needs DATABASE_URL, DATA_DIR, APP_URL, MAIL_FROM and the mail provider's variables) |
CI (run by hand from the Actions tab) runs check → typecheck → test → build. A pre-commit hook in .githooks/ is installed by npm ci.
Documentation
docs/product-spec.md— what the app does, with requirement IDs (TODAY-02,SYNC-03…)docs/tech-design.md— decisions, architecture, sync protocol, server designdocs/execution-plan.md— tasks and statusCLAUDE.md— coding and workflow rules for contributors