teta.so

Building a SaaS with SvelteKit: Architecture Guide

Learn how to architect a SaaS application with SvelteKit, covering auth, database, billing, multi-tenancy, API design, and deployment.

Building a SaaS (Software as a Service) application is one of the most common ambitions for indie hackers, startup founders, and development teams. The framework you choose shapes every architectural decision that follows — from authentication and database access to billing integration and multi-tenancy. SvelteKit is an excellent foundation for SaaS because it gives you server-side rendering for marketing pages, API endpoints for business logic, form actions for data mutations, and a reactive frontend for the application UI, all in a single, coherent framework. This guide covers the architecture of a production SaaS built with SvelteKit, including the specific libraries, patterns, and decisions that matter most.

Why SvelteKit for SaaS

SvelteKit offers several advantages for SaaS applications that become clear once you start building.

Full-stack in one framework. Your marketing site, authentication flows, dashboard UI, API endpoints, and webhooks all live in one SvelteKit project. No separate backend, no API gateway, no deploy-two-things-and-pray-they-stay-in-sync.

SSR for marketing, CSR for the app. Your landing page, pricing page, and blog are server-rendered (great for SEO and performance). Your authenticated dashboard can be client-rendered (great for interactivity). SvelteKit lets you mix rendering strategies per route.

Small bundles, fast loading. SaaS users spend hours in your app. Fast initial load and snappy interactions directly impact user satisfaction and retention. Svelte's compiled output means less JavaScript, faster hydration, and smoother interactions.

Form actions with progressive enhancement. SvelteKit's form actions handle data mutations with built-in CSRF protection, validation, and error handling. Forms work without JavaScript and enhance automatically — important for reliability.

Growing ecosystem. Libraries like shadcn-svelte, Skeleton UI, and Melt UI provide production-ready UI components. Supabase, Stripe, and Resend have solid JavaScript SDKs that integrate cleanly.

Architecture Overview

A typical SvelteKit SaaS has this structure:

src/
  routes/
    (marketing)/          # Public marketing pages
      +page.svelte        # Landing page
      pricing/+page.svelte
      blog/[slug]/+page.svelte
    (auth)/               # Authentication flows
      login/+page.server.ts
      signup/+page.server.ts
      auth/callback/+server.ts
    app/                  # Protected application
      +layout.server.ts   # Auth guard
      dashboard/+page.svelte
      settings/+page.svelte
      [teamSlug]/         # Team-scoped routes
        +layout.server.ts # Team access check
        projects/+page.svelte
    api/                  # API endpoints
      webhooks/stripe/+server.ts
      v1/[...path]/+server.ts
  lib/
    server/               # Server-only code
      database.ts
      stripe.ts
      email.ts
    components/           # Shared UI components
    stores/               # Client-side state

Route groups (marketing) and (auth) organize routes without affecting URLs. The app/ directory contains all authenticated routes, protected by a layout server load function.

Authentication and Authorization

Authentication (who is the user?) and authorization (what can they do?) are the foundation of any SaaS.

Authentication with Supabase

Supabase Auth handles the hard parts — email/password, OAuth, magic links, MFA — and integrates with SvelteKit through cookie-based sessions:

// src/routes/app/+layout.server.ts
import { redirect } from '@sveltejs/kit';

export async function load({ locals: { supabase } }) {
  const { data: { session } } = await supabase.auth.getSession();

  if (!session) {
    redirect(303, '/login');
  }

  const { data: profile } = await supabase
    .from('profiles')
    .select('id, name, avatar_url, role')
    .eq('id', session.user.id)
    .single();

  return { user: profile, session };
}

Authorization with Roles

For multi-user SaaS apps, you need role-based access control. A common pattern uses a team_members junction table:

CREATE TABLE teams (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  name TEXT NOT NULL,
  slug TEXT UNIQUE NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE team_members (
  team_id UUID REFERENCES teams(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')),
  PRIMARY KEY (team_id, user_id)
);

Check authorization in layout load functions:

// src/routes/app/[teamSlug]/+layout.server.ts
import { error } from '@sveltejs/kit';

export async function load({ params, locals: { supabase }, parent }) {
  const { user } = await parent();

  const { data: membership } = await supabase
    .from('team_members')
    .select('role, team:teams(id, name, slug)')
    .eq('team.slug', params.teamSlug)
    .eq('user_id', user.id)
    .single();

  if (!membership) {
    error(403, 'You do not have access to this team');
  }

  return { team: membership.team, role: membership.role };
}

Database and Data Modeling

For SaaS, PostgreSQL (via Supabase) is the standard choice. The relational model maps naturally to SaaS concepts: users, teams, subscriptions, resources.

Core Schema

-- Profiles extend Supabase auth.users
CREATE TABLE profiles (
  id UUID REFERENCES auth.users(id) PRIMARY KEY,
  name TEXT NOT NULL,
  avatar_url TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Teams
CREATE TABLE teams (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  name TEXT NOT NULL,
  slug TEXT UNIQUE NOT NULL,
  plan TEXT NOT NULL DEFAULT 'free' CHECK (plan IN ('free', 'pro', 'enterprise')),
  stripe_customer_id TEXT,
  stripe_subscription_id TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Team membership
CREATE TABLE team_members (
  team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
  role TEXT NOT NULL DEFAULT 'member',
  joined_at TIMESTAMPTZ DEFAULT NOW(),
  PRIMARY KEY (team_id, user_id)
);

-- Your app's core resource (e.g., projects)
CREATE TABLE projects (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  team_id UUID REFERENCES teams(id) ON DELETE CASCADE NOT NULL,
  name TEXT NOT NULL,
  created_by UUID REFERENCES auth.users(id),
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- RLS policies
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE teams ENABLE ROW LEVEL SECURITY;
ALTER TABLE team_members ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can view own profile"
  ON profiles FOR SELECT USING (auth.uid() = id);

CREATE POLICY "Team members can view their teams"
  ON teams FOR SELECT USING (
    id IN (SELECT team_id FROM team_members WHERE user_id = auth.uid())
  );

CREATE POLICY "Team members can view team projects"
  ON projects FOR SELECT USING (
    team_id IN (SELECT team_id FROM team_members WHERE user_id = auth.uid())
  );

Auto-Create Profile on Signup

Use a Supabase database trigger to create a profile when a user signs up:

CREATE OR REPLACE FUNCTION handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO profiles (id, name)
  VALUES (NEW.id, COALESCE(NEW.raw_user_meta_data->>'name', NEW.email));
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE FUNCTION handle_new_user();

Billing with Stripe

Stripe is the standard payment processor for SaaS. The integration involves creating customers, managing subscriptions, and handling webhooks.

Stripe Setup

npm install stripe
// src/lib/server/stripe.ts
import Stripe from 'stripe';
import { STRIPE_SECRET_KEY } from '$env/static/private';

export const stripe = new Stripe(STRIPE_SECRET_KEY);

Creating a Checkout Session

// src/routes/app/[teamSlug]/billing/+page.server.ts
import { stripe } from '$lib/server/stripe';
import { redirect } from '@sveltejs/kit';

export const actions = {
  subscribe: async ({ locals: { supabase }, params, url }) => {
    const { data: team } = await supabase
      .from('teams')
      .select('id, stripe_customer_id')
      .eq('slug', params.teamSlug)
      .single();

    const session = await stripe.checkout.sessions.create({
      customer: team.stripe_customer_id ?? undefined,
      mode: 'subscription',
      line_items: [{ price: 'price_xxx', quantity: 1 }],
      success_url: `${url.origin}/app/${params.teamSlug}/billing?success=true`,
      cancel_url: `${url.origin}/app/${params.teamSlug}/billing`,
      metadata: { teamId: team.id }
    });

    redirect(303, session.url!);
  }
};

Handling Webhooks

// src/routes/api/webhooks/stripe/+server.ts
import { stripe } from '$lib/server/stripe';
import { STRIPE_WEBHOOK_SECRET } from '$env/static/private';
import { json, error } from '@sveltejs/kit';

export async function POST({ request }) {
  const body = await request.text();
  const signature = request.headers.get('stripe-signature');

  let event;
  try {
    event = stripe.webhooks.constructEvent(body, signature!, STRIPE_WEBHOOK_SECRET);
  } catch (err) {
    error(400, 'Invalid signature');
  }

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object;
      // Update team with Stripe customer and subscription IDs
      // Update team plan to 'pro'
      break;
    }
    case 'customer.subscription.deleted': {
      // Downgrade team to 'free' plan
      break;
    }
  }

  return json({ received: true });
}

Multi-Tenancy

Most SaaS apps are multi-tenant — multiple organizations share the same infrastructure but their data is isolated.

URL-Based Tenancy

The simplest approach uses the URL to identify the tenant:

https://app.example.com/acme-corp/projects
https://app.example.com/another-team/projects

In SvelteKit, this maps to src/routes/app/[teamSlug]/. The layout load function fetches the team and verifies access.

Data Isolation

Row Level Security ensures data isolation at the database level. Even if application code has a bug, RLS prevents users from seeing other teams' data:

CREATE POLICY "Team isolation"
  ON projects FOR ALL
  USING (
    team_id IN (SELECT team_id FROM team_members WHERE user_id = auth.uid())
  );

Plan-Based Feature Gating

Check the team's plan to gate features:

// src/routes/app/[teamSlug]/advanced-feature/+page.server.ts
import { error } from '@sveltejs/kit';

export async function load({ parent }) {
  const { team } = await parent();

  if (team.plan === 'free') {
    error(403, 'This feature requires a Pro plan');
  }

  // ... load feature data
}

API Design

If your SaaS exposes an API (for integrations, mobile apps, or third-party developers), SvelteKit handles it through +server.ts files.

// src/routes/api/v1/projects/+server.ts
import { json, error } from '@sveltejs/kit';

export async function GET({ locals: { supabase }, url }) {
  const teamId = url.searchParams.get('team_id');

  const { data: projects, error: dbError } = await supabase
    .from('projects')
    .select('id, name, created_at')
    .eq('team_id', teamId)
    .order('created_at', { ascending: false });

  if (dbError) error(500, 'Database error');

  return json({ projects });
}

export async function POST({ locals: { supabase }, request }) {
  const body = await request.json();

  const { data: project, error: dbError } = await supabase
    .from('projects')
    .insert({ name: body.name, team_id: body.team_id })
    .select()
    .single();

  if (dbError) error(400, dbError.message);

  return json({ project }, { status: 201 });
}

Deployment

For SaaS, Vercel is the most common deployment target. It handles scaling, SSL, and global distribution:

npm install @sveltejs/adapter-vercel

Set environment variables in the Vercel dashboard for Supabase credentials, Stripe keys, and any other secrets.

For more control, Fly.io provides persistent Node.js containers with WebSocket support and global distribution. Self-hosting on a VPS is also viable for cost-conscious teams.

Teta can scaffold the initial SvelteKit + Supabase structure for your SaaS project, letting you focus on the business logic rather than boilerplate.

FAQ

Can I build a SaaS with SvelteKit?

Yes, SvelteKit is well-suited for SaaS applications. It provides server-side rendering for marketing pages, API endpoints for business logic, form actions for data mutations, and a reactive frontend for the application dashboard. The framework handles routing, authentication middleware (via hooks), and deployment through adapters — everything a SaaS needs in one coherent package.

What tech stack should I use for a SvelteKit SaaS?

The recommended stack for a SvelteKit SaaS in 2026 is: SvelteKit for the framework, Supabase for the database, authentication, and real-time features, Stripe for billing, Resend or Postmark for transactional email, and Vercel for hosting. For UI components, shadcn-svelte or Skeleton UI provide production-ready building blocks. This stack gives you a complete, scalable SaaS with minimal infrastructure management.

How do I add billing to SvelteKit?

Integrate Stripe by creating checkout sessions from SvelteKit form actions, handling webhook events in a +server.ts endpoint, and storing subscription state in your database. Stripe manages the payment UI, subscription lifecycle, and invoicing. Your SvelteKit app creates checkout sessions, processes webhooks, and gates features based on the subscription plan stored in the database.

Is SvelteKit good for production SaaS?

Yes, SvelteKit is production-ready for SaaS applications. It offers excellent performance (smaller bundles than React-based alternatives), a mature routing system, built-in SSR, and a growing ecosystem of compatible libraries. Companies are running SvelteKit SaaS applications in production with thousands of users. The framework is maintained by the Svelte team (backed by Vercel) and receives regular updates.

Frequently Asked Questions

Can I build a SaaS with SvelteKit?

Yes, SvelteKit is well-suited for SaaS applications. It provides server-side rendering for marketing pages, API endpoints for business logic, form actions for data mutations, and a reactive frontend for the application dashboard. The framework handles routing, authentication middleware (via hooks), and deployment through adapters — everything a SaaS needs in one coherent package.

What tech stack should I use for a SvelteKit SaaS?

The recommended stack for a SvelteKit SaaS in 2026 is: SvelteKit for the framework, Supabase for the database, authentication, and real-time features, Stripe for billing, Resend or Postmark for transactional email, and Vercel for hosting. For UI components, shadcn-svelte or Skeleton UI provide production-ready building blocks. This stack gives you a complete, scalable SaaS with minimal infrastructure management.

How do I add billing to SvelteKit?

Integrate Stripe by creating checkout sessions from SvelteKit form actions, handling webhook events in a +server.ts endpoint, and storing subscription state in your database. Stripe manages the payment UI, subscription lifecycle, and invoicing. Your SvelteKit app creates checkout sessions, processes webhooks, and gates features based on the subscription plan stored in the database.

Is SvelteKit good for production SaaS?

Yes, SvelteKit is production-ready for SaaS applications. It offers excellent performance (smaller bundles than React-based alternatives), a mature routing system, built-in SSR, and a growing ecosystem of compatible libraries. Companies are running SvelteKit SaaS applications in production with thousands of users. The framework is maintained by the Svelte team (backed by Vercel) and receives regular updates.

Ready to start building?

Create your next web app with AI-powered development tools.

Commencez Gratuitement
← All articles