add embedded checkout
This commit is contained in:
+168
-313
@@ -2,9 +2,9 @@
|
||||
|
||||
## 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.
|
||||
Multi-tenant chore tracking SaaS. Families ("fams") are isolated tenant groups. Admins (parents) authenticate via email/password; members (children) join via invite OTP and get a server-derived password. No `members` collection — everyone is a `users` row scoped by `famId`.
|
||||
|
||||
**Demo reference:** Current prototype at `/home/threejjjs/development/famchore/`
|
||||
**Source of truth:** `AGENTS.md` is the living reference. This doc captures the architecture.
|
||||
|
||||
---
|
||||
|
||||
@@ -12,21 +12,23 @@ Multi-tenant chore tracking SaaS. Families ("fams") are isolated tenant groups.
|
||||
|
||||
| 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 |
|
||||
| SvelteKit | SSR frontend, all UI + Stripe server routes | Coolify Docker (nginx) | :2080 internal, :3001 external |
|
||||
| Hono proxy | data layer (`/api/*`); CRON | Same container, proxied via nginx `/api/*` | :3456 internal |
|
||||
| PocketBase | DB, auth, realtime, storage, Admin UI | Coolify service (pb.chores.app.com) | :8090 |
|
||||
| Stripe | subscriptions (in SvelteKit, NOT Hono) | — | — |
|
||||
|
||||
### Deployment Topology
|
||||
|
||||
```
|
||||
chores.app.com ────┬──► SvelteKit (:3000)
|
||||
│ └── /api/* ──► Hono proxy (:3001)
|
||||
chores.app.com ────┬──► nginx (:3001)
|
||||
│ ├── /* ──► SvelteKit (:2080)
|
||||
│ └── /api/* ──► Hono proxy (:3456)
|
||||
│
|
||||
pb.chores.app.com ──► PocketBase (:8090)
|
||||
│ Admin UI at /_
|
||||
│ Volume: /pb_data (persistence + backups)
|
||||
│
|
||||
stripe.com ─────────► Hono /api/stripe/webhook
|
||||
stripe.com ─────────► SvelteKit /account/webhook
|
||||
```
|
||||
|
||||
---
|
||||
@@ -35,133 +37,39 @@ stripe.com ─────────► Hono /api/stripe/webhook
|
||||
|
||||
### 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 |
|
||||
| Role | Auth | Session | PB Entity |
|
||||
|---|---|---|---|
|
||||
| **Admin (parent)** | PB email + password | 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) | — | `_superusers` |
|
||||
|
||||
### 2.2 Member Auth Flow
|
||||
### 2.2 Details
|
||||
|
||||
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
|
||||
- **Admins** are `users` (role `parent`). Login via email/password → httpOnly `pb_token` cookie `{ id, name, username, role: "parent", famId, color }`.
|
||||
- **Members** are `users` (role `child`); PB `username` = `{famSlug}:{handle}` (globally-unique; `handle` = whitespace-free lowercase name), URL segment = `handleOf(username)`, `name` = display name. Password is **derived** server-side (`MEMBER_SECRET + famSlug + handle`). Join gated by a 20-min OTP in `otp`, then `authWithPassword`. Same httpOnly `pb_token` cookie. No `members` collection.
|
||||
- **Superuser** used only server-side by `pb-admin.ts` for cross-family queries (`/admin`) and OTP/signup writes.
|
||||
- Layout `[fam]/+layout.server.ts` derives `isParent`/`role` centrally from the session. `pb_token` is httpOnly, so the browser PB SDK is seeded from `page.data.pbToken` via `initPb(token)` in `onMount`.
|
||||
|
||||
### 2.3 PB Auth Rules (row-level security)
|
||||
### 2.3 PB Auth Rules
|
||||
|
||||
Every collection has `famId` field. PB rule pattern:
|
||||
|
||||
```
|
||||
famId = @request.auth.famId
|
||||
```
|
||||
|
||||
Super admin bypasses via admin API (PB superuser credentials).
|
||||
Every tenant-scoped collection has `famId` and enforces `famId = @request.auth.famId`; family-scoped collections have public `listRule`/`viewRule` so reads/subscribes work for both roles. Superuser bypasses via admin API.
|
||||
|
||||
---
|
||||
|
||||
## 3. Data Model
|
||||
## 3. Data Model (PB collections, all scoped by `famId`)
|
||||
|
||||
All collections live in PocketBase. Every tenant-scoped collection includes `famId` (relation to `fams`).
|
||||
- `users` — auth collection; famId, role (`parent`|`child`), username (`{famSlug}:{handle}`), name, color, email (admin only)
|
||||
- `otp` — famId, userId, otp, updatedAt (OTP gate for child join; display colour on `users.color`)
|
||||
- `fams` — name, slug, stripeCustomerId, featureFlags
|
||||
- `chore_templates` — famId, name, defaultValue, defaultFrequency
|
||||
- `assigned_chores` — famId, userId, templateId, frequency, value
|
||||
- `completions` — famId, userId, assignedChoreId, date
|
||||
- `weekly_history` — famId, userId, weekStart, pointsEarned, moneyEarned
|
||||
- `rewards` — famId, userId, source, label, value, claimed, claimedAt, claimable, settleDate
|
||||
- `monthly_bonuses` — famId, month, prizeType, prizeValue, winnerUserId
|
||||
- `settings` — famId, pointsThreshold, weeklyBonus, webhookUrl
|
||||
|
||||
### `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 |
|
||||
> **Schema/migrations:** `shared/pb/schema.ts` (`SCHEMA_PLAN`) is the single source of truth for base collections. `frontend/src/lib/server/migrate.ts` bootstraps a fresh/wiped PB (idempotent). The native `users` auth fields/rules + superuser-only `otp` are applied in `migrate.ts` (`ensureUsers`/`ensureOtp`). Data is disposable — schema change = update `SCHEMA_PLAN` + wipe PB + reboot.
|
||||
|
||||
---
|
||||
|
||||
@@ -170,228 +78,175 @@ All collections live in PocketBase. Every tenant-scoped collection includes `fam
|
||||
### 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
|
||||
/ Landing page (SaaS marketing)
|
||||
/admin Platform super-admin stats dashboard
|
||||
/login · /logout Parent 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
|
||||
/{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
|
||||
/{fam}/{username}/settings Family admin settings (parent only) — Stripe connect/manage + pause
|
||||
/account Account/billing — payment setup & subscription management
|
||||
/subscriptions 3-tier plan page (trial | monthly | yearly), access via settings
|
||||
/account/webhook Stripe webhook handler (server route)
|
||||
/api/* Hono proxy (data layer; CRON not implemented)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Data Flow
|
||||
|
||||
### 5.1 Chore Toggle
|
||||
### 5.1 Reads (both roles)
|
||||
|
||||
- `famStore.init()` fetches all collections via PB SDK, authenticated via the seeded `pb_token`. `.subscribe()` works for both roles.
|
||||
- TopNav season pills read from `famStore.seasons` (reactive).
|
||||
|
||||
### 5.2 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 via `sessionHeaders`).
|
||||
- **Member updates:** Browser → Hono proxy → PB (`Bearer <pb_token>`).
|
||||
- **Reward creation:** after completion toggle, Hono proxy creates reward if threshold met.
|
||||
- **Weekly settlement:** NOT via CRON — manual `complete-week` action or `simulateEow` preview in settings. `/api/weekly-cron` (Coolify) not implemented.
|
||||
- **Stripe:** SvelteKit server routes `/account` + `/account/webhook` (frontend app, NOT Hono).
|
||||
- **WhatsApp:** not implemented.
|
||||
|
||||
### 5.3 Stripe Subscription (embedded Checkout)
|
||||
|
||||
```
|
||||
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)
|
||||
Parent picks a tier on /subscriptions (trial | monthly | yearly)
|
||||
→ SvelteKit server action (subscriptions) creates Embedded Checkout Session
|
||||
createEmbeddedCheckoutSession() → ui_mode: "embedded" → client_secret
|
||||
→ returns { clientSecret } to the browser
|
||||
→ @stripe/stripe-js createEmbeddedCheckoutPage({ clientSecret }) mounts in-page
|
||||
→ Parent completes payment inside the embedded Stripe page
|
||||
→ Stripe sends checkout.session.completed → SvelteKit /account/webhook
|
||||
handleStripeEvent() → pbAdmin.update fams.stripeCustomerId + active = true
|
||||
→ Subsequent customer.subscription.* webhooks keep fams.active in sync
|
||||
→ Parent returns to /account?checkout=return
|
||||
```
|
||||
|
||||
### 5.2 Weekly CRON
|
||||
Pause/stop via Stripe Customer Portal (from `/account`) or the pause toggle (writes `fams.active` directly).
|
||||
|
||||
Triggered by Coolify CRON job → `GET /api/weekly-cron`:
|
||||
### 5.3.1 Payments architecture
|
||||
|
||||
```
|
||||
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
|
||||
┌────────────────────────────────────────── SVELTEKIT APP ──────────────────────────────────────────┐
|
||||
│ │
|
||||
│ Browser │
|
||||
│ ┌──────────────────────────────┐ POST ?/checkout ┌─────────────────────────────────────────┐ │
|
||||
│ │ /subscriptions (+page.svelte)│ ───────────────────► │ subscriptions/+page.server.ts (action) │ │
|
||||
│ │ • tier cards │ │ • resolves famId + parent email (PB) │ │
|
||||
│ │ • createEmbeddedCheckoutPage│ ◄─── clientSecret ─── │ • createEmbeddedCheckoutSession() │ │
|
||||
│ │ • mounts embedded Stripe UI │ └───────────────┬─────────────────────────┘ │
|
||||
│ └──────────────┬───────────────┘ │ stripe SDK (secret) │
|
||||
│ │ createEmbeddedCheckoutPage(clientSecret) ▼ │
|
||||
│ ▼ ┌─────────────────────────────┐ │
|
||||
│ ┌──────────────────────────────┐ │ STRIPE API │ │
|
||||
│ │ Embedded Checkout (Stripe │ card + pay │ checkout.sessions.create │ │
|
||||
│ │ hosted iframe, in-page) │ ───────────────────► │ (ui_mode: embedded) │ │
|
||||
│ └──────────────────────────────┘ └──────────────┬──────────────┘ │
|
||||
│ │ webhook events │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ /account/webhook (+server.ts) │ │
|
||||
│ │ • verify stripe-signature (CLI secret in dev, dashboard in prod) │ │
|
||||
│ │ • handleStripeEvent() → stripe-events.ts │ │
|
||||
│ │ └ checkout.session.completed → fams.stripeCustomerId + active = true │ │
|
||||
│ │ └ customer.subscription.* → fams.active (sync by customer id) │ │
|
||||
│ └──────────────────────────────────────────────────────┬─────────────────────────────────────┘ │
|
||||
│ │ pbAdmin (superuser) │
|
||||
└─────────────────────────────────────────────────────────┼─────────────────────────────────────────┘
|
||||
▼
|
||||
┌────────────────────┐
|
||||
│ POCKETBASE │
|
||||
│ fams.stripeCustomerId │
|
||||
│ fams.active (bool) │
|
||||
└────────────────────┘
|
||||
|
||||
Management:
|
||||
/account (+page.server.ts)
|
||||
• billing action → createBillingPortalSession(customerId) → Stripe Customer Portal
|
||||
(update card, cancel / reactivate subscription)
|
||||
• togglePause action → pbAdmin.update fams.active (hard pause, independent of Stripe)
|
||||
|
||||
Dev-only:
|
||||
stripe CLI: stripe listen -e ... --forward-to http://127.0.0.1:2080/account/webhook
|
||||
(sets STRIPE_CLI_WEBHOOK_SECRET for local signature verification)
|
||||
```
|
||||
|
||||
### 5.3 Stripe Donation
|
||||
**Key decisions**
|
||||
- **Payments live in SvelteKit, not Hono** — the app already owns SSR + server actions; Hono stays a pure data layer. Stripe secret never reaches the client.
|
||||
- **`fams.active`** is the single app-level gate: webhooks (subscription lifecycle) and the pause toggle both write it. It disables interactions + payments when `false`.
|
||||
- **Embedded Checkout** (in-page, no redirect) via `createEmbeddedCheckoutPage` — needs a same-origin `return_url`; subscriptions require a `customer` (created with `customer_creation: 'always'` + `customer_email` if the fam has none yet).
|
||||
- **Trial** is app-side: a code maps to `trial_period_days` on the subscription; the trial Stripe price is a `$0` plan. Real-world codes should move to a PB collection.
|
||||
- **Webhook secrets** — `STRIPE_CLI_WEBHOOK_SECRET` (dev) overrides `STRIPE_WEBHOOK_SECRET` (prod/dashboard); `verifyStripeEvent` picks the CLI one when set. Dev testing uses the Stripe CLI (`stripe listen --forward-to http://127.0.0.1:2080/account/webhook`) which forwards real signed events; a real checkout carries the `famId` and drives the DB write end-to-end.
|
||||
|
||||
```
|
||||
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 UI reactivity
|
||||
|
||||
### 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()`.
|
||||
- Svelte `$state` / `$derived` / `$effect`.
|
||||
- PB SDK `.subscribe()` for realtime multi-user sync (public reads → both roles).
|
||||
- `famStore.applyRecord()` for instant optimistic feedback.
|
||||
- **Rule:** if data should update via SSE, use `$derived` (not `$state` — frozen snapshot). Only high-frequency member actions use `$state` + optimistic `applyRecord`.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
/ (root)
|
||||
/shared/pb/schema.ts SCHEMA_PLAN — source of truth for base collections
|
||||
/frontend SvelteKit app (:2080)
|
||||
/src/env.ts declareEnvVars — client/server env
|
||||
/src/lib/server pb-admin, migrate.ts, services, hono.ts (sessionHeaders)
|
||||
/src/lib/client api.ts (memberApi), stores (famStore)
|
||||
/src/lib/components UI components (re-exported from index.ts)
|
||||
/src/routes SvelteKit file-based routing (incl /account, /subscriptions)
|
||||
/proxy Hono proxy (:3456)
|
||||
/docker Dockerfile (prod multi-stage + nginx), Dockerfile.dev (PB)
|
||||
/config.ts dev/build-time shared config (ports)
|
||||
/MEMORY.md decisions log
|
||||
AGENTS.md AI reference
|
||||
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`.
|
||||
Env vars declared in `frontend/src/env.ts` via `defineEnvVars` (`@sveltejs/kit/hooks`). Only `{ public: true }` are exposed client-side via `$app/env/public`; server-only via `$app/env/private`.
|
||||
|
||||
### 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`).
|
||||
- `FRONTEND_PORT`, `PROXY_PORT`, `PB_PORT`
|
||||
- `PB_EMAIL`, `PB_PASSWORD` (PB superuser, server-only)
|
||||
- `DEBUG_RECORD_ID`
|
||||
- `STRIPE_SECRET_KEY` (server-only)
|
||||
- `DONATION_MODAL_INTERVAL`
|
||||
- `SERVER_IP` (public, dev)
|
||||
|
||||
Values come from root `.env` (symlinked at `frontend/.env -> ../.env`). `.env.example` is the committed template. Ports also tracked in root `config.ts`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Implementation Phases
|
||||
## 8. Key Conventions
|
||||
|
||||
### 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
|
||||
- **`famId` on every query** — PB auth rules enforce `famId = @request.auth.famId`; superuser bypasses.
|
||||
- **Member → Proxy (server/browser):** `memberApi.*` in `$lib/client/api.ts`; `BASE_URL` resolves to Hono port on server, empty in browser (Vite proxies `/api/*`).
|
||||
- **Admin → Proxy:** `hono.admin.*` in `$lib/server/hono.ts` — `sessionHeaders(event)` (server-only, requires `RequestEvent`).
|
||||
- **Child passwords derived** — `MEMBER_SECRET + famSlug + username`; join gate is a transient OTP. Never log raw tokens/secrets.
|
||||
- **`$page`** — from `$app/state` (not `$app/stores`); no `$` prefix.
|
||||
- **Dates** — user-facing via `formatDDMMYY()` (compact `040826`); human-readable due dates use `formatShortDate()`. Never render raw `YYYY-MM-DD`.
|
||||
- **UI shell** — `[fam]/+layout.svelte` (Sidebar, TopNav, Footer); `ViewHeader` + `CardGrid`/`Card` micro-layout; `Button` for CTAs; icons as SVG strings in `lib/components/icons.ts`.
|
||||
- **Reactivity** — never `$effect` to sync local state from famStore; use `$derived` for SSE-reactive data; `$state` + `applyRecord` only for optimistic member toggles.
|
||||
- **Dev servers** — never start your own; reuse running proxy (`192.168.1.225:3456`) + frontend (`localhost:2080`).
|
||||
|
||||
---
|
||||
|
||||
## 9. Key Conventions
|
||||
## 9. Open / Deferred
|
||||
|
||||
- **`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.
|
||||
- **WhatsApp notifications** — `NotificationService` plugin for the weekly CRON handler. Deferred.
|
||||
- **Stripe payments** — flow not yet implemented. Only `fams.stripeCustomerId` + `settings.webhookUrl` exist. Building in SvelteKit `/account` + `/account/webhook`. Trial via codes (app-side validation + `trial_period_days`) — TBD.
|
||||
- **Weekly CRON** (`/api/weekly-cron`, Coolify) — not implemented; settlement is manual via `complete-week`/`simulateEow`.
|
||||
Reference in New Issue
Block a user