## 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 ### Reads (both roles) - **Parent (admin):** `famStore.init()` fetches all collections via PB SDK (authenticated via `pb_token` cookie). - **Child (member):** `famStore.init()` fetches all collections via PB SDK — **unauthenticated/anonymous**. All family-scoped collections have public `listRule` / `viewRule` (empty string = allow all), so reads work without any auth. PB SDK `.subscribe()` also works anonymously for public collections. - **TopNav season pills:** Read from `famStore.seasons` — reactive, no extra fetches needed. ### Writes (both roles go through Hono proxy) - **Chore toggle:** Browser → Hono proxy → PB (auth via device token or admin JWT) - **Admin CRUD:** Form actions → Hono proxy → PB (admin JWT via `sessionHeaders`) - **Member updates:** Browser → Hono proxy → PB (auth via `x-device-token` + `x-device-famid`) - **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` - PB SDK `.subscribe()` for realtime multi-user sync (public reads → works for both roles) - `famStore.applyRecord()` for instant optimistic UI feedback from form actions ## 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: `