SvelteKit SaaS starter
A SvelteKit 3 app with sign-in, roles, Stripe billing and an admin area, where
every data screen is an SvGrid grid. It is MIT licensed and uses only the free
@svgrid/grid package. Scaffold it, rename the sample project table to what
your product stores, and start on your own features.
npm create @svgrid@latest my-app -- --template saas
cd my-app
npm install
npm run dev
The first npm run dev writes a .env with a random auth secret, creates an
embedded Postgres (PGlite) in .data/pglite, and seeds
it. Nothing to install, no keys to fetch. Sign in as [email protected] /
starter-admin-1 (admin, Pro plan) or [email protected] / starter-user-1
(user, Free plan).
It is also a GitHub template: sv-grid/sveltekit-saas-starter. Press "Use this template" there to start a repository of your own with the same files and a CI workflow that type-checks the app and runs its Playwright tests on Node 22 and 24.
The stack
| Part | Choice |
|---|---|
| Framework | SvelteKit 3, Svelte 5 runes, adapter-node |
| Auth | Better Auth: email and password, GitHub OAuth when its keys are set, the admin plugin for roles and bans |
| Database | Drizzle on Postgres; PGlite until you set DATABASE_URL |
| Billing | Stripe Checkout, the customer portal, a signed webhook |
| UI | @svgrid/grid for the grids and the form, card and alert components; Tailwind 4 for layout |
| Resend, or the server console when no key is set |
The screens
| Screen | Grid pattern it shows |
|---|---|
| Dashboard | A read-only grid with responsive and hideBelow for phones |
| Projects | Edit in the grid: one form action per cell, the same validation in the grid (validate + rejectInvalid) and on the server, rollback with api.setCellValue when the server refuses, CSV export |
| Admin: users | Server-side sort, search and paging kept in the URL (externalSort), a role column edited with a list editor, ban and unban for the selected rows |
| Admin: subscriptions | Status filters in the URL, revenue totals, Stripe rows |
| Admin: audit log | 100,000 seeded rows through createServerDataSource in infinite mode; sort, column filters and search run in SQL |
Projects is the screen to copy for your own data. A person's projects are a
few hundred rows at most, so they load whole and the grid sorts and filters in
the browser. The admin screens can grow without limit, so they send the
grid's sort and filter model to the server. src/lib/server/grid-query.ts
turns that model into Drizzle conditions over a whitelist of columns, with
bound parameters and escaped LIKE patterns.
Access checks belong in every action
A SvelteKit layout load runs for page requests. It does not run for form
actions or +server.ts endpoints. The starter's (app) layout sends
signed-out visitors to the login page, and that alone would leave every
action open to a POST from anyone. So each action and endpoint starts with a
guard:
// src/lib/server/guards.ts
export function requireUser(locals: App.Locals) {
if (!locals.user) error(401, 'Sign in first');
return locals.user;
}
export function requireAdmin(locals: App.Locals) {
const user = requireUser(locals);
if (user.role !== 'admin') error(403, 'Admins only');
return user;
}
The same rule covers the grid's server endpoints: the audit log's
rows/+server.ts, which createServerDataSource calls, starts with
requireAdmin(locals).
Billing
Billing stays off until STRIPE_SECRET_KEY and STRIPE_PRICE_PRO are set;
the billing page says what to add. The plan limit (15 projects on Free) is
enforced either way, in the action that creates a project.
The webhook at /api/stripe/webhook verifies the signature on the raw body,
then reads the subscription back from Stripe and stores that. An event that
arrives late or twice still stores the current state. When a customer returns
from Checkout the billing page reads the session too, so the new plan shows
without waiting for the webhook. The starter's README has the steps for local
testing with stripe listen.
Page weight
Measured with the starter's node scripts/route-size.mjs after
npm run build, with @svgrid/grid 3.0.33: gzipped JS and CSS per route.
| Route | gzip | vs the landing page |
|---|---|---|
/ |
30.0 KB | |
/billing |
39.1 KB | +10.0 KB |
/login |
57.6 KB | +28.5 KB |
/dashboard |
135.0 KB | +106.0 KB |
/projects |
158.0 KB | +128.9 KB |
/admin/users |
156.3 KB | +127.3 KB |
/admin/audit |
138.4 KB | +109.3 KB |
Pages without a grid load only the components they use. The grid's export, chart, menu and editor code load when used.
Deploying
npm run build makes a Node server (node build). Set ORIGIN,
BETTER_AUTH_SECRET, DATABASE_URL and the Stripe keys on the host, and run
npm run db:migrate against the production database. adapter-node 6 takes
the origin from paths.origin at build time; without it, it assumes https
and reads the Host header, which is right behind a TLS proxy. PGlite is for
development; use a server Postgres in production.
See also
- Starters and scaffolding: every
npm create @svgridtemplate. - SvGrid with SvelteKit: the load, URL sort and form-action patterns the starter is built from.
- Server infinite scroll for the pattern behind the audit log.
Related articles
- We Asked Claude Code for a Svelte Table. It Wrote Its Own Every Time, Until the Project Said Otherwise - Ten fresh Claude Code sessions, one prompt, a bare SvelteKit app. With no hint it hand-wrote a DataTable component, even for 10,000 rows. One paragraph in AGENTS.md changed that.
- Building a SaaS Billing and Usage Table in Svelte - How to build a production billing grid with locale-aware currency formatting, grouped subtotals by cost category, and CSV export that matches what is on screen.
- SvGrid Studio Tips and Tricks: Build Svelte Data Apps, Fast - Practical SvGrid Studio tips - AI app generation, a designer that emits real SvelteKit, schema-driven edit forms, code-behind with ctx.grid, production auth/data/deploy toggles, and real data-source binding - each with a snippet.