Skip to main content

Database Adapter Pattern

SvelteBolt uses a database adapter pattern to provide a consistent API for database operations across different database providers. This abstraction allows you to switch between databases (Supabase, PocketBase, etc.) without changing your application code.

How It Works

The adapter pattern provides a unified interface for database operations, regardless of which database you're using behind the scenes.

Database Adapter Interface

// src/lib/server/database/types.ts
export interface DatabaseAdapter {
auth: {
signInWithOtp: (data: any) => Promise<any>;
signInWithOAuth: (provider: any, options?: any) => Promise<any>;
signInWithPassword: (email: string, password: string) => Promise<any>;
signOut: () => Promise<any>;
resetPasswordForEmail: (email: string, options?: any) => Promise<any>;
updatePassword: (new_password: string) => Promise<any>;
signUp: (email: string, password: string) => Promise<any>;
};
storage: {
fetchAvatarUrl: (userId: string) => Promise<string | null>;
uploadAvatar: (userId: string, file: File) => Promise<any>;
};
data: {
create: (collection: string, data: any) => Promise<any>;
readByColumn: (collection: string, query: any, column: string, eq: string) => Promise<any>;
readById: (collection: string, query: any, id: string) => Promise<any>;
read: (collection: string, query: any) => Promise<any>;
update: (collection: string, id: string, data: any) => Promise<any>;
delete: (collection: string, id: string) => Promise<any>;
};
}

Creating the Adapter

// src/lib/server/database/index.ts
import type { DatabaseAdapter } from './types';
import { SupabaseAdapter } from './supabase';

export function createDatabaseAdapter(locals: App.Locals): DatabaseAdapter {
if ('supabase' in locals) {
return new SupabaseAdapter(locals.supabase);
}
throw new Error('No database adapter found in locals');
}

Using the Database Adapter

In your SvelteKit routes, you create an adapter instance and use its methods:

// src/routes/products/+page.server.ts
import { createDatabaseAdapter } from '$lib/server/database';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ locals }) => {
const db = createDatabaseAdapter(locals);

// Use the adapter's methods
const products = await db.data.read('products', '*');

return {
products
};
};

Available Methods

Data Operations

// Create a new record
const product = await db.data.create('products', {
name: 'iPhone',
price: 999
});

// Read all records
const products = await db.data.read('products', '*');

// Read by ID
const product = await db.data.readById('products', '*', 'product-id');

// Read by column
const userProducts = await db.data.readByColumn('products', '*', 'user_id', userId);

// Update record
const updated = await db.data.update('products', 'product-id', {
price: 899
});

// Delete record
await db.data.delete('products', 'product-id');

Authentication Operations

// Sign up
await db.auth.signUp('[email protected]', 'password');

// Sign in
await db.auth.signInWithPassword('[email protected]', 'password');

// OAuth sign in
await db.auth.signInWithOAuth('google', { redirectTo: '/dashboard' });

// Password reset
await db.auth.resetPasswordForEmail('[email protected]');

// Sign out
await db.auth.signOut();

Storage Operations

// Upload avatar
await db.storage.uploadAvatar(userId, file);

// Get avatar URL
const avatarUrl = await db.storage.fetchAvatarUrl(userId);

Benefits of the Adapter Pattern

1. Database Agnostic

Your application code doesn't need to know which database you're using. You can switch from Supabase to PocketBase by just changing the adapter implementation.

2. Consistent API

All database operations use the same method signatures, making your code predictable and easy to understand.

3. Easy Testing

You can create mock adapters for testing without needing a real database.

4. Type Safety

The TypeScript interface ensures you're using the correct method signatures and return types.

Example: Supabase Adapter Implementation

// src/lib/server/database/supabase.ts
import type { DatabaseAdapter } from './types';
import type { SupabaseClient } from '@supabase/supabase-js';

export class SupabaseAdapter implements DatabaseAdapter {
constructor(private supabase: SupabaseClient) {}

data = {
create: async (collection: string, data: any) => {
const { data: result, error } = await this.supabase
.from(collection)
.insert(data)
.single();
if (error) throw error;
return result;
},

read: async (collection: string, query: any) => {
const { data, error } = await this.supabase
.from(collection)
.select(query);
if (error) throw error;
return data;
},

readById: async (collection: string, query: any, id: string) => {
const { data, error } = await this.supabase
.from(collection)
.select(query)
.eq('id', id)
.single();
if (error) throw error;
return data;
},

// ... other methods
};

auth = {
signUp: async (email: string, password: string) => {
return this.supabase.auth.signUp({ email, password });
},

signInWithPassword: async (email: string, password: string) => {
return this.supabase.auth.signInWithPassword({ email, password });
},

// ... other auth methods
};

storage = {
fetchAvatarUrl: async (userId: string) => {
// Implementation specific to Supabase Storage
},

uploadAvatar: async (userId: string, file: File) => {
// Implementation specific to Supabase Storage
}
};
}

Creating Custom Adapters

You can create adapters for other databases by implementing the DatabaseAdapter interface:

// src/lib/server/database/pocketbase.ts
export class PocketbaseAdapter implements DatabaseAdapter {
constructor(private pb: PocketBase) {}

data = {
create: async (collection: string, data: any) => {
return await this.pb.collection(collection).create(data);
},

read: async (collection: string, query: any) => {
return await this.pb.collection(collection).getFullList();
},

// ... implement all required methods
};

// ... implement auth and storage
}

Then update the factory function:

// src/lib/server/database/index.ts
export function createDatabaseAdapter(locals: App.Locals): DatabaseAdapter {
if ('supabase' in locals) {
return new SupabaseAdapter(locals.supabase);
}
if ('pocketbase' in locals) {
return new PocketbaseAdapter(locals.pocketbase);
}
throw new Error('No database adapter found in locals');
}

Best Practices

1. Keep It Simple

The adapter should provide basic CRUD operations. Complex business logic belongs in service layers.

2. Error Handling

Always handle database errors consistently across adapters.

3. Type Safety

Use TypeScript interfaces to ensure all adapters implement the same methods.

4. Testing

Create mock adapters for unit testing:

export class MockAdapter implements DatabaseAdapter {
private data: Map<string, any[]> = new Map();

data = {
create: async (collection: string, data: any) => {
const items = this.data.get(collection) || [];
const newItem = { id: 'mock-id', ...data };
items.push(newItem);
this.data.set(collection, items);
return newItem;
},
// ... other mock implementations
};
}

The adapter pattern is what makes SvelteBolt flexible and database-agnostic while keeping your application code clean and consistent.