Skip to content

I turned a CRM mock-up into a real pipeline in my Base44 admin

I rebuilt Kargul Studio's public sales-crm mock-up as a real pipeline in my Base44 admin. What I kept, what I changed for a team of one, the data model, how I tested it, and the lesson hiding in its maths: a dashboard is only as honest as the numbers behind it.

Aatman (AJ) Jain9 min readUpdated 4 Oct 2026
A flat paper shop front propped up on a stick, with an arrow to the same little shop as a real folded building with shelves and boxes inside, on peach paper.
A flat paper shop front propped up on a stick, with an arrow to the same little shop as a real folded building with shelves and boxes inside, on peach paper.

To turn a CRM mock-up into a pipeline you can trust, keep the layout and bin the numbers: every figure on the screen should be counted from a real record, never made up by a formula. I took Kargul Studio's public sales-crm mock-up from GitHub, rebuilt its screens in my Base44 admin, and wired every number to each client's own timeline, stage and fit score.

The mock-up that started it

My admin already had an inbox for my contact form. What it didn't have was anywhere for a conversation to go after the first message. Who's waiting on a proposal? Who went quiet?

Then I found kargulstudio/sales-crm (opens in a new tab) on GitHub. It's a public "Kargul Starter" project by Vipul Kumar at Kargul Studio, built with Next.js 16, React 19 and Tailwind 4, and its dark Companies screen is honestly one of the nicest CRM tables I've seen. Stage tags. Segmented win-probability bars. A tiny sparkline on every row. A detail sheet that slides in from the right.

I cloned it on 4 October, ran it locally and wanted it in my admin that afternoon.

Kargul Studio's sales-crm, running locally
Mine, with test data

Then I read the maths

Before I copy a design, I want to know where its numbers come from. So I opened lib/companies.ts.

Pipeline health is the win probability times fixed factors. Discovery is winProbability * 0.372, Evaluation is * 0.651 and Procurement is * 0.372 again. The activity block is the open deal count times constants: the total is deals * 15, touches deals * 4, emails deals + 4, meetings half the deals rounded up and calls and notes deals + 1.

So I opened two companies with the same number of open deals.

Apple: 82%, 6 open deals
Snowflake: 24%, 6 open deals

Apple and Snowflake both have 6 open deals, so both "had" exactly 24 touches, 10 emails, 3 meetings and 7 calls. The "Last 30 Days" menu changes its own label and nothing else. Every company gets the same three score cards, all four stars, all "Updated 2h ago". The Deals and Forecast tabs light up, but the table under them stays the same.

To be fair, none of that is a bug. Making up believable data is literally a mock-up's job. It only becomes a problem when someone ships it as is and makes decisions off it.

That's the one idea I built the whole pipeline around: a dashboard is only as honest as the numbers behind it. A beautiful bar that's secretly 82 * 0.372 is worse than no bar at all, because it looks like it knows something.

Same screen, two sources. One multiplies, the other counts.

What I kept

The layout, almost all of it. The table anatomy: stage tags, a win-probability bar, an activity sparkline, the last interaction and a totals row. The detail sheet's order: pipeline health, then activity trend, then a score card. And the New Company dialog, which became New client.

What I didn't keep is the code. The repo has no licence file, and GitHub's own docs say that without a licence "the default copyright laws apply". So I rebuilt the ideas in my admin kit's own components, in this site's light paper style instead of the dark theme, and credited the repo in the entity's description, at the top of the model file and in my CRM docs.

What I changed for a team of one

The mock-up is built for a sales team. I'm one person.

Kargul Studio's mock-up

  • Account owners, a Team section and an owner filter
  • Segments and motions: Enterprise, Mid-Market, Upsell, Co-Sell
  • Pipeline health: win probability × 0.372, 0.651 and 0.372
  • Activity: open deals × 15, touches = deals × 4
  • The same score cards on every company
  • Deals and Forecast tabs over the same table

My pipeline

  • Just me, so no owners at all
  • Seven stages for how my work goes, New lead to Delivered
  • Win chance: a default per stage, then a slider
  • Activity counted from each client's own timeline
  • A fit score I set: budget, timing, scope and fit
  • A real board with drag and drop, plus Needs you and an inbox link

No owners, and stages for how my work actually goes. The owner column and Team section went, because every deal's owner is me. Enterprise and Co-Sell mean nothing to a freelancer either. My stages follow what happens: someone gets in touch, we talk, we book a call, I send a proposal, and then it's won, delivered or lost. Each has a default win chance:

StageDefault win chanceWhat it means
New lead10%A fresh enquiry
Talking25%Emails going back and forth
Call booked40%A call is in the diary
Proposal sent60%Waiting on a yes
Won100%Signed, work in progress
Delivered100%Done and handed over
Lost0%Went elsewhere or went quiet

Moving a stage resets the win chance to that stage's default and logs the move on the timeline. Then a slider lets me nudge it, because a proposal to someone who's basically said yes isn't really 60%.

A fit score. Mine is four rows I rate myself, 0 to 5 stars each: budget, timing, scope and fit. The total becomes a percentage and a label: Strong fit from 75%, Promising from 50%, Unclear below that, and "Not rated" until I've rated anything. Win chance is how likely a deal is. Fit is whether I should want it.

A next step on every client. A short "what's next" with a due date. Done ticks it off and writes it on the timeline.

The inbox link. Every inbox message now shows a Pipeline card. If the sender's new, Add to pipeline fills in a client from the message and puts the message on their timeline, with "Reply to their message" due today as the next step. When a client moves to Won or Delivered, their linked messages become won leads, so my analytics can count wins by source. Base44 Partner leads land in the same pipeline by themselves, through a signed webhook.

The data model: one row per client

Each client is one row in a Client entity, defined in base44/entities/Client.jsonc (Base44's default folder for schemas). The fields map straight onto the screen:

  • stage, plus kind and source: the seven stages, what the work is and where they came from.
  • value and probability: the deal size (admin only) and the win chance.
  • fit: the four star ratings.
  • next_step and next_step_date.
  • activities: the timeline, newest first. Emails, calls, meetings, notes, inbox messages and stage moves.
  • message_ids: the inbox messages linked to this client.
  • stage_changed_at, won_at and lost_at, so "days in stage" and "won in the last 12 months" are counted, not guessed.

Nothing in it is public, so every operation is admin only:

json
"rls": {
  "create": { "user_condition": { "role": "admin" } },
  "read":   { "user_condition": { "role": "admin" } },
  "update": { "user_condition": { "role": "admin" } },
  "delete": { "user_condition": { "role": "admin" } }
}

My RLS post walks through that pattern and the others I use.

The timeline lives on the row, not in its own table. One read gets a client's whole story, and the list needs every timeline anyway for the sparklines. It's capped at 300 entries so a row can't grow forever.

A stage move is one update with everything in it. This is the real function from crmModel.js:

js
/** Everything a stage move changes, in one update payload. */
export function stagePatch(c, stage, now = new Date()) {
  const from = stageOf(c);
  const iso = now.toISOString();
  const patch = {
    stage,
    probability: stageMeta(stage).probability,
    stage_changed_at: iso,
    activities: [
      { id: newId(), at: iso, type: 'stage', text: `${stageMeta(from).label} → ${stageMeta(stage).label}`, from, to: stage },
      ...(Array.isArray(c?.activities) ? c.activities : []),
    ].slice(0, 300),
  };
  if ((stage === 'won' || stage === 'delivered') && !c?.won_at) patch.won_at = iso;
  if (stage === 'lost') patch.lost_at = iso;
  if (stage !== 'lost' && c?.lost_at) patch.lost_at = '';
  return patch;
}

One small rule matters more than it looks: stage moves go on the timeline, but they don't count as contact. Dragging a card to Talking is me doing admin, not talking to anyone, so it shouldn't make a quiet deal look busy.

Needs you: the panel the page exists for

Needs you, with test data. Every name here is made up.

Three columns, three rules, all counted from the rows:

  • Follow-ups due: next steps due today or late, plus Base44 Partner threads where the client wrote last.
  • Gone quiet: open deals with no contact for 14 days or more.
  • Not in the pipeline yet: enquiries from the last 60 days about a project, collaboration, internship or press that aren't on any client's row. Spam, archived and "just saying hi" don't count. Add turns one into a client.

Each column shows five. When all three are empty, it tells me I'm all caught up.

Same rule for the tiles at the top: until something is won or lost, the win rate shows a dash and "nothing won or lost yet", not a confident 0%.

Open a client and you get the sheet, the mock-up's detail panel rebuilt:

The client sheet, with test data. I left every value blank so nothing reads like a price.

Further down, the activity tiles and score card look a lot like the mock-up's. The difference is where they come from:

Four touches, because four things are on the timeline. Test data again.

How I tested it

I didn't want test clients anywhere near my real Base44 app. So I pointed Playwright at my local dev server and answered every Base44 API request from an in-memory store full of made-up clients. The script clicks through like I would and checks the stored rows after each step.

CRM interaction checks, all passing
32
more checks for the Base44 Partner parts
15
stages, each with its own default win chance
7
days without contact before a deal counts as gone quiet
14

A few of the 32:

  • Dragging a card to Won sets the win chance to 100%, stamps the won date, logs the move and marks the linked inbox message as a won lead.
  • Space, an arrow key and Space again move a card with only the keyboard.
  • Moving a client to Lost, then pressing Undo, puts back their hand-set 55% (not the stage default) with no stray timeline entry.
  • With no Client table yet, the page explains why and the inbox hides its Pipeline card.
  • No console errors on any of it.

Then the build. With the drag-and-drop board (@hello-pangea/dnd) inside the page, the pipeline's chunk came out at 136.02 kB. Loading the board only when you open it cut that by more than two thirds:

The pipeline page's own chunk

Board inside the pageBoard lazy-loadedLower is better
Clients, raw136.02 kB38.79 kBClients, gzip42.03 kB11.91 kB
The pipeline page's own chunk (kB), lower is better
RowBoard inside the pageBoard lazy-loadedChange
Clients, raw136.02 kB38.79 kB71% lower
Clients, gzip42.03 kB11.91 kB72% lower

vite build, 4 Oct 2026. The board's own chunk (PipelineBoard, 98.95 kB, 30.92 kB gzip) loads only when the Board view opens. The Base44 Partner features added later grew Clients to 50.59 kB (15.34 kB gzip).

Same idea as how I cut 91 KB from this site: don't ship code for a view nobody has opened yet.

If you're starting from a mock-up

  1. Find every number

    Open each screen and list every figure, bar, sparkline and chart.

  2. Trace it

    Search the code for where each one is computed. Multipliers and constants are the giveaway.

  3. Give it a real source

    Decide which field it should be counted from. If there isn't one, you've found a field you need.

  4. Be honest about no data

    Show a dash or "Not rated", never a placeholder that looks real.

  5. Cut what isn't yours

    Owners, segments and tabs built for somebody else's team can go.

  6. Credit the design

    Link the author. With no licence, rebuild the ideas instead of copying the code.

The mock-up made me want a CRM. Reading its maths made me build an honest one. Big credit to Vipul and Kargul Studio for the first part, because I wouldn't have started without it.

FAQ

Can I reuse a public UI mock-up from GitHub in my own app?

Check the licence first. GitHub's docs say that without one, default copyright applies and nobody may reproduce, distribute or create derivative works from the code. I treated kargulstudio/sales-crm as inspiration, rebuilt the layout in my own components and credited Kargul Studio (not legal advice).

How is a weighted pipeline calculated?

Add up each open deal's value times its chance of winning. In my pipeline a deal's win chance starts at its stage's default, from 10% for a new lead to 60% for a sent proposal, and I adjust it with a slider when I know better.

Should CRM activity live on the client record or in its own table?

For a solo pipeline, I keep the timeline as an array on the client's row, newest first and capped at 300 entries. One read gets the whole story, and the sparkline, last touch and "gone quiet" are all counted from it. A big team logging thousands of activities per client would probably want a separate table.

How do you test an admin page without real client data?

Point Playwright at a local dev server and answer every Base44 API request from an in-memory store seeded with made-up records. Then click through the real interface and check the stored rows after each step. My pipeline has 32 checks like that.

Keep reading

More posts on the same things.

Newsletter

Liked this one?

Get my Friday email: new posts, what I built and the Base44 and AI news worth knowing.

No spam. Unsubscribe whenever.

Got an app in mind?

I build and fix Base44 apps, and I'm looking for a remote internship. Tell me what you're working on.