Architecture
Stack
| Concern | Choice |
|---|---|
| Web framework | FastAPI on Uvicorn, one process |
| Rendering | Server-side HTML built in Python. No template engine, no bundler. |
| Database | SQLite, one file, accessed through the standard library. No ORM. Schema and migrations are hand-written in matilda/db.py. |
| Agent | Strands Agents SDK with the OpenAI-compatible client pointed at Kimi (Moonshot AI) |
| Auth | App-owned email and password, PBKDF2-HMAC-SHA256 at 200,000 rounds, HMAC-signed session cookie |
| Scheduler | APScheduler background job on a 15-minute interval for reminders |
| WeasyPrint, which needs Pango | |
| Packaging | Docker image on python:3.14-slim, published to GitHub Container Registry |
| Design | Liquid Amethyst, all tokens and components inlined from matilda/ui.py |
Modules
matilda/
├── server.py # FastAPI app: routers, /health, /webhook
├── agent.py · prompts.py # Strands agent, turn pipeline, system prompt
├── ui.py # Design system: tokens, glass, orb, sidebar shell
├── dashboard.py # Overview, leads, tasks, setup, prompt, org, devices, PMS
├── dashboard_auth.py # Email + password login, sessions, role guard
├── customer_auth.py # Web-chat phone + email OTP
├── worker_api.py · worker_auth.py # Worker link/ingest and per-device keys
├── followupboss.py # Follow Up Boss client and capture-time sync
├── netguard.py # Optional CIDR allowlist under the dashboard
├── db.py · analytics.py # SQLite layer and read-only dashboard queries
├── qualification.py # Milestones, scoring, promotion, handoff trigger
├── delivery.py · memory.py # Reply and reaction delivery; context and extraction
├── lead_page.py · mailer.py# Public lead sheet + PDF; Resend for codes and handoff
├── landing.py · chat.py # Landing page; web chat UI and API
├── roles.py # The single role policy table
├── scheduler.py # Reminder job
├── pms.py # Curated fixtures for property-management surfaces
├── seed.py # Sample lead generator
└── tools/ # Agent tools: send/react, contacts, reminders, leads
Request flow for an inbound text
- BlueBubbles posts a
new-messageevent to/webhook?token=.... - The server checks the token, de-duplicates on the message identifier, and hands off to the agent.
- The agent loads the contact, builds the known contact block, and runs one turn with the Kimi reply model.
- The model calls tools: at most one
send_imessageand optionally onereact_to_message. Replies go back through BlueBubbles. - After the turn, the extraction model reads the exchange and updates the contact's profile and memory.
- Qualification is re-scored. If the lead just crossed into
qualified, the handoff fires.
Web chat follows the same path with /api/chat as the entry point and the reply returned in the response body instead of sent through BlueBubbles.
Worker ingest flow
- The Matilda Worker on a Mac posts tagged threads to
/api/worker/ingestwith a bearer key. - Messages are stored with source
personal_imessage, de-duplicated by GUID. - Extraction runs. Lead events are recorded for each captured field.
- The person is created or enriched in the Mac's CRM. With
"suggestions": true(current workers), notes, tasks, appointments, the stage move, and fitting drip campaigns become suggestions (matilda/suggestions.py) that reach the CRM only when the operator applies one. Without it, as older workers send, notes, tasks, appointments, and the stage are pushed. Nothing is auto-replied.
Configuration precedence
Settings are read through accessors that check the settings table first and fall back to the environment. Anything saved on the dashboard Setup page therefore wins over .env with no restart.
Storage layout
All state lives under MATILDA_DATA_DIR, by default ./data:
matilda.dbis the SQLite database.sessions/holds agent session state.
Back up the directory to back up Matilda.