Skip to main content
Container Kit uses LibSQL (SQLite-compatible) with Drizzle ORM for type-safe database operations, managed through Tauri’s SQL plugin.

Database Stack

LibSQL

SQLite-compatible embedded database

Drizzle ORM

Type-safe TypeScript ORM

Tauri SQL Plugin

Bridge between Rust and frontend

Architecture Overview

The database layer spans both frontend (TypeScript) and backend (Rust):

Schema Definition

Schemas are defined using Drizzle ORM in TypeScript:
src/lib/db/schema.ts
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
import { v7 as uuid } from 'uuid';

export const registry = sqliteTable('registry', {
    id: text('id')
        .primaryKey()
        .$defaultFn(() => uuid()),
    name: text('name').notNull(),
    url: text('url').unique().notNull(),
    loggedIn: integer('logged_in', { mode: 'boolean' }).default(false)
});

export const seeds = sqliteTable('seeds', {
    id: text('id')
        .primaryKey()
        .$defaultFn(() => uuid()),
    name: text('name').notNull().unique(),
    applied: integer('applied', { mode: 'boolean' }).default(false)
});

Schema Features

Tables use UUIDv7 for primary keys, providing time-ordered unique identifiers:
id: text('id')
    .primaryKey()
    .$defaultFn(() => uuid())
Boolean fields use SQLite integers with type hints:
loggedIn: integer('logged_in', { mode: 'boolean' }).default(false)
Unique constraints ensure data integrity:
url: text('url').unique().notNull()

Database Client

The database client uses Drizzle’s sqlite-proxy to communicate with Tauri’s SQL plugin:
src/lib/db/index.ts
import { drizzle } from 'drizzle-orm/sqlite-proxy';
import * as schema from './schema';
import Database from '@tauri-apps/plugin-sql';

const selectRegex = /^\s*SELECT\b/i;

function isSelectQuery(sql: string): boolean {
    return selectRegex.test(sql);
}

export const db = drizzle<typeof schema>(
    async (sql: string, params: string[], method: string) => {
        const sqlite = await Database.load('sqlite:container-kit.db');
        let rows: any[] = [];
        let results: any = [];

        try {
            if (isSelectQuery(sql)) {
                rows = await sqlite.select(sql, params as string[]);
            } else {
                await sqlite.execute(sql, params as string[]);
                return { rows: [] };
            }

            // Convert object rows to array format for Drizzle
            rows = rows.map((row: any) => Object.values(row));
            results = method === 'all' ? rows : rows[0];
        } catch (e: any) {
            console.error('SQL Error:', e);
            return { rows: [] };
        } finally {
            await sqlite.close(); // Always close connection
        }

        return { rows: results };
    },
    { schema: schema, logger: true }
);

Key Design Decisions

1

SELECT Query Detection

Uses regex to distinguish SELECT from INSERT/UPDATE/DELETE queries, as each requires different handling.
2

Row Format Conversion

Tauri SQL plugin returns objects, but Drizzle expects arrays. The client converts between formats:
rows = rows.map((row: any) => Object.values(row));
3

Connection Management

Database connections are opened per-query and always closed in the finally block to prevent leaks.
4

Error Handling

SQL errors are logged and return empty results to prevent crashes.

Database Migrations

Migration Workflow

Container Kit uses a two-stage migration process:
  1. Define schema in src/lib/db/schema.ts
  2. Generate SQL migrations with Drizzle Kit:
pnpm db:generate
This creates SQL files in src-tauri/migrations/

Migration Configuration

drizzle.config.ts
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
    out: './src-tauri/migrations/',
    schema: './src/lib/db/schema.ts',
    dialect: 'sqlite',
    verbose: true,
    strict: true
});

Example Migration

src-tauri/migrations/0000_slow_scarecrow.sql
CREATE TABLE `registry` (
	`id` text PRIMARY KEY NOT NULL,
	`name` text NOT NULL,
	`url` text NOT NULL,
	`logged_in` integer DEFAULT false
);

CREATE UNIQUE INDEX `registry_url_unique` ON `registry` (`url`);

CREATE TABLE `seeds` (
	`id` text PRIMARY KEY NOT NULL,
	`name` text NOT NULL,
	`applied` integer DEFAULT false
);

CREATE UNIQUE INDEX `seeds_name_unique` ON `seeds` (`name`);

Database Operations

Type-Safe Queries

Drizzle provides fully typed database operations:
import { db } from '$lib/db';
import { registry } from '$lib/db/schema';
import { eq } from 'drizzle-orm';

// Insert
const newRegistry = await db.insert(registry).values({
    name: 'Docker Hub',
    url: 'https://registry.hub.docker.com',
    loggedIn: false
});

// Select all
const allRegistries = await db.select().from(registry);

// Select with filter
const loggedInRegistries = await db
    .select()
    .from(registry)
    .where(eq(registry.loggedIn, true));

// Update
await db
    .update(registry)
    .set({ loggedIn: true })
    .where(eq(registry.url, 'https://registry.hub.docker.com'));

// Delete
await db
    .delete(registry)
    .where(eq(registry.id, 'some-uuid'));

Service Layer

Database operations are typically wrapped in service functions:
src/lib/services/sqlite/registry.ts
import { db } from '$lib/db';
import { registry } from '$lib/db/schema';
import { eq } from 'drizzle-orm';

export async function getAllRegistries() {
    return await db.select().from(registry);
}

export async function addRegistry(name: string, url: string) {
    return await db.insert(registry).values({ name, url });
}

export async function loginRegistry(id: string) {
    return await db
        .update(registry)
        .set({ loggedIn: true })
        .where(eq(registry.id, id));
}

export async function removeRegistry(id: string) {
    return await db.delete(registry).where(eq(registry.id, id));
}

Database Location

The database file is stored in the application data directory:
~/Library/Application Support/com.ethercorps.container-kit/container-kit.db
The database is automatically created and migrated on first launch using Tauri’s SQL plugin.

Best Practices

Always Close Connections

Use try/finally blocks to ensure database connections are closed:
try {
    // Database operations
} finally {
    await sqlite.close();
}

Use Transactions

For multiple related operations, use transactions to ensure atomicity:
await db.transaction(async (tx) => {
    await tx.insert(registry).values(...);
    await tx.insert(seeds).values(...);
});

Validate Input

Always validate user input before database operations:
if (!isValidUrl(url)) {
    throw new Error('Invalid URL');
}

Handle Errors

Catch and handle database errors gracefully:
try {
    await db.insert(registry).values(...);
} catch (error) {
    console.error('Failed to insert:', error);
    // Show user-friendly error
}

Seeding Data

The seeds table tracks which seed scripts have been applied:
src/lib/db/seeds/registry.ts
import { db } from '$lib/db';
import { registry, seeds } from '$lib/db/schema';
import { eq } from 'drizzle-orm';

const SEED_NAME = 'default-registries';

export async function seedDefaultRegistries() {
    // Check if already applied
    const existing = await db
        .select()
        .from(seeds)
        .where(eq(seeds.name, SEED_NAME));
    
    if (existing.length > 0 && existing[0].applied) {
        return; // Already seeded
    }
    
    // Add default registries
    await db.insert(registry).values([
        { name: 'Docker Hub', url: 'https://registry.hub.docker.com' },
        { name: 'GitHub', url: 'https://ghcr.io' }
    ]);
    
    // Mark as applied
    await db.insert(seeds).values({
        name: SEED_NAME,
        applied: true
    });
}

Performance Considerations

SQLite is single-threaded. For concurrent operations, ensure queries are properly queued or use transactions.

Indexing

Unique constraints automatically create indexes:
CREATE UNIQUE INDEX `registry_url_unique` ON `registry` (`url`);
For frequently queried columns, add explicit indexes:
export const registry = sqliteTable('registry', {
    // ... fields
}, (table) => ({
    nameIdx: index('name_idx').on(table.name),
}));

Next Steps

Architecture

Learn about the overall application architecture

Security

Understand security features and data protection

Build docs developers (and LLMs) love