A proof-of-concept demonstrating SvelteKit's experimental remote functions feature with OpenTelemetry observability baked in. This project showcases how to create type-safe, async server functions that can be called directly from Svelte components while maintaining full observability through distributed tracing.
This POC combines:
query() API) - Server functions callable directly from componentsRemote functions allow you to call server-side code directly from your components with full type safety and automatic serialization. OpenTelemetry provides visibility into the performance and behavior of these calls through distributed tracing.
pnpm install
Start the PostgreSQL database:
pnpm db:start
Start the OpenTelemetry observability stack (Grafana, Loki, Tempo, Prometheus):
docker compose -f otel/docker-compose.yaml up -d
The Grafana UI will be available at http://localhost:3030 for viewing traces, logs, and metrics.
Create a .env file in the root directory:
DATABASE_URL=postgresql://root:mysecretpassword@localhost:5432/local
PUBLIC_OBSERVABILITY_ENABLED=true
Push the database schema:
pnpm db:push
pnpm dev
The application will be available at http://localhost:5173
To run without observability:
pnpm dev:no-otel
src/
├── instrumentation.server.ts # OpenTelemetry SDK initialization
├── routes/
│ ├── admin/
│ │ ├── posts.remote.ts # Example remote function
│ │ └── +page.svelte # Component using remote function
│ └── ...
└── lib/
└── server/
├── db/ # Database schema and connection
└── observability/
└── withSpan.ts # Utility for creating custom spans
Remote functions are created using the query() API from $app/server:
// src/routes/admin/posts.remote.ts
import { query } from '$app/server';
import { db } from '$lib/server/db';
import { withSpan } from '$lib/server/observability/withSpan';
export const getAllPosts = query(async () => {
return withSpan('rf:getAllPosts', async () => {
const posts = await db.query.post.findMany();
return posts;
});
});
Remote functions can be called directly in Svelte components with full type safety:
<script lang="ts">
import { getAllPosts } from './posts.remote';
// Remote functions return promises - use with #await blocks
</script>
{#await getAllPosts()}
<p>Loading posts...</p>
{:then posts}
{#each posts as post}
<li>{post.title}</li>
{/each}
{:catch error}
<p>Error: {error.message}</p>
{/await}
Use the withSpan() utility to add custom spans for observability:
import { withSpan } from '$lib/server/observability/withSpan';
export const myRemoteFunction = query(async (id: string) => {
return withSpan('rf:myRemoteFunction', async () => {
// Your function logic here
// This will create a span in OpenTelemetry
}, {
'function.id': id, // Custom attributes
'function.type': 'remote'
});
});
The withSpan() function:
PUBLIC_OBSERVABILITY_ENABLED environment variableThe observability stack uses OpenTelemetry for instrumentation and the Grafana OTEL-LGTM (OpenTelemetry, Loki, Grafana, Tempo, Mimir/Prometheus) stack for visualization and storage.
Application Side:
src/instrumentation.server.ts4318Observability Stack (otel/docker-compose.yaml):
The stack uses the grafana/otel-lgtm Docker image, which packages together:
The OpenTelemetry collector receives traces on port 4318 (OTLP/gRPC) and 4317 (OTLP/HTTP) and forwards them to Tempo for storage. Grafana queries Tempo to visualize traces.
OpenTelemetry is initialized in src/instrumentation.server.ts when SvelteKit starts (via the instrumentation.server experimental feature). The SDK is configured to:
http://localhost:4318/v1/traces using OTLP Protocol Bufferssveltekit for all spansdocker compose -f otel/docker-compose.yaml up -dCustom spans are created using the withSpan() utility. Spans include:
Set PUBLIC_OBSERVABILITY_ENABLED=false in your .env file or use:
pnpm dev:no-otel
When disabled, withSpan() will execute the function without creating spans, ensuring no performance overhead.
The project requires these experimental features in svelte.config.js:
kit: {
experimental: {
remoteFunctions: true, // Enable remote functions
tracing: {
server: true // Enable server-side tracing
},
instrumentation: {
server: true // Enable instrumentation hook
}
}
},
compilerOptions: {
experimental: {
async: true // Enable async components
}
}
OpenTelemetry configuration is detailed in the Observability section above. The SDK is initialized in src/instrumentation.server.ts and automatically loaded by SvelteKit when the instrumentation.server experimental feature is enabled.
The project uses PostgreSQL with Drizzle ORM for type-safe database queries. Drizzle provides TypeScript-first database tooling with excellent type inference and a lightweight query builder.
The database schema is defined in src/lib/server/db/schema.ts using Drizzle's declarative API. The schema file exports table definitions and relations that Drizzle uses to generate TypeScript types and SQL migrations.
pnpm db:start - Starts the PostgreSQL database using Docker Compose. The database runs on localhost:5432 with credentials defined in compose.yaml.pnpm db:push - Pushes schema changes directly to the database (useful for development). This command compares your schema file with the database and applies differences without generating migration files.pnpm db:studio - Opens Drizzle Studio, a visual database browser. Access it at the URL shown in the terminal to explore tables, run queries, and view data.pnpm db:generate - Generates migration files based on schema changes (for production workflows).pnpm db:migrate - Runs pending migration files against the database.The schema (src/lib/server/db/schema.ts) defines the following tables:
user - User accounts with email, name, role, and ban statussession - User sessions with tokens, expiration, and metadataaccount - OAuth/provider accounts linked to usersverification - Email verification and password reset tokenspost - Blog posts with title, slug, body, and author referenceRelations are defined using Drizzle's relations() API, enabling type-safe joins and nested queries. For example, users can have many sessions and accounts, and posts reference their author.
The schema file uses Drizzle's table definition API:
export const user = pgTable('user', {
id: text('id').primaryKey(),
email: text('email').notNull().unique(),
// ... other fields
});
Each table is defined with column types (e.g., text(), timestamp(), boolean()), constraints (e.g., .primaryKey(), .notNull(), .unique()), and relationships using .references(). Drizzle infers TypeScript types from these definitions, ensuring type safety throughout your application.
The database connection is established in src/lib/server/db/index.ts, which creates a Drizzle instance with the schema, enabling typed queries like db.query.user.findMany().
The project uses Better Auth for authentication, a modern authentication library with built-in support for multiple providers, session management, and admin features.
Better Auth is configured in src/lib/auth.ts:
export const auth = betterAuth({
emailAndPassword: {
enabled: true
},
database: drizzleAdapter(db, {
provider: 'pg'
}),
plugins: [admin()]
});
The client-side auth is configured in src/lib/auth-client.ts using Better Auth's Svelte integration. It provides reactive auth state and methods that work seamlessly with SvelteKit's server-side rendering.
Routes can be protected using SvelteKit's layout server files. The (protected) route group checks for authenticated sessions and redirects unauthenticated users.
Auth is integrated into SvelteKit's hooks (src/hooks.server.ts) using the svelteKitHandler from Better Auth, which handles all auth-related requests automatically.
pnpm dev # Start dev server with observability
pnpm dev:no-otel # Start dev server without observability
pnpm build # Build for production
pnpm preview # Preview production build
pnpm check # Type check
pnpm lint # Lint code
pnpm format # Format code
This is a proof-of-concept demonstrating experimental SvelteKit features:
MIT