We open-sourced our CRM: built for small teams, where AI drafts but never writes
Daedalus CRM is the application layer of our three product lines. This post covers why it went from SaaS to open source, why none of the five AI features write to the database, what “less is more” actually looks like in the code, and how two commands get it running.
Update, 2026-09-11: this was written at v0.3. Since then v0.6 replaced the home screen with a command-line style agent conversation and moved the whole visual system to white with thin lines. The screenshots below are the v0.3 interface, kept as a record of the time. For the current interface see the product page or the repository README ↗. The trade-offs described here — AI drafts but never writes, the watchlist is not a black-box score, referral attribution is fixed at entry — all still hold.

The short version
The repository is at github.com/BeckY824/daedalus-crm, under AGPL-3.0. If you have Docker, it runs:
git clone https://github.com/BeckY824/daedalus-crm.git && cd daedalus-crm
docker compose up -d
Open http://localhost:3000 with username admin and password admin123. There is nothing to configure: the session key is generated on first run and kept in the data volume, tables are created automatically, and migrations run themselves on upgrade. Your data is a single SQLite file, on your own machine.
Who it's for
Sales teams of 1–20 people. We originally built it for education and admissions consultancies: customers live in WeChat, deals close on trial classes and referrals, and nobody on the team is going to write SQL. Hence the default vocabulary — student, school / year / major, trial attended, discussing with family.
But the data model underneath is a generic sales funnel: lead → customer → follow-up → deal → signed. So we pulled the wording out of the code and into settings — what a customer is called, what the three profile fields are called, what the dropdowns contain, how statuses display. An admin changes it once and the whole site follows. A design studio renames “student” to “client” and “school” to “company”, and it fits.
Why we moved from SaaS to open source
We spent a while building it the SaaS way: subscriptions, an enterprise tier, multi-tenancy. We stopped for a simple reason — what small teams care about most is who holds their data, and whether they'll have to renew next year. For a five-person agency the customer list is the entire business; putting it on someone else's cloud never sits right. And the cost of deciding whether to renew, every year, is far higher for them than buying once.
Open source and self-hosting solve both at once: the data sits on your server, and the code can be read and changed. AGPL makes sure anyone who modifies it and offers it as a service publishes their changes. We make money on deployment, customisation and the workflow products above it — not by holding data hostage.
AI drafts, it never writes
There are five AI features in the system. They all obey one rule: whatever the AI produces is put in front of a person first, and saving goes through the normal flow after they've checked it. The AI has no path that writes to the database.
| Feature | Where | What it does |
|---|---|---|
| Quick notes | New follow-up dialog | Dictate or paste a WeChat conversation; AI pre-fills the type, key points, next step and tasks |
| Pre-call brief | Customer detail | One click before you call: the story so far, where it's stuck, what to raise, and the risks |
| Ask the data | Analytics | Ask business questions in plain language. AI only translates the question into a restricted query spec — it never writes SQL |
| Watchlist | Dashboard | Forgotten customers, stalled deals, overdue plans. Detection is pure rules; AI only writes the wording when you click “draft” |
| Referral radar | Channels | Who's bringing you people and who to ask next. Ranking is pure rules; AI only writes the invitation |
Why so conservative? Because model output is untrusted input. It will misspell an enum, invent a contact id that doesn't exist, or turn “next Wednesday evening” into an invalid timestamp. So we wrote a sanitising layer: coerce to a value the system recognises where possible, and where that fails leave the field blank for a person to complete — anything rather than let dirty values into the database and poison the statistics. “Ask the data” is stricter still: the model can only emit a restricted query spec (metric, grouping, time range) which the code then executes. Ask it “how many deals will we close tomorrow” and the system answers “that's a forecast, not a query”, rather than querying a future date and reporting “0”.
Which model you use doesn't matter either. Enter an endpoint, key and model name in settings and any OpenAI-compatible API works: DeepSeek, OpenAI, a local Ollama, any proxy. The key is encrypted with the session key before storage, the UI only echoes the last four characters, and database backups contain no plaintext. Configure no key and the AI entry points disappear entirely — everything else works as normal.

What “less is more” looks like in the code
It's our product principle, and in this repository it comes down to a few concrete things:
- Quick notes is one text box and one button. No model picker, no temperature slider, no “advanced options”.
- The watchlist is not a black-box score. The rules live in a single pure function of about a hundred lines: a high-intent customer untouched for 18 days, weight 5; one who has attended a trial, weight 4. Salespeople don't trust a black-box number, but “this high-intent student hasn't been contacted in 18 days” argues for itself. When one customer matches several signals, only the strongest is kept — the enemy of a watchlist is noise.
- Referral attribution is fixed at entry. Referrer, channel (two steps up the referral chain) and channel owner are recorded independently. Changing someone upstream never retroactively rewrites an existing customer's numbers — because the moment results can be recalculated by editing upstream, nobody trusts the reports.
- Concurrency merges per field. Two people editing the same customer: different fields merge automatically, and only a genuine clash is blocked, with the colliding fields named. The most common path — B simply logged a follow-up on this customer — must never break A's save.
- What we don't build is written down too. Custom fields, custom status flows, switchable templates — that's a different order of magnitude, and the settings page says so plainly.
Engineering discipline
Open source means other people read your code and run it on machines you've never seen. A few rules this repository has always kept:
- Migrations only add — never alter, never drop. Every statement is idempotent and the container reruns all of them on each start; nothing records “how far we got”. An older image still runs against the newer database, so a rollback touches no data.
- Three automatic gates before commit. Type check, lint and unit tests run from a git hook and again on push; E2E runs separately in CI. A batch of the 234 unit tests call the real Server Actions directly — whether the concurrency gate holds depends on the check and the write being the same statement, and you can't test that by going around the action.
- No default secret ships with it. The session key is generated on first start and kept in the data volume. CI runs a secret scan across the entire history.
- The timezone is pinned in the container. CI taught us this after we open-sourced: GitHub's machines run UTC, the server-rendered date disagreed with the browser, and React hydration complained immediately. You will never reproduce it locally.
One thing we did before open-sourcing: rebuilt the git history from scratch. Old commits contained server addresses and real employee names, and redaction doesn't mean “fix the latest version” — it means making sure they were never in the repository at all.
What's next
The roadmap is in the repository. Near term: PostgreSQL support and finer-grained permissions. Not planned: a mobile app and multi-tenancy. Issues welcome — especially the “I'm not in education and this term won't rename” kind, because that's exactly what we want to find out.
GitHub ↗ ·
Deployment docs ↗ ·
product page
Want us to deploy or customise it for you? Book a demo →