The Cloudbase Foundation · Technology Strategy

Roadmap — revised after the donation-platform pivot

Document · Revision 2 Date · 22 May 2026 Author · Jonathan Supersedes · roadmap.md (Rev. 1)

I.What changed, and why

Three sessions of research have pushed the roadmap off its original course in one specific place — donation processing. The rest of the plan still holds. This revision pulls Phase 3 apart, threads in a new background-data-integrity track, and trims Phase 6 to match.

Phases unchanged
1, 2, 4, 5
scope holds, sequencing holds
Phases revised
3 & 6
donation pipeline rethought
New track
3.5
background data integrity
Net effect
Less custom code
more platform delegation

The three findings driving the revision

  1. A real 501(c)(3) receipt pipeline is more work than it looks. Building per-gift receipts, year-end aggregation, and recurring-payment lifecycle on top of Stripe‑direct adds at least a quarter of bespoke compliance code. No clean open-source library does just this piece — the candidates that exist are bundled inside full CRMs like CiviCRM.
  2. James Brysmart's nonprofit-crm-stack is a near-perfect reference architecture for the future state. The pattern — webhook → normalize → match → either land in the CRM or stage with diagnostics — is correct and worth adopting wholesale, regardless of whether CBF ever uses his code.
  3. Donorbox replaces the entire "Stripe-direct + custom-form + custom-receipt" scope of the old Phase 3 with a single SaaS dependency that still uses Stripe underneath. CBF keeps the Stripe relationship intact (donations create real Stripe objects on CBF's account) and gets receipts, recurring lifecycle, and dunning for $17/month + 1.75% platform fee.
The original "no recurring SaaS costs" north star bends here. The trade is deliberate: 1.75% + $17/mo buys back a quarter of engineering work, the entire 501(c)(3) compliance surface, and a clean Stripe-ID-based exit path if CBF ever wants to take it back in-house.

II.Current state, May 2026

LayerTechnologyStatus
Public websiteNext.js 15, App Router, TypeScript on Cloudflare Workers (OpenNext)Live
Content managementTina CMS, Git-backed via Tina CloudLive
DNSCloudflare — cloudbase.foundationLive
CRM (dev)Twenty CRM, Docker, DigitalOcean $12/moRunning
CRM (prod)crm.cloudbase.foundationNot provisioned
Donation processingGiveLively, with manual CSV sync into TwentyIn use, planned for replacement
Project datasrc/data/projects.json (static)Phase 1; moves to Twenty in Phase 2
Internal kanbankan (self-hosted, AGPL), Docker on the Unraid VM (LAN); own Postgres + MinIO, isolated from TwentyRunning (dev)

III.The architecture shift

The diagram below contrasts the donation/CRM data flow today against the target shape after Phases 3 and 3.5. The change is not the donation platform per se — it is the introduction of a small intake service between any donation source and Twenty, owning matching, idempotency, and a staging table for records that can’t be auto-linked.

Fig. 1Current vs. target donation & data flow
flowchart LR subgraph CURRENT["CURRENT — May 2026"] direction LR D1[Donor]:::donor --> GL[GiveLively]:::saas GL -- "weekly CSV export" --> H1[Human in Twenty UI]:::human H1 --> T1[(Twenty CRM)]:::crm end subgraph TARGET["TARGET — after Phase 3.5"] direction LR D2[Donor]:::donor --> DB[Donorbox]:::saas DB -- "webhook + signed event" --> IS[cbf-intake-service]:::service IS -- "@nonprofit-crm/matching" --> ML((matching
library)):::lib IS -- "high-confidence" --> T2[(Twenty CRM)]:::crm IS -- "low / unknown" --> SR[[Staging table]]:::stage SR -. "human review" .-> T2 T2 -- "Donor.created" --> LM[ListMonk]:::saas end classDef donor fill:#ecd9a4,stroke:#9a7826,color:#15171c classDef saas fill:#fbf7ee,stroke:#0c2540,stroke-width:1.5px,color:#0c2540 classDef human fill:#ede5d2,stroke:#6c7585,color:#3a4250,stroke-dasharray:3 2 classDef crm fill:#0c2540,stroke:#c8a14a,stroke-width:2px,color:#f6f1e7 classDef service fill:#1f3b5e,stroke:#c8a14a,stroke-width:2px,color:#f6f1e7 classDef lib fill:#c8a14a,stroke:#9a7826,color:#0c2540 classDef stage fill:#f6f1e7,stroke:#a5402b,stroke-width:1.5px,color:#a5402b
The human-in-the-loop step disappears from the happy path. It returns only for genuinely ambiguous matches, surfaced as a queue rather than implicit in a CSV row.

Three execution surfaces, three roles

The revised plan deliberately splits new work across three execution surfaces: a sidecar Node service, a Twenty App, and a small Cloudflare Worker footprint. Each has a distinct shape, and the choice for each piece is not interchangeable.

Sidecar service
cbf-intake-service

  • Donorbox webhook receiver (signature verification, event-ID idempotency)
  • Provider-payload → canonical-payload normalization
  • Calls @nonprofit-crm/matching per record
  • Owns staging_record table for low-confidence or incomplete records
  • Background workers: retry, periodic dedupe sweep
  • Review queue UI over its own staging data

Anything with a public webhook URL, its own database, or background jobs.

Twenty App
cbf-data-model

  • Custom-object definitions: RecurringAgreement, Partner field, receiptStatus
  • In-Twenty logic functions: rollup recompute on Donation.created/updated
  • Twenty Workflows: ListMonk sync, partner notifications
  • Can optionally import @nonprofit-crm/matching for records entered through the Twenty UI (not just webhook intake)

Anything that extends the Twenty UI, data model, or workflow engine — portable to any vanilla Twenty instance.

Cloudflare Workers
edge handlers

  • CBF-site → Twenty REST proxy (Phase 2)
  • ListMonk unsubscribe webhook → Twenty PATCH (Phase 2)
  • Future stateless edge work

Stateless work that benefits from edge latency. Rule of thumb: if it needs a database, it's not a Worker.

The matching library cuts across all three

@nonprofit-crm/matching is a zero-dependency TypeScript package — not a Twenty App, not a service, just a library. It is imported wherever donor matching is needed: by the sidecar (on webhook intake), by the Twenty App (on UI-entered records), by a CLI (for one-shot historical reconciliation), and potentially by James Brysmart's stack as a shared dependency. Framework-agnostic by design.

Why the intake service isn't a Twenty App

A Twenty App extends Twenty's data model, UI surfaces, and workflow engine. It does not expose arbitrary public HTTPS endpoints for external services to POST to, and it does not own state outside Twenty's schema. The intake service needs both: a public webhook URL Donorbox can reach, and a staging_record table that has no business being in the canonical CRM. Coupling its lifecycle to Twenty's would also tie webhook availability to CRM uptime. The right boundary is a separate process — same pattern as James's nonprofit-crm-fundraising-service.

IV.Donor matching — the decision flow

The matching engine ships as a zero-dependency TypeScript package (@nonprofit-crm/matching). The intake service imports it; a CLI imports it for one-shot reconciliation; James’s stack could in principle import it too. Logic is identical across callers; only the Twenty client adapter is injected.

Fig. 2Match resolution — per-record decision
flowchart TD A([Incoming donation payload]):::start --> B{Email matches
existing Person?} B -- yes --> R1[/Definitive match · 1.0/]:::def B -- no --> C{External ID resolves?
givelivelyDonorId
or stripeCustomerId} C -- "yes, resolves" --> R1 C -- no --> D{First + last + city
match exactly one?} D -- yes --> R2[/High confidence · 0.8/]:::hi D -- no --> E{First + last
match exactly one?} E -- yes --> R3[/Medium · 0.5
requires review/]:::med E -- "ambiguous or none" --> R4[/No match/]:::none R1 --> P[Promote to Twenty
create Donation, link Person]:::ok R2 --> P R3 --> S[Stage with diagnostics]:::stage R4 --> S classDef start fill:#0c2540,stroke:#c8a14a,color:#f6f1e7,stroke-width:2px classDef def fill:#406b3f,stroke:#2d4e2c,color:#f6f1e7 classDef hi fill:#c8a14a,stroke:#9a7826,color:#0c2540 classDef med fill:#ecd9a4,stroke:#9a7826,color:#15171c classDef none fill:#f6f1e7,stroke:#a5402b,color:#a5402b classDef ok fill:#1f3b5e,stroke:#c8a14a,color:#f6f1e7 classDef stage fill:#a5402b,stroke:#7a2d1d,color:#f6f1e7
Thresholds are deliberate. Anything below "high" lands in staging with structured diagnostics rather than guessing — better one human-resolved match than a wrongly-linked donation propagating forever.

Background sweep — Scenario C

Per-record matching at intake handles new donations. It cannot find duplicates that accumulate over time through manual entry, varied entry paths, or imperfect historical imports. A periodic sweep job inside the intake service re-scans Twenty’s Person records using the same library and surfaces candidate duplicates in the same review queue. This is the "without manually triggered batches" promise applied to data hygiene, not just intake.

V.Phases — updated

Phase 01 — Foundation Complete

Public site, CRM dev instance, documentation in place

Everything needed for a credible public presence and a working CRM dev environment.

Held without revision. Tracked in detail in roadmap.md Rev. 1. The two outputs that matter here are the live cloudbase.foundation site and the working Twenty dev instance with the CBF data model.

Phase 02 — CRM production & data pipeline Next

Twenty in production, CBF-site ↔ Twenty REST integration, ListMonk standup

All previously-planned items hold. Two additions land in this phase because they are prerequisites for Phase 3.

Added since Rev. 1 Data-model gaps that must be closed before the donation pipeline ships. These are schema-only changes applied through the existing apply_data_model.py path.

Held from Rev. 1

New in Rev. 2

Phase 03 — Donation processing — rewritten Major revision

Adopt Donorbox; stand up cbf-intake-service; deprecate GiveLively

Replaces the original "Stripe-direct + custom donation form + custom Stripe-webhook Worker" approach. Same end state for the donor; far less custom code and full delegation of 501(c)(3) compliance.

What changed The previous plan built three things in-house: the donation form, the Stripe webhook handler, and tax-receipt issuance. The revised plan buys Donorbox for the first and third and replaces the Worker with a stateful sidecar service. CBF’s Stripe relationship is preserved (Donorbox uses Stripe under the hood; charges create real Stripe objects on CBF’s account), so this is a deferral of self-hosting, not an exit from it.

Donorbox setup

Matching library — @nonprofit-crm/matching

Intake service — cbf-intake-service

GiveLively deprecation

Phase 04 — Volunteer tool: OSS-ification & Twenty integration — scope reframed Scope reframed

Generalize chelan-comps into an open-source product; add Twenty as an optional workflow surface

Rev. 1 framed this as building a volunteer tool from zero. That framing is wrong: the production app already exists at chelancomps.org — React 19 + Vite + Supabase + Resend, deployed via Cloudflare Pages. The work that remains is generalization, documentation, and CRM integration, not feature-building.

What changed Two assumptions in Rev. 1 are dropped: (1) that an MVP feature set still needs to be built, and (2) that a pilot still needs to be recruited. The 2026 Chelan paragliding comp is the pilot. The 2026 application cycle is the production validation. What remains is making the codebase usable by other comp organizers and stitching it into the CBF CRM stack where useful.

OSS-ification track

Twenty integration track (optional workflow surface)

Resources page track (CBF-site)

Phase 05 — Tina CMS expansion Planned

Past projects, board headshots, news featured images, homepage selection

Unchanged from Rev. 1. One small dependency added: homepage featured-projects selector should read from Twenty rather than from projects.json, so this phase is technically gated on Phase 2's Twenty integration shipping first.

Phase 06 — Donor communications — scope reduced Scope cut

Non-transactional email — ListMonk-based, plus branded receipt customization

Original scope assumed CBF would own the entire email stack (Postmark/Resend/SES, React Email templates, transactional pipeline). With Donorbox handling transactional receipts and ListMonk handling broadcasts, the remaining work is much smaller.

What changed The phase used to include "evaluate Postmark, Resend, AWS SES; build React Email templates; wire into Stripe webhook." All of that is now either delegated (Donorbox) or already in Phase 2 (ListMonk). What remains is brand polish on Donorbox templates and the campaigns that ListMonk will actually send.

VI.Architecture decisions log

DecisionChosenRejectedRationale
CRM Twenty CRM CiviCRM + Backdrop CiviCRM too complex to self-host; Twenty is modern, Docker-first, has a clean REST API. Unchanged from Rev. 1.
Deployment Cloudflare Workers via OpenNext Vercel DNS already on Cloudflare; consolidates infrastructure. Unchanged.
CMS Tina CMS CloudCannon, Decap Git-backed, hosted auth, App Router support. Unchanged.
Donation platform Donorbox Stripe-direct (deferred), GiveLively (current), Givebutter, Zeffy, Every.org Revised in Rev. 2. Donorbox uses Stripe under the hood (preserving an eventual self-hosted path), has the strongest API of the candidates, auto-issues 501(c)(3)-compliant receipts including year-end aggregates, and supports recurring lifecycle webhooks. Trade: 1.75% platform fee + $17/mo for API access. See donation-platform-comparison.md for the full evaluation.
Intake-service home Sidecar Node/TS service on the CRM droplet Cloudflare Worker (Rev. 1 plan), Twenty Workflow only New in Rev. 2. Stateful work (idempotency ledger, staging table, periodic sweep) doesn't belong in a Worker. Pattern mirrors James Brysmart's nonprofit-crm-fundraising-service.
Matching engine Zero-dep TS library, @nonprofit-crm/matching CRM-coupled implementation; James's PersonIdentityService as-is New in Rev. 2. Portable, testable in isolation, reusable by CLI and intake service. Contribution candidate to James's stack (and any other Twenty-based nonprofit).
Twenty extension model Twenty Apps framework for in-Twenty work only Forking Twenty's core (James's current path); building intake/matching as Twenty Apps New in Rev. 2. Twenty's own docs recommend against forking. The Apps framework is used narrowly: to package CBF's custom-object definitions (RecurringAgreement, Partner, etc.) and in-Twenty workflow logic. It is not the home for the intake service (needs its own webhook endpoint, database, and background jobs) or the matching library (framework-agnostic by design). See § III for the full surface breakdown.
Volunteer tool Custom build, open source Off-the-shelf No existing tool fits free-flight comp workflows. Unchanged.
Internal kanban kan, self-hosted Trello / Notion (SaaS), GitHub Projects, Twenty tasks New, June 2026. An org-wide board for internal work, deliberately kept separate from donor data. kan is AGPL, Docker-first, Postgres-only, with Better Auth (Google OAuth + magic link) lockable to the thecloudbasefoundation.org Workspace. Runs as its own stack on the Unraid VM (own Postgres + MinIO), isolated from Twenty. Future CRM tie-ins go through kan's REST API + webhooks, not a shared database — see the CBF-kanban repo's docs/phase-2-crm-integration.md.

VII.Open questions & risks

Question / riskTrackResolution path
Donor-cover-fees uptake rate Phase 3 Run Donorbox sandbox, instrument fee-cover prompt copy, target ≥ 70%. If lower, re-evaluate Every.org as a $0 alternative.
Recurring vs one-off intake paths diverge Phase 3.5 Confirm Donorbox webhook shape for subscription renewal events vs first-charge; build separate handlers if needed.
PII flow into the sidecar staging DB Phase 3 Apply same backup, retention, and access controls as Twenty's DB. Document in docs/architecture.md when service stands up.
Donorbox project-designation field shape Phase 3 Verify in sandbox whether designations come through as structured metadata mapable to Twenty Project records, or as free-text requiring a lookup.
KarmaFlights partner tagging on intake Phase 2 → 3 Decide whether Partner is settable from Donorbox URL params/metadata, or whether it's assigned post-intake by workflow.
James's stack alignment Cross-cutting Tracked separately in contribution-strategy.md. Matching library is the natural first contribution; architecture-advisory conversation about Twenty Apps framework is the second.

VIII.Reference & environments

Environments

EnvironmentURLPurposeStatus
Website (prod)cloudbase.foundationLive public siteLive
Website (preview)*.pages.devPR preview deploymentsAuto
CRM (dev)http://<192.168.60.162> (local Unraid VM, LAN)Development & testingRunning
CRM (prod)crm.cloudbase.foundationLive donor dataPhase 2
ListMonk (dev)http://<192.168.60.162>:9000 (local Unraid VM, LAN)Email broadcastsPhase 2
Intake service (dev)internal — cbf-intake-service:4000Webhook + matching + stagingPhase 3
Kanban (dev)kan.cbf-dev (local Unraid VM, LAN)Internal kanban — org work trackingRunning
Kanban (prod)kan.cloudbase.foundationInternal kanban (org-wide)Planned (with CRM prod)

Related strategy documents

Technical debt & maintenance

ItemPriorityNotes
npm audit vulnerabilitiesMedium19 vulnerabilities in CBF-site as of May 2026
Tina / React 19 peer conflictLowTina targets React 18; monitor upstream
wrangler.json vs wrangler.tomlLowOpenNext migration created .json; Cloudflare prefers .toml
Build cacheLowNone configured; slower builds