Files
famapp/CLAUDE.md
2026-07-09 20:46:34 -05:00

200 lines
14 KiB
Markdown

# 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/`](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`.
- Use the drizzle-best-practices skill for any database schema changes, migrations, queries, or ORM usage.
---
## 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.
**Completion rule for module additions and behavior changes:** include the authenticated v1 API and OpenAPI documentation, assistant tools backed by that API, and a complete reminder lifecycle for every user-visible due date. API, agent, and UI must call the same scoped server services; create/update must synchronize unfired reminders and delete must cancel them.
---
## 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_events``calendar_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
```yaml
# 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.com``famapp:3000`. `auth.ginnoir.com``authentik-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/`](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)
---
## Infrastructure repo
Compose stack, Caddyfile, and `.env` for the whole homelab (including famapp) live at:
```
C:\Users\MattC\Documents\homelabstack
```
GitHub: `ginnoir/homelabstack` (private). This is the source of truth for all infra files — **not** `deploy/` in this repo.
When famapp development requires an infra change (new env var, new service, Caddyfile route, etc.):
1. `cd C:\Users\MattC\Documents\homelabstack` and run `sync-prod.ps1` first (pulls current prod state)
2. Edit `docker-compose.yml`, `.env`, and/or `Caddyfile` as needed
3. Run `apply-compose.ps1` (flags: `-Compose`, `-Caddy`, `-EnvFile`) to push to valhalla
4. Commit and push the homelabstack repo
The homelabstack `CLAUDE.md` is the authoritative brief for that repo — read it before editing infra files.
---
## 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/`
- **Infra (compose/Caddyfile/.env):** `C:\Users\MattC\Documents\homelabstack`
- **Memory (cross-session):** `C:\Users\MattC\.claude\projects\C--Users-MattC-Documents-famapp\memory\`