Skip to main content

7 Architecture Decisions I Make in the First Week of Every SaaS Build

The first week of a SaaS build determines the next six months. Here are the 7 decisions I lock in before writing a single feature - and why they matter more than your framework choice.

Suhag Al Amin
Suhag Al Amin
June 5, 20266 MIN READ
7 Architecture Decisions I Make in the First Week of Every SaaS Build by Suhag Al Amin

TL;DR

The seven architecture decisions that should be locked in during the first week of any SaaS build are: (1) multi-tenant data strategy — shared database with shared schema using Row-Level Security is the right default for 90% of products; (2) authentication and authorization architecture — set up organization membership with role-based access from day one, even if the MVP only has one role; (3) error handling and logging strategy — use typed error classes, centralized error boundaries, and structured JSON logging with correlation IDs; (4) API design pattern — Server Actions for mutations, API Routes for external consumption, Server Components for data fetching; (5) database naming conventions and migration strategy — consistent snake_case naming, standard columns (id, created_at, updated_at) on every table, forward-only numbered migrations; (6) environment and configuration management — validate all environment variables at startup using Zod schemas so missing configuration fails immediately with clear errors; (7) deployment pipeline and preview environments — push-to-deploy with automatic preview URLs so founders can review features in context. These decisions take roughly one week to implement but save months of rework because they establish patterns that every subsequent feature builds upon.

The most impactful week in any SaaS project isn't launch week. It's week one.

In the first five days, I make a set of architecture decisions that determine how fast we can move for the next six months, how expensive the product will be to operate, and how painful it will be to hire the next developer.

These aren't glamorous decisions. Nobody tweets about their database naming conventions or their error handling strategy. But I've learned - through projects that went smoothly and projects that didn't - that these early choices compound relentlessly.

Here are the seven decisions I lock in before writing a single feature, and the reasoning behind each.

Visual overview of seven critical architecture decisions for the first week of a SaaS build — multi-tenant strategy, authentication and authorization, error handling, API design, database conventions, environment management, and deployment pipeline, shown as numbered building blocks forming a foundation
Visual overview of seven critical architecture decisions for the first week of a SaaS build — multi-tenant strategy, authentication and authorization, error handling, API design, database conventions, environment management, and deployment pipeline, shown as numbered building blocks forming a foundation

1. Multi-Tenant Data Strategy

Every SaaS product serves multiple customers (tenants). How you isolate their data is the most consequential decision you'll make, because changing it later means rewriting your entire data access layer.

There are three common approaches:

Shared database, shared schema (column-based isolation). Every table has an org_id column. All tenants live in the same tables. This is what I use for 90% of projects.

SQL
-- Every query includes the tenant filter
SELECT * FROM projects WHERE org_id = 'org_abc' AND status = 'active';

-- RLS enforces this automatically
CREATE POLICY "Tenant isolation" ON projects
  USING (org_id = (SELECT current_org_id()));

Shared database, separate schemas. Each tenant gets their own PostgreSQL schema (tenant_abc.projects, tenant_xyz.projects). Better isolation, more complex migrations.

Separate databases. Maximum isolation, maximum complexity, maximum cost. Only necessary for regulated industries with strict data sovereignty requirements.

Three multi-tenant data isolation strategies compared — shared database with shared schema showing one database with org_id column filter, shared database with separate schemas showing one database with per-tenant schemas, and separate databases showing individual database instances per tenant, arranged on a spectrum from simple to complex and shared to isolated
Three multi-tenant data isolation strategies compared — shared database with shared schema showing one database with org_id column filter, shared database with separate schemas showing one database with per-tenant schemas, and separate databases showing individual database instances per tenant, arranged on a spectrum from simple to complex and shared to isolated

My default: Shared database, shared schema with RLS. It's the simplest to build, simplest to migrate, and PostgreSQL's RLS makes it secure by default. I only escalate to separate schemas for enterprise clients who require contractual data isolation.

2. Authentication and Authorization Architecture

Authentication (who are you?) and authorization (what can you do?) seem simple until you need:

  • Users who belong to multiple organizations
  • Roles that differ per organization (admin in one, viewer in another)
  • Permissions that are feature-specific, not just role-based
  • API keys for programmatic access

I set up a permission system in week one, even if the MVP only has one role. Here's the data model:

SQL
-- Organizations
CREATE TABLE organizations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  slug TEXT UNIQUE NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Organization membership with roles
CREATE TABLE org_members (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  org_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
  role TEXT NOT NULL DEFAULT 'member'
    CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
  UNIQUE (org_id, user_id)
);

-- Helper function for RLS policies
CREATE FUNCTION user_org_role(check_org_id UUID)
RETURNS TEXT AS $$
  SELECT role FROM org_members
  WHERE org_id = check_org_id AND user_id = auth.uid()
$$ LANGUAGE sql SECURITY DEFINER;

This takes two hours to set up. Retrofitting it into an existing application takes two weeks.

3. Error Handling and Logging Strategy

Most developers treat error handling as an afterthought. In production SaaS, it's the difference between "our users report intermittent issues" and "we identified and fixed the bug in 15 minutes."

My week-one error handling setup:

Structured error types. Not generic Error objects, but typed errors that carry context.

TYPESCRIPT
// lib/errors.ts
export class AppError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number = 500,
    public context?: Record<string, unknown>,
  ) {
    super(message);
    this.name = "AppError";
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string, id: string) {
    super(`${resource} not found: ${id}`, "NOT_FOUND", 404, { resource, id });
  }
}

export class PermissionError extends AppError {
  constructor(action: string, resource: string) {
    super(`Not authorized to ${action} ${resource}`, "FORBIDDEN", 403, {
      action,
      resource,
    });
  }
}

Centralized error boundary. One place that catches all unhandled errors, logs them with context, and returns appropriate responses.

Structured logging from day one. Not console.log('something broke'). Structured JSON logs with correlation IDs, user context, and request metadata. When something goes wrong at 2 AM, you need to be able to search logs by user ID, organization, or request ID.

4. API Design Pattern

For Next.js SaaS apps, I've settled on a hybrid approach that balances speed with maintainability:

Server Actions for mutations. Creating, updating, and deleting data uses Server Actions. They're type-safe, they run on the server, and they eliminate the need for API routes for form submissions.

API Routes for external consumption. Anything that a mobile app, webhook, or third-party integration needs to call gets a proper API route with versioning.

Server Components for data fetching. Reading data happens in Server Components, not through API calls from the client. This eliminates loading spinners for initial page loads and reduces client-side JavaScript.

TYPESCRIPT
// The pattern: Server Component fetches, Client Component interacts
// app/dashboard/projects/page.tsx (Server Component)
import { getProjects } from '@/lib/queries'
import { ProjectList } from '@/components/project-list'

export default async function ProjectsPage() {
  const projects = await getProjects() // Direct DB query, no API call

  return <ProjectList initialData={projects} />
}
TYPESCRIPT
// components/project-list.tsx (Client Component)
"use client";

import { createProject } from "@/app/actions/projects"; // Server Action
import { useOptimistic } from "react";

export function ProjectList({ initialData }) {
  const [projects, addOptimistic] = useOptimistic(initialData);

  async function handleCreate(formData: FormData) {
    const optimisticProject = {
      id: crypto.randomUUID(),
      name: formData.get("name"),
    };
    addOptimistic((prev) => [...prev, optimisticProject]);
    await createProject(formData);
  }

  // ...render projects with optimistic updates
}

This pattern gives you instant UI feedback, server-side security, and no boilerplate API layer.

5. Database Naming Conventions and Migration Strategy

This sounds tedious. It is tedious. It also saves more time than almost any other decision.

My conventions:

  • Tables: snake_case, plural (organizations, project_members)
  • Columns: snake_case (created_at, org_id)
  • Foreign keys: referenced_table_singular_id (org_id, user_id)
  • Indexes: idx_table_column (idx_projects_org_id)
  • Enums: snake_case values ('in_progress', not 'IN_PROGRESS' or 'InProgress')

Every table gets these columns:

SQL
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

Migration strategy: I use numbered, sequential migration files that are committed to version control. Forward-only migrations (no editing previous migrations). Every migration has a corresponding rollback.

The developer who joins your team in month four will either thank you or curse you based on these conventions. Make them consistent, make them documented, and enforce them from day one.

6. Environment and Configuration Management

Your MVP has at least three environments: local development, staging/preview, and production. The configuration for each is different (database URLs, API keys, feature flags), and mixing them up causes real damage.

My setup:

TEXT
.env.local          # Local development (gitignored)
.env.development    # Shared dev defaults (committed)
.env.production     # Production values (in Vercel/hosting)

Critical rules:

  • No secrets in version control. Ever. Use environment variables.
  • Every environment variable is typed and validated at startup.
  • If a required variable is missing, the app fails immediately with a clear error - not silently with undefined behavior.
TYPESCRIPT
// lib/env.ts
import { z } from "zod";

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  NEXT_PUBLIC_SUPABASE_URL: z.string().url(),
  NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1),
  SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),
  STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
  RESEND_API_KEY: z.string().startsWith("re_"),
});

export const env = envSchema.parse(process.env);
Comparison of application behavior with and without environment variable validation — without validation the app silently runs with undefined values and crashes later with a cryptic error, with Zod validation the app crashes immediately at startup with a clear message identifying the missing STRIPE_SECRET_KEY variable
Comparison of application behavior with and without environment variable validation — without validation the app silently runs with undefined values and crashes later with a cryptic error, with Zod validation the app crashes immediately at startup with a clear message identifying the missing STRIPE_SECRET_KEY variable

If STRIPE_SECRET_KEY is missing, the app crashes at startup with STRIPE_SECRET_KEY: Required. Not when a user tries to subscribe and gets a cryptic error.

7. Deployment Pipeline and Preview Environments

The final week-one decision: how code gets to production.

My minimum viable pipeline:

  • Developer pushes to a feature branch
  • GitHub Actions runs type-checking (tsc --noEmit) and linting
  • Vercel creates a preview deployment with a unique URL
  • Preview deployment uses a staging database (never production data)
  • After review, merge to main triggers production deployment
  • Production deployment runs database migrations automatically

Why this matters in week one: Without preview deployments, every change requires a verbal description. With preview deployments, a founder can click a link, see the feature, and give feedback in context. This eliminates 80% of miscommunication.

What I skip in week one: End-to-end tests, performance benchmarks, canary deployments. These matter at scale. At MVP stage, they're overhead that slows you down.

The Compound Effect

None of these decisions is individually dramatic. But together, they create a codebase that:

  • Onboards a new developer in hours, not weeks
  • Handles the transition from 10 users to 10,000 without a rewrite
  • Makes debugging a 15-minute task instead of a 4-hour excavation
  • Gives founders real URLs to review, not screenshots

The first week of a SaaS build is the cheapest time to make these decisions. Every week after that, the cost of changing them goes up.

Invest the time upfront. Your future self - and your clients - will thank you.

Want a senior developer who gets the architecture right from day one? I help startup founders avoid the costly rewrites. Let's plan your build.

Suhag Al Amin

WRITTEN BY

Suhag Al Amin

Senior full-stack engineer specializing in SaaS MVPs and AI-powered web apps. 6+ years shipping production products for startup founders.

FAQ

Common questions.

What is multi-tenant architecture in SaaS?
Multi-tenant architecture is how a SaaS product separates data between different customers (tenants). The most common approach uses a shared database where every table has an organization ID column, with Row-Level Security policies ensuring each customer can only see their own data. This is simpler and more cost-effective than running separate databases per customer, while still providing strong data isolation.
Why should I set up role-based access control in week one?
Because retrofitting it later is 5-10x more expensive than building it upfront. Setting up an organization membership table with roles (owner, admin, member, viewer) takes about two hours in week one. Adding it after you have 50 database tables, 30 API routes, and live users means rewriting your data access layer, testing every endpoint for authorization, and migrating existing user data.
What database naming conventions should I use for SaaS?
Use snake_case for everything — table names (plural), column names, foreign keys (referenced_table_singular_id), and index names (idx_table_column). Every table should have id (UUID), created_at (timestamptz), and updated_at (timestamptz) columns. Consistency matters more than which specific convention you choose — the developer who joins your team later will either thank you or curse you based on these decisions.
Do I need preview environments for an MVP?
Yes. Preview environments (automatic deployments for every pull request) eliminate 80% of miscommunication between you and your developer. Instead of describing a feature in a message, the developer shares a URL where you can see and interact with the feature in context. Vercel provides this for free with every push. The 15-minute setup saves hours of back-and-forth.
Should I write tests during the first week of a SaaS build?
Not comprehensive test suites — those slow you down at MVP stage. Instead, focus on testing your database policies (RLS), your authentication flow, and your environment variable validation. These are the foundations that every feature depends on. Add end-to-end tests and performance benchmarks after you've validated product-market fit, not before.

STAY IN THE LOOP

Get new essays before they're posted.

One email when something new goes up. No cadence, no filler.

WORK WITH ME

Have a pilot deadline? Let's talk.

Tell me where you are. I'll tell you honestly whether 6-8 weeks is realistic and what the first week looks like.