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.
176 lines
12 KiB
Markdown
176 lines
12 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`.
|
|
|
|
---
|
|
|
|
## 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_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)
|
|
|
|
---
|
|
|
|
## 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\`
|