# Build a CRM with AI: contacts, notes and a pipeline you own

> Build a CRM with AI in NorthernGo: contacts, notes and pipeline stages you own. What the model gets wrong, GDPR delete-account, export and honest limits.

Source: https://northerngo.com/resources/build-a-crm-with-ai/
Language: en
Updated: 2026-08-16

---
**A CRM in NorthernGo is contacts, notes and pipeline stages stored with window.NorthernGoDB in a Supabase PostgreSQL database in the EU. Describe it in text, voice or a sketch; three agents write vanilla JavaScript and Tailwind you own. Require login, a GDPR delete-account button and an export. The model will invent duplicate handling and permissions — spell those out yourself.**

### A CRM is a list with memory, not a sales platform

Most people who type "build me a CRM" into an [AI app builder](/glossary/ai-app-builder/) picture Salesforce with the lights dimmed. What they actually need is smaller and more honest: a named list of people, a place to write what was said, and a handful of stages that say where each person sits. That is a [CRUD](/glossary/crud/) app with a sales vocabulary. It is also the shape NorthernGo is good at, because every generated app already has a Supabase PostgreSQL database in the EU, e-mail and password login, and a PWA shell.

You talk, type or sketch. Architect plans the screens, Builder writes one HTML document in vanilla JavaScript and Tailwind, and on Premium and Pro the Critic reviews it before you see it. The code is yours. ZIP export is on Premium and Pro. Nothing here is a hosted CRM product with seats, workflows or a marketplace of plugins.

If you need territories, forecasting or a permission matrix, you are shopping for a CRM product. This page is for a plumber, a consultant with eighty names, or a workshop that wants the whiteboard to survive a spilled coffee.

### What the model gets wrong before you have written a prompt

Two failures show up in almost every first generation, and they are not cosmetic.

**Duplicate contacts.** Ask for "a CRM" and you get a save button, not a lookup. Two saves of the same e-mail become two contacts; the board then lies. Write the rule into the prompt: match on e-mail, update the existing record, do not insert a second one.

**Permissions that do not exist.** Auth is e-mail and password. There are no roles unless you invent them. The model will draw an "Admin" badge; the badge is paint. `get('contacts')` returns the signed-in user's records. Two colleagues share a pipeline by sharing one login.

A third, quieter failure: the model will happily store notes, phone numbers and deal values without a way to leave. Any app with login must ship a working delete-account path. That is GDPR article 17, not a nice-to-have, and the platform will put the button back if a later edit removes it. Build it in the first version.

### Step 1: Describe the records, not the brand

Do not prompt "build a modern CRM with a beautiful dashboard". Prompt the data. A working first message looks like this:

> Build a CRM for a one-person trades business. Screens: login, contact list, contact detail, pipeline board. A contact has id, name, email, phone, company, stage, valueSek, lastTouchedAt. Stages are exactly lead, quoted, booked, done, lost. Notes are a separate collection keyed by contactId. Before saving a contact, get the contacts collection and if an email already exists, update that record instead of inserting. Login with e-mail and password. A visible Delete my account button that calls window.NorthernGoDB.deleteAccount() after a confirmation. A Download my data button that exports contacts and notes as CSV.

That paragraph does more work than a mood board. It names the collections, the stages, the duplicate rule, the auth and the two exits every personal-data app needs. Architect can plan from it. Builder has fewer opportunities to invent a seventh stage called "Nurture".

Cloud generation uses Gemini and counts against the monthly cap. Local WebGPU (Qwen 3.5 4B, or larger Pro models) or Ollama is unlimited on every plan. For a CRM, local is enough for the second pass.

### Step 2: Contacts and notes as two collections

Keep people and conversation apart. One fat contact object that grows a notes string on every call will fight you the first time you want to show a timeline or delete a single remark.

```javascript
async function saveContact(contact) {
  const all = (await window.NorthernGoDB.get('contacts')) || [];
  const email = String(contact.email || '').trim().toLowerCase();
  const existing = all.find(row => String(row.email || '').trim().toLowerCase() === email);

  const record = {
    id: existing ? existing.id : crypto.randomUUID(),
    name: contact.name,
    email,
    phone: contact.phone || '',
    company: contact.company || '',
    stage: contact.stage || 'lead',
    valueSek: Number(contact.valueSek) || 0,
    lastTouchedAt: new Date().toISOString()
  };

  await window.NorthernGoDB.save('contacts', record);
  return record;
}

async function addNote(contactId, text) {
  await window.NorthernGoDB.save('notes', {
    id: crypto.randomUUID(),
    contactId,
    text,
    createdAt: new Date().toISOString()
  });
}
```

`save()` upserts on the shape you send. There is no schema to migrate and no table to create. The [database API is save, get, delete and the auth calls](/resources/connect-supabase/) — that is the whole backend. Empty collections can resolve to something other than an array, so `|| []` is not decoration; a first load with no contacts otherwise paints a blank screen.

Notes belong to a contact by `contactId`, not by pasting the person's name into the note. Rename a contact and the timeline still attaches. Delete a contact and you have a loop that can delete matching notes. The model will skip that loop unless the prompt mentions it.

### Step 3: Pipeline stages as data, not as columns you painted

A board that cannot move a card is a poster. Stages are a field on the contact, and moving a card is an update of that field plus a timestamp.

```javascript
const STAGES = ['lead', 'quoted', 'booked', 'done', 'lost'];

async function moveToStage(contactId, stage) {
  if (!STAGES.includes(stage)) return;
  const all = (await window.NorthernGoDB.get('contacts')) || [];
  const contact = all.find(row => row.id === contactId);
  if (!contact) return;
  contact.stage = stage;
  contact.lastTouchedAt = new Date().toISOString();
  await window.NorthernGoDB.save('contacts', contact);
}
```

Two rules worth putting in the prompt and then checking in the code.

**The stage list is closed.** If the model is allowed to "add a stage", you will get In progress, Waiting, Follow-up and a board nobody can report on. Five names is plenty for a one-person pipeline. Changing the list later is a data problem: every existing contact with the old name has to move.

**Counts come from the records, not from a separate metrics collection.** Sum `valueSek` per stage in JavaScript when you render. A second collection of totals will drift the first time a save fails. The dashboard is a view of `get('contacts')`, not a place that stores its own truth.

This is also where duplicate contacts do real damage. Two rows for the same person, one in quoted and one in booked, make the board look healthier than the business. The e-mail match in step 2 is the fix. Do not ask the model for "smart merge" or fuzzy name matching on the first version. Exact e-mail is boring and it works. Fuzzy matching is how you glue two different customers together because they are both called Erik.

### Step 4: Login before anyone else can see the list

Without a session, every visitor writes to the same collections. That is acceptable for a private demo on your own laptop. It is not acceptable for a CRM. Names, phone numbers and notes about unpaid invoices are personal data the moment they identify someone.

```javascript
async function ensureSession() {
  if (window.NorthernGoDB.getToken()) return true;
  showLoginForm();
  return false;
}

async function handleLogin(email, password) {
  try {
    await window.NorthernGoDB.login(email, password);
    location.reload();
  } catch (err) {
    showMessage(err.message);
  }
}
```

Gate the list, the board and every save on `getToken()`. Login is e-mail and password only. Two colleagues cannot each register and see the same contacts — records are scoped to the signed-in user. Share one login, or accept a personal CRM. There is no invite API.

### Step 5: Delete-account and a file you can take with you

[GDPR-compliant app building](/resources/gdpr-compliant-app-building-eu/) is not a later phase. The delete path has to be visible, confirmed, and routed through the injected method:

```javascript
async function deleteMyAccount() {
  const ok = confirm('This permanently deletes your account and all stored data. Continue?');
  if (!ok) return;
  try {
    await window.NorthernGoDB.deleteAccount();
    showMessage('Your account and all your data have been deleted.');
    location.reload();
  } catch (err) {
    showMessage(err.message);
  }
}
```

Do not loop `delete()` over collections and call it a day. That leaves the auth record. Do not hide the button in a page the model forgot to link. Put it next to log out, style it as destructive, say that recovery is impossible.

Export is the sibling of deletion. `get()` already returns the signed-in user's rows. Turn them into a file in the browser:

```javascript
function toCsv(rows, columns) {
  const escape = value => '"' + String(value ?? '').replace(/"/g, '""') + '"';
  const header = columns.join(',');
  const body = rows.map(row => columns.map(col => escape(row[col])).join(',')).join('\n');
  return header + '\n' + body;
}

async function downloadMyData() {
  const contacts = (await window.NorthernGoDB.get('contacts')) || [];
  const notes = (await window.NorthernGoDB.get('notes')) || [];
  const blob = new Blob(
    [toCsv(contacts, ['id', 'name', 'email', 'phone', 'company', 'stage', 'valueSek', 'lastTouchedAt']), '\n', toCsv(notes, ['id', 'contactId', 'text', 'createdAt'])],
    { type: 'text/csv;charset=utf-8' }
  );
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'crm-export.csv';
  a.click();
}
```

A founder who can download the list on a Friday is a founder who can leave. The app is vanilla JS, the ZIP is on Premium and Pro, and the data is a CSV you already know how to open.

### Step 6: Send the first version back with a punch list

The first generation will be close and wrong. Reply with facts: e-mail must update not insert; stages are a closed list; delete-account must call `window.NorthernGoDB.deleteAccount()` after confirm; export contacts and notes. Burn those passes locally. If the HTML is right and the logic is not, export the ZIP on Premium or Pro and edit `index.html` yourself — there is no hidden framework.

### What this will not become

- **Not Salesforce or HubSpot.** No automation, no tracking, no reporting suite. `window.NorthernGo.sendEmail` sends a message you compose. It is not a sales platform.
- **No roles or record sharing.** One user, e-mail and password. Shared pipelines need a shared login.
- **No merge UI and no CSV import component.** Prevent inserts on matching e-mail. Seed data by hand, or write an import in the exported code.

### What it costs

Free is 0 SEK: five cloud generations a month and unlimited local. No ZIP. Premium is 179 SEK / $19: 25 cloud generations and ZIP export. Pro is 299 SEK / $29: 50 cloud generations, custom domains, Shopify, SEO audits, webhooks and Fortnox — none required for a CRM. Database, login and PWA come with every plan.

### Troubleshooting

**Every save creates a second Anna.** The duplicate check never ran, or it compared names, or it compared e-mail with different casing. Normalize with `trim().toLowerCase()` and save the same address twice.

**A colleague sees an empty board.** Expected. Collections are per signed-in user. Share one login.

**Delete my account is missing after an edit.** Ask again by name. Do not accept a custom `fetch` route. The pipeline showing extra stages means the model padded — keep a `STAGES` array the move function consults.

## Frequently asked questions

### Can I build a real CRM with an AI app builder?

You can build a contact list, notes and a few pipeline stages that persist in a database you control, with login and a delete-account path. That is a working CRM for one person or a shared login. You cannot generate Salesforce: no roles, no automation sequences, no team permissions. If that smaller shape is what you needed, the builder is enough.

### Will the AI stop duplicate contacts on its own?

No. A prompt that only says "CRM" produces a save button and no lookup. Tell the builder to get the contacts collection before saving and to update the row when the e-mail already exists. Then test it: save the same address twice and confirm you still have one record. Exact e-mail match is the reliable first version; fuzzy name matching glues the wrong people together.

### Does a CRM with login have to let users delete their account?

Yes. Any NorthernGo app with login must ship a visible delete-account button that confirms, then calls window.NorthernGoDB.deleteAccount(). That is GDPR article 17. A hand-rolled loop of delete() leaves the auth record behind. If a later edit removes the button, generation puts it back.

### Can two colleagues share the same pipeline?

Not as two separate accounts. Records are scoped to the signed-in user, and there is no invite or role API. The practical options are one shared login, or treating the CRM as personal. Do not trust an "Admin" badge the model painted; it is not a permission system.

### How do I export my contacts out of an AI-built CRM?

Call window.NorthernGoDB.get on each collection and turn the arrays into a CSV in the browser with a Blob and a download link. That uses only vanilla JavaScript. ZIP export of the app itself is on Premium and Pro. Together those two exits are what keep you from being stuck in the builder.

---

## Related

- [How to accept payments in an AI-built app with your own Stripe keys](https://northerngo.com/resources/integrate-stripe-payments-saas/)
- [Building GDPR-compliant apps: a practical checklist for EU founders](https://northerngo.com/resources/gdpr-compliant-app-building-eu/)
- [How to avoid vendor lock-in when building with an AI app builder](https://northerngo.com/resources/avoid-vendor-lock-in-ai-builders/)

---

NorthernGo is an AI-powered platform for building production-ready web apps with zero coding. Local AI generation via WebGPU is unlimited and free, and you own all generated source code. https://northerngo.com/
