- Retention policies (Admin -> Privacy -> Retention) applied by a nightly
`retention` job: inactive accounts anonymised after 5 years with a
30-day warning (on by default; never staff roles or people with an
upcoming registration), form answers, login history, payer details in
payment notifications, audit/access logs
- User.lastLoginAt (set on login, backfilled) and inactivityWarnedAt
- Breach register (SecurityBreach) with drafts for the people affected
and the Information Regulator
- Readiness checklist with a score
- Children's/medical flag in the access log; {retention} placeholder in
the privacy notice
- Unit tests (retention.test.js), e2e (privacy-retention.spec.ts); docs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012mt6TCv85T9VKRq3cV7rXq
40 KiB
Hope Events — Feature Overview
A complete map of what the platform does, grouped by who uses it. For setup and deployment see README.md; for the API see backend/API_DOCUMENTATION.md; for setting defaults see SETTINGS.md; for history see CHANGELOG.md.
Stack: Next.js (React, Tailwind, shadcn/ui) frontend · Express + Prisma + PostgreSQL backend · Yoco for card payments · SMTP for email · WAWP for WhatsApp · PM2 for process management · optional Sentry for error monitoring.
Contents
- Roles
- Public site
- Events
- Registration
- Payments, donations & refunds
- Tickets & check-in
- Attendee forms
- Messaging & notifications
- User dashboard (My Events)
- Staff tools
- Supervisor tools
- Admin tools
- Cashup & event costs
- Reports
- Site settings & setup wizard
- Accounts & security
- Privacy (POPIA)
- Help system
- Background jobs
- Operations
- Page map
Roles
Four roles, each including everything the roles below it can do:
| Role | Can do |
|---|---|
| User | Browse events, register, pay, donate, view tickets, fill in forms, manage their own profile |
| Staff | Scan tickets, view/print event tickets, view registrations and form responses |
| Supervisor | Create and manage events, record payments/refunds, assign donations, at-the-door desk, manual registration, bulk email/WhatsApp, reports, site banner, self-service kiosk |
| Admin | Manage users and roles, all registrations, event costs and cashup (close/reopen events), audit log, site settings, WhatsApp connection, backups |
Public site
- Home page — hero section with the organisation's name and branding, upcoming and in-progress events, and a "Join Us" button (becomes "My Dashboard" when logged in).
- Events list (
/events) — event cards with picture, dates, location, price, and a Register / Sold Out / Contact us button. A diagonal "Early bird" ribbon in the top-right corner while an early-bird tier is running and registration is open. - Event detail (
/events/[id]) — description, ticket options with variant and early-bird pricing, low-stock and sold-out badges, downloadable attachments, embedded Google Map with "Directions" link, and an Add to calendar (.ics) button. - Short links — events can have an alias (e.g.
/camp-2025) that redirects to the event. Only plausible aliases hit the database, so bot probes are rejected cheaply. - Contact page (
/contact) — organisation email, phone, address and map. - Legal pages — Terms of Use and Privacy Policy, filled from Site Settings.
- Site banner — a site-wide announcement bar set by supervisors.
- Lockdown rules (
/lockdown-rules) — static rules page for the youth lockdown event. - SEO —
sitemap.xml(every public page and event),robots.txt, and Open Graph/Twitter link-preview metadata. - Theming — brand colours, logo and favicon come from Site Settings.
Events
Created and edited by supervisors through a 9-step wizard: Basic Details → Items & Pricing → Sections → Form → Visibility → Notifications → Attachments → Feedback → Attendance.
- Basic details — title, description, start/end dates, registration deadline, picture (upload or URL), location (defaults to the org's address), contact person.
- Contact-only events — for things like baptisms: shown publicly with a "Contact us" popup instead of registration, with no tickets.
- Multi-day events (sessions) — one event on several dates, e.g. every Wednesday 18:30–20:30 for 5 weeks. A repeat pattern (daily/weekly, every N, chosen weekdays, a number of sessions or an end date) creates the sessions, which can then be edited one by one (label, times, location), added to, or cancelled (kept for history; one with check-ins can't be deleted). People register and pay once and get one ticket. The event page shows the schedule with the next session highlighted; cards show "5 sessions · Wed 4 Nov – Wed 2 Dec" and stay listed until the last session. Calendar files get one entry per session. Saving moved or cancelled sessions offers to message attendees.
- Pick sessions (multi-day) — instead of the whole course, people can choose which sessions to come to (day passes, drop-in classes). Tickets set to "Pick sessions" are priced and bought per session — one line and one ticket per session, bookable until the session starts (staff: until it ends) — alongside optional whole-course tickets. Sessions can have a capacity (picked places plus whole-course tickets); a per-session ticket's stock limit is places per session. Picked tickets only scan on their own session; reminders, the register and certificates follow the sessions each person booked. "All sessions" tickets on such an event also show the sessions: people tick the ones they'll come to and pay the ticket's one price; the ticket scans, reminds and counts towards capacity for those sessions only. The waitlist works per session (a full session shows "Join waitlist"). Bookable from the registration page, Manual registration, At the door (today's session is preselected) and the kiosk. Not available on youth events.
- Late joining (multi-day) — registration closes when the first session starts, or later (until session N) with late joining on. Late joiners pay full price, a pro-rata price for the sessions left (rounded to the nearest R10), or a fixed late price — main tickets only.
- Completion certificates — optional per event: four designs (Classic, Modern, Church, Kids), custom wording with placeholders, signature name/role/ image, and a completion rule (every session, at least N sessions, or N%). Issued automatically when the event ends; each has a code and QR code checked on a public verify page.
- Ticket options — multiple options per event (e.g. Adult, Child, Dinner, T-shirt), each with a price and optional stock limit. One option is marked as the main ticket, which is what gets checked in at the door.
- Variants — sub-types of an option (sizes, colours, Adult/Child/VIP), each with an optional price override and its own stock limit.
- Early-bird tiers — deadline- and/or stock-limited cheaper prices per option or variant. Prices are locked in per purchase ("tranches"), so buying more later doesn't re-price what was already bought.
- Sections — named scan points (e.g. "Main hall", "Dinner") limited to certain ticket options, so a scanner at that point only accepts those tickets.
- Visibility — active/inactive, hidden (reachable only by link), scheduled go-live date, require login vs. allow guest registration, or no registration at all.
- Attachments — downloadable files (maps, schedules, waivers) shown on the event page.
- Notify recipients — choose which staff get registration/payment alerts and the daily summary for the event.
- Sold-out handling — an event is sold out when every stock-limited option is exhausted. "X remaining" badges appear at a threshold that scales with capacity.
Registration
- Online registration (
/register/[eventId]) — pick options, variants and quantities; see early-bird prices and stock warnings; fill in the event form. - Guest registration — when an event doesn't require login, an inactive account is created and an activation link is sent by email or WhatsApp.
- Statuses — Pending, Confirmed, Partially Paid, Paid, Cancelled.
- Edit options — users can add or change items on their registration (not for past or closed events).
- Self-cancellation — allowed only before any payment; admins can cancel any time.
- Pricing refresh — when an unpaid registration is viewed or paid, expired early-bird prices are re-evaluated, and the user is warned if the total changed.
- Manual registration (supervisor) — register someone on their behalf, creating an account if needed. Includes a "Record Payment" tab.
- Duplicate-account protection — if an email and phone belong to two different accounts, the registration is blocked instead of merging them.
- Group tickets — on events with "Named attendees" turned on, someone booking several places can name each person (with optional email/phone). Each named person gets their own ticket and their own attendee form; unnamed places share one ticket. Tickets go all to the payer, or each to the person (falling back to the payer). Attendees matching an existing account see their ticket in My Events. Names can be added or changed later from My Events, and attendees can be sent a link to fill in their own form. Available on the registration page, manual registration, At the door and the kiosk.
- Waitlist — on events with a waitlist, sold-out tickets show "Join waitlist" (on the event page, the register page and the event list). When places free up — a cancellation, a smaller booking or a higher stock limit — the next person is emailed/WhatsApped a claim link and the places are held for them (24 hours by default) so nobody else can buy them. Unclaimed offers expire and move on; people who asked for more than came free can be offered what's free or skipped. Claiming works without logging in. People can leave from My Events. On pick-sessions events people wait for a particular session; offers never run past the session's start.
- Parental consent (youth events) — on events that require it, every attendee gives a date of birth; for anyone under 18, a parent or guardian gets an email and a WhatsApp with a consent link. They read the event's consent text, optionally give medical info, an emergency contact and photo consent, type their name to sign, and give or decline. Tickets are issued but can't be checked in until consent is given (supervisors can override, audited). Reminders go after 2 days, then every 3 days. Medical details are encrypted and deleted a set number of days after the event.
- Coupon codes — percentage, fixed-amount or free (comp) codes, entered under "Have a code?" on the registration page, in My Events (for unpaid registrations), manual registration, At the door and the kiosk. Codes can be limited to events, ticket options or variants, dates, total and per-person uses, a minimum quantity, and specific people, roles or an email domain (restricted codes need a login). Early-bird prices either stack or the better price wins. The discount is locked in when applied and shows on invoices, receipts, emails and the Yoco checkout; cancelling frees the use. Managed from Supervisor → Coupon codes.
- Staff price — a ticket option or variant can have a staff price that Staff and above get automatically, without a code.
Payments, donations & refunds
- Online card payments via Yoco — checkout for the full outstanding balance, confirmed by a signed webhook. Keys and the webhook are set up from Site Settings → Payments, where providers are switched on or off and put in order (the first one is the default). Online payments are at least R15; payments are recorded at the full amount and provider fees are dealt with at cashup.
- PayFast — the second online provider (card, instant EFT…), with sandbox and live modes. When several providers are on, payers choose on the "Pay online" page; a payment of a different amount from the checkout is held for review instead of being recorded. When no provider is on (or the amount is under R15), nothing offers to pay online.
- Payment status checks — if a provider's notification never arrives, Payments → Reconcile → "Waiting for payment" checks with the provider (PayFast; Yoco's API can't be asked) and records what it finds. A job checks recent PayFast checkouts every 15 minutes.
- Payment links — supervisors can generate and send a Yoco link for any amount (including partial).
- Manual payments — cash, card, EFT, voucher or other, recorded by staff. Each payment records who recorded it as well as who it's for.
- Refunds — negative payments against an original payment, netted against the original method.
- Impersonate user (Admin → Manage users) — an admin with 2FA can view the site as a
user, to debug problems.
- It needs a reason, lasts at most 30 minutes, and shows a bright "Viewing as" banner on every page.
- It is strictly read-only. A server-side guard refuses every write made with the impersonation token before it reaches any route, and pages don't even refresh prices while viewing.
- Admins can't be impersonated.
- The start, end, reason and every page and API call are in the audit log, with each session's trail.
- Optional email to the user (
notify_user_on_impersonation).
- Registration funnel stats — first-party and consent-based, with no third-party analytics.
- A cookie notice asks once. Only visitors who allow it get a random visit id.
- We record event page views and registration starts. Submits and payments come from the registration itself.
- Each record holds only the stage, the traffic source (utm_source, or the short-link
alias, which short links now pass as
?src=) and the device type. - The Registration funnel report shows counts and conversion per stage over a date range, a daily trend, and breakdowns by source, device and ticket option.
- A "Conversion (past month)" tile on the supervisor/admin dashboards covers live events.
- Raw rows are rolled up into daily totals after 90 days.
- Post-event feedback — a Feedback step in the event wizard.
- Questions use the form builder, with new star rating (1–5) and multiple choice types (also available on attendee forms).
- The form goes out at a chosen time to everyone who checked in, or to everyone
registered, each with a personal link (
/feedback/<token>). One reminder goes 3 days later to anyone who hasn't answered. - Answers can be anonymous or named. "Send now" is available, and the questions lock once people have answered.
- Sponsor a spot — per-event setting. A "Sponsor a spot" button on the event page
lets anyone, including guests, pay for one or more spots (at the main ticket price)
or any amount through Yoco. They can give anonymously or add a name and a message.
The money is a donation tagged to the event and goes into its sponsorship pool, which
supervisors assign to registrations with the usual donation flow.
- An optional public progress bar ("12 of 20 spots sponsored") with an optional goal.
- Sponsors get a thank-you receipt, and an Automations thank-you can go to all of an event's sponsors.
- The Donations breakdown report shows each pool: received, assigned and remaining.
- Who is helped is never shown.
- Donations — users can donate from their dashboard (
/dashboard/user/donate). Supervisors can assign a donation (fully or partly) to someone's registration and unassign it later. Assignments are kept as separate records, so the original donation never changes. - Online payment reconciliation — provider (Yoco) transactions can be matched to a registration, recorded as a donation (optionally choosing the donor), or ignored.
- Invoices & receipts — branded PDFs: an invoice (itemised, early-bird discount, balance due, pay-now link and QR code) and a payment receipt. Sent automatically and re-sendable on demand by email or WhatsApp.
- Method normalisation — Apple Pay / Google Pay / checkout payments count as "Card" in reports and user views.
Tickets & check-in
- Automatic tickets — QR-coded tickets are generated when a registration is fully paid (or free), and sent as a PDF by email and/or WhatsApp.
- Quantity tickets — one ticket can cover several people and be redeemed partially (e.g. check in 2 of 3).
- Named tickets — group bookings can give each person their own ticket, shown with their name on the PDF, the scanner and the staff ticket list.
- QR scanner — works in any mobile browser. A preview checks the ticket is for the selected event/section, then a confirmation screen shows holder, type and remaining quantity.
- Check-in confirmation — holders get an email/WhatsApp saying how many were checked in and how many remain.
- Per-session check-in — multi-day tickets are checked in once per session. The scanner uses today's session (or one picked from a list). Check-in is open the whole day of a session, or only from 2 hours before it until it ends, per event. Optional reminders (1 h / 3 h / 1 day before each session) by email and/or WhatsApp.
- Attendance register — attendees × sessions with ticks, totals per session, completion, filters (completed, missed the last two), Excel/PDF export. Supervisors can tick someone present or absent (audit-logged), and issue, send, download or revoke certificates.
- Scan stats — recent scans and per-event scan counts.
- Offline scanning — while online, a staff member locks an event section (or the whole event) to their device and downloads its tickets. They can then keep scanning with no connection; scans are queued on the device and sync automatically when it's back online (never redeeming more than is left). Only one device can hold a section, and while it's locked nobody else can scan it, online or offline. Problems found while syncing go into a report for a supervisor. Supervisors can force-release a lost device. The scanner can be added to the home screen (PWA) and opens offline.
- Printing — A4 sheets (8 tickets per page) and A6 single tickets.
- Resend — send tickets to a different email/phone at the door.
Attendee forms
- Form builder per event — field types: Yes/No, Text, Paragraph, Number, Date, plus Statement and Heading blocks; required/optional, help text, reordering. The whole form can be marked required.
- Drafts — partially filled forms are auto-saved.
- Responses — view, filter (event, user, past/inactive) and print one response per A4 page. Supervisors can edit responses.
Messaging & notifications
Every user picks a notification preference: Email, WhatsApp, or Both.
Automatic notifications
| Trigger | Goes to |
|---|---|
| Registration created / updated | Registrant (with invoice if unpaid) + event notify recipients |
| Payment recorded / donation applied / unassigned | Registrant (with receipt) + admins |
| Refund processed | Registrant + admins |
| Tickets generated | Registrant (ticket PDF) |
| Ticket checked in | Ticket holder |
| Daily summary, 07:00, for each live event that hasn't started yet | Registrations inbox + event notify recipients |
| Account registered, activation, login, password changed/reset, account closed | The account holder |
Bulk messaging (supervisor)
- Email attendees and WhatsApp attendees — filter by status or pick specific attendees, preview, send now or schedule for later.
- Placeholders —
{{name}},{{event.title}},{{event.link}},{{event.start}},{{balance}}, and{{payment.link}}(a live Yoco link for each recipient's outstanding balance). - Broadcasts — ad-hoc email/WhatsApp to any users or addresses, not tied to one event.
- Message type — Event update or News & announcements; news only reaches people who opted in (see Privacy).
- Automations — schedule a sequence of lifecycle emails for an event (pre-event, final reminder, thank-you, promo for another event).
- Manage scheduled — view, edit or delete queued jobs per channel. Sends run in the background and are purged 24 h after sending.
WhatsApp is connected through the WAWP API: an admin links a number in Site Settings → WhatsApp (QR code or pairing code), with status, restart and logout controls. Disconnected sessions are recovered automatically.
User dashboard (My Events)
- Registrations with status badges, event thumbnails, and Closed/Past/Inactive tags. Past and closed events are hidden by default.
- Registration detail: itemised bill (early-bird lines grouped), payments, Invoice and Receipt buttons, edit, cancel, Make payment.
- Upcoming events and tickets side by side, with bulk ticket email/WhatsApp.
- Ticket transfer — offer a ticket to someone else before the event by typing their name and email/phone (no user search, and nothing reveals whether they have an account). They get a link to accept or decline; the ticket only moves once they accept, and the sender can cancel until then (offers expire after 7 days or at the event start). On accepting they get a new ticket and QR code, and an inactive account with an activation link if they're new. The old code stops working, and scanning it says it was transferred, both online and in offline-mode sync reports. One place can be split off a ticket covering several people. Billing stays with the payer. It's not allowed once a ticket has been scanned, and it can be turned off per event (supervisors can always transfer, from At the door → Tickets). On youth events the new holder needs their own parental consent, and the previous holder's form answers are removed. Every transfer is in the audit log.
- Payment history (
/dashboard/user/payments) — paginated, with filters by date, method and payment/refund. - Donate page.
- Forms — complete outstanding attendee forms.
- Profile & Security — edit details and notification preference, change password, see recent account activity (logins, password changes, device), sign out everywhere, deactivate or delete my data.
Staff tools
- Staff dashboard — scan stats and quick actions.
- Ticket scanning (
/dashboard/staff/ticket-scanning) — camera QR scanner with event and section filters. - Event tickets & printing (
/dashboard/staff/event-tickets) — list, search and print an event's tickets.
Supervisor tools
- Dashboard — KPIs (active events, revenue, donations, registrations, tickets sold, each vs. the previous month), revenue trend chart, top events, and recent scans. Tiles link to the matching report.
- Manage events — the 9-step event wizard, ticket options, variants, sections, form and attachments.
- At the door — search by name/phone/ticket ID, walk-in registration, payments, a Check-In tab for redeeming main tickets by quantity, and ticket printing/resending.
- Self-service kiosk (
/self-service) — a touchscreen page a supervisor unlocks for an event, where visitors register themselves. Existing accounts are matched by email/phone, and any change to their details needs confirmation. - Manual registration — register someone on their behalf and record their payment.
- Payments — tabs for Payment, Refund, Donations (assign/unassign), Reconcile (Yoco transactions), and Payment Link.
- Coupon codes (
/dashboard/supervisor/coupons) — create, edit and turn off discount codes, and see who used each one. - Rooms & groups (
/dashboard/supervisor/allocations):- Per event, allocation sets such as "Rooms", "Small groups" or "Buses". Each set has groups with an optional capacity, gender restriction and leader (staff).
- A drag-and-drop board, plus auto-allocate: fill evenly, respecting capacity and gender, keeping bookings together or splitting them.
- Printable per-group and master lists. On youth events they can include medical details, and viewing those is audited.
- The group can show on tickets and in My Events.
- Email and WhatsApp attendees can be filtered to one group.
- Leaders see their people under Staff → My groups.
- Works on named attendees. Events can ask each person's gender.
- Offline scanning (
/dashboard/supervisor/offline-scans) — devices holding offline locks (with force release) and sync reports to review. - Waitlist (
/dashboard/supervisor/waitlist) — the queue per event: offer a place now, move people up or down, or remove them. - Parental consent (
/dashboard/supervisor/consent) — consent status per attendee on youth events, resend (one or all pending), re-request after the consent text changes, and a printable medical & emergency list (audited). - Email attendees / WhatsApp attendees — bulk and scheduled messaging.
- Forms — view, filter, edit and print form responses.
- Reports — see Reports.
- Site banner (
/set-banner) — set the announcement bar.
Admin tools
- Admin dashboard — same KPIs as supervisor, plus admin quick actions.
- Manage users — server-side search and filters (role, active), paging, edit role and notification preference, revoke sessions, delete or anonymise personal data.
- Data requests — POPIA correction and deletion requests (see Privacy).
- Registrations — all registrations, with filters (event, status, text), status counts, revenue and outstanding totals, and per-registration options, form answers and payments (with who recorded each one).
- Cashup — see below.
- Audit log (
/dashboard/admin/audit-log) — who did what and when for refunds, donation assign/unassign, manual registrations, staff cancellations, event create/update/delete, settings changes, and coupon codes (created, edited, turned off, applied or removed by staff). - Site settings — see below.
Cashup & event costs
- Event costs — once-off or per-item costs (optionally linked to a ticket option), with a "paid from takings" method so cash on hand is correct.
- Expected vs. actual — expected takings per method (cash/card/EFT/other) compared with actual amounts, with variance shown.
- Cash denomination counts — entered per staff member, at any time. The event's actual cash is the sum of these counts.
- Payment accountability by staff member — what each person recorded, by method, with live cash variance.
- Close / quick close / reopen an event — closing locks it from further edits and hides it from the kiosk and user actions. Drafts are saved as you go.
- Report tab — a read-only cashup summary, shown automatically once the event is closed.
- Cashup audit trail — every close/reopen with totals and who did it.
Reports
A full-width workspace with shared filters (events, date range with presets, include past/inactive/closed) and a searchable grid of reports, each opening in a viewer with its own filters, stat tiles and bar charts.
| Category | Reports |
|---|---|
| Financial | Payments between dates · Revenue summary (by method) · Revenue detailed · Finance report (revenue & costs, after discounts) · Profit report · Discounts (coupon codes) |
| Registration | Registration funnel (views → started → submitted → paid, daily trend, by source / device / ticket option) · Attendees per event (grouped by option) · Registration type counts · Master orders breakdown · Registration status breakdown (count by registration or ticket quantity) · Parental consent status · Medical & emergency list · Feedback summary (response rate, average ratings, choice breakdowns, comments; compare events) |
| Ticketing | Ticket usage summary |
| Donations | Donations breakdown (used / unused per event, plus the sponsorship pool) |
| Cashup | Cashup reconciliation · Cashup audit trail |
Export: Print, branded PDF download, styled Excel (.xlsx) workbook,
Email (to yourself), or WhatsApp (the PDF to your own number). Dashboard tiles
deep-link into reports with ?report=&range=.
Site settings & setup wizard
Setup wizard (/setup) — on a fresh database, walks through organisation
details, the first admin account and branding. No manual seeding needed.
Site Settings (/dashboard/admin/settings) — stored in the database and
applied without a restart. Secrets are encrypted with AES-256-GCM.
| Tab | Contains |
|---|---|
| Organisation | Name, tagline, contact email/phone, address (with map link) |
| Branding | Primary/secondary/accent colours, logo, favicon, colours suggested from the logo, live website/email preview on desktop and mobile |
| Notifications | Registrations inbox and admin notification emails |
| SMTP host/port/TLS/user/password/from, with a "Test connection" button | |
| Legal | Operator, Information Officer, website, effective date for the Terms/Privacy pages |
| WAWP credentials, connect a number, status/restart/logout | |
| Payments | Online payment providers: Enabled toggle and order; PayFast merchant ID/key, passphrase and sandbox/live; Yoco's secret key (masked, test/live), webhook secret (generate by registering with Yoco, or enter), list/delete the account's webhooks |
| Backups | List, run now, download database backups |
See SETTINGS.md for the default of every setting.
Accounts & security
- Register, log in, forgot/reset password, activate account (for accounts created on someone's behalf).
- JWT authentication (HS256 pinned) with session revocation (token versioning) — users can sign out everywhere, and admins can do it for any user.
- Two-factor authentication — a 6-digit login code by email or WhatsApp
(10-minute expiry, 5 attempts, resend), with "Trust this device for 30 days".
Required for Admins and Supervisors: their next login sends a code that turns
it on, and they can change the channel but not turn it off (skipped only when no
email or WhatsApp channel can deliver a code;
TWO_FACTOR_REQUIRED_ROLES=falseis the emergency switch). Optional for everyone else from Profile & Security. Admins can reset it for a locked-out user, and the kiosk asks for the code at setup. - Login lockout after repeated failed attempts, plus login rate limiting
(
TRUST_PROXYto read real IPs behind a proxy). - Security event log — logins, password changes and resets, with device and IP.
- Account closure — deactivate, or close and ask for the personal data to be deleted (see Privacy).
- Upload hardening — server-generated filenames, and uploads served with a
sandbox CSP and
nosniff.
Privacy (POPIA)
Tools that help a church handle personal information the way POPIA expects. They support compliance; wording and retention choices are for the church's Information Officer.
For everyone — the Privacy page (/dashboard/user/privacy):
- News & announcements opt-in per channel (email, WhatsApp), with the history of every choice: when, and how (sign-up, privacy page, unsubscribe link, STOP reply…). Sign-up has an unticked opt-in box; accounts that never answered are asked once after logging in.
- Download my data — readable PDF, or the complete copy as JSON.
- Correct my data — profile fields directly; anything else as a correction request.
- Delete my account — closes the account at once and files a deletion request.
Bulk messages are either an Event update (about an event the recipients
registered for; always sent) or News & announcements (only to people who opted in
on that channel and haven't unsubscribed). News emails carry an unsubscribe link and
one-click List-Unsubscribe headers; news WhatsApps end with "Reply STOP" and a link.
Event updates say why they were sent and link to the preferences. A STOP reply on
WhatsApp turns off WhatsApp news (through WAWP's message webhook, checked against a
secret in the webhook URL).
Privacy notice (/legal/privacy) — edited in Admin → Privacy (POPIA)
(/dashboard/admin/privacy): sections start from the built-in text; drafts, preview,
and published, numbered versions (earlier ones stay readable with ?version=N).
Placeholders ({org}, {ioName}, {ioEmail}, {website}, {orgEmail},
{medicalRetentionDays}, {operators}) are filled in from Site settings and the
operator register. A version marked as a significant change is accepted once by everyone
after their next login; sign-up records the version accepted. The contact page shows the
Information Officer.
Registers and logs (same admin page):
- Operators — the services that process personal information (payment, WhatsApp, email, hosting, maps, error monitoring), what each receives and where; those in use are listed in the notice.
- Consent log — news opt-ins/opt-outs, privacy notice acceptances and parental consent answers, searchable.
- Access log — every bulk export or view of personal information (reports, attendance registers, certificates, group lists, form answers, consent and medical lists, backups) and every "view as" session, with the number of records. Children's and medical information is flagged (medical lists, and anything about a youth event).
- Retention — how long each kind of personal information is kept, applied by a nightly
job: inactive accounts anonymised after 5 years (warning 30 days before; never staff
roles or people with an upcoming registration), form answers deleted 12 months after
the event, login history kept 24 months, payer details in payment notifications removed
after 24 months, audit/access logs kept 5 years. The notice's
{retention}placeholder lists them. - Breaches — the register of security compromises, with when the Information Regulator and the people affected were told, and drafts of both notifications.
- Readiness — a scored checklist of what's set up, each item linking to its fix.
For admins — Data requests (/dashboard/admin/data-requests): correction and
deletion requests with a 30-day due date, emailed to the Information Officer and
admins, reminded daily when due within a week. Completing a deletion anonymises the
person: name, contact details, the people they registered, form answers, consent
details, sponsor names, login history and payer details in provider notifications are
removed; payments, registrations, tickets and check-ins stay as "Deleted User". It
waits while they have a registration for an upcoming event. Everything is in the audit
log.
Help system
A floating "Need help?" button on every page opens a page-specific guide
(tabs, quick links). Every page has dedicated content (frontend/src/content/help/),
falling back to a general guide.
Background jobs
Timed jobs run on a job queue in Postgres (pg-boss, backend/src/jobs/index.js): each
runs once per schedule even with several server processes, every run is recorded,
failed runs are retried twice, and a slow run never overlaps the next. Admin → Site
Settings → Background jobs shows each job's schedule, next run and last 10 runs, with
"Run now". Times are in APP_TIMEZONE (default Africa/Johannesburg). Change a schedule
with JOB_SCHEDULE_<NAME> (cron); JOBS_ENABLED=false turns them all off. If the queue
can't start, the same jobs run on in-process timers.
| Job | Schedule |
|---|---|
| Scheduled email/WhatsApp worker | Polls every 30 s (SCHEDULED_EMAILS_INTERVAL_MS) and purges sent jobs after 24 h (not on the queue) |
daily-summary — daily event summary email |
07:00 daily (DAILY_SUMMARY_ENABLED=false to disable) |
waitlist — expire unclaimed offers, offer freed places |
Every 10 minutes (WAITLIST_JOB_ENABLED=false) |
payment-status — look up PayFast checkouts (10 min – 3 days old) whose notification never arrived, and record them |
Every 15 minutes (PAYMENT_STATUS_JOB_ENABLED=false) |
feedback — post-event feedback forms and their 3-day reminder |
Every 15 minutes (FEEDBACK_JOB_ENABLED=false) |
sessions — session reminders, and certificates for events that ended in the last 14 days |
Every 5 minutes (SESSION_JOBS_ENABLED=false) |
consent — parental consent reminders, and deleting medical details after events |
09:00 daily (CONSENT_JOBS_ENABLED=false) |
privacy — remind admins of privacy requests due within a week or overdue |
08:00 daily (PRIVACY_JOBS_ENABLED=false) |
retention — delete/anonymise personal information past its retention period (Admin → Privacy → Retention) |
03:30 daily (RETENTION_JOBS_ENABLED=false) |
analytics-rollup — roll raw visit rows older than 90 days into daily totals |
04:15 daily (ANALYTICS_ROLLUP_ENABLED=false) |
backup — database backup (pg_dump, gzipped, 14-day retention) |
02:00 daily (BACKUP_ENABLED=false) |
temp-cleanup — temp files older than 7 days in /temp |
03:00 daily |
Operations
- PM2 (
ecosystem.config.js) runs backend and frontend, restarting either one above 500 MB of memory. - Sentry error monitoring when
SENTRY_DSN/NEXT_PUBLIC_SENTRY_DSNare set. - Unhandled promise rejections are logged and don't crash the server.
- Backend status page (
/) and built-in API docs page (/docs). - Tests — Jest unit tests in
backend/tests(npm test), and Playwright end-to-end tests ine2e/(npm run test:e2e) that start their own backend and frontend against a separate, freshly seeded database: public pages, free and paid registration through to check-in and cashup, multi-day check-in, the attendance register and certificates, the event wizard, the kiosk, and the admin jobs page. - CI —
.gitea/workflows/ci.ymlruns the backend tests, frontend lint/typecheck/ build and the end-to-end tests on every push (needs a Gitea Actions runner). Av*tag also deploys over SSH withscripts/deploy.shonce theDEPLOY_*secrets are set: it backs up the database, checks out the tag, migrates, builds and reloads PM2, and goes back to the previous version if a step fails before the reload. - Versioning via git tags and CHANGELOG.md.
Page map
| Path | Who | What |
|---|---|---|
/ |
Public | Home |
/events, /events/[id] |
Public | Event list and detail |
/[alias] |
Public | Event short link |
/register/[eventId] |
Public / User | Registration |
/registration/success |
Public / User | Confirmation, form and pay |
/pay/[id] |
Public | Pay online: choose a provider, or go on to PayFast |
/payment/success, /payment/failure, /payment/cancel |
Public | Return pages after paying online |
/forms |
Public / User | Attendee forms |
/contact, /legal/terms, /legal/privacy, /lockdown-rules |
Public | Info pages |
/login, /register, /forgot-password, /reset-password, /activate-account |
Public | Accounts |
/setup |
First run | Setup wizard |
/consent/[token] |
Parent/guardian | Give or decline parental consent |
/waitlist/claim/[token] |
Public | Claim a waitlist place |
/certificates/verify/[code] |
Public | Check a completion certificate |
/unsubscribe?t=… |
Public | Unsubscribe from news (link in news emails/WhatsApps) |
/self-service |
Supervisor-unlocked | Kiosk |
/set-banner |
Supervisor | Site banner |
/dashboard/user (+ /payments, /donate, /forms, /profile, /privacy, /reset-password) |
User | My Events |
/dashboard/staff (+ /ticket-scanning, /event-tickets) |
Staff | Staff tools |
/dashboard/supervisor (+ /events, /event-options, /at-the-door, /manual, /manual-registration, /payments, /coupons, /waitlist, /attendance, /consent, /offline-scans, /allocations, /email-attendees, /whatsapp-attendees, /forms, /reports) |
Supervisor | Supervisor tools |
/dashboard/admin (+ /users, /registrations, /forms, /cashup, /cashup/[id], /audit-log, /data-requests, /privacy, /settings) |
Admin | Admin tools |