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

  1. Create Project: bun create remote-app my-app sets up everything
  2. Environment Setup: Set ALCHEMY_PASSWORD and BETTER_AUTH_SECRET
  3. Local Development: bun run dev starts with automatic migrations
  4. Remote Functions: Write functions in src/routes/data.remote.ts
  5. 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