Files
famdone/ARCHITECTURE.md
2026-08-06 18:10:13 +01:00

15 KiB

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

chores.app.com ────┬──► SvelteKit (:3000)
                    │      └── /api/* ──► Hono proxy (:3001)
                    │
pb.chores.app.com ──► PocketBase (:8090)
                     │      Admin UI at /_
                     │      Volume: /pb_data (persistence + backups)
                     │
stripe.com ─────────► Hono /api/stripe/webhook

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

  1. Fam admin generates invite code → stored on fams.inviteCode
  2. Admin shares as text (chores.app.com/join/XYZ123) or QR code
  3. Member opens link, enters name, browser generates crypto.randomUUID() as device token
  4. PB API creates members record with SHA-256 hashed token
  5. Device token stored in localStorage, sent as X-Device-Token header
  6. If token lost → admin regenerates invite code, member re-registers
  7. Admin can revoke access by deleting member record

2.3 PB Auth Rules (row-level security)

Every collection has famId field. PB rule pattern:

famId = @request.auth.famId

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

/                          Landing page (SaaS marketing)
/join/:code                Member invite code + name entry

/{fam}                     Fam dashboard (weekly overview)
/{fam}/admin               Admin panel
/{fam}/admin/chores        Chore template CRUD + assignment grid
/{fam}/admin/rewards       Reward management, claim history
/{fam}/admin/settings      Thresholds, webhook, invite code, features

/{fam}/:username           Member kanban
  ?token=<deviceToken>     Auto-auth via query param (from QR/share)

Hono proxy (/api/*)

/api/stripe/create-checkout    Create Stripe Checkout Session
/api/stripe/webhook            Stripe event webhook
/api/weekly-cron               Coolify CRON target

5. Data Flow

5.1 Chore Toggle

User clicks checkbox
  → Browser → PB SDK (direct, auth= device token or admin JWT)
    → PB inserts/deletes completion
      → PB realtime SSE push to all subscribers
        → UI updates (kanban card, progress bar, money counter)
          → If weekly threshold crossed:
            → SvelteKit server handler creates Reward (weekly_bonus)

5.2 Weekly CRON

Triggered by Coolify CRON job → GET /api/weekly-cron:

Hono receives request
  → Queries all fams
  → For each fam:
    → Compute weekly summaries per member (completions tally)
    → Upsert weekly_history records
    → Evaluate monthly bonus (end-of-month)
    → If webhookUrl set on fam settings:
      → POST summary to webhook (pluggable — WhatsApp later)
  → Returns 200

5.3 Stripe Donation

User clicks "Donate" on landing or modal
  → Hono /api/stripe/create-checkout
    → Creates Stripe Checkout Session
    → Returns session.url → redirect user to Stripe
  → Stripe redirects back to app
  → Stripe webhook → Hono /api/stripe/webhook
    → Updates fam.stripeCustomerId
    → Sets fam.featureFlags.donated = true (or similar)

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

/chores
  /src                     SvelteKit app
    /lib
      /components           Shared UI components
      /stores               Svelte stores (auth, current fam)
      /pb                   PB SDK client helpers
    /routes                 SvelteKit file-based routing
    /hooks.server.ts        Auth hooks, PB client init
  /proxy                    Hono proxy
    /src
      /routes               Stripe webhook, CRON handler
      /services             PB admin client, notification service
    package.json
  /seed                     JSON dump files for PB collections
  /pb                       PB collection schema definitions
  package.json              Workspace root
  Dockerfile.frontend       SvelteKit + Hono build
  Dockerfile.backend        PocketBase (custom, or use official image)
  coolify.json              Coolify deployment config (optional)
  AGENTS.md                 AI reference (this file's sibling)
  ARCHITECTURE.md           This document

7. Environment Variables

Current (this repo)

# .env (dev; loaded by vite for the frontend + tsx --env-file for the proxy)
PB_EMAIL=debug@famchamp.dev        # PB superuser (server-only; pb-admin + proxy)
PB_PASSWORD=debug123
SERVER_IP=192.168.1.225            # dev machine IP (Tailscale IP when away) — change when it changes
  • Ports live in root config.ts (FRONTEND_PORT/PROXY_PORT/PB_PORT = 2080/3456/8090) — the proxy reads them; SvelteKit never imports config.ts.
  • SvelteKit env vars are declared in frontend/src/env.ts (PROXY_URL, SERVER_IP, PB_EMAIL, PB_PASSWORD) and read via $app/env/public / $app/env/private.
  • Compose/deploy: PORT (public port, default 3001), PB_DATA (host data dir). PUBLIC_PB_URL no longer exists (docker bakes /pb; browser PB URL derives from SERVER_IP in dev).
  • Not yet wired: STRIPE_SECRET_KEY, DONATION_MODAL_INTERVAL.

Dev vs Prod PocketBase data (⚠️)

  • Dev (pnpm dev): permanent dev PB = container pb-dev at :8090, data in host ./pb_data. This is what the code talks to via SERVER_IP:8090.
  • Prod/docker: the app container bundles its own internal PB, published loopback-only at 127.0.0.1:8091, and also bind-mounts ./pb_data.
  • Never recreate pb-dev with a fresh volume — restore it with -v "$PWD/pb_data:/pb_data" (full command in RULES.md).

8. Implementation Phases

Phase 0 — Infrastructure (2-4 hrs)

  • Deploy PocketBase on Coolify (pb.chores.app.com)
    • Official pocketbase/pocketbase image
    • Mount /pb_data volume
    • Set super admin env vars
    • Test: visit pb.chores.app.com/_/, login, data persists after restart
  • Scaffold monorepo with SvelteKit + Hono
  • Create Dockerfiles
  • Deploy to Coolify (chores.app.com)
  • Cloudflare DNS for both

Phase 1 — Auth (4-6 hrs)

  • Create PB collections: fams, members, settings
  • Super admin seed script
  • Fam signup → PB user + fam record
  • Fam admin login (24hr JWT)
  • Invite code generation
  • Member join page (/join/:code)
  • Device token auth
  • Route guards
  • Seed data (1 fam + 3 members + chores matching demo)

Phase 2 — Core Chore Tracking (6-8 hrs)

  • PB collections: chore_templates, assigned_chores, completions, weekly_history
  • Admin chore CRUD
  • Admin chore assignment grid
  • Member kanban (3 columns: Daily Pending, Weekly Pending, Completed)
  • Completion toggle
  • Realtime updates (PB SSE)
  • Weekly progress + chart
  • Admin dashboard

Phase 3 — Rewards + Claims (3-4 hrs)

  • PB collection: rewards
  • Auto-create weekly bonus reward on threshold
  • Claim section with visual feedback
  • Admin reward CRUD
  • Monthly bonus (set prize → compute winner → create reward)
  • Pluggable notification interface in CRON handler

Phase 4 — SaaS (4-6 hrs)

  • Landing page (hero, features, CTA)
  • Stripe one-time checkout (Hono)
  • Donation modal (triggered after N admin page loads)
  • Feature flags (checked in routes via settings)
  • Super admin dashboard (stats, all-fams view, feature toggles)

Phase 5 — Polish (ongoing)

  • QR invite code
  • PB backup scheduler
  • Error/loading/empty states
  • Responsive mobile layout
  • Accessibility

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.tscheckWeeklyBonus)
  • 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.