14 KiB
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 — not implemented (only
settings.webhookUrlexists) - Coolify CRON →
GET /api/weekly-cron— not implemented (weekly settlement is manual viacomplete-week/simulateEow) - Deployment: Coolify, Cloudflare DNS
Auth
| Role | Auth | Session | Record in |
|---|---|---|---|
| Admin (parent) | PB email+pass | 24hr JWT pb_token cookie |
users (role parent) |
| Member (child) | Invite OTP + server-derived password | httpOnly pb_token cookie |
users (role child) |
| Superuser | PB _superusers (server-side only, pb-admin) |
— | — |
- Admins (parents) are
usersrecords (roleparent). They authenticate via email/password login, get an httpOnlypb_tokencookie with{ id, name, username, role: "parent", famId, color }. - Members (children) are
usersrecords (rolechild); PBusername={famSlug}:{handle}(globally-unique auth identity;handle= whitespace-free lowercase name), URL segment =handleOf(username),name= display name. Their PB password is derived server-side (MEMBER_SECRET + famSlug + handle); access is gated by a 20-min OTP inuser_configs, thenauthWithPassword. They get the same httpOnlypb_tokencookie. There is nomemberscollection. - Platform superuser (
_superusers) used only server-side bypb-admin.tsfor cross-family queries (e.g./adminstats dashboard) and OTP/signup writes. Not an app role. - The layout (
[fam]/+layout.server.ts) derivesisParentandrolecentrally from the session — child pages usepage.data.isParentorpage.data.rolefrom$app/state. - Because
pb_tokenis httpOnly, the browser PB SDK is seeded frompage.data.pbTokenviainitPb(token)in the layoutonMount(notdocument.cookie).
PB Collections (all scoped by famId; child/member = users row)
users— auth collection; famId, role (parent|child), username ({famSlug}:{handle}), name, color, email (admin only)user_configs— famId, userId, otp, colour, updatedAt (OTP gate for child join)fams— name, slug, stripeCustomerId, featureFlagschore_templates— famId, name, defaultValue, defaultFrequencyassigned_chores— famId, userId, templateId, frequency, valuecompletions— famId, userId, assignedChoreId, dateweekly_history— famId, userId, weekStart, pointsEarned, moneyEarnedrewards— famId, userId, source, label, value, claimed, claimedAt, claimable, settleDatemonthly_bonuses— famId, month, prizeType, prizeValue, winnerUserIdsettings— famId, pointsThreshold, weeklyBonus, webhookUrl
Routes
/ Landing (SaaS marketing)
/admin Platform super-admin stats dashboard (and any donations)
/login · /logout Parent email/password login / logout
/signup Parent + family signup
/{famSlug}/join/{username} Member invite (OTP join), auto-fills from ?code=
/{fam} Fam dashboard
/{fam}/{username} Parent → admin overview, Child → member kanban (role from session)
/{fam}/{username}/chores Chore templates & assignment grid
/{fam}/{username}/ledger Rewards / chores / todos ledger
/{fam}/{username}/bonuses Bonus configs & evaluation
/{fam}/{username}/preferences User preferences (parent→users, member→users)
/{fam}/{username}/settings Family admin settings (parent only)
/api/* Hono proxy (data layer; webhooks/CRON not implemented)
Data Flow
Reads (both roles)
- Parent (admin):
famStore.init()fetches all collections via PB SDK (authenticated via thepb_tokencookie / seededinitPb(token)). - Child (member):
famStore.init()fetches all collections via PB SDK — authenticated via theirpb_token(rolechild). Family-scoped collections have publiclistRule/viewRule, so reads work regardless;.subscribe()works for both roles. - TopNav season pills: Read from
famStore.seasons— reactive, no extra fetches needed.
Writes
- Chore toggle: Browser → Hono proxy → PB (member auth via
Authorization: Bearer <pb_token>) - Admin CRUD: Form actions /
hono.admin.*→ Hono proxy → PB (admin JWT viasessionHeaders) - Member updates: Browser → Hono proxy → PB (auth via
Bearer <pb_token>) - Reward creation: After completion toggle, Hono proxy creates reward if threshold met
- Weekly settlement: NOT via CRON — manual
complete-weekaction orsimulateEowpreview in settings./api/weekly-cron(Coolify) is not implemented. - Stripe / WhatsApp: not implemented — only the
settings.webhookUrlfield exists.
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:
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:
- Add it to
.envwithPUBLIC_prefix - Add it to
frontend/src/env.tswith{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:
<!-- ❌ WRONG: $effect syncing local state from famStore fights Svelte reactivity -->
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:
<!-- ✅ RIGHT: Initialize from famStore, mutate via applyRecord in callbacks -->
let items = $state(famStore.initialized ? famStore.items : (data.items || []));
⚠️ CRITICAL: $state vs $derived for famStore collections
When a page reads data that should update via SSE (realtime from other users/actions), you MUST use $derived — NOT $state.
This is the most commonly missed rule. Agents repeatedly initialize collections from famStore with $state, which captures a frozen snapshot at init time. When PocketBase pushes a record via SSE → famStore.applyRecord(), the $state variable never reacts — the UI stays stale until a page refresh.
Rule: if the data should update without a refresh, use $derived.
// ❌ WRONG — frozen snapshot, won't react to SSE updates
let configs = $state(famStore.initialized ? famStore.bonusConfigs : data.configs);
// ✅ RIGHT — reactive, updates when famStore changes via SSE
let configs = $derived(
famStore.initialized ? famStore.bonusConfigs : (data.configs || [])
);
The only exception: high-frequency member actions (like chore toggles) that need optimistic UI before the server responds. Those use $state + famStore.applyRecord() for instant feedback, then reconcile on the server response. See applyRecord pattern below.
All other pages (admin CRUD, bonuses, rewards, settings, etc.) must use $derived so that SSE updates flow through famStore → $derived → UI automatically.
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) | Authorization: Bearer <pb_token> |
| Form action | Admin | Low (CRUD) | High (settings, members) | No — form is server-side, wait for round trip | httpOnly pb_token cookie |
Member direct fetch — optimistic UI via local state mutation, reconciled on response:
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:
<ViewHeader title="..." subtitle="..." tabs={...} weeknav={...} sort={...} />
<CardGrid>
<Card {cols} title="..." accent="...">
<!-- card content → micro-layout per page -->
</Card>
</CardGrid>
- 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) andactions(right slot). Cardcolsprop:1 | 2 | 3— spans that many columns in the 3-columnCardGrid.Buttonfor all CTAs:<Button variant="primary|secondary|ghost|danger" size="sm|md|lg">.Accordionfor expand/collapse sections (settings, logs).- Icons: defined as SVG strings in
lib/components/icons.ts. No icon library dependency. - Components live in
frontend/src/lib/components/and are re-exported fromindex.ts.
Conventions
- Every collection query includes
famId = @request.auth.famIdfilter - Super admin bypasses famId filter (access via PB admin API)
- Child PB passwords are derived (
MEMBER_SECRET + famSlug + username); the child join gate is a transient OTP inuser_configs. No device tokens. Never log raw tokens/secrets. - Admin → Proxy:
hono.admin.*in$lib/server/hono.ts— usessessionHeaders(event)(server-side only, requiresRequestEvent) - Member → Proxy (server):
memberApi.*in$lib/client/api.ts— use inside+page.server.tsload/actions;BASE_URLresolves to Hono port on server - Member → Proxy (browser):
memberApi.*in$lib/client/api.ts— use inside+page.svelte;BASE_URLis empty, Vite proxies/api/*to Hono $page: import{ page }from$app/state(NOT$app/stores— that's the old Svelte 4 API). Reference aspage.params.fam,page.url.pathnameetc. without$prefix- Dates: all user-facing dates are DDMMYY (compact, e.g.
040826for 4 Aug 2026). Use the sharedformatDDMMYY()helper infrontend/src/lib/format.ts. Never render rawYYYY-MM-DDto users. Exception: single human-readable dates like todo due dates should useformatShortDate()(also informat.ts, renders5 Aug/5 Aug 26) — the compact DDMMYY code is ambiguous and bad UI for those. config.tsat root for dev/build-time shared config (e.g.PROXY_PORT); runtime config via env vars.envat root tracks port values (PROXY_PORT,PORT);.env.examplecommitted as template- Docker:
docker/Dockerfile(prod, multi-stage + nginx) +docker/Dockerfile.dev(PocketBase) - Nginx routes in prod:
/api/*→ Hono (:3456),/*→ SvelteKit (:2080) - Ports: frontend
2080, proxy3456, container ext3001(port3000is reserved) - Dev servers: NEVER start your own. Always reuse the running dev servers — proxy
192.168.1.225:3456(tsx watch, reloads on edit), frontendlocalhost:2080(vite HMR). Don't spawnnohup pnpm dev/tsx watch/ extra vite instances. Only restart when the user explicitly asks. - Environment:
FRONTEND_PORT,PROXY_PORT,PB_PORT,PB_EMAIL,PB_PASSWORD,DEBUG_RECORD_ID,STRIPE_SECRET_KEY,DONATION_MODAL_INTERVAL - Seed via JSON dump (portable for dev)
- Monorepo: SvelteKit in
frontend/, Hono inproxy/, two Dockerfiles - Decisions tracked in
MEMORY.md
Build Phases (must validate each before next)
Phase 1 — Infrastructure
1.1 Scaffold SvelteKit + Hono monorepo
1.2 Write Dockerfiles (frontend + backend, correct port mapping)
1.3 Sort out vars (
.env + .env.example)1.5 Validate Hono
/api/* reachable, env vars injected1.6 Validate SvelteKit↔PB connectivity (admin API read/write)
Phase 2 — Backend Core
2.1 Create PB collections via schema/migration 2.2 Super admin seed + fam signup flow 2.3 Fam admin login (email/pass → 24hr JWT) 2.4 Invite code generation + member join flow 2.5 Device token auth + route guards 2.6 Svelte reactive state management (no PB SSE)
Phase 3 — Backend Data Streams
3.1 Chore template CRUD + assignment grid (admin) 3.2 Completion toggle (member → PB direct) 3.3 Weekly progress + history computation 3.4 Reward auto-creation on threshold 3.5 Reward claim flow + admin CRUD 3.6 Monthly bonus evaluation 3.7 CRON handler (Coolify → Hono) 3.8 Stripe checkout + webhook 3.9 Notification interface (WhatsApp deferred)
Phase 4 — Frontend App
4.1 Member kanban (3-column, live SSE updates) 4.2 Admin dashboard (weekly overview, chart) 4.3 Admin panel (members, chores, rewards, settings) 4.4 Landing page (SaaS marketing) 4.5 Super admin stats dashboard 4.6 Donation modal 4.7 QR invite code 4.8 Polish (loading, empty, error states, responsive)