- Nix 50.6%
- TypeScript 23.8%
- Svelte 20.9%
- CSS 2.9%
- JavaScript 1.2%
- Other 0.6%
| .claude/agents | ||
| .vscode | ||
| i18n | ||
| src | ||
| static | ||
| .env.example | ||
| .envrc | ||
| .gitignore | ||
| .prettierignore | ||
| .prettierrc | ||
| bun.lock | ||
| bun.nix | ||
| CLAUDE.md | ||
| COMMITS.md | ||
| eslint.config.js | ||
| flake.lock | ||
| flake.nix | ||
| package.json | ||
| README.md | ||
| svelte.config.js | ||
| tsconfig.json | ||
| vite.config.ts | ||
| vitest-setup-client.ts | ||
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-nodewith full server runtime - Svelte 5 -- Runes (
$state,$derived,$effect,$props), snippets, attachments - Remote Functions --
query,command,formfrom$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.