Skip to content
CRMNovember 9, 20268 min read

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.

ShareTelegram
Back to blog
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:

  1. the lead is created in SQLite;
  2. a deliveries row is added with queued;
  3. a worker sends the payload to the webhook;
  4. on 2xx it becomes sent, on 5xx it retries with backoff;
  5. after eight attempts it becomes failed with 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:

  1. source + external_id;
  2. normalized email;
  3. Telegram without @;
  4. E.164 phone;
  5. 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.

CRM: AI sync, leads and tasks on demo data

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:

  1. 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.
  2. Task queue. The plan produces concrete tasks: first message, follow-up, reply review, retry after PEER_FLOOD.
  3. Next action. A lead always has one nearest step, not ten equal ones.
  4. 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.

Валера · webdev07

Senior Systems Engineer. I build systems that work.

@webdevs07

Need similar automation?

Message me on Telegram — let's discuss the task and the format.

Discuss a project