SvelteKit starter with Supabase
  • Nix 50.6%
  • TypeScript 23.8%
  • Svelte 20.9%
  • CSS 2.9%
  • JavaScript 1.2%
  • Other 0.6%
Find a file Use this template
2026-02-24 19:04:10 -05:00
.claude/agents init: everything 💩 2026-02-24 18:58:39 -05:00
.vscode init: everything 💩 2026-02-24 18:58:39 -05:00
i18n init: everything 💩 2026-02-24 18:58:39 -05:00
src init: everything 💩 2026-02-24 18:58:39 -05:00
static init: everything 💩 2026-02-24 18:58:39 -05:00
.env.example init: everything 💩 2026-02-24 18:58:39 -05:00
.envrc init: everything 💩 2026-02-24 18:58:39 -05:00
.gitignore init: everything 💩 2026-02-24 18:58:39 -05:00
.prettierignore init: everything 💩 2026-02-24 18:58:39 -05:00
.prettierrc init: everything 💩 2026-02-24 18:58:39 -05:00
bun.lock init: everything 💩 2026-02-24 18:58:39 -05:00
bun.nix init: everything 💩 2026-02-24 18:58:39 -05:00
CLAUDE.md init: everything 💩 2026-02-24 18:58:39 -05:00
COMMITS.md init: everything 💩 2026-02-24 18:58:39 -05:00
eslint.config.js init: everything 💩 2026-02-24 18:58:39 -05:00
flake.lock init: everything 💩 2026-02-24 18:58:39 -05:00
flake.nix init: everything 💩 2026-02-24 18:58:39 -05:00
package.json init: everything 💩 2026-02-24 18:58:39 -05:00
README.md docs: readme 2026-02-24 19:04:10 -05:00
svelte.config.js init: everything 💩 2026-02-24 18:58:39 -05:00
tsconfig.json init: everything 💩 2026-02-24 18:58:39 -05:00
vite.config.ts init: everything 💩 2026-02-24 18:58:39 -05:00
vitest-setup-client.ts init: everything 💩 2026-02-24 18:58:39 -05:00

SvelteKit SSR Starter

A server-side rendered SvelteKit starter template with Supabase auth, remote functions, and a pre-built component library. Built with Svelte 5, TailwindCSS 4, and DaisyUI 5.

This is a generic starting point for building full-stack SvelteKit applications where auth lives on the server, data flows through remote functions, and forms work without JavaScript.

Features

  • Server-Side Rendering -- adapter-node with full server runtime
  • Svelte 5 -- Runes ($state, $derived, $effect, $props), snippets, attachments
  • Remote Functions -- query, command, form from $app/server (experimental SvelteKit feature)
  • Supabase Auth -- httpOnly cookies, server-managed sessions, route protection
  • Two Data Access Patterns -- Supabase direct (RLS) and external API (ServerApiClient)
  • Progressive Enhancement -- Forms and navigation work without JavaScript
  • TailwindCSS 4 + DaisyUI 5 -- Light and dark themes with theme toggle
  • Paraglide i18n -- Compile-time internationalization (English, Spanish)
  • Navigation System -- Dynamic slot injection for Header, Footer, Sidebar, and Peek panel
  • Component Library -- Accordion, Button, Dialog, Form, Field, SearchBar, Window, and more
  • Svelte 5 Attachments -- Reusable DOM behaviors (autoResizeTextarea, copyToClipboard)
  • TypeScript -- Strict mode throughout
  • Vitest -- Unit testing with browser mode support
  • Dummy Mode -- Runs without Supabase configured (auth is bypassed)

Tech Stack

Category Tool
Framework SvelteKit (adapter-node)
UI Svelte 5
Styling TailwindCSS 4, DaisyUI 5
Auth Supabase SSR (httpOnly cookies)
Validation Valibot
i18n Paraglide (Inlang)
Icons Lucide
Testing Vitest
Package Manager Bun
Build Vite

Getting Started

Prerequisites

  • Bun installed
  • A Supabase project (optional -- the app runs in dummy mode without one)

Setup

# Install dependencies
bun install

# Copy environment template
cp .env.example .env

# Start development server
bun run dev

The app will start at http://localhost:5173. Without Supabase credentials configured, it runs in dummy mode -- auth middleware is bypassed and all routes are accessible.

To enable auth, fill in your Supabase credentials in .env (see Environment Variables).

Project Structure

src/
├── lib/
│   ├── client/           # Browser-only (auth state listener, real-time)
│   ├── server/           # Server-only (API client, Supabase factory, constants)
│   ├── remote/           # Remote functions (server-side, callable from components)
│   ├── components/
│   │   ├── base/         # Reusable components (Button, Dialog, Form, Field, etc.)
│   │   └── features/     # Feature components (Navigation, UserMenu)
│   ├── stores/           # Reactive state with Svelte 5 runes (.svelte.ts)
│   ├── attachments/      # DOM attachments for {@attach} directive
│   ├── types/            # TypeScript type definitions
│   └── utils/            # Utility functions (cn, redirect)
├── routes/
│   ├── login/            # Public login page
│   └── (app)/app/        # Protected routes (server-guarded)
│       ├── +page.svelte          # Dashboard
│       ├── items/[[itemId]]/     # Example list/detail route
│       └── settings/             # Settings page
├── hooks.server.ts       # Auth middleware, Supabase session management
├── hooks.ts              # Universal hooks (i18n reroute)
├── app.css               # Tailwind + DaisyUI theme config
└── app.html              # HTML template

i18n/messages/            # Translation files (en.json, es.json)

Key Patterns

Remote Functions

Remote functions are the primary data access pattern. They run on the server but are callable directly from Svelte components. This is an experimental SvelteKit feature using query, command, and form from $app/server.

// src/lib/remote/example.remote.ts
import { query, command } from '$app/server'

export const getItems = query(async (event, params?: { search?: string }) => {
  const { data, error } = await event.locals.supabase
    .from('items')
    .select('*')
  if (error) throw error
  return data
})

export const createItem = command(async (event, input: CreateItemInput) => {
  const apiClient = new ServerApiClient(event)
  return apiClient.post('/items', input)
})
<script lang="ts">
  import { getItems } from '$lib/remote'

  const items = getItems()
  // items.current is reactive, items.loading and items.error available
</script>

{#each items.current ?? [] as item}
  <p>{item.name}</p>
{/each}

Use query for reads, command for mutations, and form for progressive enhancement with <form> elements.

Auth

Auth is fully server-side. The hooks.server.ts middleware creates a per-request Supabase client, validates sessions via httpOnly cookies, and populates event.locals.supabase, event.locals.session, and event.locals.user.

Login and signup are remote functions using form for progressive enhancement:

<form action={login}>
  <input name="email" type="email" />
  <input name="password" type="password" />
  <button type="submit">Log In</button>
</form>

The client-side auth store ($lib/client/auth.svelte.ts) is read-only -- it syncs state from the server for UI rendering but never performs auth operations directly.

Protected routes under (app)/ are guarded by the server hook, which redirects unauthenticated users to /login.

Two Data Access Patterns

Both patterns are used inside remote functions:

Supabase Direct -- Query your database using Row Level Security:

const { data } = await event.locals.supabase
  .from('items')
  .select('*')

External API -- Call Edge Functions or third-party services:

import { ServerApiClient } from '$lib/server'

const apiClient = new ServerApiClient(event)
const items = await apiClient.get<Item[]>('/items')

ServerApiClient automatically attaches the user's access token as a Bearer header.

Navigation Slot System

The navigation system uses Svelte context to let child routes inject content into layout slots (Header, Footer, Sidebar, Peek panel):

<!-- In a page component -->
<script lang="ts">
  import { getSidebarContext } from '$lib/components/features/Navigation'

  const sidebar = getSidebarContext()
</script>

{#snippet sidebarContent()}
  <nav>Page-specific sidebar</nav>
{/snippet}

<!-- Set content on mount, clear on unmount -->
{sidebar.setContent(sidebarContent)}

Stores

Shared reactive state uses .svelte.ts files with module-level runes:

// src/lib/stores/theme.svelte.ts
let current = $state<Theme>(getThemeFromDOM())

export const theme = {
  get current() { return current },
  get isDark() { return current === Theme.Dark },
  toggle() { /* ... */ }
}

Attachments

Reusable DOM behaviors using the Svelte 5 {@attach} directive:

<textarea {@attach autoResizeTextarea({ minHeight: 2, maxHeight: 8 })} />

Environment Variables

Server-Only (never exposed to browser)

SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key

# Optional: external API backend
# API_BASE_URL=http://localhost:9119

Access via $env/dynamic/private in server code only.

Public (embedded in client bundle)

VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key

Access via $env/static/public. Used for the client Supabase instance (real-time subscriptions, auth state listener). The anon key is safe to expose -- RLS enforces security.

See .env.example for a complete template.

Scripts

bun run dev          # Start development server
bun run build        # Build for production (outputs to build/)
bun run preview      # Preview production build
bun run check        # Type-check with svelte-check
bun run lint         # Lint with Prettier + ESLint
bun run format       # Format with Prettier
bun run test         # Run tests (Vitest, single run)
bun run test:unit    # Run tests (Vitest, watch mode)

The production build produces a Node.js server application. Deploy to any platform that supports Node.js (Docker, Fly.io, Railway, VPS, etc.).

i18n

This project uses Paraglide for compile-time internationalization with English and Spanish.

<script lang="ts">
  import * as m from '$lib/paraglide/messages'
</script>

<h1>{m.welcome()}</h1>

Add message keys to both i18n/messages/en.json and i18n/messages/es.json.