Files
hope-events/docs/ROADMAP-v2.md
T
joshuaandClaude Opus 5.5 aeb5d6d678
CI / Backend tests (push) Canceled after 0s
CI / Frontend lint, typecheck, build (push) Canceled after 0s
CI / End-to-end tests (push) Canceled after 0s
CI / Deploy tagged release (push) Canceled after 0s
Bump to 1.37.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:26:09 +02:00

38 KiB
Raw 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), and maintainable (tests, CI, typed backend, versioned API).

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. Next: 1.38.0 SnapScan, then Zapper.

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, SnapScan, Zapper
  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. Suggested build order
  16. 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, SnapScan, Zapper

Today Yoco is the only online provider. Add PayFast, SnapScan and Zapper, and let the admin enable one or more in Site settings. At checkout the user picks from the enabled ones.

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

Site settings → Payments

Setting Options
Providers Yoco · PayFast · SnapScan · Zapper — 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 · SnapScan · Zapper
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, SnapScan and Zapper are added one release each.
  • SnapScan and Zapper are QR/app-based: on desktop show the QR, on mobile deep-link into the app; the page polls/waits for the webhook and moves on automatically when payment lands.
  • 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/domain (e.g. hub.<your-domain>). If the Hub is down, no church install is affected.
  • 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.
    • Optional (off by default, customer chooses): payment totals.
  • 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.
  • Optional plan limits later (e.g. number of campuses, WhatsApp) carried in the token.

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

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

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.39.0 Payment provider interface (Yoco moved behind it) → PayFast → SnapScan → Zapper, one release each Payment code gets split out of the big controller as part of this
5 1.40.0 – 1.41.0 POPIA toolkit (user privacy centre + unsubscribe first, then admin tools, retention jobs, readiness page) Main selling point; retention jobs use the job queue
6 1.42.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.43.0 Multi-campus Kids rooms and volunteer roles are per campus, so campuses come first
8 1.44.0 – 1.45.0 Kids check-in, Volunteer rosters Depend on sessions and campuses
9 1.46.0 – 1.48.0 Event series & cloning, Wallet passes, Outbound webhooks Smaller, independent wins
10 1.49.0 – 1.51.0 Admin dashboard/Ctrl+K/bulk actions, Afrikaans, Accessibility pass Polish before the big release
11 1.52.0 TypeScript backend + OpenAPI + /api/v2 alongside v1 The breaking-change groundwork
12 2.0.0 v1 API removed, migration guide, marketing site/docs for selling The release

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 · SnapScan · Zapper. Joshua can supply documentation links
— 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

Still open

Hub (needed before phase 6, not before 1.33.0):

  1. Hub domain and server, and where licence keys come from (created by hand in the Hub per customer?).
  2. Grace period length (proposed 14 days) and suspended = read-only (tickets still scan, data export still works) — agreed?
  3. Should payment totals be in the heartbeat at all (opt-in per customer), or counts only?
  4. Licence plans/tiers (e.g. limits on campuses or WhatsApp), or one plan for now?