<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Fly IT Solution]]></title><description><![CDATA[Fly IT Solution builds custom software, SaaS products and MVPs for startups from Minneapolis, MN and Mohali, India. Notes on MVPs, SaaS architecture and the dif]]></description><link>https://flyitsolution.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 01 Nov 2026 23:04:08 GMT</lastBuildDate><atom:link href="https://flyitsolution.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Inside a Custom CRM: The Schema, the Pipeline, the Sync and the Migration]]></title><description><![CDATA[The data model, pipeline design, save-first integration pattern and migration method behind a custom CRM, with Postgres and Node.js examples.]]></description><link>https://flyitsolution.hashnode.dev/inside-a-custom-crm-schema-pipeline-sync-migration</link><guid isPermaLink="true">https://flyitsolution.hashnode.dev/inside-a-custom-crm-schema-pipeline-sync-migration</guid><category><![CDATA[crm]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[backend]]></category><category><![CDATA[software architecture]]></category><dc:creator><![CDATA[Ankush Tarika]]></dc:creator><pubDate>Fri, 09 Oct 2026 09:40:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6abb8b4d507bd152de93eaa6/30df4f45-1b63-4f0b-924a-5140fceded6a.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>The part nobody shows you</h2>
<p>Every "what is a custom CRM" article stops at the benefits. Fits your process, no seat fees, you own it.</p>
<p>All true. None of it tells a developer what is actually inside one.</p>
<p>So here is the inside. This is the shape we use when we build a CRM from scratch for a business: the data model, the pipeline, the integration pattern and the migration method. It is less clever than people expect, and that is the point.</p>
<h2>Five tables do most of the work</h2>
<p>A CRM is a system of record for who you deal with, what you are trying to sell them, and what has happened so far. That is three ideas, and they map to a small set of tables.</p>
<pre><code class="language-sql">-- who
create table companies (
  id         uuid primary key default gen_random_uuid(),
  name       text not null,
  created_at timestamptz not null default now()
);

create table contacts (
  id         uuid primary key default gen_random_uuid(),
  company_id uuid references companies (id) on delete set null,
  full_name  text not null,
  email      text,
  phone      text,
  created_at timestamptz not null default now()
);

-- where in the process
create table pipeline_stages (
  id       smallserial primary key,
  name     text not null,          -- "Site survey", "Quote sent", "Deposit paid"
  position int  not null,
  is_won   boolean not null default false,
  is_lost  boolean not null default false
);

-- what you are trying to sell
create table deals (
  id               uuid primary key default gen_random_uuid(),
  contact_id       uuid not null references contacts (id),
  stage_id         smallint not null references pipeline_stages (id),
  owner_id         uuid not null references users (id),
  value_cents      bigint,
  stage_changed_at timestamptz not null default now(),
  created_at       timestamptz not null default now()
);

-- what happened
create table activities (
  id          uuid primary key default gen_random_uuid(),
  deal_id     uuid references deals (id) on delete cascade,
  contact_id  uuid references contacts (id) on delete cascade,
  user_id     uuid not null references users (id),
  kind        text not null,       -- call, note, email, meeting
  body        text,
  occurred_at timestamptz not null default now()
);
</code></pre>
<p>A few things worth noticing.</p>
<p>Pipeline stages are a table, not an enum. The business will rename them, reorder them and add one in month three. A table makes that a row update, not a deployment.</p>
<p><code>stage_changed_at</code> is on the deal. Without it you cannot answer the first question every sales manager asks: how long has this been sitting in Quote sent?</p>
<p>Activities belong to a deal <em>or</em> a contact. A call before there is a deal still needs a home.</p>
<p>Everything else in a CRM, from custom fields to tags to documents, hangs off these five tables. Start here, and the rest is screens.</p>
<h2>The pipeline comes from discovery, not from a template</h2>
<p>Off-the-shelf CRMs ship with Lead, Qualified, Proposal, Closed. Most businesses do not sell like that.</p>
<p>A moving company we built for runs jobs through survey, quote, deposit, crew assigned, moved, invoiced. A training company runs enquiry, call booked, proposal, signed, delivered. If the stages do not match the real process, the real process moves to a spreadsheet and the CRM becomes where things get pasted later.</p>
<p>So the first week or two of a build is not code. It is sitting with the people who use the current system and writing down how a deal actually moves, including the workarounds. The <code>pipeline_stages</code> rows come out of that conversation. If you want the business-side view of that process, I wrote up <a href="https://flyitsolution.com/blog/what-is-custom-crm-software-development/">how a custom CRM build works step by step</a> on our blog.</p>
<h2>Roles are enforced on the server, or they are not enforced</h2>
<p>Every CRM needs at least three kinds of user: admins, people who work deals, and people who only look.</p>
<p>The mistake is hiding menu items and calling that access control. Enforce it where the data is. In Postgres that can be row-level security; in a Node.js API it is a check on every handler. Either way, a rep must not be able to see another rep's deals by editing a URL, and a read-only manager must not be able to change a stage by calling the endpoint directly.</p>
<pre><code class="language-js">// Express middleware: a user may read a deal if they own it or are a manager
async function canReadDeal(req, res, next) {
  const deal = await db.deals.findById(req.params.id);
  if (!deal) return res.sendStatus(404);

  const isOwner = deal.owner_id === req.user.id;
  const isManager = ['admin', 'manager'].includes(req.user.role);
  if (!isOwner &amp;&amp; !isManager) return res.sendStatus(403);

  req.deal = deal;
  next();
}
</code></pre>
<p>Simple, boring, and the thing that lets a business say the leads belong to the company rather than to whoever answered the phone.</p>
<h2>Integrations: save first, sync second</h2>
<p>A custom CRM is only useful if leads arrive in it without a human retyping them. Website forms, email, accounting, ERP.</p>
<p>The pattern that never loses a record is the same every time. Write the lead to your own database first. Then, from the server, push it to the other system, match on email or phone to avoid duplicates, and retry if the other side is down.</p>
<pre><code class="language-js">// 1. save the lead locally (this is the only step that must succeed)
const lead = await db.leads.insert({ ...form, synced: false });

// 2. sync in the background; failures are retried, never shown to the visitor
queue.add('sync-lead', { leadId: lead.id });
</code></pre>
<p>The visitor sees a thank-you page in milliseconds. The CRM, the accounting tool or the marketing platform catches up a moment later, and if it is unavailable for an hour, nothing is lost. We use exactly this shape to <a href="https://flyitsolution.com/blog/website-forms-to-gohighlevel-integration/">connect website forms to a CRM without losing leads</a>, and the write-up has the field mapping if you want it.</p>
<h2>Migration: export, map, dry run, cut over</h2>
<p>The step clients fear most has the clearest method.</p>
<p>Export everything from the old system: contacts, companies, deals, notes, tasks, attachments, and the history of stage changes if the old tool exposes it. Clean it, which means dedupe on email and phone, drop fields nobody ever filled, and normalize phone numbers once.</p>
<p>Then write the mapping as code, not as a spreadsheet of intentions:</p>
<pre><code class="language-js">const mapDeal = (old) =&gt; ({
  contact_id:  contactIdByOldId.get(old.ContactId),
  stage_id:    stageIdByName.get(old.Stage) ?? stageIdByName.get('Unsorted'),
  owner_id:    userIdByEmail.get(old.OwnerEmail) ?? defaultOwnerId,
  value_cents: Math.round(Number(old.Amount || 0) * 100),
  created_at:  new Date(old.CreatedDate),
});
</code></pre>
<p>Run it against a copy of the database. Compare counts: same number of contacts, same number of open deals, same total pipeline value. Spot-check twenty records by hand. Only then run it for real, usually over a weekend, so the team opens the new CRM on Monday with its history intact.</p>
<p>The Unsorted fallback stage matters. Old systems always have deals in states that no longer exist. Put them somewhere visible rather than silently dropping them.</p>
<h2>Reports are queries, not a module</h2>
<p>In a per-seat CRM, reporting is often a paid tier. In a custom one it is a query over tables you designed.</p>
<pre><code class="language-sql">-- deals stuck in a stage longer than 14 days, by owner
select u.full_name, s.name as stage, count(*) as stuck
from deals d
join pipeline_stages s on s.id = d.stage_id
join users u on u.id = d.owner_id
where d.stage_changed_at &lt; now() - interval '14 days'
  and not s.is_won and not s.is_lost
group by u.full_name, s.name
order by stuck desc;
</code></pre>
<p>That one query answers a question most businesses currently answer by exporting to Excel on a Friday.</p>
<h2>When you should not build one</h2>
<p>Honest version: if the business is small, the sales process is standard and the CRM bill is a rounding error, a ready-made tool is the right call. The code above is not hard, but somebody has to own it after launch, and a custom CRM with no owner rots faster than a subscription.</p>
<p>It earns its cost when the process is part of how the business wins, when license fees grow with every hire, or when the integrations it needs do not exist off the shelf.</p>
<h2>Where this comes from</h2>
<p>This is the shape behind our <a href="https://flyitsolution.com/custom-crm-software-development/">custom CRM software development</a> work: Node.js, React and PostgreSQL, built around the client's process, with the code and the data owned by the client. The moving-company example above replaced a per-user system that was costing them around $15,000 a year.</p>
<p>If you are building one yourself, start with the five tables and the honest pipeline. If you would rather talk it through, I am easy to find.</p>
]]></content:encoded></item><item><title><![CDATA[How to Build a SaaS MVP in 6 Weeks: A Realistic Plan, Stack and Budget]]></title><description><![CDATA[A realistic six-week plan for a SaaS MVP: what to cut in week one, when Lovable and Supabase are enough, when to write React and Node.js, and cost.]]></description><link>https://flyitsolution.hashnode.dev/how-to-build-a-saas-mvp-in-6-weeks-plan-stack-budget</link><guid isPermaLink="true">https://flyitsolution.hashnode.dev/how-to-build-a-saas-mvp-in-6-weeks-plan-stack-budget</guid><category><![CDATA[SaaS]]></category><category><![CDATA[mvp]]></category><category><![CDATA[Startups]]></category><category><![CDATA[supabase]]></category><category><![CDATA[React]]></category><dc:creator><![CDATA[Ankush Tarika]]></dc:creator><pubDate>Tue, 29 Sep 2026 10:16:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6abb8b4d507bd152de93eaa6/b4989c39-c7fc-4601-b803-b62b84699032.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I have watched a lot of six-week MVPs turn into six-month ones. The code was rarely the problem.</p>
<p>The problem was decided before anyone opened an editor. The MVP was a full product with "minimum" written on the front.</p>
<p>This is the plan I give founders and developer friends who ask. It is not clever. It is a week number next to every step, a stack decision made on purpose, and a budget that holds because the scope holds.</p>
<h2>The mistake that eats the six weeks</h2>
<p>Ask a founder what their MVP does and you get a list. Ask what it proves and you get silence.</p>
<p>An MVP is an experiment. It exists to answer one question: will a specific kind of person use this to solve a specific problem, and would they pay?</p>
<p>Every feature that does not help answer that question is not "nice to have". It is a week you do not have.</p>
<p>So the first job is not building. It is cutting.</p>
<h2>The six weeks, one at a time</h2>
<h3>Week 1: Decide what it proves</h3>
<p>Write three sentences. Who the user is. What problem they have today. What one action in your product would show it is solved.</p>
<p>Then list every feature you want and delete until three are left. Keep the deleted list; it becomes the roadmap for week seven onward.</p>
<p>If you cannot get to three, you have two products. Pick one.</p>
<h3>Week 2: Screens, schema, stack</h3>
<p>Sketch every screen between sign-up and the core action. Ugly is fine; complete is not optional.</p>
<p>Then design the data model, because it is the one thing that is expensive to change once users have data in it.</p>
<p>For most SaaS MVPs the schema is smaller than people expect. Here is the shape I start from:</p>
<pre><code class="language-sql">-- organizations own everything; users belong to organizations
-- auth.users is Supabase's built-in users table
create table organizations (
  id uuid primary key default gen_random_uuid(),
  name text not null,
  created_at timestamptz not null default now()
);

create table memberships (
  user_id uuid not null references auth.users (id) on delete cascade,
  org_id uuid not null references organizations (id) on delete cascade,
  role text not null default 'member',
  primary key (user_id, org_id)
);

-- the one core object your MVP exists to create
create table projects (
  id uuid primary key default gen_random_uuid(),
  org_id uuid not null references organizations (id) on delete cascade,
  title text not null,
  status text not null default 'active',
  created_at timestamptz not null default now()
);
</code></pre>
<p>Three tables. Rename "projects" to whatever your core object is. Add Row Level Security policies before launch; I have left them out here to keep the shape visible.</p>
<p>With the schema done, choose the stack. More on that below, because it is its own decision.</p>
<h3>Week 3: The one path that matters</h3>
<p>Build the core flow end to end before anything else. Sign in, do the thing, see the result.</p>
<p>No settings page. No admin. No second feature. If the core flow does not work by Friday, nothing else you built this week matters.</p>
<h3>Week 4: The rest of the three</h3>
<p>Now the second and third features, roles if the product has them, and payments if it charges money.</p>
<p>Add a plain admin view for yourself. You need to see what users are doing without opening the database.</p>
<h3>Week 5: Break it on purpose</h3>
<p>Test the core flow with real data, on a phone, with someone who did not build it. They will find the bug you stopped seeing in week three.</p>
<p>Fix what breaks the core flow first, then what confuses people. Then stop fixing.</p>
<p>Wire up error tracking and basic analytics now. You cannot learn from a launch you cannot see.</p>
<h3>Week 6: Twenty users, not twenty thousand</h3>
<p>Put it in front of twenty to fifty people who actually have the problem. Not Product Hunt. Not a newsletter blast.</p>
<p>Watch what they do, not what they say. Do they come back? Would they pay? That is the data the whole six weeks was for.</p>
<h2>The stack decision, made honestly</h2>
<p>This is a week-two decision and it moves the budget more than anything else. There are three real options.</p>
<h3>Lovable, when speed is the whole point</h3>
<p>Lovable generates a working app from a description: frontend, backend, database, auth and integrations, as editable code.</p>
<p>What makes it usable for a real MVP rather than a demo is boring: it integrates with Supabase, and it syncs the code to GitHub, GitLab or Bitbucket. So the data is in a real database and a developer can take over.</p>
<p>What it is bad at is also predictable. Complex logic is easier to write than to prompt, and generated code is fast to get and slower to extend.</p>
<p>My rule: use it to reach a working product in days, then harden what earns hardening.</p>
<h3>Supabase, almost always</h3>
<p>Supabase is Postgres with auth, storage, edge functions and realtime attached. For an MVP its best feature is that nothing is throwaway.</p>
<p>The schema above is plain Postgres. It does not change when you scale; the code around it does.</p>
<p>The free plan covers an MVP comfortably (as of September 2026: a 500 MB database, 1 GB storage, 50,000 monthly active users), but free projects pause after a week of inactivity. Move to Pro at $25 a month before launch, not after someone finds the site down.</p>
<h3>React and Node.js, when the core is custom</h3>
<p>If the thing your product does is logic no builder can express, write it. React or Next.js in front, Node.js or Laravel behind, Postgres underneath.</p>
<p>Custom code is the expensive option in weeks three and four and the cheap one by month six. It is the right call for real-time features, heavy data processing and unusual integrations.</p>
<p>Most MVPs I work on end up as a mix: a generated or no-code front end on Supabase, with one hand-written service for the hard part.</p>
<h2>A budget that actually holds</h2>
<p>Scoped the way I have described, an MVP with auth, one core workflow, a simple payment step and an admin view typically costs between $1,500 and $3,500 when my team builds it.</p>
<p>That number is low by agency standards, and the reason is not cheap hours. It is that week one removed everything that was not the experiment.</p>
<p>The things that push it above the range are always the same: a fourth feature, a second user type, a custom integration, a native mobile app, real-time anything, or a compliance review.</p>
<p>Notice that every one of those is a scope decision, not a technology decision.</p>
<h2>No-code or custom: a three-line decision</h2>
<p>No-code or low-code if the product is a standard web app and the question is whether anyone wants it.</p>
<p>Custom code if the core is something the tools cannot express, or a month-four migration would cost more than building it right in week three.</p>
<p>A mix if it is standard except for one hard part. That is most of them.</p>
<p>Whichever you pick, the data model is never no-code. Real database, day one.</p>
<h2>After the six weeks</h2>
<p>If twenty people come back every day and complain it is slow, congratulations. Find the one part that is straining and scale that, and only that.</p>
<p>If two hundred people signed up and nobody returned, do not add features. Go back to week one and change the three sentences.</p>
<p>Either way, do not rebuild "properly" yet. The Postgres schema is fine. Replace the front end when it earns it.</p>
<h2>What I'd actually do</h2>
<p>If you are a developer, run this plan yourself; the whole point is that it fits in six weeks of evenings if the scope is honest.</p>
<p>If you are a founder without a technical co-founder, this is the same plan we run as <a href="https://flyitsolution.com/mvp-software-development">MVP development</a> for startups, and the scoping session is the part that saves the money.</p>
<p>Either way: one user, one problem, three features. Everything else waits for week seven.</p>
<p>I keep a longer version of this plan, with the cost-driver table and a comparison of the three stacks, in <a href="https://flyitsolution.com/blog/how-to-build-a-saas-mvp-in-6-weeks/">the SaaS MVP guide on our blog</a>. If you build something from it, tell me what you cut. That is always the interesting part.</p>
]]></content:encoded></item></channel></rss>