Build Bigin

KINDAreplaces $9/mosaves $108/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A small CRM whose hard problems are importing a messy contact list without creating five of everybody, merging duplicates in a way that can be reversed, and recording stage changes as events so that time in stage is a fact rather than a guess.

estimated effort multi-daythe files for this build are in the project pack

RuntimeOne web application and one worker, both in the same repositoryStoragePostgreSQL, with every state change written as a row in an event tableDeploymentDocker Compose with the database as a named volume and a documented dump commandMatchingNormalised keys computed on write for email, phone and company nameExportEvery object and every attachment, as files, on demand

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the application and the database together so the first run is one command and the restore is reproducible.

    Get it docker.com, or your package manager. open ↗

    Verify docker compose version prints a version

  2. installfree

    Why Runs the web application and the import worker.

    Get it nodejs.org, or your version manager. open ↗

    Verify node --version prints v22 or higher

  3. installfree

    Why Normalising a phone number to the international format needs the current numbering plan for each country, and hand-rolled stripping of spaces and brackets matches the wrong records.

    Get it Any well-maintained port of the widely used phone number library for your runtime.

    Verify Three national formats of one number parse to the same international value

  4. have readyfree

    Why Every interesting failure comes from real data: duplicate rows, a person at two companies, an empty required column, a number stored as text.

    Get it Export contacts from whatever you use now, or take a messy spreadsheet you already have.

    Verify The file imports with a dry run report you can read

  5. decidefree

    Why Each becomes a test, and deciding them after seeing the results means choosing whatever the code already did.

    Get it Write down which fields make a duplicate, how many days without activity makes a deal stale, and exactly what an erasure removes.

    Verify All three appear in configuration and are named in tests

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- people(id, given_name, family_name, email, email_key, phone_e164, company_id, owner, created_at, updated_at, merged_into_id)
- companies(id, name, name_key, domain, domain_key, created_at, updated_at, merged_into_id)
- deals(id, title, person_id, company_id, pipeline_id, stage_id, amount_minor, currency, expected_close, state, created_at, closed_at)
- stage_events(id, deal_id, from_stage_id, to_stage_id, at, actor, note) · append-only, and the only source of time in stage
- interactions(id, subject_type, subject_id, kind, at, summary, body, created_by) · calls, meetings and emails logged by hand
- tasks(id, subject_type, subject_id, title, due_at, done_at, owner) · the next action, which is the reason anyone opens a CRM
- merges(id, kind, survivor_id, absorbed_id, field_choices_json, at, undone_at) · enough to put both records back
- import_rows(id, batch_id, source_key, raw_json, decision, matched_id, error) · every row of every import, including the rejected ones
- Nothing is updated in place that a report later depends on. Stage changes, merges and imports are all rows you can read back.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_URLsecretrequiredpostgres://crm:crm@db:5432/crmThe whole CRM. Backed up by the documented dump command.
ATTACHMENT_DIRrequired./data/attachmentsFiles attached to records. Included in the export archive and in the backup.
DEFAULT_COUNTRYrequiredGBUsed when a phone number in an import has no country code.
STALE_DEAL_DAYSrequired21Days without an interaction before a deal is called stale. Shown beside the number.
FORECAST_MIN_SAMPLErequired20Below this many closed deals in a stage, no probability is shown at all.
EXPORT_DIRrequired./data/exportsWhere the export archive is written. Clear it after downloading.

The build, in order

  1. People, companies and a history that is only appended

    Every interaction and every stage change is a row, and nothing that a report reads is overwritten.

    1. The column can be derived from events; events cannot be derived from a column.

      Files src/deals.tssrc/events.ts

    2. Against a hand-worked example including a deal that moved backwards.

      Files tests/time-in-stage.test.ts

    3. Two sources of history is one source of arguments.

      Files src/timeline.ts

    done when · tick each as it passes
    watch out
    • Keeping only a current stage column and adding a reporting screen later means average days in stage is computed from data that was never recorded, so the first pipeline report is fiction that looks plausible, and nobody notices because the numbers are the right shape. Write the event on the first day, before there is anything to report.
  2. Importing a real contact list

    A messy CSV imported twice produces the same records, not twice as many.

    1. It is what makes a second run an update instead of a duplicate.

      Files src/import/rows.ts

    2. The counts from the dry run are the contract the real run must meet.

      Files src/import/dryrun.tstests/import.test.ts

    3. Matching happens on the stored key, never on the raw string.

      Files src/normalise.tstests/normalise.test.ts

    done when · tick each as it passes
    watch out
    • Re-running an import because the first attempt half failed creates a second copy of every row that succeeded, and since the copies differ slightly the deduplication screen then offers hundreds of pairs nobody will work through. Give each row a stable source key from the file, record it, and make a second run update the matched record rather than insert beside it.
  3. Merging duplicates without losing either of them

    A merge is a reversible decision, and an over-eager merge can be put back.

    1. It holds the notes somebody will look for.

      Files src/merge.ts

    2. Unmerge is only possible if you know what was chosen.

      Files src/merge.tstests/unmerge.test.ts

    3. An automatic merge on a shared company is how two customers become one.

      Files src/dedupe.tstests/dedupe.test.ts

    done when · tick each as it passes
    watch out
    • Merging by copying the loser's fields into the winner and deleting the row destroys the one thing the CRM is for, because the deleted record carried the notes from the call that actually closed the deal, and the person who noticed is looking at a timeline with a gap. Keep the absorbed row, repoint its interactions, and record the field choices so the whole operation can be reversed.
  4. The next action, and honest pipeline numbers

    Opening the app shows what to do today, and every figure on the dashboard can be traced to rows.

    1. A dashboard tile that cannot be drilled into is decoration.

      Files src/dashboard.ts

    2. With the sample size beside it, so four deals looks like four deals.

      Files src/forecast.tstests/forecast.test.ts

    3. Otherwise the number means whatever the reader assumes.

      Files src/dashboard.ts

    done when · tick each as it passes
    watch out
    • Multiplying deal value by a stage probability copied from a blog post produces a forecast number that people quote in meetings and plan against, and it has no relationship to this business at all. Either omit the weighting, or compute it from your own closed deals per stage and show the count it was computed from so a figure based on four deals looks like one.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more crm.