Files

9.6 KiB

Cross Code Hub

Joshua's central management app for every Cross Code product a church has — Cross Events, Accounts, the Church Website and the Church App: customers, a licence per product, releases per product, install health and usage, AI, billing and alerts. Customers never log in here. (Until 2.0 this was the Cross Events Hub; the plan for 2.0 is docs/HUB-2.0-PLAN.md, and the roadmap is crosscode-platform/docs/ROADMAP-v3.md.)

Naming: Cross Events was called Hope Events until October 2026. Older commits, the events-hub repo name, the CEH- licence-key prefix and the hel1. licence-token prefix still use the old names. The prefixes are part of the protocol, so they stay.

  • backend/ — Express + Prisma (Postgres) API, port 5200
  • frontend/ — Next.js dashboard, port 3200
  • docs/hub-protocol.md — the contract with installs, protocol 1 (Cross Events 2.1) and 2 (every product); a copy lives in the hope-events repo, and the v2 formats are in @crosscode/contracts
  • backend/vendor/crosscode-contracts-<version>.tgz — the shared formats, committed so deploys need no package registry. To update: npm pack in crosscode-platform/packages/contracts at its new tag, put the file here, npm install --save file:vendor/<file> in backend, remove the old file

Products (Hub 2.0): every licence and install is for one product. A church has at most one licence per product that isn't revoked; generating its first product licence also makes its free Accounts licence. Keys: CEH- Events, CCA- Accounts, CCW- Website, CCP- App.

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, per product (Releases page → Release sources): each source is a repository, a tag prefix and a notes file. By default every product comes from joshua/crosscode-platform (events@1.2.3, accounts@…, website@…, app@…, notes in apps/<product>/WHATS-NEW.md), and Cross Events before the move also from joshua/hope-events (v1.2.3, WHATS-NEW.md). A product's newest tag is its latest version; notes show for tagged versions only (so they can be written ahead or corrected later). A heading like ## 3.0.0 — requires accounts 1.2.0 makes Request update wait until the church's Accounts is new enough. The Hub checks every 15 minutes (and on Sync now); an unreachable source never removes releases. So releasing is: write the version's section in the notes file, tag, push — that product's 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

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 church has a monthly AI cap in rand (0 = off) and an optional markup override, on the customer's page, shared by all its sites (Hub 2.0). 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

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; Hub 2.0: one invoice per church for all its products): founding church (35% off every tier), billing email and postal address, the billing schedule (monthly or yearly — 10 months' price for 12 —, the date billing started, whose day of the month is the billing day, the next invoice date and the next annual tier review), and for each product the date it's charged from (blank = not charged, e.g. a trial; Accounts is always free) and an optional custom monthly price (e.g. a Network tier). Next invoice shows the next draft as it would be made today.
  3. The job (every 15 minutes): 14 days before a church's next invoice date it drafts the invoice and emails it to you with the draft PDF. Each product is a line at its price; a founding church gets the founding price as a line under each; bundles take 15% off two products and 25% off three (after founding); extra website storage (R20 per 5 GB above 5 GB, from the website's heartbeat) is added last and never discounted (12 months' price on a yearly invoice). A product added to a church that's billed already gets its free month, then the days before its first full period are charged by the day (a year's price over 365 days). At the yearly review the draft uses what each product reports — Cross Events its 12-month event-income band (2.0.2+), the App its active-member band — and can move each tier up or down; drafts in between keep the tiers. Approve it on Billing (you can change its lines, dates, recipient and notes first); each 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 prices of every product 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

See deployment.md (and its "Upgrading to 2.0" before deploying 2.0 the first time). In short: the same pattern as Cross Events: on the server (events-hub.crosscode.co.za, with hub.crosscode.co.za as the new address), 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), alert rules, products and licences, protocol v1 and v2 (every contract sample through the routes), releases per product and requirements, billing (prices, bundles, add-ons, prorating, the address, monthly recurring revenue) and AI. cd frontend && npx eslint src and npx tsc --noEmit check the dashboard.