73 KiB
Cross Events — Road to 2.0.0
Planned features and platform work to take Cross 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-30): all features in this document are approved, and everything before 2.0.0
has shipped (1.32.0 – 1.51.17). 2.0.0 is in progress on the release/2.0 branch, tagged only
when all of it is in (decided 2026-10-30):
| # | Part | Where | State |
|---|---|---|---|
| 1 | AI proxy: key, metering in rand, caps, markup, usage views; model list with prices (Sonnet 5.5 default) | Hub 1.6.0 – 1.6.1 (events-hub) |
✅ tagged; real Claude calls tested 2026-10-30 |
| 2 | AI plumbing: /api/v2/ai/status, usage, Site settings → AI, <AiButton>, useAiStatus(), AiCall log, Anthropic operator entry |
release/2.0 |
✅ committed (WIP) |
| 3 | The five helpers (§15) with their buttons, an eval set, privacy notice text | release/2.0 |
next |
| 4 | v1 removed (webhook paths and old message links keep working) | release/2.0 |
to do |
| 5 | Migration guide, docs | release/2.0 |
to do |
| 6 | Marketing site: Cross Code with a Cross Events page (§17) | its own repo | to do |
The full version list is in HANDOVER.md and the build order below.
- Multi-day events (§1): 1.33.0 phase A (sessions, late joining, per-session check-in, attendance register, reminders, certificates); 1.35.0 phase B (pick sessions, per-session capacity); 1.35.1 per-session waitlist, session picker at the door.
- Platform: 1.34.0 job queue (pg-boss), Playwright E2E tests and deploy script (the Gitea CI workflow was removed in 1.46.1 — no runner).
- Payments (§2): 1.36.0 provider interface (Yoco behind it); 1.37.0 – 1.37.2 PayFast, "Pay online", payment status checks. SnapScan and Zapper dropped (2026-10-23).
- POPIA toolkit (§3): 1.38.0 – 1.40.0.
- Hub (§13): 1.41.0 connection (licence, heartbeat, read-only mode,
/health, install script) with Hub 1.0.0; 1.41.3 renamed Hope Events → Cross Events; 1.42.0 Request update with Hub 1.1.0; 1.42.1 production Hub key built in. - Multi-campus (§12): 1.43.0 campuses; 1.44.0 campus-scoped staff.
- Kids check-in (§4) 1.45.0; volunteer rosters (§5) 1.46.0.
- Event series and cloning (§6): 1.47.0 copy an event, 1.47.1 series, 1.47.2 year-on-year report, 1.47.3 season passes.
- Outbound webhooks (§8) 1.48.0.
- Dashboard, palette, bulk actions, saved filters (§9): 1.49.0 – 1.49.2.
- Accessibility (§11) 1.50.0; 1.50.1 long lists; 1.50.2 event picker and webhook guide.
- TypeScript backend and
/api/v2(§14): 1.51.0 – 1.51.3 foundations, registrations and payments, accounts and check-in, Express 5; 1.51.4 security and dependency updates; 1.51.5 Next.js 16; 1.51.6 command palette and ExcelTS; 1.51.7 – 1.51.16 every remaining area on v2 — every endpoint is on v2. - 1.51.17 security fix: forgot password and sign-in don't reveal whether an account exists.
Removed from scope (2026-10-28): Wallet passes (§7) and languages (§10).
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/events-hub (new) |
The Hub app (§13): its own versions, CHANGELOG, releases and server |
Contents
- Multi-day events (sessions) ← first
- Payment providers: PayFast
- POPIA toolkit
- Kids check-in (digital pickup codes)
- Volunteer / serving rosters
- Event series and cloning
Apple and Google Wallet tickets— removed from scope- Outbound webhooks
- Admin dashboard, command palette, bulk actions
Languages (Afrikaans first)— removed from scope- Accessibility pass (WCAG 2.2 AA)
- Multi-campus (one install per church)
- Central management platform (Cross Events Hub)
- Foundations (platform work)
- AI helpers (2.0 headline)
- AI: later stages
- Marketing site (Cross Code)
- Suggested build order
- 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/endDatestay 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.jswith 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.methodgets the provider name;YocoTransactionis generalised toProviderTransaction(providercolumn) 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 (digital pickup codes)
A secure check-in/check-out flow for children's ministry, run from a staff desk. Builds on sessions (§1), attendees, allocation groups and campuses (§12). Fully digital — no label printers (2026-10-28).
Who: Kids-ministry staff at the check-in desk and in the rooms. Supervisors set up rooms. Parents manage their family's children in My account and see pickup codes on their phone.
How it works
- Families and children are kept between events (weekly kids church): a family has one or more parent accounts, its children (name, date of birth, allergies, medical notes, custody notes — the last three encrypted) and authorised adults (name, phone, relationship — no account needed). Parents manage them under My account → Kids; staff can find a family by phone, name or the parent's QR code and add a family or child on the spot.
- An event gets a Kids check-in switch (single events or one session per Sunday). The first
check-in makes a free registration under the parent with an attendee and ticket per child, and
each check-in writes a
TicketUsage, so the attendance register, reports and campus scoping work as they do for tickets. - Each check-in gets a pickup code (4 characters, no 0/O/1/I, unique per event and day). The parent sees it live on their phone (code + QR) and gets it by WhatsApp/email; it can also be sent to an authorised adult. Anyone with the code can collect the child.
- "Write on the sticker" panel on the desk screen: first name, room, code and a short alert (ALLERGY / MEDICAL) in large print, for volunteers to copy onto a blank sticker. The full details stay on screen.
- Rooms are a "Kids rooms" allocation set: each room has an age range, capacity and a ratio (children per leader). The room is suggested from the child's age. Room screens show live cards (name, code, allergy/medical icons, custody notes) and counts: children in, leaders on duty ("I'm on duty"), over-ratio and full warnings.
- Check-out by scanning the parent's QR or typing the code; the leader records who collected. A wrong code alerts the room and is logged; "Override" needs a reason.
- Page a parent: "Call parent" → WhatsApp (email fallback) and a notice on the parent's page.
Plumbing / edge cases
- Medical and custody notes: room leaders, supervisors and admins only; never in messages; room lists showing them are written to the access log.
- POPIA: families and children in
anonymiseUser/exportUserData; retention rule — a child's profile is deleted after 6 months without a check-in; readiness flags children's data. - Offline: the desk caches the event's families and checks in offline (device-prefixed codes, synced later). Check-out offline only for children checked in on the same device.
- Audit log: overrides, wrong codes and custody-note changes.
5. Volunteer / serving rosters
Plan who serves where, for an event or each session. (Shipped in 1.46.0 — decisions below.)
- 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
Four releases, one per part (2026-10-28): 1.47.0 clone, 1.47.1 series, 1.47.2 year-on-year reports, 1.47.3 season pass.
- Clone event (1.47.0 ✅): "Copy" opens the New event popup filled in from the source, so everything can be changed before saving: options, variants, early-bird tiers, form, consent text, feedback form, sections, notification recipients, certificates, reminders, allocation groups, cost template (admins) and sessions. A "New start" moves every date with it. Of the newer features only the campus is copied — not kids check-in or volunteer roles. No registrations, no coupons.
- Series (1.47.1 ✅): 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. A series can span several campuses. Following is News & announcements (opted-in only, unsubscribe link); followers get one email when a new event in the series goes live.
- Year-on-year reports (1.47.2 ✅): registrations, income and attendance compared across events in a series.
- Season pass (1.47.3 ✅): a bundle across events in a series. One checkout registers the buyer at every event on the pass; the supervisor picks the events (and ticket at each); the price is split in proportion to the events' normal prices for each event's cashup (2026-10-28).
7. Apple and Google Wallet tickets
Removed from scope (2026-10-28). Kept here for reference only; not planned for 2.0.
- "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 Cross Events. (Shipped in 1.48.0: Site settings → Webhooks; events noticed through a Prisma client extension; each endpoint added to the operator register; deliveries kept 30 days.)
- 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
Three releases (2026-10-28): 1.49.0 dashboard (Today, Waiting for you) and the Ctrl+K command palette ✅; 1.49.1 bulk actions ✅; 1.49.2 saved filters ✅.
- 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)
Removed from scope (2026-10-28). Kept here for reference only; not planned for 2.0.
- 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
axechecks in the Playwright suite so it doesn't regress.
Shipped in 1.50.0 for the pages above. The app has no dark mode, so only the light theme is checked. Staff/supervisor/admin screens are left for when they're next worked on (see HANDOVER).
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. It ships in two releases (2026-10-27):
| Release | What | Why separate |
|---|---|---|
| 1.43.0 Campuses | Campuses in Site settings, events per campus, public filter and /campus/<slug> pages, home campus, campus on cards and tickets, campus filters on reports/cashup/dashboard/funnel, "everyone at campus X" messages, per-campus summary recipients |
Adds features and filters; who can see what doesn't change |
| 1.44.0 Campus-scoped staff | Supervisors and staff limited to campuses, across every staff/supervisor endpoint (115 in 27 route files at 1.42.1) and their dashboards | Security-heavy: anything missed lets staff see another campus's data, so it gets its own release and testing |
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. Campus landing pages
(
/campus/<slug>) are part of 1.43.0. - 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 (1.44.0): 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. "All campuses" events: scoped staff can view and scan them; only admins and unscoped supervisors can edit them.
- Reports, cashup, dashboard, funnel stats: filter by campus plus a church-wide total. (Report PDFs/Excel are rendered from data the page sends, so the filters go on the data endpoints.)
- Broadcasts: target "everyone at campus X" — people whose home campus is X, plus (tick box) people registered for campus X's events in the last 12 months. Still filtered by news consent.
- 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.
Plumbing (1.44.0)
- Today access depends only on role — nothing limits staff to particular
events — so scoping is a new layer: one helper (
utils/campusScope.js) that works out a user's campuses once per request, gives awherefor event lists and checks single records (registrations, tickets, payments… traced back to their event). - A test lists every staff/supervisor route and fails unless each is campus-checked or explicitly marked campus-independent (the same pattern as the read-only route test), so new endpoints can't skip the check.
- The heartbeat's
features.campuses/stats.campusesreport the real setting (hard-codedfalseuntil 1.43.0). - How each kind of endpoint is checked: event id in the URL → a route middleware
(
scopeEvent); a record that belongs to an event (registration, ticket/QR, waitlist entry, option, bundle, allocation set…) → traced back to its event, same check; lists across events → the controller adds the scopewhere(route marked so the test knows); an event named in the body → checked before saving; nothing to do with an event → allowed or unscoped-only, listed with a reason. - Rules (decided 2026-10-27):
- Admins are never scoped. Scoped supervisors and staff see their campuses' events plus "All campuses" events.
- "All campuses" events: scoped staff can view, scan, take payments at the door and do manual registrations; event setup, options, bundles, cashup and refunds stay with admins and unscoped supervisors.
- Site banner, payment-provider reconciliation and donations not linked to an event: admins and unscoped supervisors only. Ad-hoc broadcasts: scoped supervisors only to their campuses' audiences and their events' attendees. Coupons: only for their campuses' events.
- Another campus's records answer "not found" (nothing leaks); the scanner instead says which campus the ticket is for.
- While campuses are off, scopes are ignored (everyone sees everything); the Campuses tab warns which staff are limited before turning campuses off.
- Scopes are set by admins in Admin → Users ("Limit to campuses"); changes are audited
(
user_campus_scope_changed).
Data model (sketch)
model Campus {
id String @id @default(uuid())
name String
slug String @unique
address String?
contactName String?
contactEmail String?
contactPhone String?
colour String?
logoUrl String?
defaultLocation String?
order Int @default(0)
archivedAt DateTime? // archive instead of delete when it has events
events Event[]
notifyRecipients User[] // daily summary for this campus's events
}
// Event.campusId String? (null = all campuses) — 1.43.0
// User.homeCampusId String? — 1.43.0
// AppSetting campuses_enabled — 1.43.0
// UserCampusScope { userId, campusId } — empty = unscoped — 1.44.0
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/setupin 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
/healthendpoint (version, DB, jobs, last backup), reported to the Hub.
13. Central management platform (Cross Events Hub)
A separate app, run by Joshua, to see and manage every Cross 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/events-hub(https://c-git.crosscode.co.za/joshua/events-hub.git) 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,/api/installs/heartbeatand/api/installs/update-requestendpoints, alerts. - Cross 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, "Request update".
- Hub repo: dashboard, customers, licences, licence signing (private
key — only ever on the Hub server),
- The contract between them (register / heartbeat payloads, licence token
format) is versioned with a
protocolVersionfield 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)
- The
/setupwizard gets a Licence step: the customer enters the licence key Joshua issued (created in the Hub first). - When setup completes, the install sends
POST /api/installs/registerwith: licence key, site name, URL, admin contact, version, server info (Node/Postgres version, OS), timezone, enabled features. - 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.
- 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 with a Request update button (1.42.0 / Hub 1.1.0). The request is signed like a heartbeat, shows as an alert (emailed once) and clears when the install reports a newer version. Joshua does the update.
- Customers: contact details, plan, licence(s), billing notes.
Licences
| State | What the install does |
|---|---|
| Active | Everything works |
| Grace (expired or Hub unreachable) | Everything works; only site admins see a banner "Licence needs renewing — X days left" (on their dashboard pages — never supervisors, staff, users or the public site). 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. Updates stay that way: the install asks (Request update) and Joshua updates the server; the Hub never runs anything on an install.
- 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, update requests
// 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 |
|---|---|
| Added in 1.34.0, removed in 1.46.1 (no machine to run a runner): lint, tests and deploy are done by hand | |
| Playwright E2E ✅ | Register → pay (sandbox) → ticket → scan → cashup; multi-day check-in; kiosk (1.34.0) |
| Job queue ✅ | Move setInterval jobs out of index.js into pg-boss (uses Postgres): retries, no double runs, job history in admin (1.34.0; pg-boss 11 in 1.51.7) |
| Split big controllers | registrationController (2.3k lines), eventController, paymentController → services by domain. Done as each feature touches them, not as a big-bang rewrite. Partly done (1.51.1 – 1.51.10); the handlers behind v1Route are a 2.x clean-up |
| TypeScript backend ✅ | Incremental (allowJs), new code in TS first; share zod schemas/types with the frontend (1.51.0) |
| API v2 ✅ | /api/v2 with consistent errors, pagination and an OpenAPI spec (replaces the hand-written API docs). /api (v1) kept during the transition; every endpoint on v2 since 1.51.16 |
| Dependencies | Express 5 ✅ (1.51.3); native fetch ✅ (nothing uses node-fetch any more — it's still in backend/package.json, to remove); password hashing upgrade with rehash-on-login — not done (still bcryptjs), decide whether it's in 2.0 |
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 Sonnet 5.5 for every helper (decided 2026-10-30) — $2 per million input tokens, $10 per million output (October 2026 list prices). First planned as Haiku 5.5 ($0.10 / $0.50) for cost; a trial of all five helpers on both (2026-10-30) found Haiku wrong on a report figure, over-counting feedback themes and misspelling words in messages, while Sonnet was right on all of them. One model for all helpers, no per-helper choice. The model is a setting on the Hub (Settings → AI: a list with prices filled in, Hub 1.6.1), so changing it later needs no Cross Events release.
- Measured cost (trial, charged at R17/$ and 5% markup): R0.02 – R0.31 per use with Sonnet — a WhatsApp message about R0.02, a report summary R0.17 – R0.22, a full event draft with costs R0.31. A typical church month (10 event drafts, 20 messages, 20 text improvements, 10 reports, 4 feedback summaries) comes to about R8.33, so a R50 allowance covers it about 6 times. 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/completeon 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 × (1 + markup). Usage is kept per install per month, as both the real cost and the charged (marked-up) amount.
- Markup (decided 2026-10-29): one percentage set in the Hub (a default for every licence, with an override per licence), e.g. 40%. It does two jobs: it covers Joshua if a month's real usage runs a little over (exchange-rate moves, a last call that costs more than expected), and it is his profit on AI. Churches only ever see the charged amount in rand. The real cost, the markup and the profit are shown only in the Hub.
- 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. The cap is counted in charged rand, so the real spend can never be more than cap ÷ (1 + markup).
- Hard stop, checked before each call: the Hub refuses a call when charged so far + the
worst case for this call (its prompt plus the helper's
max_tokens, priced with markup) would pass the cap. A month can't run over by more than the markup already covers, even with several people clicking at once (the check and the usage record happen in one transaction). At 80% the install's admins get a warning email. Optional per-user daily limit against runaway use. - The buttons follow the cap (decided 2026-10-29):
- Cap R0 (or AI off on the licence, or the install not connected to the Hub): the AI buttons are not shown at all, anywhere.
- Cap above R0, room left: the buttons show and work.
- Cap reached: the buttons stay where they are but are disabled, with a tooltip like "This month's AI allowance is used up — it renews on 1 December". They come back by themselves next month, or as soon as Joshua raises the cap.
- The install learns this from
GET /api/v2/ai/status→{ enabled, capReached, renewsOn, helpers }. The answer is cached briefly from the Hub and refreshed after every call. Every AI call also returns the new status, so the buttons disable straight after the call that used up the cap. The backend checks again on each request, so a stale page can't get past the cap.
- 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. The AI endpoints exist only in/api/v2(/api/v2/ai/status,/api/v2/ai/<helper>), built on 1.51's schemas and error format. The frontend has oneuseAiStatus()hook and an<AiButton>that hides, enables or disables itself from it. - Hub: real cost and charged amount per call; the markup default and per-licence override; a per-install month view with cost, charged and profit; the cap check before each call.
- 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.
Progress (2026-10-30)
- Done — Hub 1.6.0:
POST /api/installs/ai/statusand/api/installs/ai/complete(protocol §3b); the worst case of each call held against the cap in a locked transaction; Settings → AI (key, model, prices, exchange rate, default markup, test button, use per site); markup override per licence; AI use (real cost, charged, profit) on the install page. Tested with real calls: a plain message (118 tokens in, 70 out, ~1.3 s) and a JSON-schema event draft (483 in, 95 out, ~3 s), each charged a fraction of a cent (R0.0008 – R0.0017 at R17/$ and 5% markup). - Done — Cross Events,
release/2.0:backend/src/ai/(status from the licence token, then the Hub, cached a minute; per-helper switches; per-person daily limit;AiCalllog; 80% email),/api/v2/ai/status,/ai/usage,/ai/settings, theUNAVAILABLE(503) error code,frontend/src/lib/ai.ts(runAi, which posts to/api/v2/ai/<helper-with-dashes>),useAiStatus(),<AiButton>/<AiNotice>, Site settings → AI. - Next: the helpers in
backend/src/ai/helpers.ts(prompt, schema,maxTokens, and what each sends — no personal information), their/api/v2/ai/<helper>routes, the buttons on the five screens, the eval set, and the privacy notice text (aBUILT_IN_UPDATESentry).
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, 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.
17. Marketing site (Cross Code)
Part of 2.0 (decided 2026-10-30): a simple static HTML site for selling, not part of the app.
- Cross Code (Joshua's company) is the main site: who we are, what we make, contact.
- Cross Events has its own page: what it does for a church (registrations and payments, multi-day events, kids check-in, volunteer rosters, series and season passes, campuses, POPIA toolkit, the AI helpers), how it's set up (one site and database per church, updates on request, support through the Hub), and how to get in touch for a licence.
- Cross Events is the only product on sale for now. The site is laid out so more products can be added later — planned: a church app and a full church website — without a redesign (shown as "coming later" at most, until they exist).
- Plain HTML and CSS (no build step, no framework), fast on a phone, no tracking cookies.
- To decide before building: the repository (its own, e.g.
joshua/crosscode-site, not this one), the domain(s) (e.g. crosscode.co.za, with Cross Events at /cross-events or its own domain), hosting, pricing shown or "contact us", and the contact route (email address or a form).
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 (removed in 1.46.1) + 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 ✅ – 1.42.0 ✅ + Hub 1.0 ✅ – 1.1 ✅ | Install/update scripts, /health, licence step in /setup, "Connect to Hub", heartbeat, licence states — plus the Hub app (separate repo); rename to Cross Events (1.41.3); Request update (1.42.0 / Hub 1.1.0) |
Must exist before the first church is sold; Hope Family Church registers as install #1 |
| 7 | 1.43.0 ✅ – 1.44.0 ✅ | Multi-campus: 1.43.0 campuses (settings, events, public filter and landing pages, home campus, filters, messages, summaries); 1.44.0 campus-scoped staff | Kids rooms and volunteer roles are per campus, so campuses come first; scoping is split out because it's security-heavy |
| 8 | 1.45.0 ✅ – 1.46.0 ✅ | Kids check-in, Volunteer rosters | Depend on sessions and campuses |
| 9 | 1.47.0 ✅ – 1.47.3 ✅, 1.48.0 ✅ | Event series & cloning (1.47.0 clone, 1.47.1 series, 1.47.2 year-on-year reports, 1.47.3 season pass), Outbound webhooks (Wallet passes removed from scope) | Smaller, independent wins |
| 10 | 1.49.0 ✅ – 1.49.2 ✅, 1.50.0 ✅, 1.50.1 ✅, 1.50.2 ✅ | Admin dashboard/Ctrl+K/bulk actions/saved filters, Accessibility pass, pagination and search on long lists, event picker and webhook guide (Afrikaans removed from scope) | Polish before the big release |
| 11 | 1.51.0 ✅, 1.51.1 ✅, 1.51.2 ✅, 1.51.3 ✅ | TypeScript backend + OpenAPI + /api/v2 alongside v1: 1.51.0 foundations (build, launcher, route declarations, errors, paging, OpenAPI at /docs/v2, first read endpoints); 1.51.1 registrations and payments as TS services, typed frontend calls; 1.51.2 check-in and accounts; 1.51.3 Express 5, the scanner on v2, v1 deprecation headers, v2-shaped shared middleware |
The breaking-change groundwork |
| 11b | 1.51.4 ✅ – 1.51.16 ✅ | Every endpoint on v2 before 2.0 (decided 2026-10-29): 1.51.4 dependency audit and Node 24 LTS; 1.51.5 Next.js 16 and ESLint 10 (ESLint 9 is out of support); 1.51.6 the complete command palette and ExcelTS instead of exceljs; 1.51.7 setting events up (the staff list, create, change, deactivate, copy-from, notification recipients) and pg-boss 11; 1.51.8 the rest of event set-up (the event in full, ticket options and variants, attachments, sections, costs, uploads, calendar file, alias); 1.51.9 series, season passes, bundles, coupons and campuses; 1.51.10 managing registrations (the staff lists, status, at the door, bulk actions, items, coupons, attendees, attendee forms); 1.51.11 money, staff side (payments, cashup, provider transactions, waitlist, sponsorship, consent, form builder, feedback); 1.51.12 tickets and attendance, staff side (tickets, scan locks, attendance, allocations); 1.51.13 kids check-in and volunteer rosters; 1.51.14 people and messaging (users, WhatsApp, broadcasts, scheduled emails, automations, banner, saved filters, dashboard, stats, analytics, reports); 1.51.15 privacy and admin (privacy, admin, settings, setup, Hub, outbound webhooks, backups); 1.51.16 incoming webhooks and links in sent messages: Yoco, PayFast and WAWP webhooks and the links in emails and WhatsApp (unsubscribe, consent, ticket transfer, calendar feeds, waitlist offers) get v2 URLs — new messages and provider registrations use them, and the v1 paths keep answering (links by a 308 redirect to v2, provider webhooks handled by the same code, since providers may not follow a redirect on a POST). Each puts its area's endpoints on v2 (1.51.1 – 1.51.10 by moving the logic into TS services; from 1.51.11 by v1Route, which runs the v1 handler behind a v2 route — moving those handlers into TS services is a 2.x clean-up that doesn't change the API), its screens onto apiV2, and its v1 routes into SUCCESSORS. Then 1.51.17, a security fix (forgot password and sign-in don't reveal accounts) |
2.0 can then remove v1 |
| 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 removed (all of it is on v2 by then; the old webhook URLs and links already sent in messages keep forwarding to v2), migration guide, docs, and the marketing site (§17: a static Cross Code site with a Cross Events page). Built on release/2.0 in parts; Hub 1.6.0 (AI proxy) ✅ and the AI plumbing ✅ are in |
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 | Cross Events Hub in its own git repo (joshua/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) |
| — | Grace banner, Hub repo, Hub development | The grace-period banner shows only to site admins. The Hub repo is joshua/events-hub (https://c-git.crosscode.co.za/joshua/events-hub.git); it is developed locally first, then deployed by Joshua (2026-10-24) |
| — | 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. Joshua provides the key; installs call AI through the Hub, which holds the key and meters usage (2026-10-24). Model: Sonnet 5.5 for every helper (2026-10-30, after the Haiku/Sonnet trial; was Haiku 5.5 for cost), chosen on the Hub |
| — | 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) |
| — | v1 in 2.0 | Every endpoint moves to v2 before 2.0 (1.51.7 – 1.51.16, by area), then 2.0 removes v1. Changed the same day from "retire only what's replaced". Incoming webhooks (Yoco, PayFast, WAWP) and links in emails and WhatsApp (unsubscribe, consent, ticket transfer, calendar feeds) also get v2 URLs (1.51.16); their v1 paths keep working — links redirect (308) to v2, provider webhooks are handled by the same code — because old messages and provider registrations still use them (2026-10-29) |
| — | AI buttons and markup | AI buttons show only when the licence's monthly cap is above R0. When the cap is reached they stay visible but disabled until next month or until the cap is raised. Usage is charged at the real cost plus a markup % set in the Hub (a default, plus an override per licence). The markup covers small overruns and is the profit. The cap is counted in charged rand, and each call is checked against its worst-case cost before it's sent (2026-10-29) |
| — | Building 2.0 | On a release/2.0 branch with a WIP commit per part, tagged 2.0.0 only when everything is in; the Hub's AI proxy ships first as its own release (Hub 1.6.0) (2026-10-30) |
| — | Marketing site | Part of 2.0: a simple static HTML site — Cross Code as the main site, with a page for Cross Events, the only product on sale for now. Laid out to add a church app and a full church website later. Repo, domain and hosting still to decide (2026-10-30) |
| — | 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) |
| — | Product name | Cross Events (was Hope Events), Hub: Cross Events Hub. UI text and docs renamed in 1.41.3; technical identifiers (hope-events repo, storage keys, encryption salt, migrations, PM2/nginx names) stay, so installs and sign-ins keep working (2026-10-27) |
| — | Campus-scoped staff (1.44.0) | Scoped staff: their campuses' events plus "All campuses" events, which they can view, scan, take door payments and do manual registrations for, but not set up, cash up or refund. Banner, provider reconciliation and unlinked donations: unscoped only; broadcasts limited to their campuses' audiences and their events' attendees; coupons only for their events. Other campuses' records answer "not found" except the scanner, which names the campus. Scopes ignored while campuses are off, with a warning (2026-10-27) |
| — | Multi-campus split | Two releases: 1.43.0 campuses, 1.44.0 campus-scoped staff; everything after moves up by one. Scoped staff can view and scan "All campuses" events, only admins and unscoped supervisors edit them. "Everyone at campus X" = home campus X, plus (tick box) registrants of campus X's events in the last 12 months. Campus landing pages are in 1.43.0 (2026-10-27) |
| — | Updates | Sites show Request update, never the deploy command: the request goes to the Hub, which tells Joshua; he updates the server. 1.42.0 / Hub 1.1.0; Multi-campus and everything after moves up by one (2026-10-27) |
| — | Wallet passes | Removed from scope (2026-10-28); everything after volunteer rosters moves up by one |
| — | Languages (Afrikaans) | Removed from scope (2026-10-28); the accessibility pass becomes 1.50.0 and the TypeScript backend / /api/v2 1.51.0 |
| — | Kids check-in (1.45.0) | One release. Fully digital — no label printers: pickup code on the parent's phone, a "write on the sticker" panel for blank stickers. Staff desk only (no kiosk). Anyone with the pickup code can collect; families list authorised adults without accounts. Child profiles deleted after 6 months without a check-in (2026-10-28) |
| — | Volunteer rosters (1.46.0) | Anyone with an account can volunteer. Roles are per event (or per session) — rosters cover one event/session and never repeat. A swap needs no approval: it happens once someone accepts, and the event's notify recipients are emailed. Volunteers check in by scanning a QR code or are checked in by staff. The free volunteer ticket is off by default and chosen per event. A role linked to a kids room counts its checked-in volunteers as leaders on duty for the room's ratio (2026-10-28) |
| — | Event series and cloning (1.47.x) | One release per part: 1.47.0 clone, 1.47.1 series, 1.47.2 year-on-year reports, 1.47.3 season pass. A clone opens the New event popup filled in from the source, so everything is set there before saving. Of the newer features a clone copies only the campus (not kids check-in or volunteer roles); coupons are never copied. A series can span several campuses. Following a series is News & announcements; copies keep the source's certificate and reminder settings. Season pass: one checkout registers for every event on it, the supervisor picks the events, the price is split in proportion to normal prices (2026-10-28) |
| — | 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
- Marketing site (§17): repository, domain(s), hosting, whether prices are shown, and the contact route.
- Password hashing upgrade (§14): in 2.0 or later.