Anpa

A CRM for course programs and coaching orgs — built by replacing one, then generalizing.

Role
Designer & sole developer
When
2025 — present
Stack
  • React
  • TypeScript
  • PostgreSQL
  • Drizzle
  • Zod
  • WorkOS

Anpa started as the engine underneath The Paradox Process and turned into a product. Replacing Keap meant building, from scratch, what a CRM-plus-marketing-automation platform actually does: contacts, segmentation, automated sequences, and the runtime that drives them. Once it worked for one organization, it was clear the engine wasn't specific to one client. In the codebase's own words, Anpa is "the CRM for course programs and coaching organizations" — and the work since has been deliberately evolving a single-tenant app toward that product.

The engine

The heart of the system is the walker — the runtime that executes automation recipes. A recipe is a sequence of steps an organization builds to move someone through a journey, and the walker advances each enrollment through it one step at a time. (The subject is usually a person, but it's polymorphic — a class session or a replay bundle can run through a recipe too.) The engine supports around fifteen step types: email and SMS sends, fixed and condition-gated waits, yes/no and A/B branching, CRM task and opportunity actions, course-access provisioning, internal notifications, and outbound webhooks.

It doesn't run on an in-process loop. A Vercel cron drains a Postgres-backed task queue every minute; each step run does its work and enqueues the next, so the walker is self-rescheduling, and delays or quiet-hours are just future-dated rows in the queue.

The design decision I care most about: the walker reads the live recipe, not a copy frozen when an enrollment began. If an organization edits a sequence, contacts already mid-flight feel the change — which is how Keap behaves and what these teams expect. The honest, more interesting version is that it's live reads with deliberate carve-outs:

Send-deduplication keys stay pinned to the version a contact entered on — a retry can't double-send.
Delays already scheduled fire at their original time instead of lurching forward when the recipe changes.
If a step is deleted out from under active enrollments, those enrollments complete before the edit applies.
Immutable snapshots of every recipe version exist — but for audit and rollback only. The walker never executes from them; a rollback writes a snapshot's steps back into the live tables.

Separating "what runs" (always live) from "what happened" (snapshotted history) is what keeps the engine both flexible and accountable.

Built to run without me

The constraint that shaped everything: the system has to be operable by a non-technical team after I'm gone. That drove real architecture. It's why automation recipes are data an admin edits rather than code only I can change. And it's why there's a page composer — an authoring tool where operators build public landing pages from a library of approved section types, with readiness gates before anything goes live and immutable snapshots of what was published. "Don't make me the single point of failure" turned out to be a more demanding spec than any single feature.

The composer is one workspace of eleven: Today, Contacts, Automations, Tasks, Schedule, Communications, Commerce, Analytics, Operations, Content, and Site — 42 admin screens over 198 internal APIs, with five operator roles and 38 distinct permissions deciding who sees what, behind single sign-on.

Anpa's composed pages list showing the site's pages with their publish states
The composed-pages list — every page on the public site, with its draft and publish state.
A gallery of 29 section presets, organized by the six jobs a page does: open, explain, prove, price, answer, close.
Autosave with a visible status, draft history with undo and redo, and a polite conflict warning when two tabs disagree.
Inline editing on the page itself — rich text constrained to a safe schema, so pasted chaos can't break a layout.
Anpa's Insert a section gallery — preset thumbnails organized under tabs for the six jobs: open, explain, prove, price, answer, close
The section gallery — presets organized by the six jobs a page does, with live thumbnails rendering real components.
The Anpa composer editing a page: section list, live page preview, and per-section motion controls in the right rail
The composer itself — section list, live preview, per-section motion controls. Captured in demo mode with sandbox data.

Color is handled the same way: an operator picks three brand colors, and the theme engine hands back four complete section tones that are guaranteed readable — any combination that fails gets nudged toward light or dark in 5% steps until it clears WCAG AA. There is no way to publish an inaccessible page, because the bad combinations are never offered. A color change ripples across the whole site.

Worksheet diagram: accent, deep, and paper colors flow through deriveTones() into four guaranteed-readable tones — paper, mist, ink, accent

And the editor itself is engineered like software people live in:

Autosave fires 1.5 seconds after you stop typing, with a six-second cap if you never stop.
Every save echoes the draft's last-known timestamp — if the server's copy is newer, you get a polite conflict warning instead of a silent overwrite.
Undo, redo, save, duplicate: ⌘Z, ⇧⌘Z, ⌘S, ⌘D — with Ctrl equivalents on Windows, and matching that survives CapsLock.
Rich text has hard ceilings — 2,000 characters per run, 60 blocks per page — and a strict link allowlist: javascript: bounces, and unknown pasted content is rescued as plain text instead of deleted.

Toward a product

Turning a single-client app into a product is usually pitched as "extract the reusable part into a service." I deliberately didn't, and the decision is written down: keep the one application and harden the boundaries first, keep the data single-tenant until the product boundary is clean, and introduce tenancy only after. Premature extraction would have frozen interfaces I didn't fully understand yet and bought distributed-systems problems I didn't have. Instead the seams get made explicit in place — shared contract modules, an organization/membership/identity model, Paradox seeded as "tenant zero" — so multi-tenancy can grow along those seams later instead of fighting them.

Identity is where that's furthest along. Authentication runs through WorkOS, backed by real identity tables (organizations, operator memberships, auth identities) and provider-agnostic SSO routes, so an organization can eventually bring its own sign-in. I scoped it honestly: it's wired and working but currently locked to a single org, with directory-sync deliberately deferred until a real second customer needs it. The product isn't multi-tenant yet — it's built so that becoming multi-tenant is an increment, not a rewrite.

Anpa is the part of my work that's about systems and judgment rather than screens: a runtime that interprets user-defined logic with the failure modes designed in, a deliberate and documented stance on when not to split a system apart, and an architecture shaped by who has to operate it. It's also a working example of the most common honest path to a product — solve one real problem completely, then notice you've built something general.