Files
joshuaandClaude Opus 5.5 2d4bc5c38d
CI / Backend tests (push) Waiting to run
CI / Frontend lint, typecheck, build (push) Waiting to run
CI / End-to-end tests (push) Blocked by required conditions
CI / Deploy tagged release (push) Blocked by required conditions
Roadmap: AI helpers as the 2.0 headline, and later AI stages
- §15 AI helpers (2.0): one-button helpers (draft an event, summarise
  reports and feedback, draft messages, improve text) on Claude Haiku 5.5,
  called through the Hub, which holds the key, meters usage in rand and
  enforces a monthly cap per licence; admin usage view; POPIA notes
- §16 AI later stages (unversioned): Ask AI, help/navigation assistant,
  actions with confirmation, more helpers, long-term self-hosted AI
- Hub: AI cap as the first per-licence limit, AiUsage model; decisions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012mt6TCv85T9VKRq3cV7rXq
2026-10-24 14:36:25 +02:00

50 KiB
Raw Permalink Blame History

Hope Events — Road to 2.0.0

Planned features and platform work to take Hope Events from 1.32.x to 2.0.0. Each section covers what the feature does, who uses it, how it fits the existing platform, and the edge cases to handle. See FEATURES.md for what already exists, and FEATURES-ROUND-1.md for the previous round (1.12.0 – 1.23.0). Where to pick up: HANDOVER.md.

Theme for 2.0: turn a feature-rich church app into a product that can be sold — trustworthy (POPIA), flexible (multi-day events, several payment providers), maintainable (tests, CI, typed backend, versioned API), and with AI helpers as the headline feature (§15).

Status (2026-10-23): all features in this document are approved. Shipped so far: 1.32.0 early-bird ribbon on event cards; 1.33.0 multi-day events phase A (sessions, late joining, per-session check-in, attendance register, reminders, certificates); 1.34.0 job queue (pg-boss), Playwright E2E tests, Gitea CI workflow (waiting for a runner) and deploy script; 1.35.0 multi-day events phase B (pick sessions, per-session capacity), with 1.35.1 adding the per-session waitlist, the session picker at the door and session choice on "All sessions" tickets; 1.36.0 payment provider interface (Yoco moved behind it, Site settings → Payments); 1.37.0 PayFast and the "Pay online" page; 1.37.1 "Pay online" hidden when it can't be used; 1.37.2 payment status checks for missing notifications. SnapScan and Zapper are dropped — Yoco and PayFast are enough (2026-10-23); 1.38.0 POPIA toolkit part 1 (privacy page, unsubscribe, WhatsApp STOP, data requests); 1.39.0 part 2 (privacy notice editor and versions, operator register, consent and access logs); 1.40.0 part 3 (retention policies and job, breach register, children’s-data flags, readiness page). Next: 1.41.0 install/update tooling + the Hope Events Hub (§13).

Repositories:

Repo What
joshua/hope-events (this repo) The church app. Everything in this document except the Hub app itself — including the Hub client (licence step, "Connect to Hub", heartbeat, licence checks)
joshua/hope-events-hub (new) The Hub app (§13): its own versions, CHANGELOG, releases and server

Contents

  1. Multi-day events (sessions) ← first
  2. Payment providers: PayFast
  3. POPIA toolkit
  4. Kids check-in with security labels
  5. Volunteer / serving rosters
  6. Event series and cloning
  7. Apple and Google Wallet tickets
  8. Outbound webhooks
  9. Admin dashboard, command palette, bulk actions
  10. Languages (Afrikaans first)
  11. Accessibility pass (WCAG 2.2 AA)
  12. Multi-campus (one install per church)
  13. Central management platform (Hope Events Hub)
  14. Foundations (platform work)
  15. AI helpers (2.0 headline)
  16. AI: later stages
  17. Suggested build order
  18. Decisions

1. Multi-day events (sessions)

One event that runs over several dates, e.g. "Alpha Course — every Wednesday 18:30–20:30 for 5 weeks", a 3-day conference, or a camp with an evening session each day. People register and pay once, get one ticket, and are checked in per session.

Not the same as Event series. A series groups separate events (Camp 2026, Camp 2027) that each have their own registrations. Sessions are dates inside one event.

Who: Supervisors set up the schedule. Users see it on the event page, in their tickets and in their calendar. Staff check in per session.

Event setup

New Schedule section on the event form:

Setting Options
Schedule type Single date (today's behaviour) · Multiple sessions
Pattern builder Repeat daily / weekly / every N weeks on chosen weekday(s), start date, time from–to, number of sessions or until date
Generated sessions Editable list: change date/time/location/label of any one session, delete, or add an extra one-off date
Session label Optional, e.g. "Week 1 — Is there more to life?" (defaults to "Session 1, 2, …")
Session location Optional; falls back to the event location
Attendance mode Whole course (one registration covers all sessions) · Pick sessions (see phase B below)
Late joining Per event: Off (registration closes when the first session starts) · Allow until the start of session N
Late-joiner price Per event, only when late joining is on: Full price · Pro-rata (price × remaining sessions ÷ total sessions, rounded to the nearest R10, e.g. R183 → R180, R185 → R190) · Fixed late price
Check-in window Per event: Whole day (default — any time on the session's date, in case someone was let in without being scanned) · Session time only (2 h before start until the session ends)
Session reminders Per event, off by default: when on, choose timing (e.g. 3 h before, the day before at 18:00, or both) and channels (email / WhatsApp)
Certificates Per event, off by default: when on, choose a template and the completion rule (see Certificates)

The pattern builder is only a helper: it generates rows, then the session list is the source of truth. Typing "every Wednesday for 5 weeks" and then skipping a public holiday is just deleting one row.

How it works

  • Event.startDate / endDate stay and are kept in sync automatically (first session start, last session end). Everything that already reads them — consent age checks, transfer cut-off, early-bird, feedback timing, ICS, listings, sitemap — keeps working unchanged. Only places that need per-session behaviour are updated.
  • Public event page: shows the full schedule, with past sessions greyed out and a "Next session" highlight. Event cards show "5 sessions · Wed 4 Nov – Wed 2 Dec".
  • Event listings: a multi-session event stays "upcoming / running" until its last session ends, and sorts by its next session.
  • Tickets (PDF, email, WhatsApp): list all session dates. One QR code.
  • Calendar (ICS): one VEVENT per session (not an RRULE, so edited or skipped sessions are exact).
  • Check-in: the scanner auto-selects today's session (on a day with more than one session, the one happening now or next), with a dropdown to override. Whether a scan is accepted depends on the event's check-in window (whole day or session time only). A ticket can be checked in once per session (per person for group tickets). Scanning again in the same session shows "Already checked in for Session 3 at 18:42".
  • Manual check-in: supervisors can tick someone as present for a session from the attendance register afterwards (e.g. let in without a scan), logged in the audit log with who marked it.
  • Reminders: off by default; when switched on for the event, a reminder goes out before each session (email/WhatsApp, via the scheduled-message worker), e.g. "See you tonight at 18:30".
  • Late joiners: when late joining is on, the registration page shows which sessions are left and the late price ("3 of 5 sessions left — R180").
  • Changes: moving or cancelling a session after people registered prompts the supervisor to notify registrants (pre-filled broadcast), and updates the ICS/tickets.

Phase B: pick sessions (drop-in)

Second step, released separately. Lets people register for some sessions, e.g. a conference where each day is sold separately or a weekly drop-in class.

  • Ticket options can be scoped to sessions: "Day pass (pick days)" priced per session, "Full conference pass" covering all.
  • Optional capacity per session on top of the option stock limit.
  • The ticket stores which sessions it covers; scanning on a session it doesn't cover shows "Not registered for this session" (with "Add & charge" at the door for supervisors).
  • Waitlist works per session when sessions have their own capacity.

Reporting

  • Attendance register: grid of attendees × sessions with ticks, totals per session, attendance % per person. Export to Excel and PDF.
  • Completion filter: "attended at least 4 of 5" — useful for certificates and for follow-up ("missed the last 2 weeks").
  • Daily summary email includes "Sessions today".
  • Funnel stats unchanged (they are per event).

Certificates

Optional completion certificates (PDF), chosen per event.

  • 3–4 built-in templates, e.g. Classic (border, serif), Modern (clean, accent colour bar), Church (logo-led, scripture line), Kids (playful, for children's programmes). All use the site logo and brand colour from Site settings.
  • Event settings: Certificates on/off · Template · Title ("Certificate of Completion") · Body text with placeholders ({name}, {event}, {sessionsAttended}, {totalSessions}, {endDate}) · Signature name and role (optional uploaded signature image) · Completion rule: attended at least N sessions or N % (default: all sessions).
  • Preview button in the event form renders a sample PDF.
  • Who gets one: attendees meeting the completion rule after the last session. Supervisors can issue/withdraw one manually (audit logged).
  • Delivery: download from My Events, bulk download (ZIP) from the attendance register, and optional email/WhatsApp send.
  • Each certificate has a short verification code + QR linking to a public "certificate valid" page (name, event, date only).
  • Generated with pdfkit, like tickets and receipts (utils/pdfDocs.js).
  • Also usable on single-date events (completion = checked in).

Edge cases

Case Behaviour
Delete a session that has check-ins Blocked; offer Cancel session instead (kept for history, hidden from schedule, excluded from attendance %)
Switch Multiple → Single after registrations Allowed only if no session check-ins exist
Ticket transfer Allowed until the first session starts (current rule). Optional later: allow mid-course transfers with check-in history kept on the original holder
Parental consent Age is measured at the first session (current startDate rule)
Medical details deletion Runs after the last session (end date), as today
Feedback form Sent after the last session
Cashup Still per event. At-the-door cash per session is captured with the session on the payment so reports can split it
Offline scanning Session list and per-session usages are part of the offline lock bundle; sync keeps the session id per scan
Timezones Sessions stored as UTC instants, same as Event.startDate today (see icsUtils.js)
Session overlaps / out of order Allowed (conferences have parallel tracks later), list always sorted by start
Kiosk / At the door Shows the current session; check-in counts against it

Data model (sketch)

enum ScheduleType { single sessions }
enum AttendanceMode { whole pick }

enum LatePricing { full prorata fixed }
enum CheckInWindow { whole_day session_only }

model Event {
  // ...existing fields
  scheduleType     ScheduleType   @default(single)
  attendanceMode   AttendanceMode @default(whole)
  lateJoinEnabled  Boolean        @default(false)
  lateJoinUntilSessionId String?
  latePricing      LatePricing    @default(full)
  lateFixedPrice   Float?
  checkInWindow    CheckInWindow  @default(whole_day)
  sessionReminders Json?          // null = off; { offsets: [...], channels: [...] }
  certificatesEnabled Boolean     @default(false)
  certificateConfig   Json?       // template, title, body, signature, completion rule
  sessions         EventSession[]
  certificates     Certificate[]
}

model Certificate {
  id          String   @id @default(uuid())
  eventId     String
  attendeeId  String?
  userId      String?
  name        String
  code        String   @unique   // verification code
  manual      Boolean  @default(false)
  issuedAt    DateTime @default(now())
  revokedAt   DateTime?
  event       Event    @relation(fields: [eventId], references: [id], onDelete: Restrict)

  @@index([eventId])
}

model EventSession {
  id          String    @id @default(uuid())
  eventId     String
  label       String?
  startsAt    DateTime
  endsAt      DateTime
  location    String?
  capacity    Int?      // phase B
  cancelledAt DateTime?
  order       Int       @default(0)
  event       Event     @relation(fields: [eventId], references: [id], onDelete: Cascade)
  usages      TicketUsage[]
  ticketSessions TicketSession[]

  @@index([eventId, startsAt])
}

model TicketUsage {
  // ...existing fields
  sessionId String?          // null on single-date events
  session   EventSession? @relation(fields: [sessionId], references: [id], onDelete: Restrict)
  @@index([sessionId])
}

// Phase B only: which sessions a "pick sessions" ticket covers.
model TicketSession {
  ticketId  String
  sessionId String
  @@id([ticketId, sessionId])
}

Ticket.isUsed keeps its meaning for single-date events. For session events it's not used for check-in decisions; "checked in" is per session (TicketUsage.sessionId).

Plumbing

  • Session CRUD + pattern generator in backend/src/utils/sessions.js with Jest tests (DST-free but still test month boundaries, skipped dates, "until" vs "count").
  • Scan endpoint takes an optional sessionId; defaults to the current session.
  • Help content, audit log entries (session created/moved/cancelled), CHANGELOG, FEATURES.md, SETTINGS.md.

2. Payment providers: PayFast

Add PayFast next to Yoco, and let the admin enable one or both in Site settings. At checkout the user picks from the enabled ones. (Shipped in 1.36.0–1.37.2. SnapScan and Zapper were planned too, but dropped on 2026-10-23: two providers are enough.)

Who: Admins configure providers. Users choose at payment. Supervisors reconcile.

Site settings → Payments

Setting Options
Providers Yoco · PayFast — each with an Enabled toggle
Credentials Per provider (merchant id/key, API key, passphrase, webhook secret…), stored encrypted (utils/encryption.js, same as WhatsApp)
Mode Live / Sandbox per provider (where supported)
Display order / default Which one is shown first. Default order: Yoco · PayFast
Minimum amount R15 for all providers (one site-wide minimum for online payments)

Fees are not handled automatically. Payments are recorded at the full amount paid; provider fees are not deducted, estimated or added to the payer's total. Fees are dealt with by people during cashup, as today. There is no "pass fee to payer" option.

"Test connection" button per provider, like the WhatsApp setup.

How it works

  • A single PaymentProvider interface in the backend: createCheckout, handleWebhook (verify signature → mark paid), getTransaction, refund (if the provider supports it via API), listTransactions (for reconciliation).
  • Yoco is moved behind the same interface first (no behaviour change), then PayFast is added.
  • Payment.method gets the provider name; YocoTransaction is generalised to ProviderTransaction (provider column) so the existing reconciliation page works for all providers.
  • Refunds: if the provider has no refund API, the supervisor refunds in the provider's portal and records it here (existing manual refund flow).

Edge cases

  • Provider disabled while a checkout is in progress → webhook still accepted and processed.
  • Duplicate / late webhooks → idempotent on the provider's transaction id.
  • Webhook never arrives → "Check payment status" button and a periodic reconcile job.
  • Amount mismatch (user edits amount in app) → payment held as "needs review".
  • Only enabled providers appear in kiosk / self-service.
  • Amount below R15 → online payment options hidden, with a message to pay at the door / contact the organiser.
  • Before building each one: get the documentation links from Joshua and confirm current API docs for webhooks, sandbox, refunds and QR deep-link support.

3. POPIA toolkit

Tools that make the platform POPIA-ready out of the box. This is the main selling point for other churches and organisations: "your attendee data is handled properly, and you can prove it."

This is tooling to support compliance, not legal advice. Wording and retention periods should be checked by whoever is the customer's Information Officer.

Who: Admins (and the appointed Information Officer) manage it. Users get self-service privacy tools.

For users (data subjects)

  • Privacy centre in My account:
    • Download my data — JSON + readable PDF of profile, registrations, payments, tickets, form answers, consents, messages sent.
    • Correct my data — edit profile; request correction of locked records.
    • Delete my account — creates a deletion request (see below).
    • Communication preferences — email / WhatsApp / marketing opt-in with timestamps (extends the existing notification preferences).
  • Unsubscribe link in every bulk email and an opt-out keyword for bulk WhatsApp. Opted-out users are skipped automatically in broadcasts.
  • Privacy notice acceptance — versioned; users accept the current version at registration, and are asked again when it changes.

For admins

Tool What it does
Information Officer Name, contact details, shown on privacy notice and contact page
Privacy notice editor Versioned text, published version history
Consent log Who consented to what, when, which notice version, how (web, kiosk, WhatsApp)
Data requests inbox Access, correction and deletion requests with status, due dates and audit trail
Deletion workflow Admin approves → personal info is anonymised; financial records kept (amounts, dates) but unlinked from the person, as tax law requires them to be kept
Retention policies Per data category (inactive accounts, form answers, medical details, analytics, audit log, backups): keep for N months, then anonymise/delete via nightly job. Sensible defaults shipped
Operator register List of third parties that process data (Yoco/PayFast/…, WhatsApp provider, email provider, Sentry, hosting) with purpose; feeds the privacy notice
Access log Who exported or viewed personal data in bulk (exports, attendee lists, medical details)
Breach register Record a security compromise, affected users, actions taken; templates for notifying users and the Information Regulator
Children's data Already covered by parental consent; flagged as special/children's info in reports
POPIA readiness page Checklist with score: officer set, notice published, retention set, operators listed, unsubscribe working…

Edge cases

  • Deletion of a user with an open registration / upcoming event → request waits until after the event, or the admin cancels and refunds first.
  • Anonymised users still count in historic reports ("Deleted user").
  • Backups contain deleted data until they age out — stated in the notice; backup retention is one of the retention policies.
  • Impersonation and exports must be logged in the access log.

4. Kids check-in with security labels

A secure check-in/check-out flow for children's ministry and youth events. Builds on sessions (§1), attendees, parental consent and allocation groups.

Who: Parents check in children (self-service kiosk or staff desk). Kids-ministry staff run rooms. Supervisors set up rooms.

How it works

  • Parent finds their family (phone number / QR from My account) and ticks the children being dropped off. New children can be added on the spot.
  • The system prints two labels:
    • Child label: name, room, allergy/medical icons, short pickup code.
    • Parent label: the same pickup code.
  • Check-out requires scanning or typing the matching code. A mismatch alerts the room leader.
  • Rooms come from allocation groups (age/grade rules), with ratios (e.g. 1 leader per 5 toddlers) and live room counts.
  • Page a parent: room leader taps "Call parent" → WhatsApp/SMS to the parent ("Please come to the Toddlers room").
  • Attendance per child per session feeds the attendance register.

Plumbing / edge cases

  • Label printing via browser print with label-size CSS (Dymo / Brother label printers, 62 mm and 89×36 mm presets); PDF fallback.
  • Allergies and medical info come from the consent/medical fields; respect the medical-info deletion rules.
  • Custody notes ("Do not release to …") visible to room leaders only.
  • Works on the existing offline-scanning foundations for flaky Wi-Fi.

5. Volunteer / serving rosters

Plan who serves where, for an event or each session.

  • Serving roles per event/session: Welcome, Parking, Kids, Sound, Kitchen… with the number of people needed.
  • Volunteers sign up for open slots, or supervisors assign people.
  • Reminders via email/WhatsApp before their slot; "can't make it" → swap request to others in the role.
  • Volunteer check-in on the day (separate from attendee check-in).
  • Volunteers can get a free/discounted ticket automatically (uses coupons/comp).
  • Reports: roster sheet per session, fill rate, hours served per person.

6. Event series and cloning

  • Clone event: copy an event with its options, variants, early-bird tiers, forms, consent text, feedback form, cost template, allocation groups and sessions (dates shifted to the new start). No registrations.
  • Series: group separate events (Camp 2025, Camp 2026; monthly Men's Breakfast) on one series page, with "Next in this series" and a series follow/notify option.
  • Year-on-year reports: registrations, income and attendance compared across events in a series.
  • Season pass: a bundle across events in a series (extends event bundles).

7. Apple and Google Wallet tickets

  • "Add to Apple Wallet" / "Add to Google Wallet" buttons next to the PDF ticket, in email and in My Events.
  • Pass shows event, date (next session for multi-day), location, holder name, QR code.
  • Passes update when the event/session changes (time, venue, cancelled).
  • Requires an Apple Developer account (pass signing certificate) and a Google Wallet issuer account; both configured in Site settings and optional — hidden when not configured.

8. Outbound webhooks

Let other systems react to what happens in Hope Events.

  • Admin adds endpoints and picks events: registration.created, registration.paid, registration.cancelled, payment.refunded, ticket.checked_in, user.created, event.published…
  • Payloads signed with HMAC (secret per endpoint), retries with back-off, delivery log with "Resend".
  • Enables integrations (ChurchSuite, Planning Center, Google Sheets via Zapier/Make) without building each one. Native integrations can follow later.

9. Admin dashboard, command palette, bulk actions

  • Dashboard as supervisors' home: today's events/sessions, check-ins so far, money in (today/week), pending items (unreconciled payments, consent outstanding, waitlist offers, data requests), alerts.
  • Command palette (Ctrl+K): jump to any event, user, registration, ticket or setting; quick actions ("New event", "Find payment R450").
  • Bulk actions on registration/attendee lists: message, export, mark paid, move option, cancel, allocate.
  • Saved filters per user on list pages.

10. Languages (Afrikaans first)

  • i18n framework on the frontend (e.g. next-intl); English + Afrikaans.
  • User language preference; emails, WhatsApp messages and PDFs sent in the user's language, with per-language templates.
  • Event content stays as typed by the supervisor (optional translated title / description fields later).

11. Accessibility pass (WCAG 2.2 AA)

  • Audit public pages, registration, payment, My Events and the kiosk.
  • Keyboard navigation, focus states, labels, colour contrast (light and dark), screen-reader names for icon buttons, error messages linked to fields.
  • Automated axe checks in the Playwright suite so it doesn't regress.

12. Multi-campus (one install per church)

Decision: one install per church. Each customer gets their own install and database (the /setup wizard already makes this possible). This gives full data separation — which also helps the POPIA story — and lets each church run on a different server. True multi-tenant is out of scope.

Multi-campus is in scope: one church with several campuses inside one install.

Setup

  • Site settings → Campuses: off by default. When on, admins add campuses (name, address, contact, optional logo/colour accent, default location for events).
  • Single-campus churches never see campus fields anywhere.

How it works

  • Events belong to one campus, or to All campuses (church-wide conferences, camps).
  • Public site: campus filter on the events list, remembered per visitor; campus shown on event cards and tickets. Optional campus landing pages (/campus/<slug>).
  • Users can set a home campus (used as the default filter and for broadcasts). Accounts stay shared across campuses — one login for the church.
  • Campus-scoped roles: a supervisor or staff member can be limited to one or more campuses (sees and manages only those events, registrations, scanning, cashups). Admins and unscoped supervisors see everything.
  • Reports, cashup, dashboard, funnel stats: filter by campus plus a church-wide total.
  • Broadcasts: target "everyone at campus X".
  • Kids check-in rooms and volunteer roles are per campus.
  • Notification recipients per campus (e.g. the campus pastor gets that campus's daily summary).

Edge cases

  • Turning campuses off with campus data present → hidden, not deleted.
  • Deleting a campus with events → blocked; archive instead.
  • Moving an event to another campus → scoped supervisors of the old campus lose access; logged in the audit log.

Data model (sketch)

model Campus {
  id        String   @id @default(uuid())
  name      String
  slug      String   @unique
  address   String?
  isActive  Boolean  @default(true)
  order     Int      @default(0)
  events    Event[]
}
// Event.campusId String? (null = all campuses)
// User.homeCampusId String?
// UserCampusScope { userId, campusId } — empty = unscoped

Running many installs

Because every church is a separate install, a few tools make that manageable:

  • Install script: fresh server → Node, Postgres, PM2, reverse proxy, .env, migrations, then /setup in the browser.
  • Update script: pull a tagged version, back up the database, run migrations, build, restart; roll back on failure.
  • Version and health: current version shown in admin and on a /health endpoint (version, DB, jobs, last backup), reported to the Hub.

13. Central management platform (Hope Events Hub)

A separate app, run by Joshua, to see and manage every Hope Events install from one place: who the customers are, which version each runs, whether they're healthy, basic usage stats, and their licence status.

Who: Joshua (and any future support staff). Customers never log in to it.

Separate app, separate repo, separate server

  • Its own git repository: joshua/hope-events-hub on the same Gitea server (c-git.crosscode.co.za), with its own semver tags, CHANGELOG and deploys. It is never shipped to churches, so none of its code (customers, licence signing key, other churches' data) ends up on a customer's server.
  • Same stack (Next.js + Express + Prisma + Postgres) so it's familiar.
  • What lives where:
    • Hub repo: dashboard, customers, licences, licence signing (private key — only ever on the Hub server), /api/installs/register and /api/installs/heartbeat endpoints, alerts.
    • Hope Events repo: the Hub client — licence step in /setup, "Connect to Hub" in Site settings, heartbeat job, licence verification with the Hub's public key, read-only mode, banners.
  • The contract between them (register / heartbeat payloads, licence token format) is versioned with a protocolVersion field and documented in both repos (docs/hub-protocol.md). The Hub accepts the current and previous protocol version, so churches on older versions keep reporting while they wait to update.
  • Runs on its own server at events-hub.crosscode.co.za. If the Hub is down, no church install is affected.
  • Licence keys are generated by the Hub: Joshua clicks "Generate" on a customer and the Hub creates the key (not typed by hand).
  • Hope Family Church's own site is the first registered install.

Registration (setup → Hub)

  1. The /setup wizard gets a Licence step: the customer enters the licence key Joshua issued (created in the Hub first).
  2. When setup completes, the install sends POST /api/installs/register with: licence key, site name, URL, admin contact, version, server info (Node/Postgres version, OS), timezone, enabled features.
  3. The Hub checks the key, links the install to the customer, and returns an install id + install secret (stored encrypted in AppSettings) and a signed licence token.
  4. Existing installs (already past setup) get a "Connect to Hub" button in Site settings → Licence.

All later calls are signed with the install secret (HMAC) and go over HTTPS.

Heartbeat (install → Hub)

  • Every 15 minutes (job queue) the install POSTs a heartbeat:
    • Health: version, uptime, DB OK, job queue OK, last successful backup, disk free, last error count (from Sentry or local logs), pending migrations.
    • Basic stats (aggregate counts only): users, active/upcoming events, registrations and tickets (30 days / all time), check-ins, WhatsApp/email sent, enabled payment providers, campuses on/off.
    • Income in ranges, never exact amounts: total payments received (30 days / all time) reported as a band — R0–R1 000, R1 001–R5 000, R5 001–R20 000, R20 001–R50 000, R50 001–R100 000, over R100 000. The band is worked out on the install; the exact figure never leaves it.
  • Install → Hub only. The install pushes; the Hub never needs to reach into the install, so it works behind firewalls and the Hub has no access to church data.
  • No personal information is ever sent (no names, emails, phones, attendee data). Listed in the install's POPIA operator register and privacy notice as "aggregate usage statistics to the software provider".

Hub dashboard

  • Installs list: customer, URL, version (flag if behind the latest tag), licence status, last heartbeat (red if > 1 h ago), health status.
  • Install detail: health history, stats charts, version history, licence history, notes, contacts.
  • Alerts (email/WhatsApp to Joshua): install offline > 1 h, backup failed or older than 48 h, disk low, errors spiking, licence expiring.
  • Releases: latest version and changelog; installs see "Update available" in their admin.
  • Customers: contact details, plan, licence(s), billing notes.

Licences

State What the install does
Active Everything works
Grace (expired or Hub unreachable) Everything works; admins see a banner "Licence needs renewing — X days left". Default 14 days
Suspended Read-only: no new events, no new registrations or payments. Existing tickets still scan, and admins can still view and export all data
Revoked Same as suspended, with a permanent banner
  • Joshua turns a licence on/off, sets expiry dates and plan in the Hub; the install picks up the change on its next heartbeat.
  • The licence is a signed token (Ed25519: the Hub signs with its private key, installs only hold the public key). Installs verify it offline, so a Hub outage never switches a church off, and a token can't be faked by editing the database.
  • Never a hard kill: a church must always be able to scan tickets for events people already paid for, and get its own data out (POPIA access rights). Suspension blocks new activity only.
  • One plan for now. Plan limits (e.g. number of campuses, WhatsApp) can be carried in the token later. The first per-licence limit comes with 2.0: the monthly AI cap in rand (§15), kept and enforced on the Hub, since the Hub meters AI.

Things to be aware of

  • Code runs on the customer's server, so a determined developer could patch out the licence check. Real protection is the contract plus updates and support only being available to valid licences — the licence check keeps honest customers honest.
  • The Hub should not offer remote commands (run updates, impersonate, database access) in v1: it's the most valuable target an attacker could hit, so keeping it read-only toward installs keeps every church safe if it is ever compromised. Remote "trigger update" can be revisited later.
  • The Hub itself needs 2FA, backups and the same audit log approach.

Data model (Hub, sketch)

model Customer { id, name, contactName, email, phone, notes, licences, installs }
model Licence  { id, customerId, key, plan, status, expiresAt, graceDays, issuedAt }
model Install  { id, customerId, licenceId, name, url, secretHash, version,
                 lastHeartbeatAt, health Json, registeredAt }
model Heartbeat { id, installId, receivedAt, version, health Json, stats Json }
                 // rolled up to daily after 30 days
model InstallEvent { id, installId, type, message, createdAt } // alerts, version changes
// 2.0, AI (§15): Licence gets aiMonthlyCapCents; usage per call, no prompt/response text
model AiUsage   { id, installId, helper, model, inputTokens, outputTokens, costCents, createdAt }

14. Foundations (platform work)

Not visible to users, but this is what makes 2.0 a safe product to sell and keep adding to.

Item What
CI (Gitea Actions) Lint, typecheck, Jest, build on every push; deploy on tag
Playwright E2E Register → pay (sandbox) → ticket → scan → cashup; multi-day check-in; kiosk
Job queue Move setInterval jobs out of index.js into pg-boss (uses Postgres): retries, no double runs, job history in admin
Split big controllers registrationController (2.3k lines), eventController, paymentController → services by domain. Done as each feature touches them, not as a big-bang rewrite
TypeScript backend Incremental (allowJs), new code in TS first; share zod schemas/types with the frontend
API v2 /api/v2 with consistent errors, pagination and an OpenAPI spec (replaces the hand-written API docs). /api (v1) kept during the transition
Dependencies Express 5, native fetch instead of node-fetch, password hashing upgrade with rehash-on-login

15. AI helpers (2.0 headline)

Small, one-button AI helpers in the places people already work. No chat in 2.0: each helper is one request with a predictable cost, and a person always reviews the result before anything is saved or sent.

Who: Supervisors and admins (the people who create events and read reports).

The helpers

Helper Where What it does
Draft an event Event wizard, first step: "Describe your event" "Youth camp 14–16 March at Camp Wartburg, R450, 60 places, kids need consent" → fills in title, description, dates/sessions, location, ticket options and prices, capacity, consent settings. The supervisor checks every field and saves as usual
Summarise this report Every report (Reports page) A few plain-language insights under the report: trends, outliers, comparisons with the previous period
Draft a message Email / WhatsApp attendees, broadcasts Turns a short note ("remind them to bring a sleeping bag, bus leaves 7:30") into a friendly message in the right tone and length (shorter for WhatsApp), with placeholders like {{name}}
Summarise feedback Post-event feedback results Themes, what people liked, what to improve, notable comments
Improve this text Event description, banner, message boxes Rewrite for clarity, fix spelling, shorten

Every helper shows "AI suggestion — check before saving", can be undone, and never saves, sends or publishes by itself.

Provider and model

  • One provider: Claude (Anthropic), model Claude Haiku 5.5 — the cheapest current model ($0.10 per million input tokens, $0.50 per million output, for prompts under 100K tokens; prices as of 2026-10). Good enough for drafting and summarising; a helper can move to a bigger model later if quality needs it.
  • Rough cost: a helper call is ~2,000–10,000 tokens in and ~500 out, so well under US$0.002 per click — about R1.50–R3 per thousand clicks. Fixed instructions are prompt-cached, which makes repeat calls cheaper still.
  • Structured output (a JSON schema) for "Draft an event", so the reply maps straight onto the event form and is validated before it's shown.

Key, caps and billing — through the Hub

Joshua provides the API key; churches don't need their own. The key must never sit on an install (one leaked install would leak it for everyone), so:

  • Installs call AI through the Hub: POST /api/ai/complete on the Hub, signed with the install secret like the heartbeat. The Hub adds the key, calls Claude, and returns the result. Still install → Hub only — the Hub never reaches into an install.
  • The Hub meters every call in rand: tokens used × the model's price × an exchange rate Joshua sets in the Hub (with an optional margin). Usage is kept per install per month.
  • Monthly rand cap per licence, set by Joshua in the Hub to match what the church pays (e.g. R0 = AI off, R50, R200). The licence's cap is the first per-plan limit. At 80% the install's admins get a warning email; at 100% the helpers are switched off until the next month (or until Joshua raises the cap). Optional per-user daily limit against runaway use.
  • Admins see their usage in Site settings → AI: used this month (R and number of uses), the cap, a daily chart, use per helper and per person, and an on/off switch per helper.
  • If the Hub or Claude is unavailable, the buttons say so; nothing else in the app is affected.

POPIA

  • Anthropic is added to the operator register (and Joshua's Hub, which relays the requests) and to the privacy notice: processing outside South Africa (POPIA s72), for drafting and summarising only.
  • Send as little personal information as possible: reports and feedback are sent as counts, totals and anonymised comments — no names, contact details, medical or children's information. Drafting an event or a message needs none.
  • The Hub stores no prompt or response text — only the install, helper, tokens, cost and time. Check Anthropic's commercial terms on retention and training before launch.
  • Uses that include personal information (e.g. feedback comments) go in the access log.

Plumbing

  • Install: backend/src/ai/ — one client for the Hub's AI endpoint, one prompt + schema per helper, usage shown from the Hub. Feature flag per helper, all off when the licence has no AI or the Hub isn't connected.
  • Hub: the AI proxy, metering, caps, warnings, an exchange-rate setting, and a per-install usage view for Joshua (alongside the existing install detail page).
  • A small eval set (example event descriptions, reports, notes, with what a good answer looks like) to check quality when changing the prompt or model.
  • Help content, audit log entries (AI settings changed), CHANGELOG, FEATURES.md, SETTINGS.md.

16. AI: later stages

Ideas for after 2.0, in rough order. Not tied to versions — each is decided when it's picked up, and all of them reuse the Hub proxy, metering and caps from §15.

Ask AI (read-only assistant)

A chat panel for supervisors and admins: "How many paid for Camp compared with last year?", "Which events this month are under half full?", "Who hasn't paid yet for Men's Breakfast?".

  • The AI can only call tools the app defines — the existing stats and report queries, "find an event", "registrations by week" — never its own SQL. It runs with the logged-in person's permissions, so it sees nothing they couldn't.
  • Answers show the numbers they're based on and link to the matching report.
  • Costs more per question than a button (several tool calls), so it counts against the same cap; may need a bigger model for harder questions.

Help and navigation assistant

"How do I add a coupon?", "Where do I see who checked in?" → a short answer from the in-app help content (frontend/src/content/help/) with a link straight to the right page. No personal information involved, so it's cheap and simple — could also be offered to ordinary users.

Actions with confirmation

The Ask assistant can also do things — create an event draft, schedule a reminder, offer waitlist places, record a cash payment — always as "here's what I'll do → Confirm", with each confirmed action in the audit log.

More helpers

Smaller one-button additions as they come up: suggest an event's price tiers from past events, write a post-event thank-you, draft certificate wording, translate a message into Afrikaans (ties in with §10), flag unusual payments for the cashup.

Self-hosted AI (long term)

Run an open-weight model on Joshua's own GPU server instead of a paid API, once usage and income justify the hardware:

  • Because every install already talks to the Hub's AI endpoint, moving to self-hosting is a change on the Hub only — the Hub gets a provider adapter (Claude now, self-hosted later, or both: simple helpers self-hosted, harder questions on Claude).
  • POPIA bonus: the data can stay in South Africa — no cross-border transfer.
  • When it makes sense: compare the monthly API bill with the cost of a GPU server (hardware or cloud rental, power, upkeep), and check the self-hosted model against the eval set from §15 before switching any helper over.

Suggested build order

Phase Version Features Why
0 1.32.0 ✅ Early-bird ribbon on event cards (shipped 2026-10-22) Quick win
1 1.33.0 ✅ Multi-day events — phase A (sessions, whole-course tickets, late joining + late pricing, per-session check-in with whole-day window, attendance register, optional reminders, certificates) Requested first; touches registration/tickets/scanning, so it shapes later work
2 1.34.0 ✅ CI + Playwright E2E + job queue Safety net before the riskiest work (payments). Session reminders move to the queue
3 1.35.0 ✅ Multi-day events — phase B (pick sessions, per-session capacity) Builds on phase A, now with E2E coverage
4 1.36.0 ✅ – 1.37.2 Payment provider interface (Yoco moved behind it) → PayFast, then hiding "Pay online" when it can't be used and payment status checks (SnapScan and Zapper dropped) Payment code gets split out of the big controller as part of this
5 1.38.0 ✅ 1.39.0 ✅ 1.40.0 ✅ POPIA toolkit (1.38.0: user privacy centre, unsubscribe, WhatsApp STOP, data requests inbox; 1.39.0: privacy notice editor + versions, operator register, consent log, access log; 1.40.0: retention jobs, breach register, children’s-data flags, readiness page) Main selling point; retention jobs use the job queue
6 1.41.0 + Hub v1 Install/update scripts, /health, licence step in /setup, "Connect to Hub", heartbeat, licence states — plus the Hub app (separate repo) Must exist before the first church is sold; Hope Family Church registers as install #1
7 1.42.0 Multi-campus Kids rooms and volunteer roles are per campus, so campuses come first
8 1.43.0 – 1.44.0 Kids check-in, Volunteer rosters Depend on sessions and campuses
9 1.45.0 – 1.47.0 Event series & cloning, Wallet passes, Outbound webhooks Smaller, independent wins
10 1.48.0 – 1.50.0 Admin dashboard/Ctrl+K/bulk actions, Afrikaans, Accessibility pass Polish before the big release
11 1.51.0 TypeScript backend + OpenAPI + /api/v2 alongside v1 The breaking-change groundwork
12 2.0.0 AI helpers (§15 — headline feature: draft an event, summarise reports and feedback, draft messages, improve text; through the Hub with a rand cap per licence), v1 API removed, migration guide, marketing site/docs for selling The release. AI needs the Hub (phase 6) for the key, metering and caps

Each phase should add matching help content (frontend/src/content/help/), audit log entries, Jest tests, a CHANGELOG entry, and updates to FEATURES.md and SETTINGS.md — same as the previous round. Branch → merge → tag → push per feature.


Decisions (2026-10-22)

# Question Decision
1 Pick sessions in the first release? No — whole-course first (1.33.0), pick sessions as a separate release (1.35.0)
2 Late joining and late price Set per event: late joining on/off (until session N), and the late price (full / pro-rata / fixed)
3 Check-in window Per event, whole day by default (any time on the session's date); "session time only" as the alternative
4 Certificates Yes — four templates (Classic, Modern, Church, Kids); the event creator chooses whether to issue certificates and which template
5 Session reminders Off by default, configurable per event
6 Selling model One install per church — data separation, any server. No multi-tenant. Multi-campus inside an install is in scope
— Pro-rata rounding Nearest R10
— Multi-campus In scope (§12), inside one install
— Central management Hope Events Hub in its own git repo (joshua/hope-events-hub): installs register on setup, send heartbeats (health, version, aggregate stats), licences switched on/off centrally
7 Payment provider order Yoco · PayFast. Joshua can supply documentation links
— SnapScan, Zapper Dropped (2026-10-23): Yoco and PayFast are enough. Later versions moved up by two
— Provider fees Not deducted or estimated automatically; handled by people during cashup. No "pass fee to payer"
— Minimum online payment R15 for all providers
— Scope All features in this document approved; POPIA toolkit and kids check-in are highlights
— Early-bird ribbon Added before everything else; shipped as 1.32.0, all later versions moved up by one
— Hub domain events-hub.crosscode.co.za (2026-10-23)
— Licence keys Generated by the Hub when Joshua clicks "Generate" on a customer (2026-10-23)
— Grace period / suspension 14-day grace; suspended = read-only (tickets still scan, data export still works) (2026-10-23)
— Income in the heartbeat Included, as ranges only (R0–R1 000, R1 001–R5 000, …), never exact amounts (2026-10-23)
— Licence plans One plan for now (2026-10-23)
— AI in 2.0 The headline feature of 2.0: one-button helpers only (draft an event, summarise reports and feedback, draft messages, improve text) — no chat yet (2026-10-24)
— AI provider and key One provider, Claude (Haiku 5.5 to start, for cost). Joshua provides the key; installs call AI through the Hub, which holds the key and meters usage (2026-10-24)
— AI cost limits A monthly rand cap per install, set in the Hub per licence to match what the church pays; admins see their usage in Site settings → AI (2026-10-24)
— AI later Ask AI (read-only, tools only), help/navigation assistant, actions with confirmation, more helpers, and long-term self-hosted AI behind the same Hub endpoint — not tied to versions (2026-10-24)
— POPIA: message kinds Bulk messages are either Event updates (to registrants — still sent after unsubscribing) or News & announcements (marketing — opt-in, one-click unsubscribe). Every bulk email links to the preferences (2026-10-23)
— POPIA: existing users Kept opted in to news (logged as "existing account"), asked once at their next login to confirm or opt out. New sign-ups choose with an unticked box (2026-10-23)
— POPIA: deleting an account A request: the account is deactivated at once, personal info is anonymised when an admin approves. Waits while the person has an open registration for an upcoming event (2026-10-23)
— POPIA part 2 split 1.39.0: privacy notice editor + versions + re-acceptance, operator register, Information Officer on the contact page, consent log, access log. 1.40.0: retention policies + nightly job, breach register, children's-data flags, readiness page. Hub moves to 1.41.0 (2026-10-23)
— POPIA: privacy notice Today's notice becomes the starting template, one editable box per section; org name, Information Officer, retention periods and the operator list fill in automatically. Publishing saves a numbered version (2026-10-23)
— POPIA: re-accepting the notice The admin decides per version ("significant change — ask everyone to accept again"); people are asked once after login (2026-10-23)
— POPIA: inactive accounts Anonymised after 5 years without a login or registration, on by default, with a warning email 30 days before; staff/supervisor/admin accounts never (2026-10-23)
— POPIA: WhatsApp STOP In 1.38.0, via WAWP's incoming message webhook with a secret in the URL (WAWP doesn't sign webhooks) (2026-10-23)

Still open

Nothing for now.