REMOTE STACK
SvelteKit + Better Auth + Durable Objects on Cloudflare
STARTER TEMPLATE 2025.09.22
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 StateSTACK 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-appsets up everything - Environment Setup: Set
ALCHEMY_PASSWORDandBETTER_AUTH_SECRET - Local Development:
bun run devstarts with automatic migrations - Remote Functions: Write functions in
src/routes/data.remote.ts - Deployment:
bun run deploydeploys 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