Compare commits

...
1 Commits
Author SHA1 Message Date
joshuaandClaude Opus 5.5 a9584cdde5 Roadmap v3: Accounts, Church Website, Church App, Cross Code Hub
Plan for selling three products from one Hub: shared users through a
per-church Accounts service, event sync in any direction on one shared
format (crosscode.event/1), PM2 installs per product per church.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:14:24 +02:00
+396
View File
@@ -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.