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

- 01Interface
- Next.js 16 / React 19
- TypeScript + Tailwind
- 02Edge
- Cloudflare Tunnel
- 03Application
- Python 3.12 (asyncio)
- FastAPI
- SQLAlchemy 2.0 async
- arq job queue
- 04Data
- asyncpg
- PostgreSQL 16 (pgvector, pg_trgm…
- Redis 7
- 05Intelligence
- LiteLLM gateway
- Anthropic Claude + Ollama/Qwen
- 06Operations
- Docker Compose
- Prometheus/Grafana/Loki/Alertman…
Chrysalis holds live data, so this shows the verified technology stack by layer rather than the interior of the app. Layers, not connections — which service calls which is not something a dependency list can prove. Hosts, ports and topology are deliberately absent.
Problem
A small real-estate team running on eXp Realty paid recurring per-seat SaaS fees to two vendors: Follow Up Boss for CRM, and Sisu for transaction and pipeline tracking. Their data split across both platforms, and neither one 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. 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, not 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. 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. It also needed AI features whose marginal cost the team controlled, not 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 on desktop and mobile, plus 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. It uses a local model for routine work and only escalates 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 instead of running the math itself. 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, instead of 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, so the user is never left 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. The app sets Postgres GUCs (app.team_id, app.user_id) per request instead of 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 runs behind a Cloudflare Tunnel with no open ports: Postgres, Redis, arq worker, LiteLLM gateway, Next.js frontend, plus a full Prometheus/Grafana/Loki/Alertmanager observability stack. The AI layer routes through a LiteLLM gateway that can serve requests from a local Ollama/Qwen model or escalate to Anthropic Claude. A Redis-backed 'billing breaker' gates that escalation: it 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. That separation is deliberate — it 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, where contact notes and message bodies rendered as raw HTML. It was 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, not a component-library dashboard. Every view — contacts, deals, pipeline, recruiting, AI console — lives in one HTML file with a live-data bridge script. The approach prioritizes fast iteration and a boot-performance budget (preloader plus lazy-loaded views) over conventional frontend architecture. A distinct visual design pass, 'Chrysalis 2250,' unified 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 — pair deliberately with plain, friendly copy. The reasoning: 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 keeps the same colors, type, button shapes, and glass treatment on every screen. A copy contract requires every word to make sense to a realtor with no tech background. Copy wins 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 covers the design system in phases across desktop and iPhone viewports. 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, instead of existing in the SPA's static HTML. That 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, not an afterthought. Instead of routing every AI feature to a cloud model and hoping usage stays reasonable, the platform defaults to a local model. It 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, instead of paying a vendor's per-seat or per-query tax.
Process and what failed
The project ran through a sequence of adversarial audits, not 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, not 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 instead of 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. Those were 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. The owner's own real-estate team runs 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. 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. It explicitly distinguished confirmed bugs from features that were honestly not yet built, a distinction the audit treated as load-bearing, not 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.
Related work
- Internal2026Evolve ParcelOSA parcel-first real-estate intelligence platform that replaced a paid parcel-lookup app with a self-built system covering every parcel in Florida for $0 in vendor fees.
- Parked2026OmniOSA self-hosted, AI-native social operating system built around a six-gate autonomy pipeline that decides whether an AI-drafted reply or post is safe to publish without a human.
- Internal2026A.I. PortalAn internal AI command center that gives a small real-estate team shared, permissioned access to curated Claude agents, multi-agent pipelines, and direct media generation…