## 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 || [])); ``` **Form action callbacks must call `famStore.applyRecord()`:** ```ts // In use:enhance callback: famStore.applyRecord('collection_name', record, 'create' | 'update' | 'delete'); ``` The `famStore` has reactive `$state` properties and an `applyRecord()` method designed for instant UI feedback from form actions. Calling `applyRecord()` mutates the store directly — the UI updates immediately without waiting for PB SSE. The SSE subscription is a backup for multi-user sync only. 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: `