Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9584cdde5 |
@@ -0,0 +1,396 @@
|
||||
# Cross Code — Road to three products
|
||||
|
||||
Planned work to go from one product (Cross Events 2.1.x) to three that can be sold separately or
|
||||
together: **Cross Events**, a **Church Website** and a **Church App**, all managed from one
|
||||
**Cross Code Hub**. Based on the two concept specs (church website platform, church app v1.1,
|
||||
3 November 2026), with the changes decided on 2026-11-03 (see [Decisions](#decisions-2026-11-03)).
|
||||
See [ROADMAP-v2.md](./ROADMAP-v2.md) for how Cross Events got to 2.x.
|
||||
|
||||
**Theme:** a church can have any mix of products (Events, Website, App, all, or just the
|
||||
Website). People sign in to all of them with **one account**, and events flow between them in
|
||||
whatever direction the church chooses.
|
||||
|
||||
---
|
||||
|
||||
## Contents
|
||||
|
||||
1. [The shape](#1-the-shape)
|
||||
2. [Contracts (shared JSON formats)](#2-contracts-shared-json-formats)
|
||||
3. [Accounts (shared users)](#3-accounts-shared-users)
|
||||
4. [Event sync in any direction](#4-event-sync-in-any-direction)
|
||||
5. [Cross Code Hub (all products)](#5-cross-code-hub-all-products)
|
||||
6. [Church Website](#6-church-website)
|
||||
7. [Church App](#7-church-app)
|
||||
8. [Servers and deployment](#8-servers-and-deployment)
|
||||
9. [Build order](#9-build-order)
|
||||
10. [Decisions](#decisions-2026-11-03)
|
||||
11. [Still open](#still-open)
|
||||
|
||||
---
|
||||
|
||||
## 1. The shape
|
||||
|
||||
Each church gets its own **church stack**: one install per product it has bought, each with its
|
||||
own process and its own database. The Hub only sees aggregate stats and licences.
|
||||
|
||||
```
|
||||
Cross Code Hub (hub.crosscode.co.za)
|
||||
customers · licences per product · stats · AI proxy · billing
|
||||
▲ register / heartbeat / licence (install → Hub only)
|
||||
┌───────────────┼────────────────────────────────────────┐
|
||||
│ Church stack (e.g. Hope Family Church) │
|
||||
│ │
|
||||
│ Accounts (accounts.church.co.za) ◄── sign-in (OIDC) ──┤
|
||||
│ ▲ ▲ ▲ │
|
||||
│ Website (www.) ◄─feed─► Events (events.) ◄─feed─► App (app.)
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Rules**
|
||||
|
||||
1. **No shared databases, ever.** Products only talk over HTTPS using documented contracts (§2).
|
||||
2. **The Hub is never on the runtime path.** It stays read-only toward installs: installs push to
|
||||
it, it never reaches into them, and a Hub outage switches nobody off.
|
||||
3. **One maintained codebase per product.** Each church runs a tagged release of it. Nothing is
|
||||
built specially for one church.
|
||||
4. **The origin of a record owns it.** Copies elsewhere are read-only and link back to the origin.
|
||||
|
||||
**Products and what each one owns**
|
||||
|
||||
| Product | Owns | Doesn't do |
|
||||
|---------|------|------------|
|
||||
| **Accounts** | People's accounts: name, email, phone, password, 2FA, privacy notice acceptance, news consent | Anything product-specific |
|
||||
| **Cross Events** | Events with registrations, tickets, payments, check-in, kids, rosters, POPIA toolkit | Website content |
|
||||
| **Church Website** | Public pages, leaders, ministries, messages (sermons), service times, simple events | Registrations or payments |
|
||||
| **Church App** | House of Prayer, announcements, devotionals, reading plans, groups, care, simple events | Registrations or payments |
|
||||
|
||||
---
|
||||
|
||||
## 2. Contracts (shared JSON formats)
|
||||
|
||||
Everything that crosses between products is defined **once**, in a shared package
|
||||
`@crosscode/contracts` (zod schemas + generated JSON Schema + TypeScript types + sample payloads).
|
||||
Every product, including Cross Events and the Hub, uses it. This is what keeps the key fields the
|
||||
same in every product's JSON.
|
||||
|
||||
| Contract | Used for |
|
||||
|----------|----------|
|
||||
| `crosscode.event/1` | The event feed every product publishes and reads (§4) |
|
||||
| `crosscode.church/1` | The church profile: name, logo, colours, campuses, service times, contact details. Set once and shared |
|
||||
| `crosscode.identity/1` | The claims Accounts puts in a sign-in token, and the back-channel calls (`user.updated`, `user.deleted`, `user.export`) |
|
||||
| Hub protocol v3 | Register, heartbeat, licence token, AI calls, for every product (§5) |
|
||||
|
||||
**Versioning**
|
||||
|
||||
- Adding an **optional** field keeps the version (`/1`). Consumers ignore fields they don't know.
|
||||
- Removing or renaming a field, or changing its meaning, needs a new version (`/2`). The old
|
||||
version is served alongside it until every install has updated, as with `/api/v2`.
|
||||
- Every payload carries its `schema` name, so a consumer always knows what it's reading.
|
||||
- The package lives in the platform monorepo (§8) and is published to Gitea's package registry so
|
||||
the Events and Hub repos can install it.
|
||||
|
||||
---
|
||||
|
||||
## 3. Accounts (shared users)
|
||||
|
||||
A small per-church **OpenID Connect** provider (`oidc-provider` on the server, `openid-client` in
|
||||
each product). Every church stack has one, including website-only churches (where it only holds
|
||||
staff logins).
|
||||
|
||||
**Why a separate service:** if one product held the users, cancelling that product would take
|
||||
everyone's login with it. With Accounts on its own, products can be added or cancelled without
|
||||
touching anyone's account.
|
||||
|
||||
### What lives where
|
||||
|
||||
| Accounts | Each product (keyed by the account's `sub`, a UUID) |
|
||||
|----------|-----------------------------------------------------|
|
||||
| Name, email, phone, password (Argon2id), 2FA, lockout, password reset, login history | Roles in that product. An Events supervisor isn't automatically an App prayer coordinator |
|
||||
| Privacy notice versions and acceptance, news consent (email / WhatsApp) | Product data: registrations and tickets (Events), prayer commitments (App), content edits (Website) |
|
||||
| "Download my data" and "Delete my account" entry points | Its share of the export and the anonymising, when Accounts asks |
|
||||
| Inactive-account retention | — |
|
||||
|
||||
### How it works
|
||||
|
||||
- Sign-in is the standard authorisation-code flow with PKCE. People sign in once at
|
||||
`accounts.church.co.za` and are signed in to every product of that church.
|
||||
- Each product keeps a local copy of the profile (name, email) for lists and messages, updated on
|
||||
sign-in and by `user.updated`.
|
||||
- **POPIA:** the church is one responsible party. "Download my data" asks every linked product for
|
||||
its part (`user.export`) and bundles the answers. "Delete my account" deactivates the account at
|
||||
once, then sends `user.deleted` to each product when an admin approves (the existing Events
|
||||
rules apply: it waits while the person has an open registration for an upcoming event).
|
||||
- Back-channel calls are signed with a secret per product link and retried through pg-boss.
|
||||
- Accounts gets its own access log entries for exports and admin views of people.
|
||||
|
||||
### Moving Cross Events onto Accounts (Cross Events 3.0)
|
||||
|
||||
This is a breaking change, so it's a major version.
|
||||
|
||||
- A migration copies every Events user into Accounts **with their existing password hash**, 2FA
|
||||
setting, consent and notice acceptance, so nobody has to reset anything. The Events `User`
|
||||
table keeps its id (now equal to the account `sub`), role, campus scope and product data.
|
||||
- Sign-in, forgot password, 2FA, the news question and the notice re-acceptance move to Accounts.
|
||||
The Events privacy centre links to Accounts for account-level items.
|
||||
- Staff accounts work the same way: roles stay in Events.
|
||||
|
||||
---
|
||||
|
||||
## 4. Event sync in any direction
|
||||
|
||||
Every product can **publish** the events it owns and **show** events from its siblings. Each
|
||||
install has two settings:
|
||||
|
||||
- **Publish my events:** on / off.
|
||||
- **Show events from:** ☐ Events ☐ Website ☐ App (only linked products are listed).
|
||||
|
||||
That covers every setup: Events → Website and App; Events and App → Website; everything to
|
||||
everything; or nothing.
|
||||
|
||||
### Rules
|
||||
|
||||
- **Only the origin edits an event.** Imported events are read-only copies with a link back.
|
||||
Corrections happen at the origin and arrive with the next sync.
|
||||
- **No loops:** a product only publishes events it *originated*, never ones it imported.
|
||||
- **No duplicates:** an event is identified by `origin.product` + `origin.id`.
|
||||
- **Only Cross Events takes registrations and payments.** The Website and App can create
|
||||
**simple events** (title, dates, place, image, description, and an optional external link or
|
||||
contact instructions).
|
||||
- Each product keeps the last good copy of each feed. If a source is down, its events keep
|
||||
showing, and admins see "last synced 2 hours ago".
|
||||
|
||||
### Transport
|
||||
|
||||
- `GET /api/feed/v1/events?updatedSince=<ISO>` → `{ schema, generatedAt, events: [...], deleted: [...] }`.
|
||||
Public events only, so no personal information ever goes through the feed.
|
||||
- A signed webhook `event.changed` tells consumers to fetch straight away.
|
||||
- Consumers also poll every 15 minutes as a fallback (pg-boss job).
|
||||
- Timeouts, retries with back-off, and one failing source never blocks another.
|
||||
|
||||
### `crosscode.event/1`
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "crosscode.event/1",
|
||||
"id": "8f1c2a4e-…",
|
||||
"origin": {
|
||||
"product": "events",
|
||||
"installId": "inst_…",
|
||||
"url": "https://events.hopefamily.co.za/events/youth-camp"
|
||||
},
|
||||
"status": "published",
|
||||
"updatedAt": "2026-11-03T10:00:00Z",
|
||||
"title": "Youth Camp 2026",
|
||||
"summary": "Three days at Camp Wartburg",
|
||||
"description": "Sanitised markdown…",
|
||||
"imageUrl": "https://events.hopefamily.co.za/uploads/…",
|
||||
"timezone": "Africa/Johannesburg",
|
||||
"startsAt": "2026-03-14T08:00:00Z",
|
||||
"endsAt": "2026-03-16T14:00:00Z",
|
||||
"allDay": false,
|
||||
"sessions": [
|
||||
{ "id": "…", "label": "Day 1", "startsAt": "…", "endsAt": "…", "location": null, "cancelled": false }
|
||||
],
|
||||
"location": { "name": "Camp Wartburg", "address": "…", "onlineUrl": null },
|
||||
"campus": { "slug": "meyerton", "name": "Meyerton" },
|
||||
"series": { "id": "…", "name": "Youth Camp" },
|
||||
"tags": ["youth"],
|
||||
"registration": {
|
||||
"mode": "origin",
|
||||
"url": "https://events.hopefamily.co.za/events/youth-camp",
|
||||
"status": "open",
|
||||
"opensAt": null,
|
||||
"closesAt": "2026-03-10T22:00:00Z",
|
||||
"priceFrom": { "amountCents": 45000, "currency": "ZAR" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Values / notes |
|
||||
|-------|----------------|
|
||||
| `origin.product` | `events` · `website` · `app` |
|
||||
| `status` | `published` · `cancelled` (unpublished or deleted events go in `deleted`) |
|
||||
| `sessions` | Empty for a single-date event. `startsAt` / `endsAt` are always the first start and last end |
|
||||
| `campus` | `null` = all campuses or campuses off |
|
||||
| `registration.mode` | `origin` (register at the origin, Events only) · `external` (a link) · `contact` (instructions in the description) · `none` |
|
||||
| `registration.status` | `open` · `not_open` · `closed` · `full` · `waitlist`. Only Events sends anything but `open` |
|
||||
| `priceFrom` | Lowest current price in cents, or `null` for free / not applicable |
|
||||
|
||||
---
|
||||
|
||||
## 5. Cross Code Hub (all products)
|
||||
|
||||
The Events Hub (`joshua/events-hub`, Hub 1.7.0) becomes **Cross Code Hub 2.0**: one place for every
|
||||
product's customers, licences, stats, AI usage and billing.
|
||||
|
||||
- **Customer → installs.** Each install has a `product` (`events` · `website` · `app` ·
|
||||
`accounts`) and its **own licence**. Accounts is licensed with whatever else the church has
|
||||
(no charge of its own).
|
||||
- **Billing per customer:** prices per product on the income bands, bundles at **15% off two
|
||||
products** and **25% off all three**, founding prices and the free month (as on
|
||||
crosscode.co.za). Invoices are recorded in the Hub (sent / paid / overdue). A payment provider
|
||||
integration comes later.
|
||||
- **Protocol v3:** adds `product` to register and heartbeat, and per-product stats (aggregate
|
||||
counts only, as today: no personal information). The Hub keeps accepting protocol v2, so Events
|
||||
installs keep reporting until they update.
|
||||
- **Sibling discovery:** the licence token carries the `customerId`. The heartbeat reply lists the
|
||||
church's other installs (product, URL, public key; nothing else). Each product shows them
|
||||
under **Settings → Linked products** with a **Connect** button that sets up the feed (§4) and
|
||||
the Accounts client (§3). The list is cached, so a Hub outage changes nothing.
|
||||
- **AI:** the existing proxy, cap and markup serve every product. Usage is recorded per product,
|
||||
and the cap stays per customer.
|
||||
- **Request update** works for every product. The Hub still never runs anything on an install.
|
||||
- **Domain:** `hub.crosscode.co.za`. `events-hub.crosscode.co.za` stays as an alias so existing
|
||||
installs keep working.
|
||||
- Dashboard: installs per product, versions behind latest, health, licence status, monthly
|
||||
recurring revenue, overdue invoices.
|
||||
|
||||
---
|
||||
|
||||
## 6. Church Website
|
||||
|
||||
A professional public website the church manages itself. From the website spec, adjusted.
|
||||
|
||||
### Public site
|
||||
|
||||
- **Pages:** Home, New Here? / Plan Your Visit, About (story, mission, vision, values, beliefs),
|
||||
Leadership, Ministries (overview + one page each), Events (list, detail, calendar, past),
|
||||
Messages (sermons, series), Give (information and provider links), Contact, Privacy /
|
||||
POPIA / safeguarding / terms.
|
||||
- **Homepage order:** hero with Plan Your Visit, service times and location, mission,
|
||||
introduction, ministries and team, upcoming events, latest message, final invitation.
|
||||
- Branding from `crosscode.church/1`: logo, colours, a choice of themes. Approved section types
|
||||
that can be reordered, not free-form layout editing.
|
||||
- Server-rendered for SEO, fast on a phone, accessible (WCAG 2.2 AA, axe checks in Playwright).
|
||||
|
||||
### Admin (`www.church.co.za/admin`, signed in through Accounts)
|
||||
|
||||
- Dashboard with a setup checklist, recent edits and drafts.
|
||||
- Pages and sections, About, Leadership, Ministries, Messages, Media library, Contact and
|
||||
service times, Navigation, Settings.
|
||||
- **Events:** simple events, plus the feed settings (§4) and sync health.
|
||||
- **Draft → preview → publish**, archive instead of delete, an audit log of who changed what.
|
||||
- **Roles:** Owner, Admin, Editor, Ministry leader (own ministry only), Viewer.
|
||||
|
||||
### Not in the first release
|
||||
|
||||
Prayer requests and other forms with personal information (they belong in the App), a member
|
||||
directory, online giving processing.
|
||||
|
||||
---
|
||||
|
||||
## 7. Church App
|
||||
|
||||
A church-branded **web app** (PWA) at `app.church.co.za`, added to the home screen. No app-store
|
||||
release. From the app spec, adjusted.
|
||||
|
||||
### House of Prayer (the headline)
|
||||
|
||||
- **24/7 coverage view:** a 24-hour timeline, coverage % and gaps, slot length set by the church
|
||||
(15 / 30 / 60 min), in the church's timezone. Live updates through server-sent events, with
|
||||
polling as a fallback.
|
||||
- **Commitments:** one-off and recurring slots, change or cancel your own, optional reminders
|
||||
(email / WhatsApp / push), a voluntary "I prayed" marker clearly shown as self-reported.
|
||||
- **Anonymous:** public views show coverage only, never names. Admin access to bookings is
|
||||
permission-controlled and goes in the access log.
|
||||
- **Prayer requests:** private by default (submitter and prayer team only). Public or group
|
||||
prayer needs an explicit choice. Moderation (approve, redact, remove) and a safeguarding
|
||||
escalation route. Never used for analytics or AI.
|
||||
- **Campaigns and testimonies:** time-bound campaigns with collective progress (no rankings),
|
||||
testimonies moderated before publishing.
|
||||
|
||||
### Other modules (each can be switched off by the church)
|
||||
|
||||
Announcements, devotionals, reading plans, sermons (shared with the Website where both exist),
|
||||
connect groups, pastoral care (restricted access, audited), serving opportunities, giving links,
|
||||
an opt-in directory, and **events** (simple events plus the feed, §4).
|
||||
|
||||
**Every module is available to every church.** Pricing follows the income bands, not features.
|
||||
|
||||
### Roles
|
||||
|
||||
Member, Prayer coordinator, Content admin, Pastoral care, Church admin. Public content can be
|
||||
browsed without an account. Personal actions need sign-in through Accounts.
|
||||
|
||||
---
|
||||
|
||||
## 8. Servers and deployment
|
||||
|
||||
**PM2 and nginx, like Cross Events today.** No Docker.
|
||||
|
||||
- **Per church:** one PM2 process (or front/back pair) per product, one PostgreSQL database and
|
||||
database user per product, its own `.env`, secrets, uploads folder, logs and backups. Names
|
||||
carry the church slug (`hopefamily-website`, `hopefamily_website` DB, …).
|
||||
- **Install and update scripts** for each product, built from the Cross Events ones: fresh
|
||||
install, then a tagged update with a database backup, migrations, build, restart and roll back
|
||||
on failure. An `add-church` script runs them for the products a church has bought and prints
|
||||
the nginx config and DNS records.
|
||||
- **Domains:** `www.`, `events.`, `app.` and `accounts.` on the church's own domain. Certificates
|
||||
through certbot. Only configured domains are served (no trusting arbitrary Host headers).
|
||||
- Every product has `/health` (version, DB, jobs, last backup), reported to the Hub.
|
||||
- Several churches can share a server at first. Any church can move to its own server later
|
||||
without code changes. The Hub stays on a separate server from church installs.
|
||||
|
||||
### Repositories
|
||||
|
||||
| Repo | What |
|
||||
|------|------|
|
||||
| `joshua/hope-events` | Cross Events (unchanged home) |
|
||||
| `joshua/events-hub` | Cross Code Hub (renamed in the UI; repo name can stay) |
|
||||
| `joshua/crosscode-platform` (new) | Monorepo: `apps/accounts`, `apps/website`, `apps/app`, `packages/contracts`, `packages/ui` |
|
||||
| `joshua/crosscode-site` | The marketing site. The Church Website and Church App move from "coming later" to their own pages when they ship |
|
||||
|
||||
**Stack:** the same as Cross Events (Next.js, Express, TypeScript, Prisma, PostgreSQL, pg-boss,
|
||||
Playwright, Jest), so code and habits carry over.
|
||||
|
||||
---
|
||||
|
||||
## 9. Build order
|
||||
|
||||
| Phase | Version | What | Why this order |
|
||||
|-------|---------|------|----------------|
|
||||
| 0 | contracts 1.0.0 | `crosscode.event/1`, `crosscode.church/1`, `crosscode.identity/1`, Hub protocol v3: schemas, docs, sample payloads, the monorepo set up | Everything else depends on them |
|
||||
| 1 | **Hub 2.0.0** | Cross Code Hub: products, licence per product, per-customer billing with bundles, protocol v3 (v2 still accepted), sibling discovery, AI usage per product | Must exist before a second product is sold |
|
||||
| 2 | **Cross Events 2.2.0** | Reports `product: "events"` (protocol v3), **Linked products**, the event feed (publish + show events from siblings) | Small; ready for when the Website exists |
|
||||
| 3 | **Accounts 1.0.0** | OIDC provider, sign-in, 2FA, password reset, consent and notice, export / delete fan-out, admin, install script | The Website is built on it from day one |
|
||||
| 4 | **Church Website 1.0.0** | Public site, admin, simple events + feed, Hope Family Church as the first church, then a second test church to prove it repeats | More churches need a website than an app; it also covers the "website only" case |
|
||||
| 5 | **Cross Events 3.0.0** | Sign-in moves to Accounts; user migration keeping password hashes; migration guide | Breaking change, so a major version, after Accounts is proven on the Website |
|
||||
| 6 | **Church App 1.0.0** | Foundation (PWA, branding, Accounts), House of Prayer, announcements, devotionals, events (simple + feed) | Starts with shared users and the feed already in place |
|
||||
| 7 | Church App 1.x | Reading plans, sermons, groups, pastoral care, serving, giving links, directory, push notifications | After House of Prayer is in use |
|
||||
|
||||
Each phase adds help content, audit log entries, tests, a CHANGELOG entry and updates to the
|
||||
docs. Branch → merge → tag → push per feature, as before.
|
||||
|
||||
### Release checks (before selling a product)
|
||||
|
||||
- Two churches installed from the same tagged release, with separate databases, secrets and uploads.
|
||||
- Church A's admin can't read or change Church B's data through direct API calls.
|
||||
- Stopping one church's install, or the Hub, leaves the others running.
|
||||
- A feed source going down: its last events keep showing, the other sources keep working.
|
||||
- Signing in once works across all of a church's products; deleting an account reaches every product.
|
||||
- A database backup is restored successfully.
|
||||
|
||||
---
|
||||
|
||||
## Decisions (2026-11-03)
|
||||
|
||||
| # | Question | Decision |
|
||||
|---|----------|----------|
|
||||
| 1 | Separate infrastructure | **Yes:** one install and database per product per church, no shared databases, Hub not on the runtime path |
|
||||
| 2 | Shared users | **A separate Accounts service per church** (OpenID Connect), used by every product. Roles stay per product |
|
||||
| 3 | Docker | **No.** PM2 and nginx, like Cross Events |
|
||||
| 4 | Simple events in the Website and App | **Yes.** Only Cross Events takes registrations and payments. (The app spec's "no event creation in the App" is overruled) |
|
||||
| 5 | Event sync | Any direction, chosen per install ("Publish my events", "Show events from"). One shared format, `crosscode.event/1`. Only originated events are published |
|
||||
| 6 | Domains | `www.`, `events.`, `app.` (and `accounts.`) on the church's own domain |
|
||||
| 7 | Hub | Becomes **Cross Code Hub**: every product's licences, stats, AI and billing in one place, still read-only toward installs |
|
||||
| 8 | Stack | Next.js + Express + Prisma + PostgreSQL + pg-boss, as Cross Events (not the specs' React + Vite, Redis + BullMQ, Socket.IO) |
|
||||
| 9 | Pricing | Income bands per product, bundles 15% (two) / 25% (three); every App module for every church (not the website spec's Starter / Growth / Pro tiers) |
|
||||
| 10 | Hub provisioning | Scripts Joshua runs; the Hub records installs but never deploys or rolls back (not the specs' automated provisioning) |
|
||||
|
||||
## Still open
|
||||
|
||||
- Exact prices for the Website and App on the income bands (`crosscode-site/src/data/pricing.mjs`).
|
||||
- Whether a church can link more than one install of the same product (e.g. two websites).
|
||||
- House of Prayer defaults: slot length, booking horizon, recurrence limits, cancellation rules.
|
||||
- Push notifications in the App (web push support varies by phone) — in 1.0 or 1.x.
|
||||
- Whether Accounts is shown to people by name ("Hope Family account") or kept invisible.
|
||||
Reference in New Issue
Block a user