Bhalli B.
  • Home
  • About
  • Services
  • Projects
  • Blogs
  • Booking
  • Contact
  • Home
  • About
  • Services
  • Projects
  • Blogs
  • Booking
  • Contact
Back to Blogs
  1. Home
  2. Blogs
  3. Micro SaaS Architecture: Stack, Schema, and Real Costs

Micro SaaS Architecture: Stack, Schema, and Real Costs

Micro SaaS architecture: a single Next.js application, a shared PostgreSQL database with row level security, managed authentication, and Stripe billing in one connected stack

Bhalli B - Full-Stack Engineer & SaaS MVP Architect

Written by

Bhalli B

Full-Stack Engineer & SaaS MVP Architect

Certified Full-Stack Developer & MVP Specialist · Lahore, Pakistan

I'm Bhalli B, a Full-Stack Engineer and SaaS MVP Architect based in Lahore, Pakistan, with 5+ years of experience shipping production web and mobile applications. I write about Next.js, the MERN stack, AI integration, and MVP delivery based on systems I have actually built and run in production - not theory. Every article here comes from real client work, real debugging sessions, and real launch deadlines.

  • BSc Software Engineering - Hajvery University, Lahore
  • Minimum Viable Product (MVP) Development Certified - Alison.com
  • Agile Project Management Essentials Certified - Management and Strategy Institute
  • Web Designing and Development Certified - PNY Trainings Lahore
About Author•GitHub•LinkedIn•X / Twitter•Daily.dev

Micro SaaS architecture is the smallest system design that can run a paid subscription product safely: one Next.js application, one shared PostgreSQL database that enforces tenant isolation with Row Level Security, managed authentication, and Stripe for billing. In the worked example below, that stack costs about $100 a month in infrastructure at $10K MRR.

Most solo founders get one of two things wrong. They copy enterprise patterns such as Kubernetes, a database per customer, and a self-built auth layer, and they burn their margin. Or they ship a single-tenant prototype and discover at customer number three that isolation is a rewrite. This guide gives you a multi-tenant schema with working RLS policies, the Supabase Auth versus Clerk trade-off with real October 2026 prices, a cost model by revenue stage, and a migration workflow. The target: infrastructure under 3% of MRR by the time you pass $5K MRR, and no more than five hours a month of operations work.


1. What is micro SaaS architecture and why does it matter?

Micro SaaS architecture is a deliberately constrained system design that one founder can build, run, and support alone: one application, one shared database, managed services for everything that is not your product, and an infrastructure bill measured in tens of dollars a month instead of thousands.

The point is not technical elegance. The point is profit per hour of maintenance. A product at $10K MRR that costs $100 a month to host but eats 20 hours a month in incidents, migrations, and auth bugs is a worse business than one that costs $250 and eats five. Pricing pages show you the first number. Only the architecture decides the second one.

What counts as a micro SaaS for architecture purposes?

I use a narrow working definition: one product, one founder, one core workflow, a price point between $19 and $99 per month, and customers who are small businesses or individual professionals. Inside that envelope, the right design is boring on purpose. Outside it, the rules change. If your first three customers ask for a SOC 2 report, SAML single sign-on, or data residency in a specific region, you are building a different kind of company, and this stack is only a starting point.

The three constraints I design against

Every decision in this guide has to pass three tests. They are my rules of thumb, not industry statistics, and I use them to keep scope honest.

  1. Single-operator operability. A budget of five hours a month of operations work once the product has paying customers. Operations means patching, incident response, migrations, support escalations that need a code change, and vendor billing surprises. A design that needs a pager rotation fails this test on day one.
  2. An infrastructure cost ceiling. Infrastructure under 3% of MRR once you pass $5K MRR, counted separately from payment processing fees, which scale with revenue and run higher. Below $5K MRR the fixed cost of paid plans makes the percentage look bad, and that is acceptable.
  3. Tenant isolation from day one. Even with one customer, the schema and policies are designed for hundreds. Retrofitting tenancy means adding a tenant column to every table, backfilling it, rewriting every query, and adding policies on top. For a schema of around 15 tables, my estimate is two to three focused working days plus a maintenance window, and that is before you find the queries that forgot a filter.

Why does the operating budget come before the stack?

As a Certified Project Manager, I fill in a one-page operating budget before choosing any tool. It has three lines: the hours per month I am willing to spend operating the product, the dollars per month the infrastructure may cost at the current revenue stage, and the one measurable event that would justify re-architecting. If I cannot fill in all three lines, the design is not finished.

The third line is the one founders skip, and it prevents both premature Kubernetes and a panicked rewrite. Write down in advance that you will revisit the design when a background job outlives a web request, when p95 latency on your core page passes 200 ms after indexing, or when a customer contract requires something the stack cannot give. Then you are reacting to a trigger you chose while calm, not to an incident at midnight.


2. What does a production micro SaaS architecture look like in 2026?

A production micro SaaS architecture in 2026 is a single Next.js 16 application on Vercel, PostgreSQL with Row Level Security on Supabase, authentication from Supabase Auth or Clerk, Stripe for subscriptions, and Resend for transactional email, with a small Railway worker added only when a job outlives a web request.

Everything a customer touches runs inside one deployable unit. Public pages, the signed-in product, and the handful of server endpoints that Stripe and other providers call live in one repository and ship in one release. The database is the only stateful system you own, and it is managed. If you want the reasoning for choosing that framework in the first place, why choose Next.js for your MVP covers it in depth; this post assumes the decision is made.

Micro SaaS architecture diagram: a Next.js 16 app on Vercel calling Supabase PostgreSQL with row level security, managed authentication, Stripe billing, and a small Railway worker Figure 1: The diagram shows a browser reaching a single Next.js 16 application, which reads and writes a shared PostgreSQL database through Row Level Security, validates sessions with the auth provider, receives Stripe webhooks, and hands long jobs to a small worker.

Layer-by-layer defaults with October 2026 prices

The prices below come from each vendor's pricing page in early October 2026 and describe floor prices, meaning the plan fee before any usage overage. Check them before you commit, because pricing pages change faster than blog posts.

LayerDefaultPrice floorTrap to knowSwap it when
App and serverNext.js 16 on Vercel Pro$20 per developer seat per month, includes a $20 usage creditHobby plan is non-commercialYou need long-running jobs or a container host
DatabaseSupabase Pro (PostgreSQL)From $25 per month, 8 GB disk, 250 GB egressFree projects pause after 1 week of inactivity and have no automatic backupsDisk or compute outgrows the plan, after you measure
AuthenticationSupabase Auth, or ClerkSupabase Auth: 100,000 MAU on Pro. Clerk Pro: $25 monthly, $20 billed annuallyClerk organizations past 100 retained orgs or 20 members per org need a $100 add-onA customer asks for SSO or an invitations UI
PaymentsStripe2.9% + 30¢ per domestic card charge, plus 0.7% of volume for Stripe BillingFees scale with revenue, so they outgrow infrastructureYou want a merchant of record to handle sales tax
EmailResendFree: 3,000 per month. Pro: $20 for 50,000 per monthFree tier is capped at 100 emails per dayVolume passes 50,000 a month
Background workerRailwayHobby: $5 minimum usage with $5 credit. Pro: $20Billed per second on CPU, memory, and egress, so a leaky worker raises the billYou need queues with retries at higher volume

Three of those traps are easy to miss until they cost you. Vercel's pricing page describes the Hobby plan as for personal, non-commercial use, so a product that charges customers belongs on Pro. Supabase's Free plan pauses a project after a week without activity and does not include automatic backups, while Pro keeps daily backups for 7 days. And Clerk bills on monthly retained users, meaning users who come back at least 24 hours after signing up, not on monthly active users, so its numbers will not match your analytics.

What do I deliberately leave out of a micro SaaS architecture?

Leaving things out is half the design. Each item below is something founders commonly add in the first month, and each has a trigger that justifies it later.

  • Kubernetes and container orchestration. The trigger is a workload that cannot run on Vercel or a single Railway service, not a feeling that you should be ready.
  • Redis or a separate cache. Postgres with proper indexes handles early read load. Add a cache when a measured query still misses your latency target after indexing.
  • A message queue service. A table of jobs plus one Railway worker covers the first few thousand jobs a day. Move to a queue product when you need retries, scheduling, and concurrency limits you would otherwise hand-write.
  • A separate API service. Route Handlers and Server Functions in the Next.js app cover webhooks and internal mutations. Split an API out only when a second client, such as a mobile app, needs the same contract.
  • A staging cluster. A second Supabase project on the Free plan and Vercel preview deployments give you a staging environment. Accept that the staging database pauses when idle.

When does this architecture stop being enough?

It does not fail all at once; it outgrows itself one piece at a time. A background job that outlives a web request is the first trigger, and it adds the worker. A p95 latency above 200 ms on a core page after indexing is the second, and it usually means a larger database instance before any rewrite. An enterprise customer asking for single sign-on is the third, and it is a Clerk or similar decision, not a re-architecture. A second developer joining is the fourth, and it changes your Vercel bill because Pro is priced per seat.


3. How do you design a multi-tenant database schema for micro SaaS?

Design a multi-tenant micro SaaS schema as one shared PostgreSQL database where every tenant-owned table carries a tenant_id, a memberships table maps users to tenants, and Row Level Security policies make the database, not your application code, enforce the boundary.

This is the pool model: one database, one schema, logical isolation. It is the right default for micro SaaS because the alternatives multiply your operational work by the number of customers.

Tenancy modelIsolationMigrations per releaseOperational loadFits micro SaaS?
Shared schema with RLSLogical, enforced by PostgreSQL policies1One database to back up and monitorYes, the default
Schema per tenantSeparate namespaces inside one databaseN, one per tenant schemaTooling and search path handling grow with every tenantOnly for a specific isolation requirement
Database per tenantStrongest, physically separateN, one per databaseN backups, N connection pools, N billsNo, not before an enterprise contract demands it

The N in the migrations column is the whole argument. With 200 customers on a database-per-tenant design, one release is 200 migration runs and 200 places for drift. With the shared schema it is one run and one place. For how tenancy choices shape plans and pricing, see multi-tenant SaaS and your pricing model ; this post stays on schema, policies, and operations.

What does the core schema with memberships and RLS look like?

The schema below uses Supabase Auth's auth.users table and a memberships join table, so one user can belong to several tenants. Policies check membership with a subquery, which needs no custom JWT claims and no session settings, and it keeps working behind a connection pooler.

-- supabase/migrations/001_tenancy.sql
create table public.tenants (
  id uuid primary key default gen_random_uuid(),
  name text not null check (char_length(name) between 2 and 80),
  created_at timestamptz not null default now()
);

create table public.memberships (
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  user_id uuid not null references auth.users(id) on delete cascade,
  role text not null default 'member' check (role in ('owner', 'admin', 'member')),
  created_at timestamptz not null default now(),
  primary key (tenant_id, user_id)
);

-- The composite primary key only helps queries that lead with tenant_id,
-- so policy lookups by user_id need their own index.
create index memberships_user_id_idx on public.memberships (user_id);

create table public.projects (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  name text not null check (char_length(name) between 2 and 80),
  status text not null default 'active' check (status in ('active', 'archived')),
  created_at timestamptz not null default now()
);

create index projects_tenant_id_idx on public.projects (tenant_id);
-- The database, not an application pre-check, guarantees unique names per tenant.
create unique index projects_tenant_name_key on public.projects (tenant_id, lower(name));

-- RLS must be enabled on every table in the exposed schema.
alter table public.tenants enable row level security;
alter table public.memberships enable row level security;
alter table public.projects enable row level security;

-- A user sees only their own membership rows.
create policy memberships_select_own on public.memberships
  for select to authenticated
  using (user_id = (select auth.uid()));

-- A tenant is visible to its members.
create policy tenants_select_member on public.tenants
  for select to authenticated
  using (id in (
    select tenant_id from public.memberships where user_id = (select auth.uid())
  ));

-- Projects: read and write only inside tenants the caller belongs to.
create policy projects_member_all on public.projects
  for all to authenticated
  using (tenant_id in (
    select tenant_id from public.memberships where user_id = (select auth.uid())
  ))
  with check (tenant_id in (
    select tenant_id from public.memberships where user_id = (select auth.uid())
  ));

-- No policy lets a user insert into tenants directly, so signup goes through a function.
create function public.create_tenant(tenant_name text)
returns uuid
language plpgsql
security definer
set search_path = ''
as $$
declare
  new_id uuid;
begin
  if (select auth.uid()) is null then
    raise exception 'not authenticated';
  end if;

  insert into public.tenants (name) values (tenant_name) returning id into new_id;
  insert into public.memberships (tenant_id, user_id, role)
    values (new_id, (select auth.uid()), 'owner');

  return new_id;
end;
$$;

revoke all on function public.create_tenant(text) from public, anon;
grant execute on function public.create_tenant(text) to authenticated;

Four details in that migration matter in production, and each follows Supabase's RLS guidance. Wrapping auth.uid() in a select lets Postgres evaluate it once per statement instead of once per row. The extra index on user_id exists because a composite primary key on (tenant_id, user_id) does not index user_id on its own, and an unindexed policy column turns reads into sequential scans. The with check clause stops a user from inserting a row into a tenant they do not belong to. And the security definer function pins its search path to empty and schema-qualifies every name, so it cannot be hijacked by a look-alike object.

How do you pick the current tenant for each request?

Put the tenant in the URL, for example a path segment such as /t/acme/projects, and filter by it in your queries. RLS already allows the user to see every tenant they belong to, so the URL decides which one is in view. If someone edits the URL to a tenant they do not belong to, the policy returns zero rows instead of leaking data. This is simpler and safer than a global "current tenant" setting, which breaks the moment a user opens two tenants in two browser tabs.


4. How do you handle authentication and tenant isolation in micro SaaS?

Use Supabase Auth by default and add Clerk only when you need organization invitations, member management screens, or enterprise single sign-on, because both can drive the same Row Level Security boundary and the price difference comes from how many organizations you serve.

Supabase Auth is included in your Supabase plan, with 100,000 monthly active users on Pro, and it plugs straight into auth.uid() in the policies above. Clerk gives you prebuilt sign-in screens, organization switching, and invitations, and Supabase supports it as a third-party auth provider.

How much does Clerk really cost for a B2B micro SaaS?

Clerk's pricing page lists a free Hobby tier with 50,000 monthly retained users, a Pro plan at $25 per month billed monthly or $20 billed annually, and separate B2B organization pricing. The base B2B tier is free on every plan and includes 100 monthly retained organizations and up to 20 members per organization. The Enhanced B2B add-on is $100 per month, or $85 billed annually, with unlimited members, and the page lists a $1 charge per retained organization from the 101st to the 1,000th.

Read that against your customer model. If each of your 345 customers at $10K MRR is its own organization, the published tiers imply about $100 for the add-on plus 245 organizations at $1, which is $345 a month on top of the Pro plan. I am reading the tier table, not quoting an invoice, so confirm how your organization count is measured before you commit. If instead each customer is a single user, organizations never come into play and Clerk costs the plan fee alone.

One more trap: many tutorials still show Clerk JWT templates sharing the Supabase JWT secret. Supabase deprecated that integration on April 1, 2025 and recommends the native third-party auth path, where Clerk session tokens go to Supabase directly.

Why is the service-role key the most dangerous line in a micro SaaS codebase?

Because it silently turns off the feature you built your isolation on. Supabase's documentation says the service_role key bypasses RLS when a request carries no user access token. A client built with that key will return every tenant's rows no matter what your policies say, and nothing will warn you.

❌ SERVICE-ROLE CLIENT PLUS A TENANT HEADER
const supabase = createClient(url, process.env.SUPABASE_SERVICE_ROLE_KEY!, {
global: { headers: { "X-Tenant-ID": tenantId } },
});
await supabase.rpc("set_tenant_context", { tenant_id: tenantId });

The service role bypasses RLS, so every policy is dead code on this client. A header is only a claim the caller makes about itself, and a setting applied in one request has no guaranteed link to the next, especially on a pooled connection.

✅ USER-TOKEN CLIENT, RLS DOES THE FILTERING
const supabase = await createClient(); // publishable key plus the user's session
const { data } = await supabase
.from("projects")
.select("id, name")
.eq("tenant_id", tenantId);

The query runs as the signed-in user, so PostgreSQL applies the policy to every row. A wrong or forged tenant ID returns an empty list instead of someone else's data.

The server-side client for that pattern uses the @supabase/ssr package and the cookie store from Next.js. In Next.js 16, the cookies function is asynchronous, so it must be awaited.

// src/lib/supabase/server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";

export async function createClient() {
  const cookieStore = await cookies(); // async in Next.js 16

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, // safe to expose; RLS protects the data
    {
      cookies: {
        getAll: () => cookieStore.getAll(),
        setAll: (cookiesToSet) => {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options),
            );
          } catch {
            // Server Components cannot set cookies; the session refresh runs in proxy.ts.
          }
        },
      },
    },
  );
}

Keep the service-role key out of anything a user request can reach. Use it only in trusted jobs such as the Stripe webhook worker, and never behind a NEXT_PUBLIC_ variable, because Next.js inlines those into the browser bundle at build time.

How does the Clerk version of the same boundary work?

If you choose Clerk, the Supabase client takes a token callback that returns the current Clerk session token, and the policy reads the organization from the token's claims. Supabase's Clerk guide shows both the older org_id claim and the compact o.id form, so the policy accepts either.

// src/lib/supabase/clerk.ts
import { createClient } from "@supabase/supabase-js";
import { auth } from "@clerk/nextjs/server";

export function createClerkSupabaseClient() {
  return createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      // Supabase calls this on each request, so the token is always current.
      async accessToken() {
        return (await auth()).getToken();
      },
    },
  );
}
-- Clerk organization IDs are strings such as org_abc, so this variant uses a text column.
create policy projects_clerk_org on public.projects
  for all to authenticated
  using (
    org_id = coalesce(
      (select auth.jwt() ->> 'org_id'),
      (select auth.jwt() -> 'o' ->> 'id')
    )
  )
  with check (
    org_id = coalesce(
      (select auth.jwt() ->> 'org_id'),
      (select auth.jwt() -> 'o' ->> 'id')
    )
  );

What changed with middleware in Next.js 16?

Next.js 16 deprecated the middleware file convention and renamed it proxy. The file is proxy.ts, the exported function is proxy, and the Next.js proxy documentation says it runs on the Node.js runtime by default. A codemod, npx @next/codemod@canary middleware-to-proxy, renames the file and function. Use it for session refresh and redirects, and do not treat it as your authorization layer. The docs warn that Server Functions are POST requests to the route where they are used, so a matcher change can silently remove proxy coverage. Check authentication inside every Server Function and Route Handler, and let RLS be the last line of defense.

If you want a second opinion on your tenancy model before you write the first migration, book a call and bring your table list.


5. How much does micro SaaS architecture cost at each revenue stage?

Micro SaaS infrastructure costs about $0 while you validate, about $45 a month at roughly $1K MRR, and about $100 a month at $10K MRR on the stack above, while payment fees at the same $10K MRR run roughly four times that.

The model below keeps infrastructure and payments apart on purpose. Infrastructure is mostly fixed plan fees. Payments are a percentage of revenue plus a per-charge fee, and they are the line most founders forget to budget.

Monthly Micro SaaS Cost

T = (H + D + A + E + W + M) + R × (r + b) + f × N

T: Total monthly cost
H: Hosting plan fees (Vercel)
D: Database plan (Supabase)
A: Auth fees (Clerk, if used)
E: Email plan (Resend)
W: Worker plan (Railway)
M: Misc allowance (monitoring, domain)
R: Monthly revenue (MRR)
r, b: Card rate 2.9%, Billing rate 0.7%
f, N: Fixed fee $0.30, number of charges

Take a product at $10K MRR with 345 subscribers paying $29 a month. The first worked example puts every optional upgrade on the bill: three Vercel seats, Clerk with the Enhanced B2B add-on, and the Railway Pro plan. The second keeps one seat, uses Supabase Auth, and runs a small Railway worker on the Hobby plan. Both include a $30 allowance for monitoring and domains, and neither includes usage overages.

Everything switched on
$60 + $25 + $25 + $100 + $20 + $20 + $30 = $280
Vercel Pro (3 seats), Supabase Pro, Clerk Pro, Clerk Enhanced B2B, Resend Pro, Railway Pro, allowance. That is 2.8% of $10K MRR.
Scoped to one operator
$20 + $25 + $0 + $20 + $5 + $30 = $100
Vercel Pro (1 seat), Supabase Pro, Supabase Auth, Resend Pro, Railway Hobby, allowance. That is 1.0% of $10K MRR.
Stripe fees at the same revenue
$290.15 + $103.50 = $393.65 (+ $70.04 with Billing = $463.69)
2.9% of $10,005 plus 345 charges at $0.30, then 0.7% more if you use Stripe Billing. That is 3.9% to 4.6% of MRR, roughly four times the optimized infrastructure bill.

The payment line is the finding. You can shave $180 a month off infrastructure with careful choices, and it is still smaller than the fees on the same revenue. Fees vary with card mix, international charges, and currency conversion, so treat $394 to $464 as a domestic-card baseline, and model your own average price and charge count before you set a price.

What does the cost look like by revenue stage?

The stage table uses the same $29 price point and plan floor prices. Each row adds only what the stage forces.

StageMRRSubscribers at $29What the stage addsInfrastructure per monthShare of MRR
Validate$00Free tiers only, before you charge anyone$0Not applicable
First revenue$1,000about 34Vercel Pro ($20) and Supabase Pro ($25)$454.5%
Traction$5,000about 172Resend Pro ($20) and a Railway Hobby worker ($5)$701.4%
Scale$10,000about 345A $30 allowance for monitoring and domains$1001.0%

The first-revenue row breaks my 3% ceiling, and that is expected: the fixed fee of two paid plans dominates a small revenue base. The ceiling is meant to bind from $5K MRR, where the same stack costs 1.4%. Beyond $10K MRR I stop trusting plan floors, because egress, database size, and compute add-ons start to move the bill, and the honest move is to read your own usage graphs. For the cost of building the product rather than running it, SaaS MVP cost with AI tools in 2026 covers the other half, and if you want a scoped estimate against your own idea, hire a SaaS MVP developer.


6. What are the most common micro SaaS architecture mistakes?

The costliest micro SaaS architecture mistakes are putting the Supabase service-role key in a request path, leaving a table without Row Level Security, trusting a tenant ID the client sent, running paying customers on free plans, and leaving payment fees out of unit economics.

None of these show up in a demo. They show up in the first security review, the first week-long outage, or the first month your margin is thinner than your spreadsheet said.

Mistake 1: Using the service-role key where user requests can reach it

Covered in section 4, and worth repeating because it is the one that fails silently. With the service role, RLS is not weak; it is absent. Search your codebase for the key name and confirm every hit is a trusted job, never a Server Component, Route Handler, or Server Function that a signed-in user can trigger with their own input.

Mistake 2: A table in the exposed schema without RLS

Supabase's documentation warns that a table in an exposed schema without RLS can be read and written by any role that holds a grant on it. On projects where the anon and authenticated roles receive grants by default, adding policies later does not remove those grants. Run a check before every launch and every migration that adds a table:

-- Any row returned is a public-schema table with RLS switched off.
select schemaname, tablename
from pg_tables
where schemaname = 'public'
  and not rowsecurity;

Mistake 3: Expecting RLS to throw errors

RLS fails in two different ways, and only one is loud. A write that violates a policy fails with the error new row violates row-level security policy, code 42501. A read that violates a policy returns no rows and no error, so a broken policy looks exactly like an empty account. That asymmetry is why I write the isolation test before the first feature:

// tests/tenant-isolation.test.ts — Vitest against a local Supabase stack
import { describe, expect, it } from "vitest";
import { signedInClient, TENANT_A_ID } from "./helpers"; // seeded test users and tenants

describe("tenant isolation", () => {
  it("returns no rows for another tenant's projects", async () => {
    const userB = await signedInClient("b@example.test");

    const { data, error } = await userB
      .from("projects")
      .select("id")
      .eq("tenant_id", TENANT_A_ID);

    expect(error).toBeNull();
    expect(data).toEqual([]); // reads are filtered silently, so assert emptiness
  });

  it("rejects writes into another tenant", async () => {
    const userB = await signedInClient("b@example.test");

    const { error } = await userB
      .from("projects")
      .insert({ tenant_id: TENANT_A_ID, name: "should fail" });

    expect(error?.code).toBe("42501"); // RLS violation on write
  });
});

Mistake 4: Running paying customers on free plans

Vercel's Hobby plan is for personal, non-commercial use. Supabase's Free plan pauses projects after a week of inactivity and has no automatic backups. Neither is a plan for a product that takes money. The upgrade costs $45 a month for the pair, and it is the cheapest insurance in this post.

Mistake 5: Skipping the unit-economics line for payment fees

At $29 a month, the 30¢ fixed fee is about 1% of each charge on its own, and the 2.9% rate is on top of it. A founder who prices at $9 a month pays the same 30¢ on a much smaller charge, a fixed fee of about 3.3% before the percentage. Put fees in the pricing spreadsheet before you pick a price point, not after.

Mistake 6: Building for the company you hope to become

Kubernetes, a separate auth service, and a second database before $10K MRR are not preparation; they are a second job. Every additional system is another thing that can page you, another bill, and another upgrade path. The stack in this post can carry you well past the point where most micro SaaS products plateau.

Having architected SaaS MVPs from idea to launch, I turn these mistakes into an eight-item gate that has to pass before the first charge:

  1. RLS is enabled on every table in the public schema, confirmed with the query above.
  2. No service-role key is reachable from a user-triggered code path.
  3. The cross-tenant isolation tests pass in CI.
  4. Every column used in a policy is indexed, including the second column of any composite key.
  5. Vercel and Supabase are on paid plans.
  6. A database backup has been restored once into a scratch project.
  7. Stripe webhooks verify signatures and record event IDs, as covered in why choose Next.js for your MVP.
  8. Payment fees are in the unit-economics sheet.

7. How do you deploy a micro SaaS with safe database migrations?

Deploy a micro SaaS by running tests and additive database migrations from GitHub Actions on every merge to main, letting Vercel build the app, and keeping each migration backward compatible so old and new code can share one schema for the few minutes a release takes.

The word that matters is additive. Vercel's Git integration starts building when you push, so it runs in parallel with your workflow, not after it. For a few minutes the previous deployment may still be serving traffic against the new schema, or the new deployment may start before a migration finishes. If every migration only adds things, both orders are safe.

What does a safe migration workflow look like?

The workflow below runs tests first, then applies pending Supabase migrations with the CLI. It uses Node.js 22 and the official setup action for the Supabase CLI. Store the access token, the database password, and the project reference as repository secrets.

# .github/workflows/main.yml
name: Test and migrate

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm test

  migrate:
    needs: test
    runs-on: ubuntu-latest
    env:
      SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
      SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }}
      SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }}
    steps:
      - uses: actions/checkout@v4
      - uses: supabase/setup-cli@v1
        with:
          version: latest
      # Link the CLI to the hosted project, then apply only pending migrations.
      - run: supabase link --project-ref "$SUPABASE_PROJECT_ID"
      - run: supabase db push

How do you make every migration backward compatible?

Use the expand and contract pattern. Anything destructive takes two releases, not one:

  1. Expand. Add the new column, table, or index. Make new columns nullable or give them defaults so old code keeps working.
  2. Backfill and dual-write. Release code that writes both the old and the new shape, and backfill existing rows.
  3. Switch reads. Release code that reads only the new shape.
  4. Contract. In a later release, drop the old column or table.

A rename is the classic trap. Renaming a column in one migration breaks the previous deployment the instant it runs, because that deployment still queries the old name. Treat a rename as add, copy, switch, drop, and it stops being an incident.

What do you need besides migrations to ship safely?

Four things, in the order I set them up:

  • Backups you have restored. Supabase Pro keeps daily backups for 7 days, and the Free plan has no automatic backups. A backup you have never restored is a hypothesis. Restore one into a scratch project once before launch.
  • Secrets in the right place. Anything with a NEXT_PUBLIC_ prefix is inlined into the browser bundle at build time. Keep service keys, webhook secrets, and database passwords in server-only variables.
  • A forward-fix habit. Vercel lets you promote a previous deployment, which rolls the app back in minutes. Database migrations do not roll back that way, so ship a corrective migration instead of editing history.
  • A preview environment. Vercel preview deployments pointed at a second Supabase project catch migration mistakes before main does. Accept that a Free-plan staging project pauses when idle, and wake it before you test.

8. Conclusion and Actionable Roadmap

Micro SaaS architecture is not about choosing the most fashionable stack. It is about a design one person can run: one Next.js application, one shared PostgreSQL database that refuses cross-tenant access through Row Level Security, managed authentication, and a deploy path built on additive migrations. On the stack in this post, infrastructure is about $100 a month at $10K MRR, which is 1.0% of revenue, while Stripe fees on the same revenue run about $394 to $464.

Do the work in this order. This week, write the tenants, memberships, and first tenant-owned table with their policies, and write the cross-tenant test before any feature. Next, pick Supabase Auth unless you can name the feature that needs Clerk, and price its organization model against your customer count. Then add billing with signed, idempotent webhooks, move to paid Vercel and Supabase plans before the first charge, and wire the migration workflow. Revisit the design only when one of the triggers you wrote down fires.

Ship your micro SaaS on a stack you can run alone: I design and build production micro SaaS products with Next.js 16, TypeScript, Supabase (PostgreSQL with Row Level Security), Stripe billing, and GitHub Actions migrations, sized so one founder can operate them. Contact me today to book a 30-minute micro SaaS architecture audit.


9. Frequently Asked Questions

Q: Can I start with a single-tenant schema and add multi-tenancy later?
A: You can, but it is a rewrite rather than a migration. Every table needs a tenant column, every existing row needs a backfill, every query needs a filter, and policies have to be added and tested on top. For a schema of around 15 tables, I would budget two to three focused working days plus a maintenance window, which is my own estimate. Starting with a tenants table, a memberships table, and a tenant column on tenant-owned tables costs roughly an afternoon at the beginning.
Q: Should I use Clerk or Supabase Auth for a micro SaaS?
A: Start with Supabase Auth unless you need organization invitations, member management screens, or enterprise single sign-on on day one. It is included with your Supabase plan and works directly with Row Level Security through auth.uid. Clerk is worth paying for when those features would otherwise take weeks, but check its organization pricing first: the pricing page lists 100 retained organizations in the base tier and a $100 monthly add-on for the enhanced tier, which matters if every customer is its own organization.
Q: How much does a micro SaaS cost to run at $10K MRR?
A: In the worked example in this guide, infrastructure comes to about $100 a month, which is 1% of revenue: Vercel Pro, Supabase Pro, Supabase Auth, Resend Pro, a small Railway worker, and a $30 allowance for monitoring and domains. Payment fees are separate and larger. With 345 subscribers at $29 a month, Stripe card fees come to about $394 a month, or about $464 with Stripe Billing. These figures exclude usage overages and assume plan floor prices from early October 2026.
Q: Do I need Row Level Security if I already check tenant_id in my API?
A: Yes, because API checks fail one forgotten filter at a time, while a policy applies to every query that reaches the table. Row Level Security makes the database refuse cross-tenant reads and writes even when application code has a bug. It also changes how bugs look: reads silently return no rows, and writes fail with error 42501, so add a test that signs in as a second tenant and expects an empty result.
Q: Can I run a paying micro SaaS on Vercel Hobby and Supabase Free?
A: No. Vercel's pricing page describes the Hobby plan as for personal, non-commercial use, so a product that charges customers belongs on Pro at $20 per developer seat per month. Supabase Free pauses projects after one week of inactivity and does not include automatic backups, while Pro starts at $25 per month with daily backups kept for 7 days. Use the free tiers for validation, then upgrade before you take the first payment.
Free Scoping Session

Have Something to Build?

Pick what you're trying to build below, and see exactly what a working engagement with me looks like - timeline, stack, and deliverables.

Product LaunchEst. Timeline: 4 to 8 Weeks

Build a SaaS MVP Roadmap

Turn your idea into a production-ready SaaS - architected, built, and shipped by one engineer, not a handoff chain.

Tech Stack

Next.js 16 + Tailwind v4 + PostgreSQL or MongoDB

Deliverables

Fully functional app with auth, billing, and database integrations.

Included With Your Scoping Call

MoSCoW-scoped feature list and a database architecture roadmap.

Book Free MVP ScopingRequest MVP Cost Estimate
🔒 NDA Available⚡ Free scoping call, no obligation

Latest Articles

Why Choose Next.js for Your MVP? Cost, Speed, and Scale

October 8, 2026

Why Choose Next.js for Your MVP? Cost, Speed, and Scale

How to Take Your SaaS Codebase In-House (Handoff Checklist)

October 7, 2026

How to Take Your SaaS Codebase In-House (Handoff Checklist)

How to Price Your SaaS Product: Which Model Should You Pick?

October 6, 2026

How to Price Your SaaS Product: Which Model Should You Pick?

Offshore vs. Local Developers: Which Should You Hire?

October 5, 2026

Offshore vs. Local Developers: Which Should You Hire?

Who Owns the Code a Freelance Developer Writes for You?

October 3, 2026

Who Owns the Code a Freelance Developer Writes for You?

Does Your MVP Need HIPAA or GDPR Compliance? (2026)

October 2, 2026

Does Your MVP Need HIPAA or GDPR Compliance? (2026)

View All Articles

Bhalli B.

Full-Stack Developer and SaaS MVP Architect, helping startups turn ideas into production-ready products with clean architecture, AI-powered features, and reliable delivery.

Quick Links

  • Home
  • About
  • Projects
  • Services
  • Blogs
  • Book Consultation
  • Contact

Services

  • SaaS MVP Development
  • Full-Stack Development
  • AI & Agentic Systems
  • Technical Architecture & Strategy
  • Project Delivery & Engineering

Contact

  • +92 349 7273482
  • WhatsApp
  • me@bhalli.dev
BHALLI B.BHALLI B.

© 2026 Bhalli B.All rights reserved.

Designed & Built by Bhalli B.

Terms of Service|Privacy Policy