## Project Configuration
- **Language**: TypeScript
- **Package Manager**: pnpm
- **Add-ons**: prettier, tailwindcss, sveltekit-adapter, experimental
---
# FamChore v2 — AI Agent Reference
## Stack
- SvelteKit (SSR frontend, internal :2080) + Hono proxy (internal :3456) + nginx (container :3001)
- PocketBase (separate Coolify service at `pb.chores.app.com`, :8090)
- Stripe one-time donations
- Coolify CRON → `GET /api/weekly-cron`
- Deployment: Coolify, Cloudflare DNS
## Auth
| Role | Auth | Session | Record in |
| ---------------- | -------------------------- | --------------------------- | -------------------- |
| Admin (parent) | PB email+pass | 24hr JWT | `fam_admins` |
| Member (child) | Invite code + device token | `device_token` cookie only | `members` |
- **Admins** (parents) have a PB auth record + `fam_admins` record. They authenticate via email/password login, get a session cookie (`session`) with `{ famId, userId, famSlug, memberName, role: "parent" }`.
- **Members** (children) exist only in the `members` collection. They authenticate via invite code + device token (SHA-256 hashed). The `device_token` cookie is set on join; no session cookie.
- **Platform superuser** (`_superusers`) used only server-side by `pb-admin.ts` for cross-family queries (e.g. `/admin` stats dashboard). Not an app role.
- The layout (`[fam]/+layout.server.ts`) derives `isParent` and `role` centrally from the session cookie — child pages use `page.data.isParent` or `page.data.role` from `$app/state`.
## PB Collections (all scoped by `famId`)
- `fams` — name, slug, inviteCode, stripeCustomerId, featureFlags
- `members` — famId, name, color, deviceToken(hashed), deviceTokenHint
- `chore_templates` — famId, name, defaultValue, defaultFrequency
- `assigned_chores` — famId, memberId, templateId, frequency, value
- `completions` — famId, memberId, assignedChoreId, date
- `weekly_history` — famId, memberId, weekStart, pointsEarned, moneyEarned
- `rewards` — famId, memberId, source, label, value, claimed, claimedAt
- `monthly_bonuses` — famId, month, prizeType, prizeValue, winnerMemberId
- `settings` — famId, pointsThreshold, weeklyBonus, webhookUrl
## Routes
```
/ Landing (SaaS marketing)
/admin Admin panel - statistic dashboard, and any donations made
/join/:code Member invite code
/join/:code/:member Member invite with pre-selected member
/{fam} Fam dashboard
/{fam}/admin Admin panel
/{fam}/:username Member kanban & admin dashboard (role determined by session)
/{fam}/:username/preferences User preferences (admin→fam_admins, member→members)
/{fam}/:username/settings Family admin settings (session required)
/{fam}/:username/chores Chore templates & assignment grid
/{fam}/:username/rewards Rewards overview
/{fam}/:username/bonuses Bonus configs & evaluation
/api/* Hono proxy (webhooks, CRON)
```
## Data Flow
- **Chore toggle:** Browser → Hono proxy → PB (auth via device token or admin JWT)
- **Admin CRUD:** Browser → Hono proxy → PB (admin JWT)
- **Reward creation:** After completion toggle, Hono proxy creates reward if threshold met
- **Weekly CRON:** Coolify → `GET /api/weekly-cron` on Hono → Hono queries PB, computes summaries, upserts weekly_history
- **Stripe donate:** Browser → Hono `/api/stripe/create-checkout` → Stripe → Hono webhook → update fam
- **WhatsApp:** Deferred — Hono CRON handler has pluggable notification interface
- **UI reactivity:** Svelte `$state` / `$derived` / `$effect` — no PB SDK `.subscribe()` / SSE
## Env Vars (SvelteKit 3.0.0-next.4)
Env vars must be declared in `frontend/src/env.ts` using `defineEnvVars` from `@sveltejs/kit/hooks`:
```ts
import { defineEnvVars } from '@sveltejs/kit/hooks';
export const variables = defineEnvVars({
DEBUG_RECORD_ID: {},
SERVER_IP: {public: true},
PB_PORT: {public: true}
});
```
Only vars with `{public: true}` are exposed to client-side code via `$app/env/public`. If you need a new client-side env var (e.g., `PUBLIC_PB_URL`), you must:
1. Add it to `.env` with `PUBLIC_` prefix
2. Add it to `frontend/src/env.ts` with `{public: true}`
The actual values come from `.env` (symlinked from project root at `frontend/.env -> ../.env`). Server-only env vars (no `{public}` flag) are available via `$app/env/private` but only in server modules.
## ⚠️ CRITICAL: Reactivity Pattern (NEVER use `$effect` to sync from famStore)
**Do NOT do this:**
```svelte
let items = $state(data.items);
$effect(() => {
if (data.items?.length) items = data.items;
if (famStore.initialized && famStore.items.length) items = famStore.items;
});
```
**Do this instead:**
```svelte
let items = $state(famStore.initialized ? famStore.items : (data.items || []));
```
## Update Patterns
Two patterns based on who's acting:
| Pattern | Who | Frequency | Sensitivity | Optimistic? | Auth |
|---------|-----|-----------|-------------|-------------|------|
| Direct `fetch` + `memberApi` | Member | High (chore toggles) | None | Yes (instant UI, reconcile on response) | `x-device-token` header |
| Form action | Admin | Low (CRUD) | High (settings, members) | No — form is server-side, wait for round trip | httpOnly `session` cookie |
**Member direct fetch** — optimistic UI via local state mutation, reconciled on response:
```svelte
let completions = $state(data.completions)
async function toggle(chore) {
// optimistic update
completions = [...completions, { id: 'optimistic-...', ... }]
try {
await memberApi.toggleCompletion(token, famId, chore.id, date)
// reconcile — remove optimistic, keep server truth
} catch { /* revert */ }
}
```
**Admin form actions** — no optimistic `applyRecord` needed in `use:enhance` callbacks. The form action is a server round trip, and PB SSE pushes the change back through `famStore.handleRealtime()` within milliseconds. The famStore + SSE subscription is the single source of truth for cross-user sync. Do NOT add `$effect` watchers to bridge the gap between form actions and reactive state.
## UI Component Conventions
All admin and member pages use the following pattern:
```svelte
```
- **Layout shell** lives in `[fam]/+layout.svelte` — Sidebar, TopNav, Footer. All fam-scoped routes inherit it.
- **Sidebar** role-aware: shows admin CTAs on `/admin/*`, member CTAs on `/[username]/*`.
- **TopNav** has `announcement` (center slot) and `actions` (right slot).
- **`Card` `cols` prop**: `1 | 2 | 3` — spans that many columns in the 3-column `CardGrid`.
- **`Button`** for all CTAs: `