Monarch Movement
A self-hosted community and onboarding platform that turns a real estate team's recruiting SOP into software members can't skip a step of.
- 808
- passing tests (2 skipped) across 95 files
- 56
- applied Drizzle migrations
- 7 modules / 27 lessons / 21 PDFs
- classroom content shipped
- 3
- public hosts on one Cloudflare Tunnel

Problem
A real estate team's downline onboarding ran as a manual SOP - three meetings, 30/60/90-day check-ins, a delegation roadmap - tracked ad hoc and hosted on Skool, a third-party community tool with no control over data, workflow enforcement, or cost at scale. Nothing stopped a sponsor from skipping a stage, and the group had no owned system to grow a ten-year, multi-thousand-member community on. The group the platform serves is an eXp Realty downline extending six referral levels deep from the core team, and the operator's stated horizon is 10,000 members over ten years — a scale at which a manual SOP tracked in spreadsheets and Skool posts was never going to hold up, since there was no mechanism forcing a sponsor five referral levels removed from the core team to actually run the same three meetings and three check-ins every new recruit above them ran. Skool also had no way to scope an AI tutor to a specific lesson's content, no way to cap what an AI feature cost per member, and no path to encrypting member PII at rest — control the group didn't have because the platform wasn't theirs.
What was built
Monarch Movement is a self-hosted member platform that encodes the onboarding SOP directly into the product: invitations move through a pipeline with meeting logs and RAG (red/amber/green) health check-ins at fixed intervals, and stage progression can't be skipped. Around that core sit a full community feed (posts, comments, reactions, polls, hashtags, mentions, DMs), a gated classroom with an AI tutor scoped to each lesson, a searchable member directory with a map, gamification (points, levels, streaks, badges), a calendar with ICS export, and an operator console with analytics, moderation, an audit log, and outbound webhooks. It replaced Skool as the group's member hub and serves three public hosts off one Cloudflare Tunnel into a Mac Mini at home.
The three-meeting/three-check-in cascade is server-enforced, not just UI-suggested: the API rejects logging M2 before M1 is marked complete, and a pipeline stage only advances on an actual check-in being recorded, never on the calendar date alone. A member who lands amber or red at the 30-day check-in automatically triggers a reactivation follow-up job to their assigned leader rather than waiting for someone to notice; separate scheduled jobs nudge a leader three days ahead of each check-in milestone, escalate to an admin if a check-in goes seven or more days overdue, and prompt a new member who hasn't opened their first lesson within five days or hasn't logged in for ten. Two accelerator programs — a cohort-based fast-track and a class-attendance tracker for the group's Global Alliance program — run alongside the main pipeline with their own operator-side enrollment and completion tracking. The platform carries six distinct AI surfaces beyond the per-lesson tutor: a personal daily coach, a community composer assist, a concierge, a roleplay practice-conversation tool, a librarian, and a content studio, each routed to one of three Claude model tiers by task cost rather than defaulting every call to the most expensive model. Every automated message, check-in prompt, and AI response is constrained by an explicit voice guard that blocks retention/production-metric/operator-facing language in favor of growth-and-community framing — enforced as a review blocker on any AI-generated or operator-facing copy, not a style suggestion.
Technical approach
Next.js 16 App Router on React 19 with TypeScript strict throughout; Drizzle ORM against Postgres 16 with hand-reviewed migrations only (56 applied). Auth.js v5 handles credentials (argon2id, zxcvbn strength scoring, lockout after 5 failures) plus Google OAuth as secondary, with mandatory TOTP 2FA for operators and per-device session revocation. Background work runs on pg-boss - nine Postgres-backed queues (digest, reminders, backup, retention, onboarding follow-up/behavior/reactivation, wins roundup) - deliberately avoiding a Redis dependency. Realtime notifications use Postgres NOTIFY/LISTEN rather than a separate pub/sub service; browser push follows RFC 8030/8291 with VAPID keys. PII is encrypted at rest with AES-256-GCM, AAD-bound, plus HMAC email hashing for lookups without decrypting; the whole thing backs up encrypted nightly to Backblaze B2 with a 3-pass restore drill already validated. A single edge middleware (proxy.ts) does host-based routing across the three public subdomains through one cloudflared tunnel. A prod incident forced a WAL-surgery recovery (pg_resetwal) after the DB container's restart policy wasn't hardened yet - fixed by making restart:unless-stopped explicit in docker-compose.yml immediately after. Outbound webhooks carry HMAC-signed, timestamp-bound payloads and an SSRF url-guard that blocks loopback/private targets by design - which also means it's the one flagship feature never smoke-tested live in prod, since testing it properly requires an external catcher. Password security stacks three independent checks rather than one: argon2id hashing at OWASP-2024 parameters, a zxcvbn strength score gate that blocks anything under a minimum score at registration, and a HAVE-I-BEEN-PWNED k-anonymity check that hashes the password with SHA-1 and sends only the first five hex characters to the public breach-check API, so no plaintext or full hash ever leaves the server. A dual rate-limiting layer backs sign-in: a per-email failure counter locks an account for 15 minutes after five consecutive failures, and a separate per-IP Postgres token bucket (an atomic INSERT … ON CONFLICT DO UPDATE, no Redis) caps sign-in attempts independently of which account is being tried. A startup preflight script hard-fails the app if required secrets like the auth signing key or the PII encryption key are missing, rather than booting in a silently-degraded state. The AI layer's model-tier registry exists because the prior codebase this replaced had hardcoded the most expensive Claude model everywhere by default; profiling the actual tasks (lesson Q&A, one-line coaching nudges, composer suggestions) found none of them were deep-reasoning problems, so the registry collapses model selection to a single-file, one-line change if a model is ever superseded, and routes routine high-volume calls to the cheapest tier by default.
Creative approach
Craft
The design system is deliberately minimal and dark: obsidian backgrounds, gold-champagne CTAs, parchment body text, with RAG status colors (forest/amber/rust) reused consistently from onboarding check-ins to operator dashboards. lib/tokens.ts is the single source of truth for every color, spacing, and typography value - a PR introducing a raw hex value outside it fails review. Fonts are self-hosted as variable woff2 files specifically to avoid the Google Fonts DNS leak and its GDPR exposure, and to remove a network round trip from the critical path. The typeface pairing is Fraunces for display and headings (using its optical-size axis to tighten letter-spacing at large sizes) against Inter for body and UI text, with JetBrains Mono reserved for code — a deliberate serif/sans split that reads as considered rather than default-Tailwind.
Motion is CSS-only by rule, with no JavaScript animation library in the stack: interactive state changes run a 150ms ease-out transition, route changes fade over 200ms, and every transition collapses to none under prefers-reduced-motion. Cards never carry drop shadows on the near-black background — shadows are imperceptible at that luminance, so elevation is communicated by border contrast instead, a rule that came directly from testing shadows on the actual obsidian palette rather than assuming a design-system default would translate. The brand mark is a stylized butterfly silhouette rendered as the app icon, favicon, and loading state, paired with a damask background pattern derived from the same motif and rendered as a zero-raster SVG rather than an image asset. Accessibility is enforced as a checklist against the token system itself, not left to component-level judgment: every text/background pairing is tested to WCAG AA contrast ratios, minimum touch targets are 44×44px, focus rings are a 2px gold-champagne outline with a 2px offset visible against every brand surface, and a gated /dev/brand route renders every token, type specimen, and card variant as the standing acceptance check for any visual change before it ships.
Reframe
The core insight is that the SOP is the software, not a feature bolted onto a generic community app: the pipeline enforces stage order so a sponsor literally cannot skip a check-in, which is the actual behavior change the group needed from a downline of agents with wildly inconsistent follow-through. Everything else - community feed, classroom, gamification - exists to keep members inside a system that's already doing the enforcement work a manual process couldn't.
That reframe only matters at the scale the group is actually building for. A three-meeting cascade tracked by hand is survivable when a core team is running it directly on a handful of new agents; it breaks down at six referral levels of remove, where the person responsible for a new agent's onboarding may themselves be several levels removed from the core team and have no direct relationship holding them accountable to the standard. Software that structurally cannot advance a pipeline stage without a completed prerequisite closes that gap without requiring the core team to personally audit every sponsor's behavior — the enforcement travels with the platform rather than depending on who happens to be doing the onboarding. The gamification and points system was built on the same reframe in miniature: points are earned only from genuine learning and community activity (posting, commenting, completing a lesson) and explicitly never from referral or production metrics, so the incentive layer reinforces the same growth-not-extraction framing the voice guard enforces in copy, rather than quietly becoming a leaderboard for recruiting volume.
Process and what failed
The build ran as a compressed multi-week sprint (May-August 2026) with heavy in-prod verification using disposable throwaway accounts that get swept after each smoke walk - a pattern used repeatedly to catch real bugs before real members hit them: 2FA recovery codes were never actually displayed after enrollment because a revalidatePath call unmounted the form carrying them (fixed, every pre-fix enrollee had to regenerate); a recurring-event RSVP guard compared against the series' original anchor date instead of the next projected occurrence, silently dropping RSVPs to the recurring 'Movement Monday' meeting for every member. A Resend suppression list quietly blackholed a claim email to the org's root sponsor after an earlier hard bounce, caught only by checking the suppression list before the real send. A credentials CSV was found sitting in Downloads mid-launch and had to be moved to Keychain and deleted.
Outcome
Live at monarchmovement.org since 2026-05-30, with onboarding.monarchmovement.org running the member platform and assets.monarchmovement.org serving lesson PDFs via signed R2 URLs. CI gates every push and PR on typecheck, lint, test, next build, and a CVE scan. The first real (non-seed) member was provisioned and walked through claim-to-dashboard in prod on 2026-08-04; the org's ten eXp accounts are staged for the wider rollout.
The platform shipped Phase 0 (infrastructure scaffold) and Phase 1 (full feature build) together at launch rather than as separate milestones, with all seven classroom modules, 27 lessons, and 21 supporting PDFs live from day one instead of a stub curriculum backfilled later. Production runs as launchd-managed processes on the operator's own Mac Mini rather than a container orchestrator, with a dedicated host-health monitor watching the app and worker processes and a separate warm-up keepalive job so the local AI-adjacent tooling doesn't take a cold-start hit on the first real request of the day. The nightly backup job runs four times daily rather than once, encrypted to Backblaze B2, with a 3-pass restore drill already validated before go-live — the operator's standing rule that a backup nobody has restored from isn't a backup. Two production defects were caught by the same in-prod, disposable-account verification pattern that shaped the rest of the build: a recovery-code display bug and a recurring-event RSVP bug that would otherwise have silently affected every member's ability to recover 2FA or RSVP to the standing weekly meeting, both closed before real members could hit them.