Files
famapp/AGENTS.md
T
ginnoir 6183fb62c8
CI / checks (push) Failing after 2m8s
CI / build (push) Successful in 4m42s
fix: dashboard edit previews, migration journal, and notes comments
Register drizzle journal entries for 0022/0023 so prod migrations apply.

Key edit-mode widget previews by index so bangs.stats shows live data.

Add comments to notes detail pages.
2026-07-04 22:39:21 -05:00

12 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.

Codex and Claude Code both work on this project. Keep AGENTS.md, CLAUDE.md, STATUS.md, task briefs, and dev notes synchronized so either agent can pick up the next task without relying on agent-specific memory.

Regular agent skills

  • Use the React best-practices skill for any React or Next.js page/component work, data-loading changes, bundle/performance work, or review of those areas.
  • Use the shadcn skill for any UI work involving shadcn components, Tailwind styling, overlays, forms, icons, component composition, or updates to components.json / src/components/ui.

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 24 LTS In node:24-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. Edit mode (?edit=1) must pre-render live widget content (task 81): server-side DashboardWidgetContent per placement, keyed by index in widgetContents; render loads real data; never show meta-description placeholders for saved placements.
  • 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 Codex and Claude Code 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:24-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).
  • Teardown after validation. After build/test/E2E validation, stop any dev servers or compose services started for the task unless the user explicitly asks to keep them running. This avoids stale instances and port conflicts in later sessions.

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: AGENTS.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\.Codex\projects\C--Users-MattC-Documents-famapp\memory\