- Invoices drafted 14 days before each licence's invoice date and emailed to the alert email for approval; approved ones go out on their date from 08:00 SA time. Drafts can be edited, recalculated, sent early, put back to draft or voided; one-off invoices too. - Annual tier review from the 12-month event-income band; approving a tier change emails a notice of licence change, applied when the invoice goes out. Notices can also be sent by hand. - Founding churches (35% off as its own line), payments with receipts, statements for any dates. - Every billing email is styled HTML with a PDF (invoice / tax invoice, receipt, statement, licence change) and logged per customer. - Settings -> Billing: business and bank details, VAT (off: plain invoices; on: VAT-inclusive tax invoices), timing and numbering. - Migration 20261103111645_invoicing; pdfkit 0.17.2; tests/billing.test.js; scripts/sample-billing-pdfs.js. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7.0 KiB
Cross Events Hub
Joshua's central management app for every Cross Events install: customers, licences, install health and usage, and alerts. Customers never log in here.
Naming: Cross Events was called Hope Events until October 2026. Older commits, the
hope-eventsrepo name and thehel1.licence-token prefix still use the old name. The prefix is part of the protocol, so it stays.
backend/— Express + Prisma (Postgres) API, port 5200frontend/— Next.js dashboard, port 3200docs/hub-protocol.md— the contract with Cross Events installs (a copy lives in thehope-eventsrepo; change both together)
Installs only ever call the Hub (register, heartbeat); the Hub never reaches into an install and holds no church data — just versions, health and aggregate counts.
Run it locally
Needs Node 20+ and Postgres.
cd backend
cp .env.example .env # set DATABASE_URL, JWT_SECRET, HUB_ENCRYPTION_KEY
npm install
npx prisma migrate deploy && npx prisma generate
npm run generate-keys # licence signing key → keys/hub-signing-key.pem
npm run create-admin -- --email you@example.com --name "Your Name"
npm run dev # http://localhost:5200
cd ../frontend
cp .env.example .env.local # NEXT_PUBLIC_API_URL=http://localhost:5200
npm install
npm run dev # http://localhost:3200
The first login sets up two-factor with an authenticator app (required).
To try it with Cross Events locally, point the install at the Hub with HUB_URL=http://localhost:5200
and HUB_PUBLIC_KEY (the public key shown in Hub → Settings) in Cross Events' backend/.env.
Releases and notices
- Releases come from git: every
v1.2.3tag ofjoshua/hope-eventsis a release, the newest is the latest version, and the notes churches see come fromWHATS-NEW.mdon that repo's default branch (only for tagged versions, so notes can be written ahead or corrected later). The Hub checks every 15 minutes (and on Sync now, Releases page). So releasing Cross Events is: write the version's section inWHATS-NEW.md, tag, push — installs see the update with their next heartbeat. - Notices (Notices page) are banners on churches' sites for their admins and supervisors, emails to them, or both — e.g. planned maintenance.
AI (Cross Events 2.0)
Sites call AI through the Hub (protocol §3b), so the Claude API key never leaves this server.
Set it up in Settings → AI: the key, the model (Claude Sonnet 5.5 by default; choosing one from the
list fills in its prices, kept in backend/src/utils/aiModels.js — update that list in a release when
Anthropic's prices change), the exchange rate and the default markup — AI stays off for every site
until the key and exchange rate are saved;
Test the saved key checks them. Each licence has a monthly AI cap in rand (0 = off) and an
optional markup override, on the customer's page. The cap counts what the church is charged
(real cost plus markup); the Hub checks the worst case before every call, so a month can't run over.
Use per site (real cost, charged, profit) is on Settings → AI and on each install's page. No prompt
or response text is stored.
Billing (1.7.0)
Invoices, receipts, statements and notices of licence change, each emailed (styled HTML) with a PDF.
The code is in backend/src/utils/billing/ (compute.js the rules, pdf.js the documents, email.js
the emails, index.js the job and actions) and backend/src/routes/billing.js.
- Set up Settings → Billing: business and banking details, VAT, timing and numbering. Set the alert email (Settings → Alerts): approval requests go there. SMTP must work.
- Per church (customer page → Billing): founding church (35% off every tier), billing email and postal address, and for each licence: monthly or yearly (10 months' price for 12), the date billing started (its day of the month is the billing day), the next invoice date, the next annual tier review and an optional custom monthly price (e.g. the Network tier).
- The job (every 15 minutes): 14 days before a licence's next invoice date it drafts the invoice and emails it to you with the draft PDF. At the yearly review the draft uses the site's 12-month event-income band (Cross Events 2.0.2+) and can move the tier up or down; drafts in between keep the tier. Approve it on Billing (you can change its lines, dates, recipient and notes first); a tier change sends the church a notice of licence change at once. Approved invoices go out on their date from 08:00 (South African time) to the billing email, else the site's admin contact, else the customer's email; the tier change takes effect when that invoice goes out. If a date comes and it isn't approved, you're emailed once and it waits.
- Payments: Billing → Record payment (or on an invoice or customer). A receipt is emailed with its PDF unless you untick it. Statements: customer page → Billing → Statement (any dates; open the PDF or email it). One-off invoices (e.g. standby support): Billing → One-off invoice.
Invoices are in advance (the invoice dated the 1st covers that month). VAT: not registered →
plain invoices with no VAT; once registered, tick it in Settings → Billing: prices are then
VAT-inclusive (totals unchanged), documents become tax invoices with the VAT number and the VAT
included. The tiers are in backend/src/utils/billing/tiers.js and frontend/src/lib/tiers.ts (a test
checks they match) and on crosscode-site src/data/pricing.mjs. node backend/scripts/sample-billing-pdfs.js <dir>
writes sample PDFs (no database needed) to check the layout.
The licence signing key
Licences are tokens signed with an Ed25519 key. The private key exists only on the Hub server;
installs check licences with the public key, which is built into Cross Events
(backend/src/hub/publicKey.js).
- Create the production key once, on the Hub server:
npm run generate-keys. Back upkeys/hub-signing-key.pemsomewhere safe and offline. - Put the public key it prints into Cross Events (
backend/src/hub/publicKey.js) and release. - Losing or replacing the key means every install needs a Cross Events update with the new public key before it can trust new licences — so don't.
HUB_ENCRYPTION_KEYencrypts install secrets: it must never change after installs register.
Deploying
Same pattern as Cross Events: on the server (events-hub.crosscode.co.za), behind nginx with
HTTPS, two PM2 processes (backend: npm start; frontend: npm run build && npm start),
TRUST_PROXY=1, FRONTEND_URL=https://events-hub.crosscode.co.za, and the frontend's
NEXT_PUBLIC_API_URL set to the public API address (e.g. https://events-hub.crosscode.co.za
with nginx routing /api to port 5200). Back up the database daily, plus the signing key.
Tests
cd backend && npm test — encryption, licence tokens, request signatures, two-factor codes
(RFC 6238 vector) and alert rules.