Skip to content
Platforms & CRMInternal2026

Chrysalis

A self-hosted CRM and transaction-management platform for real-estate teams, built to replace Follow Up Boss and Sisu on team-owned hardware.

10
active tenant subdomains
1,380
passing pytest tests (as of 2026-07-25)
173
Alembic migrations
14
Docker Compose services
ArchitectureGated system — no screenshot
Interface
  • Next.js 16 / React 19
  • TypeScript + Tailwind
Edge
  • Cloudflare Tunnel
Application
  • Python 3.12 (asyncio)
  • FastAPI
  • SQLAlchemy 2.0 async
  • arq job queue
Data
  • PostgreSQL 16 (pgvector, pg_trgm…
  • Redis 7
Intelligence
  • LiteLLM gateway
  • Anthropic Claude + Ollama/Qwen
Operations
  • Docker Compose
  • Prometheus/Grafana/Loki/Alertman…
Supporting
  • asyncpg

Chrysalis holds live data, so this shows the verified technology stack by layer rather than a screenshot. Hosts, ports and topology are deliberately absent.

Problem

A small real-estate team running on eXp Realty was paying recurring per-seat SaaS fees to Follow Up Boss for CRM and Sisu for transaction/pipeline tracking, with data split across two vendor platforms neither of which modeled eXp-specific structures like capping cycles or FLQA recruiting counts. There was no single system of record for contacts, deals, commissions, and AI-assisted follow-up, and every query to either vendor's AI features carried a per-call cost with no team-level budget control. Both vendors also priced by seat, so cost scaled with headcount rather than usage — adding an agent to the team meant adding a line item to two separate subscriptions, regardless of whether that agent generated any AI queries or touched the transaction pipeline at all. Neither platform's data model had a concept of eXp's recruiting org tree, so front-line counts, capping-cycle status, and FLQA numbers had to be tracked by hand outside both systems. The team needed one system of record instead of two vendor silos, and it needed AI features whose marginal cost the team controlled rather than one set by either vendor's per-query pricing.

What was built

Chrysalis is a unified CRM and transaction-management workspace that runs entirely on team-owned hardware (a Mac Mini) instead of a SaaS vendor's servers. It handles contacts, deals, listings, conversations, commission/GCI tracking, and an eXp-specific recruiting pipeline in one place. Agents work through a browser-based interface (desktop and mobile) and an installable phone app; clients get their own portal to track their transaction. An AI assistant drafts messages, summarizes calls and voicemails, and answers questions grounded in the team's own data, using a local model for routine work and only escalating to a paid cloud model when needed, capped by a daily dollar budget per team. Ten teams (the owner's own brokerage plus nine pilot teams) run on isolated subdomains of the same deployment.

The recruiting side models eXp's org tree as CRM-derived facts — front-line counts, FLQA numbers, and org volume — while deliberately not computing revenue-share dollars: eXp's August 2026 policies-and-procedures update restricts rev-share math to the official eXp calculator, so Chrysalis reports the underlying facts and links out rather than running the math itself, and a test tripwire in the recruiting-analytics test suite fails CI if rev-share-shaped keys or a capping ladder ever reappear in that module's output. An Admin Ops area groups GDPR data-erasure requests, review-funnel settings, referral-partner tracking, and agent offboarding into one place rather than scattering them across separate settings screens. The mobile AI surface renders tool calls as cards, answers the assistant's own clarifying questions inline, and surfaces citations as badges rather than leaving the user to trust an unsourced answer.

Technical approach

The backend is a FastAPI/Python 3.12 async service on Postgres 16, with SQLAlchemy 2.0 async and asyncpg. The core architectural decision is that tenant isolation is enforced by the database, not application code: every tenant table has FORCE ROW LEVEL SECURITY, and the app sets Postgres GUCs (app.team_id, app.user_id) per request rather than filtering with WHERE team_id = ... anywhere in router code. The API refuses to boot if its DB role has superuser privilege, closing off a whole class of RLS-bypass misconfiguration. A 14-service Docker Compose stack (Postgres, Redis, arq worker, LiteLLM gateway, Next.js frontend, plus a full Prometheus/Grafana/Loki/Alertmanager observability stack) runs behind a Cloudflare Tunnel with no open ports. The AI layer routes through a LiteLLM gateway that can serve requests from a local Ollama/Qwen model or escalate to Anthropic Claude, gated by a Redis-backed 'billing breaker' that trips on a credit error and self-heals via an hourly probe job. Commission math lives in a pure-function module (finance.py) with no DB or I/O dependencies, taking plan parameters and returning computed splits — a deliberate separation that keeps the commission engine testable and auditable apart from the routers that assemble its inputs. The primary agent-facing UI is a single-file ~29k-line HTML/JS SPA (not a component framework), fetching from the API via a bridge script; a parallel Next.js /app/* surface is the declared long-term replacement and is being migrated to feature parity. A recurring defect class was stored-XSS in that SPA (contact notes and message bodies rendered as raw HTML), closed with a sanitizer function and a custom lint ratchet that scans all SPA surfaces for unescaped interpolation on every commit.

Creative approach

Craft

The interface is deliberately a dense, single-file operational SPA rather than a component-library dashboard — every view (contacts, deals, pipeline, recruiting, AI console) lives in one HTML file with a live-data bridge script, prioritizing fast iteration and a boot-performance budget (preloader plus lazy-loaded views) over conventional frontend architecture. A distinct visual design pass ('Chrysalis 2250') was applied to unify the look across the desktop SPA, mobile SPA, agent PWA, and client portal so the four surfaces read as one product despite being separate HTML entry points.

The 2250 design language follows a stated philosophy of 'warm futurism' — aggressive visual elements (a Three.js particle field of 4,500 particles morphing between six formations, glassmorphic panels with heavy blur and saturation, HUD-style corner brackets and a blinking status dot) paired deliberately with plain, friendly copy, on the reasoning that an agent closing a deal from a phone at 9pm should never feel like the software is intimidating them. Two explicit contracts govern every UI decision: a visual contract (same colors, type, button shapes, and glass treatment on every screen) and a copy contract (every word has to make sense to a realtor with no tech background), with copy winning whenever the two are in tension. Design tokens for color, the DM Sans/JetBrains Mono type pairing, spacing, and glass treatment live as CSS custom properties in one file, with a static reference mockup as the source of truth that every surface is checked against. A dedicated Playwright suite (desktop and iPhone viewports) covers the design system in phases, and a real defect class came directly out of the visual rework: the Automations hub is injected into the DOM at runtime by the bridge script 700ms after boot rather than existing in the SPA's static HTML, which caused e2e flake until tests were rewritten to wait on the injected node instead of a fixed timeout.

Reframe

The non-obvious insight is treating AI cost as a first-class architectural constraint rather than an afterthought: instead of routing every AI feature to a cloud model and hoping usage stays reasonable, the platform defaults to a local model and only escalates to paid inference for approved high-value use cases, under a hard per-team daily dollar cap enforced server-side. Combined with self-hosting on team hardware instead of a SaaS subscription, the product's entire cost structure is inverted relative to Follow Up Boss or Sisu — the team owns the infrastructure and controls the marginal cost of every AI call, rather than paying a vendor's per-seat or per-query tax.

Process and what failed

The project ran through a sequence of adversarial audits rather than a single 'ship it' pass — one session ran a 76-agent audit across the whole platform, surfacing 99 candidate defects that were then verified down to 53 confirmed bugs (versus honest not-yet-built stubs, which were explicitly not counted as bugs). A stored-XSS vulnerability class was found and fixed with a sanitizer plus a permanent lint check rather than a one-off patch. A full-disk incident once corrupted Docker's container storage mid-deploy and required a multi-step recovery runbook; live production stayed up throughout because the deploy script gates the container swap on a post-deploy health check rather than swapping first and checking after. A recurring theme across the handoff logs is distrusting 'looks fixed': several commits are explicitly about controls that reported success while doing nothing (fabricated CSV exports, fake copy-to-clipboard, phantom event syncing), replaced with real implementations or honest stubs after adversarial verification caught the gap between claimed and actual behavior.

Outcome

Chrysalis is live in closed beta at a single production deployment, running the owner's own real-estate team as the sole fully active tenant, with nine additional pilot teams provisioned on isolated subdomains under the same instance (beta onboarding paused pending go-live comms). It has replaced Follow Up Boss and Sisu for the active tenant's day-to-day CRM and transaction tracking.

Engineering for the pilot-team rollout is complete; what remains is a comms step, not a build step. Every deploy goes through a scripted gate before it counts as shipped — the deploy script health-checks the new containers before swapping them into production, and a July 2026 session closed out 51 bug-hunt findings and verified the fixes with 29 passing network-gate checks and 15 passing mobile-parity checks against the deployed build, not just against a local branch. The stored-XSS defect class found in the SPA's rendering layer was swept to zero across all eight live surfaces (desktop, mobile, PWA, portal) and is now enforced by a lint ratchet that runs on every commit, so the same class of bug cannot silently reappear. A 76-agent adversarial audit of the full platform, run separately from feature work, surfaced 99 candidate defects and verified 53 as real (2 P0, 24 P1, 16 P2, roughly 11 P3), explicitly distinguishing confirmed bugs from features that were honestly not yet built — a distinction the audit treated as load-bearing rather than cosmetic. A disk-full incident once corrupted Docker's container storage mid-deploy; because the deploy script gates the swap on a post-deploy health check, production stayed up on the previous build throughout the incident and recovery, with zero user-facing downtime.