Files
events-hub/README.md
T
joshuaandClaude Opus 5.5 6fce0fe90e Billing: invoices, approval, receipts, statements, licence changes (1.7.0)
- 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>
2026-11-03 13:42:45 +02:00

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-events repo name and the hel1. licence-token prefix still use the old name. The prefix is part of the protocol, so it stays.

  • backend/ — Express + Prisma (Postgres) API, port 5200
  • frontend/ — Next.js dashboard, port 3200
  • docs/hub-protocol.md — the contract with Cross Events installs (a copy lives in the hope-events repo; 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.3 tag of joshua/hope-events is a release, the newest is the latest version, and the notes churches see come from WHATS-NEW.md on 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 in WHATS-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.

  1. 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.
  2. 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).
  3. 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.
  4. 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 up keys/hub-signing-key.pem somewhere 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_KEY encrypts 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.