REMOTE STACK
SvelteKit + Better Auth + Durable Objects on Cloudflare
STARTER TEMPLATE 2025.01.21
THE EDGE AUTH CHALLENGE
Problem: Building authenticated apps with persistent state on the edge requires complex orchestration between auth, database, and real-time features.
Solution: Remote Stack combines SvelteKit's remote functions with Better Auth and Durable Objects for a complete edge-first development experience.
Client → SvelteKit → Remote Functions → Durable Objects ↓ ↓ ↓ Better Auth Cloudflare D1 Persistent State
STACK OVERVIEW
FRONTEND
- • SvelteKit 5 - Modern reactive framework
- • Remote Functions - Type-safe edge calls
- • Tailwind CSS - Utility-first styling
- • TypeScript - End-to-end type safety
BACKEND
- • Better Auth - Modern authentication
- • Durable Objects - Persistent state
- • Cloudflare D1 - Edge SQLite database
- • Drizzle ORM - Type-safe queries
KEY FEATURES
- Direct function calls - No manual API routes or fetch calls needed
- Automatic serialization - Type-safe data passing between client and server
- Built-in authentication - Better Auth with session management
- Works in SSR and CSR - Same code works in both contexts
- Environment switching - HTTP calls in dev, service bindings in production
- Zero-config deployment - Alchemy handles all infrastructure
- Persistent edge state - Durable Objects with automatic migrations
QUICK START
# Create new project bun create remote-app my-app cd my-app # Set environment variables echo 'ALCHEMY_PASSWORD=your-password' > .env echo 'BETTER_AUTH_SECRET=your-secret-key' >> .env # Start development (migrations run automatically) bun run dev
Creates a complete SvelteKit app with auth, database, and Durable Objects. Alchemy handles migrations automatically.
ALCHEMY CONFIGURATION
alchemy.run.ts - Zero-Config Deployment
// alchemy.run.ts import alchemy from "alchemy"; import { SvelteKit, Worker, DurableObjectNamespace, D1Database } from "alchemy/cloudflare"; const projectName = "remote-stack"; const project = await alchemy(projectName, { password: process.env.ALCHEMY_PASSWORD || "default-password" }); // Create Durable Object namespace const COUNTER_DO = DurableObjectNamespace(`${projectName}-do`, { className: "CounterDO", scriptName: `${projectName}-worker`, sqlite: true }); // Create D1 database for auth (required for Better Auth) const DB = await D1Database(`${projectName}-db`, { name: `${projectName}-db`, migrationsDir: "migrations", adopt: true, }); // Create the worker that hosts your Durable Objects export const WORKER = await Worker(`${projectName}-worker`, { name: `${projectName}-worker`, entrypoint: "./worker/index.ts", adopt: true, bindings: { COUNTER_DO, }, url: false }); // Create the SvelteKit app export const APP = await SvelteKit(`${projectName}-app`, { name: `${projectName}-app`, bindings: { COUNTER_DO, WORKER, DB, }, url: true, adopt: true, env: { BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET || "your-secret-key", BETTER_AUTH_URL: process.env.BETTER_AUTH_URL || "http://localhost:5173", } }); console.log({ url: APP.url }); await project.finalize();
BETTER AUTH SETUP
Authentication Configuration
// src/lib/auth.ts import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { db } from "./db"; export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "sqlite" }), emailAndPassword: { enabled: true, requireEmailVerification: true }, socialProviders: { google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET! }, github: { clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET! } }, session: { expiresIn: 60 * 60 * 24 * 7, // 7 days updateAge: 60 * 60 * 24 // 1 day }, user: { additionalFields: { name: { type: "string", required: true }, avatar: { type: "string", required: false } } } }); // SvelteKit integration export const authClient = auth.createAuthClient({ baseURL: process.env.BETTER_AUTH_URL });
DURABLE OBJECT
Persistent State Management
// worker/index.ts import { DurableObject } from "cloudflare:workers"; export class CounterDO extends DurableObject { async fetch(request: Request): Promise<Response> { const url = new URL(request.url); const counterId = url.pathname.split('/').pop() || 'default'; // Get current count from storage const count = (await this.ctx.storage.get<number>('count')) || 0; if (request.method === 'POST') { // Increment counter const newCount = count + 1; await this.ctx.storage.put('count', newCount); return Response.json({ count: newCount, id: this.ctx.id.toString(), timestamp: new Date().toISOString() }); } // GET request - return current count return Response.json({ count, id: this.ctx.id.toString(), timestamp: new Date().toISOString() }); } } // Worker entry point export default { async fetch(request: Request, env: any): Promise<Response> { const url = new URL(request.url); // Route to appropriate Durable Object if (url.pathname.startsWith('/counter/')) { const counterId = url.pathname.split('/').pop() || 'default'; const id = env.COUNTER_DO.idFromName(counterId); const obj = env.COUNTER_DO.get(id); return obj.fetch(request); } return new Response('Not found', { status: 404 }); } };
SVELTEKIT REMOTE FUNCTIONS
Direct Function Calls with Auth
// src/routes/data.remote.ts import { query, command } from "sveltekit-remote"; import { getRequestEvent } from "sveltekit"; import { createAuth } from "$lib/auth"; // Environment switching helper const dev = process.env.NODE_ENV === 'development'; function buildWorkerUrl(endpoint: string): string { return dev ? `http://localhost:1337${endpoint}` : `http://worker${endpoint}`; } async function callWorker(platform: App.Platform | undefined, endpoint: string, options: RequestInit = {}): Promise<Response> { const url = buildWorkerUrl(endpoint); if (dev) return fetch(url, options); const request = new Request(url, options); return platform!.env!.WORKER.fetch(request); } async function callWorkerJSON(platform: App.Platform | undefined, endpoint: string, options: RequestInit = {}): Promise<any> { const response = await callWorker(platform, endpoint, options); return response.json(); } // Query function - no auth required export const getCounter = query('unchecked', async (counterId: string = 'default') => { const platform = getRequestEvent().platform; return callWorkerJSON(platform, `/counter/${counterId}`); }); // Command function - requires authentication export const incrementCounter = command('unchecked', async (counterId: string = 'default') => { const platform = getRequestEvent().platform; const auth = createAuth(platform?.env?.DB, platform?.env); const session = await auth.api.getSession({ headers: getRequestEvent().request.headers }); if (!session) throw new Error('Please sign in to increment the counter'); return callWorkerJSON(platform, `/counter/${counterId}`, { method: 'POST' }); }); // Additional remote functions export const getUserProfile = query('unchecked', async () => { const platform = getRequestEvent().platform; const auth = createAuth(platform?.env?.DB, platform?.env); const session = await auth.api.getSession({ headers: getRequestEvent().request.headers }); if (!session) throw new Error('Please sign in to view profile'); return session.user; }); export const updateUserProfile = command('unchecked', async (updates: Record<string, any>) => { const platform = getRequestEvent().platform; const auth = createAuth(platform?.env?.DB, platform?.env); const session = await auth.api.getSession({ headers: getRequestEvent().request.headers }); if (!session) throw new Error('Please sign in to update profile'); // Update user in database const db = platform?.env?.DB; if (db) { await db.prepare('UPDATE users SET name = ?, updated_at = ? WHERE id = ?') .bind(updates.name, new Date().toISOString(), session.user.id) .run(); } return { success: true }; });
DRIZZLE CONFIGURATION
Database Schema & Migrations
// drizzle.config.ts import { defineConfig } from "drizzle-kit"; export default defineConfig({ schema: "./src/lib/schema.ts", out: "./migrations", driver: "d1", dbCredentials: { wranglerConfigPath: "./wrangler.toml", dbName: "remote-stack-db" } }); // src/lib/schema.ts import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"; import { createId } from "@paralleldrive/cuid2"; export const users = sqliteTable("users", { id: text("id").primaryKey().$defaultFn(() => createId()), email: text("email").notNull().unique(), name: text("name").notNull(), avatar: text("avatar"), createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => new Date()), updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => new Date()) }); export const sessions = sqliteTable("sessions", { id: text("id").primaryKey().$defaultFn(() => createId()), userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), token: text("token").notNull().unique(), expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => new Date()) }); export const accounts = sqliteTable("accounts", { id: text("id").primaryKey().$defaultFn(() => createId()), userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), provider: text("provider").notNull(), providerAccountId: text("provider_account_id").notNull(), accessToken: text("access_token"), refreshToken: text("refresh_token"), expiresAt: integer("expires_at", { mode: "timestamp" }), createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => new Date()) }, (table) => ({ pk: primaryKey({ columns: [table.provider, table.providerAccountId] }) })); // Database connection import { drizzle } from "drizzle-orm/d1"; import type { D1Database } from "@cloudflare/workers-types"; export function createDb(d1: D1Database) { return drizzle(d1); } export type Db = ReturnType<typeof createDb>;
ARCHITECTURE PATTERNS
REMOTE FUNCTIONS
- • Direct function calls from components
- • Automatic serialization/deserialization
- • Built-in authentication checks
- • Works in both SSR and CSR
ENVIRONMENT SWITCHING
- • HTTP calls in development
- • Service bindings in production
- • Zero network latency in prod
- • No code changes between envs
AUTHENTICATION
- • Better Auth with D1 database
- • Session management
- • Built-in auth checks in functions
- • Multiple OAuth providers
DEPLOYMENT
- • Alchemy handles infrastructure
- • Automatic migrations
- • Service binding configuration
- • Zero-config deployment
PERFORMANCE CHARACTERISTICS
- Development: HTTP calls to localhost:1337 for Worker
- Production: Zero-latency service bindings
- Database queries: Sub-5ms for D1 operations
- Authentication: Session-based with D1 storage
- Global distribution: 200+ edge locations
- Deployment: Single command with Alchemy
USE CASES
- Authenticated applications with persistent user state
- Multi-tenant SaaS platforms with isolated data
- Real-time applications with Durable Object state
- E-commerce platforms with user sessions and carts
- Gaming applications with persistent player progress
- Analytics dashboards with user-specific data
- Content management systems with user permissions
DEVELOPMENT WORKFLOW
- Create Project:
bun create remote-app my-app
sets up everything - Environment Setup: Set
ALCHEMY_PASSWORD
andBETTER_AUTH_SECRET
- Local Development:
bun run dev
starts with automatic migrations - Remote Functions: Write functions in
src/routes/data.remote.ts
- Deployment:
bun run deploy
deploys with Alchemy
OPERATIONAL CONSIDERATIONS
- Durable Object limits: 1,000 concurrent instances per account
- D1 limits: 5GB storage, 100K reads/day on free tier
- Worker limits: 10ms CPU time per request
- Environment variables: Required for auth and deployment
- Migration strategy: Alchemy handles automatic migrations
- Service bindings: Zero-latency communication in production