Files
famapp/CLAUDE.md
T
ginnoir 09d6adbf18 Calendars as entities, uniform widget contract, multi-dashboard tasks
Architecture refinements before module work begins:

- Calendars become first-class entities (mirror of lists). Each
  calendar has its own visibility (private | household) and is
  independently shareable. Updated CLAUDE.md data model and task 10.

- Drop the singleton/parameterized split for dashboard widgets. Every
  widget declares a configSchema + defaultConfig + resolveConfigOptions
  and every placement is an independent instance — same widgetId can
  appear multiple times on one dashboard pointed at different things.
  Updated task 04 contract; tasks 11/12 widget sections aligned.

- Add tasks 25 (multiple dashboards per user) and 26 (customizable
  layout + widget configuration). Replaces the previously-considered
  three-task plan with a cleaner two-task split that the uniform
  contract enables. Phase 3 index updated; STATUS records the rationale.

- Task 20 reframed as a single-dashboard MVP that exercises the new
  contract end-to-end before the customization layer lands.
2026-05-06 01:14:49 -05:00

11 KiB

famapp

Self-hosted family coordination web app for Matt and his wife. Replaces a commercial family app with a shared calendar, shopping/task lists, and notes/reminders. Designed to be deliberately extensible — Matt expects to bolt on niche features over time, so the architecture treats every feature as a module.

This file is the canonical brief. Read it at the start of every session before making changes. Sub-task briefs in docs/tasks/ reference this file; do not duplicate its contents there.


Goals

  • Replace current family app: shared calendar, shopping list, task list, notes/reminders.
  • Self-hosted on Matt's home server, accessible from outside the house with auth.
  • Wife-friendly: installable PWA, passkey/SSO login, no extra apps to install for notifications.
  • "Share anything" — most entities can produce a temporary public link for outsiders.
  • Architecture optimized for adding many small features later without core changes.

Stack (decided)

Layer Choice Notes
Frontend + backend Next.js 15 App Router + TypeScript Single Node process, server actions for mutations
UI Tailwind + shadcn/ui
DB Postgres 16 Dedicated container per app (Matt's rule)
ORM Drizzle Lightweight, SQL-friendly, good for evolving schema
Auth/SSO Authentik at auth.ginnoir.com Picked over Pocket-ID because it ships forward-auth (Proxy Provider) for the rest of Matt's stack (Sonarr/Radarr/Prowlarr/NZBGet/qBittorrent/Tautulli/Overseerr/FreshRSS/ntfy) which has no native OIDC. famapp itself uses OIDC.
Push Web Push (VAPID) Works in installed PWAs incl. iOS Safari 16.4+. ntfy stays optional.
Realtime Postgres LISTEN/NOTIFY → SSE Simpler than WebSockets, fine for two users
Reverse proxy Caddy (existing) fam.ginnoir.com → famapp:3000, auth.ginnoir.com → authentik
Package manager pnpm Fast, content-addressed store, strict deps
Runtime Node 22 LTS In node:22-alpine container
Host Ubuntu Server 24.04 x64, Docker + Compose Existing self-host stack

Domain: fam.ginnoir.com. SSO: auth.ginnoir.com.


Architecture: extensibility-first

Every feature is a self-contained module under src/modules/<name>/. A module declares its tables, routes, dashboard contributions, quick-add actions, and supported share/reminder/search behaviors via a manifest. Core services (sharing, push, activity log, search, reminders) operate generically against any registered entity — adding a new module should not require touching core code.

src/
  modules/
    _core/                   # auth, household, sharing, push, activity log, reminders
    calendar/
      schema.ts              # drizzle tables
      server/                # server actions + queries
      components/
      routes.ts              # registers /calendar pages + API
      dashboard.tsx          # widget contributed to dashboard
      manifest.ts            # name, icon, nav, entity types, share/reminder handlers
    lists/
    notes/
  app/                       # Next.js routes — thin, mounts modules
  lib/

Core primitives every module gets

  • Entity registry. Modules declare entity types; share-link, activity log, search, reminders all work against any registered entity.
  • Dashboard widget registry. Every widget is uniformly configurable (no singleton/parameterized split) and reusable — each placement on a dashboard is an independent instance with its own config. Each user has multiple dashboards; the active dashboard composes whatever widgets they've placed.
  • Quick-add registry. Modules register quick actions for the dashboard's + menu.
  • Share-link service. createShareLink(entityType, entityId, { expiresAt, capabilities })fam.ginnoir.com/s/<token>. Generic.
  • Notification bus. notify(userId, { title, body, url }) fans out to web push + in-app + (optional) ntfy.
  • Permissions. Household-scoped by default. Share-tokens grant scoped read/write per entity.
  • Feature flags. Env/db-driven for staging experiments.

Rule for contributors (including future Sonnet sessions): if a feature requires a change to _core to support a new entity type, that's a smell — extend the registry instead.


Data model (v1)

  • users, households, household_members
  • calendars — name, color, owner_id, visibility (private | household). First-class entity; users create as many as they want, each independently shareable via the share-link service.
  • calendar_eventscalendar_id fk, title, start, end, all_day, location, notes, owner. rrule text column reserved (no recurrence in v1). external_source/external_id nullable for future Google/Apple sync.
  • lists (type text — not enum, to allow extension), list_items
  • notes — title, body, pinned, remind_at
  • dashboards — per-user named dashboards (any user can have many), layout jsonb of placed widgets [{ widgetId, config, x, y, w, h }].
  • share_links — entity_type, entity_id, token, capabilities jsonb, expires_at
  • activity_log — entity_type, entity_id, actor, action, payload jsonb
  • push_subscriptions
  • reminders — entity_type, entity_id, fire_at, channel. Generic; used by notes/events/anything.

All household-scoped entity tables include household_id. Sub-entities (calendar_events, list_items) inherit scope via their parent. All reads/mutations are gated by the caller's household membership and, where relevant, per-entity visibility (e.g. private calendars).


Pages (v1)

  • / Dashboard — today + next 3 days events, top of shopping list, open tasks, pinned notes, recent activity, quick-add +
  • /calendar — month/week/day, drag-create
  • /lists/shopping, /lists/tasks — defaults; create more
  • /notes
  • /s/<token> — public share viewer
  • /settings — household, push toggles, share-link management

Deploy shape

# compose.yaml (sketch — final lives in /deploy)
services:
  famapp: # node:22-alpine, Next.js
  famapp-db: # postgres:16
  authentik-server:
  authentik-worker:
  authentik-db: # postgres:16 (separate per Matt's rule)
  authentik-redis:

Caddy: fam.ginnoir.comfamapp:3000. auth.ginnoir.comauthentik-server:9000.

Backups: nightly pg_dump to a host volume. Out of scope for phase 1 but reserve the cron slot.


Build phases

  1. Scaffold — repo init, Next.js + Drizzle + Tailwind + shadcn, module loader, compose stack, Caddy snippet, Authentik wired up, seeded household with both users.
  2. Core modules — calendar, lists, notes (CRUD only).
  3. Dashboard + quick-add + activity log.
  4. Sharing — share-link service + /s/<token> viewer.
  5. Push notifications + reminders.
  6. PWA polish — manifest, icons, offline shell, install prompt.
  7. Hardening — backups (pg_dump cron), rate limiting on share links, logging.

Tasks for each phase live in docs/tasks/. Sub-sessions should pick up a task file, follow its scope, and stop at its acceptance criteria.


Conventions

  • TypeScript strict. No any without a written reason.
  • Server actions for mutations; route handlers only for webhooks/SSE/share-link viewer.
  • Drizzle migrations committed under drizzle/. Never edit a shipped migration — add a new one.
  • No comments unless the why is non-obvious. Names should carry intent.
  • Module isolation. A module imports from _core and lib/ only — never from a sibling module.
  • One module = one PR/commit boundary when possible.
  • Tests: Vitest for units (where it pays off), Playwright for one happy-path E2E per module. Don't write tests for trivial CRUD.
  • Secrets via .env (gitignored) and .env.example (committed, no values).

Out of scope for v1 (do not build)

  • ICS export / Google / Apple / Outlook calendar sync (data model leaves room)
  • Recurrence editor (column reserved, no UI)
  • Native iOS/Android app (PWA only; architecture must not preclude it)
  • Migration from any existing app (starting fresh)
  • Forward-auth wiring for the rest of Matt's stack (Authentik is installed; wiring the *arr apps is a separate later task)
  • Multi-tenant / multi-household (schema has household_id but UI is single-household)

Where things live

  • This brief: CLAUDE.md
  • Task briefs for sub-sessions: docs/tasks/NN-name.md
  • Architecture decisions worth preserving: docs/decisions/NNNN-title.md (lightweight ADR — only when a non-obvious choice is made)
  • App code: src/
  • Drizzle migrations: drizzle/
  • Deploy: deploy/ (compose.yaml, Caddyfile snippet, Authentik bootstrap)
  • Memory (cross-session): C:\Users\MattC\.claude\projects\C--Users-MattC-Documents-famapp\memory\