FamChore v2 — Architecture & Developer Reference
Overview
Multi-tenant chore tracking SaaS. Families ("fams") are isolated tenant groups. Fam admins use email/password. Members join via invite code + device token (no password). Super admin (you) can see everything.
Demo reference: Current prototype at /home/threejjjs/development/famchore/
1. Stack
| Component |
Role |
Deploy |
Port |
| SvelteKit |
SSR frontend, all UI |
Coolify Docker (chores.app.com) |
:3000 |
| Hono proxy |
Stripe, CRON, webhooks |
Same container as SvelteKit, proxied via /api/* |
:3001 (internal) |
| PocketBase |
DB, auth, realtime, storage, Admin UI |
Coolify Docker (pb.chores.app.com) |
:8090 |
Deployment Topology
2. Auth Model
2.1 Roles & Methods
| Role |
Auth |
Session |
Scope |
PB Entity |
| Super admin (you) |
PB email + password |
24hr JWT |
All collections, all fams |
PB users (set via seed) |
| Fam admin |
PB email + password |
24hr JWT |
Their fam only |
PB users |
| Member |
Invite code + device token |
localStorage, no expiry |
Own data only |
members collection |
2.2 Member Auth Flow
- Fam admin generates invite code → stored on
fams.inviteCode
- Admin shares as text (
chores.app.com/join/XYZ123) or QR code
- Member opens link, enters name, browser generates
crypto.randomUUID() as device token
- PB API creates
members record with SHA-256 hashed token
- Device token stored in
localStorage, sent as X-Device-Token header
- If token lost → admin regenerates invite code, member re-registers
- Admin can revoke access by deleting member record
2.3 PB Auth Rules (row-level security)
Every collection has famId field. PB rule pattern:
Super admin bypasses via admin API (PB superuser credentials).
3. Data Model
All collections live in PocketBase. Every tenant-scoped collection includes famId (relation to fams).
fams
| Field |
Type |
Notes |
id |
auto |
PB default |
name |
text |
Display name |
slug |
text |
URL segment, unique |
inviteCode |
text |
Short alphanumeric, regeneratable |
stripeCustomerId |
text? |
Set after first donation |
featureFlags |
json |
{ "monthlyBonus": true } |
created |
auto |
|
members
| Field |
Type |
Notes |
famId |
relation→fams |
|
name |
text |
|
color |
text |
Hex |
deviceToken |
text |
SHA-256 hash of raw token |
deviceTokenHint |
text |
First 8 chars of raw token (for admin display) |
pointsThreshold |
number? |
Override fam default |
weeklyBonus |
number? |
Override fam default |
created |
auto |
|
chore_templates
| Field |
Type |
Notes |
famId |
relation→fams |
|
name |
text |
|
description |
text |
|
defaultFrequency |
select |
daily or weekly |
defaultType |
select |
points or money |
defaultValue |
number |
|
assigned_chores
| Field |
Type |
Notes |
famId |
relation→fams |
|
memberId |
relation→members |
|
templateId |
relation→chore_templates |
|
frequency |
select |
|
type |
select |
|
value |
number |
|
customName |
text? |
|
completions
| Field |
Type |
Notes |
famId |
relation→fams |
|
memberId |
relation→members |
|
assignedChoreId |
relation→assigned_chores |
|
date |
date |
ISO date |
completedAt |
auto |
|
rewards
| Field |
Type |
Notes |
famId |
relation→fams |
|
memberId |
relation→members |
|
source |
select |
weekly_bonus, monthly_bonus, custom |
label |
text |
|
value |
number |
|
weekStart |
date? |
|
month |
text? |
"2026-06" |
claimed |
bool |
|
claimedAt |
auto? |
|
weekly_history
| Field |
Type |
famId |
relation→fams |
memberId |
relation→members |
weekStart |
date |
pointsEarned |
number |
moneyEarned |
number |
choresCompleted |
number |
bonusEarned |
number |
monthly_bonuses
| Field |
Type |
Notes |
famId |
relation→fams |
|
month |
text |
"2026-06" |
prizeType |
select |
cash, string |
prizeValue |
text |
|
winnerMemberId |
relation→members? |
Nullable until computed |
pointsScored |
number? |
|
claimed |
bool |
|
settings (singleton per fam)
| Field |
Type |
Notes |
famId |
relation→fams |
Unique |
pointsThreshold |
number |
Default: 100 |
weeklyBonus |
number |
Default: 2 (e.g. £2) |
webhookUrl |
text? |
N8N/notification URL |
4. Routes
SvelteKit
Hono proxy (/api/*)
5. Data Flow
5.1 Chore Toggle
5.2 Weekly CRON
Triggered by Coolify CRON job → GET /api/weekly-cron:
5.3 Stripe Donation
5.4 Admin CRUD
All admin operations go through PB admin API (Hono proxy or +page.server.ts). This ensures:
- Server-side validation of famId
- Audit trail option
- Consistent error handling
5.5 Family Chat
Real-time right-slideout chat panel (TopNav chat icon, slideout on desktop / full-screen on mobile).
- Writes go through the Hono proxy (
POST /api/chat/:famId/messages, POST /api/chat/:famId/typing) — the proxy resolves the actor via the session cookie / x-session-* headers (admin) or x-device-token headers (member). GET /api/chat/me returns the actor's identity ({id, type, name, color}) for both roles.
- Reads use the anonymous PB SDK client-side (
chatStore in frontend/src/lib/stores/chat.svelte.ts), the same pattern as famStore. Collections messages and chat_typing have public listRule/viewRule (empty string); browser .subscribe('*') gives realtime SSE sync for both roles.
- Collections:
messages (famId, authorType admin/member, authorId, authorName, authorColor, content, createdAt), chat_typing (transient per-actor presence: famId, actorId, actorType, authorName, authorColor, typing).
- History = last 14 days. Filter uses the custom
createdAt field — the auto created/updated fields are NOT filterable in this PocketBase version (400), so a custom date field is set by the proxy at create time.
createdAt vs created: all sorting, optimistic-temp messages, and the timestamp label use createdAt. The PB auto created field is not returned on records, so referencing it throws (localeCompare of undefined).
- UI (
Chat.svelte): @mention tokens, typing indicators, unread badge, optimistic send with reconcile. The panel starts closed (no auto-open on mount) and is toggled via chatStore.toggle().
6. Project Structure
7. Environment Variables
Frontend + Hono container
PocketBase container
8. Implementation Phases
Phase 0 — Infrastructure (2-4 hrs)
Phase 1 — Auth (4-6 hrs)
Phase 2 — Core Chore Tracking (6-8 hrs)
Phase 3 — Rewards + Claims (3-4 hrs)
Phase 4 — SaaS (4-6 hrs)
Phase 5 — Polish (ongoing)
9. Key Conventions
famId on every query — PB auth rules enforce famId = @request.auth.famId
- PB SDK client-side for members — Browser talks to PB directly for toggles, reads. PB auth rules handle security.
- PB admin API server-side for admins — Hono proxy or SvelteKit server handlers for admin CRUD.
- Device tokens as SHA-256 — Never store or log raw tokens.
- JSON dump for seed data — Portable, version-controllable, restorable via PB backup CLI.
- All collection schema files in
/pb/ — Tracked in git, used for CI/CD schema migration.
- Notifications via pluggable interface — Hono CRON handler has
NotificationService interface; WhatsApp is one implementation (deferred).
10. Open / Deferred
- WhatsApp notifications — Will be implemented as a
NotificationService plugin for the weekly CRON handler. N8N or Twilio, TBD.
- Subscription payments — Currently one-time donation only. Subscription model can be added later via Stripe webhooks.
- Member re-auth on new device — Current design requires admin to regenerate invite code. Could add "re-issue link" feature in admin panel.
- Multi-language — Not yet scoped. All text in English for now.
11. Reference: Current Prototype
The existing HonoJS prototype at /home/threejjjs/development/famchore/ contains the reference logic for:
- Weekly bonus calculation (
src/services.ts → checkWeeklyBonus)
- Monthly bonus evaluation
- Reward claim flow
- Chore toggle event delegation
- Chart/stat formatting
- Date/timezone helpers
Refer to src/types.ts for the original type definitions, and src/views/ for the Alpine.js template structure that maps to the new SvelteKit components.