🏗️ Modular Structure
11 focused modules replace monolithic 1,464-line file. Each module has a single responsibility.
The astro-basics database architecture uses a modular abstraction layer (v2.0.0) that provides a clean, unified API for database operations while supporting multiple providers. The architecture implements the delegation pattern for improved maintainability and scalability.
The database abstraction layer provides:
🏗️ Modular Structure
11 focused modules replace monolithic 1,464-line file. Each module has a single responsibility.
🔄 Delegation Pattern
Provider classes delegate operations to specialized feature classes for clean separation of concerns.
🎯 Single API
Same code works with both Supabase (PostgreSQL) and Turso (LibSQL) - no provider-specific logic needed.
🚀 Easy Extension
Add new features by creating feature-specific modules. No risk to existing operations.
src/libs/database/├── index.ts # Public API├── factory.ts # Provider detection├── shared/│ └── field-validators.ts # Shared utilities (DRY)└── providers/ ├── turso/ │ ├── index.ts # TursoDatabase (delegation) │ ├── messages.ts # Message operations │ ├── events.ts # Event operations │ ├── users.ts # User operations │ └── converters.ts # Type conversions └── supabase/ └── (same structure)import { getDatabase } from '#libs/database'
// Automatically detects and uses configured providerconst db = getDatabase()
// Check which provider is activeconsole.log(db.getProviderName()) // 'turso' or 'supabase'import type { ClientData } from '#libs/database-types'
// Create clientconst clientData: ClientData = { nickname: 'John Doe', gender: 'prefer_not_to_say', email: 'john@example.com',}
const clientId = await db.insertClient(clientData)
// Query clientsconst clients = await db.getClients({ limit: 10,})
// Fetch one backconst client = await db.getClientById(clientId)import type { EventData } from '#libs/database'
// Create eventconst eventData: EventData = { name: 'Team Meeting', event_date: '2025-12-15', start_time: '14:00', duration: 60, location: 'Conference Room A', created_by: userId,}
const eventId = await db.insertEvent(eventData)
// Query upcoming eventsconst upcomingEvents = await db.getEvents({ from_date: new Date().toISOString().split('T')[0], limit: 50,})Write your code once using the Database interface. The abstraction layer handles provider-specific details:
// Same code works with both providersconst clients = await db.getClients({ limit: 10 })// LibSQL-specific implementation (hidden from you)const result = await tursoClient.execute({ sql: 'SELECT * FROM clients LIMIT ?', args: [10],})// PostgreSQL-specific implementation (hidden from you)const { data } = await supabaseClient .from('clients') .select('*') .limit(10)Provider classes delegate operations to specialized feature modules:
export class TursoDatabase implements Database { private clientOps: TursoClientOperations private eventOps: TursoEventOperations private userOps: TursoUserOperations
async insertClient(data: ClientData): Promise<string> { return await this.clientOps.insertClient(data) // Delegation! }
async insertEvent(data: EventData): Promise<string> { return await this.eventOps.insertEvent(data) // Delegation! }}Benefits:
Common validation and security logic is centralized in shared/ directory:
// Single source of truth for allowed event fieldsexport const EVENT_ALLOWED_FIELDS: Set<keyof Event> = new Set([ 'id', 'name', 'event_date', 'location', // ... all valid fields])
// Prevents SQL injection via field whitelistingexport function validateEventFields(fields?: (keyof Event)[]): string { const validFields = fields?.filter(f => EVENT_ALLOWED_FIELDS.has(f)) return validFields?.join(', ') ?? '*'}Used by both providers: No code duplication, consistent security.
The system automatically detects available providers:
import { detectDatabaseProviders } from '#libs/database'
const detection = await detectDatabaseProviders()console.log(detection)// {// provider: 'supabase',// available: ['turso', 'supabase'],// configured: ['supabase'],// recommended: 'supabase'// }Detection Priority:
DATABASE_PROVIDER environment variableThe modular architecture makes it easy to add new features without touching existing code.
Example: Adding a “comments” feature
export class TursoCommentOperations { async insertComment(data: CommentData): Promise<number> { // Turso-specific implementation }
async getComments(options?: CommentQueryOptions): Promise<Comment[]> { // Turso-specific implementation }}export class SupabaseCommentOperations { async insertComment(data: CommentData): Promise<number> { // Supabase-specific implementation }
async getComments(options?: CommentQueryOptions): Promise<Comment[]> { // Supabase-specific implementation }}// providers/turso/index.ts (and supabase/index.ts)import { TursoCommentOperations } from './comments'
export class TursoDatabase implements Database { private commentOps: TursoCommentOperations
constructor() { // ... existing operations this.commentOps = new TursoCommentOperations() }
async insertComment(data: CommentData): Promise<number> { return await this.commentOps.insertComment(data) }}export interface Database { // ... existing operations insertComment(data: CommentData): Promise<number> getComments(options?: CommentQueryOptions): Promise<Comment[]>}Done! New feature added without modifying existing message or event code.
// ✅ CORRECTimport { getDatabase } from '#libs/database'const db = getDatabase()const clients = await db.getClients()
// ❌ WRONG - Direct provider accessimport { createClient } from '@supabase/supabase-js'const client = createClient(url, key)// ✅ CORRECTimport { getDatabase } from '#libs/database'import type { Event } from '#libs/database'
// ❌ WRONG - Relative importsimport { getDatabase } from '../../libs/database'// ✅ CORRECTconst client = await db.getClientById(clientId)if (client) { console.log(client.nickname)} else { console.log('Client not found')}
// ❌ WRONG - Potential null referenceconst client = await db.getClientById(clientId)console.log(client.nickname) // Error if client is null// ✅ CORRECTimport type { EventData, EventQueryOptions } from '#libs/database'
const eventData: EventData = { name: 'Meeting', event_date: '2025-12-10',}
const options: EventQueryOptions = { from_date: '2025-12-01', limit: 50,}
// ❌ WRONG - No type checkingconst eventData = { name: 'Meeting', date: '2025-12-10', // Wrong field name, no error!}The modular architecture has no performance impact:
Benchmark (10,000 operations):
| Operation | v1.x (monolithic) | v2.0 (modular) | Difference |
|---|---|---|---|
| insertEvent | 1,543ms | 1,537ms | -0.4% |
| getEvents | 945ms | 949ms | +0.4% |