Documentation

Everything you need to go from download to deployed SaaS.

Your First 30 Minutes

Follow these steps in order. You'll have a running SaaS with auth and billing by the end.

1. Unzip and install

cd my-saas
npm install

2. Start a local database

You need PostgreSQL running locally. The easiest way is Docker:

docker run -d --name myapp-db \
-e POSTGRES_USER=myapp \
-e POSTGRES_PASSWORD=myapp \
-e POSTGRES_DB=myapp \
-p 5432:5432 postgres:16-alpine

Or use Homebrew (brew install postgresql@16), a hosted DB (Supabase, Neon), or any PostgreSQL instance.

3. Configure environment

cp .env.example .env

Open .env and fill in the minimum to get started:

DATABASE_URL=postgres://myapp:myapp@localhost:5432/myapp
AUTH_SECRET=(run: openssl rand -base64 32)
PUBLIC_APP_URL=http://localhost:5173

You can add Google OAuth, Stripe, and Resend keys later. The app runs without them — you just won't have OAuth, billing, or email features until configured.

4. Create database tables

npx drizzle-kit push

This creates the users, accounts, sessions, and subscriptions tables. Run this again whenever you modify src/lib/server/db/schema.ts.

5. Start the dev server

npm run dev

Open http://localhost:5173. You should see the landing page. Magic link sign-in works once you add Resend keys. Google OAuth works once you add Google credentials.

6. Verify it works

  • Landing page loads at localhost:5173
  • /login shows sign-in form
  • /pricing shows Free and Pro tiers
  • No errors in the terminal

Project Structure

src/
├── lib/server/
│   ├── auth/       Auth.js config (Google + Email)
│   ├── db/         Drizzle schema + connection
│   ├── email/      Resend templates
│   └── stripe/     Checkout, portal, webhooks
├── routes/
│   ├── +page.svelte          Landing page
│   ├── +layout.svelte        Root layout with footer
│   ├── login/                Sign-in (OAuth + magic link)
│   ├── dashboard/            Auth-gated app with stats + quick actions
│   ├── pricing/              Free/Pro with monthly/annual toggle
│   ├── settings/             Profile + billing management
│   ├── admin/                Role-gated admin panel
│   └── api/
│       ├── billing/          POST /checkout, GET /portal
│       └── webhooks/stripe/  Webhook handler
├── app.css                   Tailwind styles
├── app.html                  HTML shell
└── hooks.server.ts           Auth middleware

Stripe Billing Setup

Complete walkthrough to get subscriptions working.

1. Create a product and prices

  1. Go to Stripe Dashboard → Products
  2. Click "Add product" — name it (e.g., "Pro Plan")
  3. Add a recurring price — e.g., $24/month
  4. Optionally add a second price for annual billing (e.g., $228/year = 20% off)
  5. Copy each Price ID (price_xxx) into your .env:
STRIPE_PRICE_MONTHLY=price_1abc...
STRIPE_PRICE_ANNUAL=price_2xyz...

The Price ID is on the product detail page, not the product ID.

2. Set up webhooks (local dev)

Install the Stripe CLI and run:

stripe listen --forward-to localhost:5173/api/webhooks/stripe

Copy the whsec_xxx signing secret it prints into .env as STRIPE_WEBHOOK_SECRET.

3. Set up webhooks (production)

  1. Go to Stripe Dashboard → Developers → Webhooks
  2. Click "Add endpoint"
  3. URL: https://yourdomain.com/api/webhooks/stripe
  4. Select events: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted
  5. Copy the signing secret into your production STRIPE_WEBHOOK_SECRET

4. Test the full flow

  1. Sign in to your app
  2. Go to /pricing and click "Upgrade to Pro"
  3. Use Stripe test card: 4242 4242 4242 4242 (any future date, any CVC)
  4. After checkout, you should be redirected to /dashboard?payment=success
  5. Your plan should show "Pro" on the dashboard
  6. Check the terminal — the webhook should log the event

5. Go live

When ready for real payments:

  • Switch from sk_test_ to sk_live_ in STRIPE_SECRET_KEY
  • Create live-mode prices and update STRIPE_PRICE_MONTHLY / STRIPE_PRICE_ANNUAL
  • Add a live-mode webhook endpoint with the production URL

Authentication Setup

Two sign-in methods out of the box. Add more with one line of code.

Google OAuth

  1. Go to Google Cloud Console → Credentials
  2. Click "Create Credentials" → "OAuth client ID"
  3. Application type: Web application
  4. Authorized redirect URI: http://localhost:5173/auth/callback/google
  5. Copy the Client ID and Client Secret into .env

In production, add your domain as an additional redirect URI: https://yourdomain.com/auth/callback/google

Magic Link Email

Works automatically once Resend is configured (see Email Setup below). Users enter their email, receive a one-time sign-in link, and click to authenticate.

Adding other providers

Auth.js supports 80+ providers. To add GitHub, for example:

// src/lib/server/auth/index.ts
import GitHub from '@auth/sveltekit/providers/github';
// Add to providers array:
GitHub({ clientId: env.GITHUB_CLIENT_ID, clientSecret: env.GITHUB_CLIENT_SECRET }),

Making a user admin

Connect to your database and run:

UPDATE users SET role = 'admin' WHERE email = '[email protected]';

Admin users see the "Admin" link in the dashboard sidebar and can access /admin.

Email Setup

Transactional email via Resend. Required for magic link sign-in.

  1. Create a Resend account
  2. Verify your sending domain (so emails come from [email protected])
  3. Copy your API key into .env as RESEND_API_KEY
  4. Set FROM_EMAIL to an address on your verified domain

Email templates live in src/lib/server/email/index.ts. Add new templates by creating new async functions — see the inline comments for examples.

Customization

Make the template your own.

Branding

  • Search for YourApp and replace with your app name (landing page, dashboard sidebar, pricing page)
  • Update PUBLIC_APP_NAME in .env (used in email subjects)
  • Change colors: search for blue-500 and blue-400 across src/routes/ and replace with your brand color
  • Edit the footer in src/routes/+layout.svelte

Adding pages

Create a new directory in src/routes/ with a +page.svelte file. To require authentication, add a +page.server.ts that checks the session:

// src/routes/my-page/+page.server.ts
const session = await locals.auth();
if (!session?.user) throw redirect(303, '/login');

Adding database tables

  1. Define the table in src/lib/server/db/schema.ts
  2. Run npx drizzle-kit push
  3. Import and query in your routes — see the inline comments in schema.ts for an example

Updating pricing

Edit src/routes/pricing/+page.svelte to change the displayed prices, feature list, and plan names. Make sure the amounts match your Stripe prices.

Environment Variables

Database

DATABASE_URL PostgreSQL connection string

Authentication

AUTH_SECRET Random string for session encryption — generate with: openssl rand -base64 32
GOOGLE_CLIENT_ID Google OAuth client ID
GOOGLE_CLIENT_SECRET Google OAuth client secret

Stripe

STRIPE_SECRET_KEY Stripe secret API key (sk_test_ for dev, sk_live_ for prod)
STRIPE_WEBHOOK_SECRET Webhook signing secret (whsec_...)
STRIPE_PRICE_MONTHLY Price ID for monthly plan (price_...)
STRIPE_PRICE_ANNUAL Price ID for annual plan (price_...)

Email

RESEND_API_KEY Resend API key for sending email
FROM_EMAIL Sender email address (must be on a verified Resend domain)

App

PUBLIC_APP_NAME App name used in email subjects
PUBLIC_APP_URL Your app URL (e.g. https://myapp.com)

Deployment

Deploy to any platform that runs Node.js or Docker.

Docker (any platform)

docker build -t my-saas .
docker run -p 3000:3000 --env-file .env my-saas

Works with AWS ECS, Google Cloud Run, Kubernetes, or any Docker host.

Railway

  1. Push code to GitHub/GitLab
  2. Create a new project on Railway and connect your repo
  3. Add a PostgreSQL database service
  4. Railway auto-detects the Dockerfile — set all env vars in the Railway dashboard
  5. Railway provides DATABASE_URL automatically when you link the DB service

Fly.io

fly launch
fly postgres create --name myapp-db
fly postgres attach myapp-db
fly secrets set AUTH_SECRET=... STRIPE_SECRET_KEY=... ...
fly deploy

Production checklist

  • Switch Stripe keys from sk_test_ to sk_live_
  • Create live-mode Stripe prices and update env vars
  • Add production webhook endpoint in Stripe Dashboard
  • Add production domain to Google OAuth redirect URIs
  • Set PUBLIC_APP_URL to your production URL
  • Verify Resend domain for production email sending