A one-screen CRM: lead intake, filters and n8n delivery
How to build a simple single-user CRM: SQLite, intake API, dedupe, AI-powered Telegram sync every 20 minutes, smart routing by 15 parameters, tasks from a plan and an n8n outbox.
Featured
The CRM syncs Telegram through AI every 20 minutes: AI reads replies, tells a client from a bot, updates the CRM and routes leads by 15 parameters. Tasks come from a plan: first touch → follow-up → reply review → retry.
Contents
The problem: leads in three places
Before this CRM, leads lived in three disconnected places: the portfolio form wrote to JSONL and Telegram, AI OPS stored demo requests in Supabase, and Telegram leads sat in CSV files. Every source had its own format, but there was no shared answer to “what happens to this lead next?”
The goal was not a clone of a large CRM. It was one working screen: a lead arrives, gets a stage, a next action, and goes to n8n. No roles, departments or forty settings tabs.
What went into the MVP
- one user and one login;
- intake from the portfolio and AI OPS through a shared API;
- manual entry and Telegram lead import;
- a lead card with stage, temperature, score, budget, tags, notes and tasks;
- filters by stage, source, temperature, tags, budget, date and next action;
- n8n delivery with a journal and retries;
- SQLite, systemd, Caddy and a daily backup.
One intake instead of three integrations
Every source sends the same lead.v1 shape:
{
"schema": "lead.v1",
"external_id": "portfolio:8f3c...",
"source": "portfolio",
"name": "Ivan",
"email": "ivan@example.com",
"message": "I need a CRM for the sales team",
"consent": { "granted": true, "at": "2026-11-09T10:00:00Z" }
}
The API accepts a Bearer key and an Idempotency-Key. Repeating the same request does not create a second lead. If the email, Telegram or phone already exists, the CRM updates the existing card and records a “repeated inquiry” event.
This matters more than it sounds. Forms retry requests, people submit twice, and the owner should not clean up duplicates by hand.
SQLite and an outbox instead of infrastructure
For one user and dozens of leads a day, SQLite is enough. WAL, indexes and busy_timeout cover reads, filters and background writes. The database is one file, and a backup is a file copy, not another Postgres server.
n8n delivery uses an outbox:
- the lead is created in SQLite;
- a
deliveriesrow is added withqueued; - a worker sends the payload to the webhook;
- on 2xx it becomes
sent, on 5xx it retries with backoff; - after eight attempts it becomes
failedwith a “Retry” button.
A direct fetch from the form looks simpler, but if n8n is down the lead has no delivery. The queue survives a restart and does not block the user response.
Filters that actually matter
CRM filters are not decoration. They answer daily questions: what is new, what is hot, who has not been contacted, what is ready to send, where n8n failed.
The current version has:
- stage, source and temperature;
- tags, budget and score;
- creation date and last activity;
- next action: overdue, today, this week, not set;
- n8n delivery status;
- archive and spam;
- saved views and CSV export of the current filter.
Filter state lives in the URL, so a view can be saved, sent to yourself in Telegram or opened on a phone.
Contact dedupe
Dedupe keys, in priority order:
source + external_id;- normalized email;
- Telegram without
@; - E.164 phone;
- fuzzy name + company — flag only, no automatic merge.
Normalized fields are stored separately. This finds the same person when one form says @Username and another contains https://t.me/username.
The contract with n8n
n8n receives a stable contract, not an improvised flat JSON:
{
"schema": "lead.v1",
"delivery_id": "dlv_...",
"event": "lead.created",
"lead": {
"id": "ld_...",
"name": "Ivan",
"source": "portfolio",
"temperature": "hot",
"score": 70
},
"consent": { "granted": true },
"meta": { "attempt": 1 }
}
The request is signed with HMAC-SHA256:
X-CRM-Signature: sha256=<hmac(secret, timestamp + "." + body)>
n8n verifies the signature in the first node. After that, the workflow decides where the lead goes: Telegram, a sheet, a client CRM or email.
Telegram lead import
The scenario covers 120+ direct contacts from a Telegram export. The import is idempotent: external_id = telegram-tools:<id>, dedupe by @username.
The card keeps the group, category, offer, score, post link and post date. The original date stays in metadata, while created_at is set at import time so the dashboard reflects that the leads were actually added.
Smart sync: a lead never lives in two places
Every 20 minutes an AI layer reads new messages and updates the CRM: the first message, replies, stage, temperature, tags, last activity and delivery errors. The model tells a live client from a bot or aggregator, while a deterministic sync moves the data without duplicates. No manual copy-paste of chats or statuses.
The sync is idempotent. Re-running it updates the same card instead of creating a duplicate: dedupe works by Telegram, email and phone. If a reply arrives at 16:32, it appears in the CRM on the next tick, and the “Review reply” task closes once the message is marked as read.

Routing: 15 parameters
A lead does not just fall into a common list. It is routed into queues and priorities by:
- source and group where the request came from;
- stage: new, contacted, qualified, proposal, negotiation;
- temperature: hot, warm, cold;
- score 0–100;
- budget and currency;
- language and timezone;
- contact channel: Telegram, email, phone;
- last activity;
- lead age;
- SLA without a reply;
- whether a reply exists;
- follow-up date;
- tags: tier, day, sent, replied, flood, bot-reply;
- n8n delivery status;
- whether an open task and next action exist.
In practice this gives clear slices: “hot without a reply”, “overdue follow-up”, “replied but not reviewed”, “flood — retry after the pause”. The same lead no longer gets lost between a chat, a spreadsheet and notes.
Tasks: planning first, then action
Automatic tasks come from a plan, not from chaos:
- Plan by dates. First touch by the date and time from the plan, follow-up in three working days, retry after a block after the pause.
- Task queue. The plan produces concrete tasks: first message, follow-up, reply review, retry after PEER_FLOOD.
- Next action. A lead always has one nearest step, not ten equal ones.
- Auto-close. The reply is reviewed or sent — the task closes and the next action is recalculated.
This way tasks move the lead forward instead of creating a false sense of work.
A workspace, not a table
After the first weeks it became clear that storing leads is not enough. The list has to be the workspace.
What was added:
- 21 columns — from company and Telegram to tags and last contact; the selection is stored in the URL.
- Inline editing — stage, temperature, score, budget and next-action date change directly in the row.
- Saved views — filters + sorting + columns under a name.
- Advanced filters — score, contact channel, last activity and lead age.
- SLA badge — “N days without a reply” on new leads.
- Global search — ⌘K / Alt+K across name, company, email, Telegram and message text.
- Bulk actions and selected export — stage, temperature, tag, “Contacted”, archive, n8n.
- Pinning and j/k — important leads stay on top; row navigation from the keyboard.
- Tasks and next steps — a separate screen; completing a task updates the next action.
- State preservation — returning from a lead card opens the same filtered list.
These are not “ten more buttons”, but workflows that remove card-hopping and manual routine.
What came out of it
The scenario covers 120+ Telegram contacts and 30 active conversations. 10+ features, one user, one database, one screen.
What proved important:
- not building roles and teams that do not exist;
- not dragging in Postgres for one user;
- storing normalized contacts instead of trusting “clean” data;
- separating lead intake from n8n delivery;
- making filters links, not state in your head.
What is next
The next steps are not “more fields”, but less manual work: an in-app CSV import, manual duplicate merge, automatic delivery rules and source reporting. The current version already closes the main pain: a lead no longer gets lost between the form, Telegram and n8n.
The full case with architecture and details: CRM for leads.
Related
DOC OPS: turning an invoice into a verified table
A case study: OCR, JSON-schema extraction, deterministic validation, human-in-the-loop and a benchmark on 44 synthetic documents.
ReadTraffic Arbitrage in 2026: More Expensive, More Honest, More Engineering
The state of traffic arbitrage in November 2026: rising CPMs, cookie limits, AI creatives, Telegram Ads, first-party data and unit economics. What to do next.
ReadNeed similar automation?
Message me on Telegram — let's discuss the task and the format.