Drizzle Plugin
Drizzle ORM is a modern, TypeScript-first SQL mapper that prioritizes type safety and zero runtime overhead. The DrizzlePlugin brings this philosophy into Karin, providing a structured way to manage SQL connections across various drivers (Postgres, MySQL, SQLite, LibSQL).
Concept: Type-Safe SQL
Unlike legacy ORMs that treat the database as a "Black Box" of objects, Drizzle treats it as a relational engine. It doesn't try to hide SQL; it tries to make SQL safe.
If your schema says email is NOT NULL, Drizzle ensures your TypeScript code cannot insert null. It effectively extends your compiler's checks all the way to the database storage.
Multi-Driver Architecture (The Adapter Pattern)
Karin uses an Adapter Pattern to decouple the framework from the underlying database driver. You can switch from sqlite locally to postgres in production by simply swapping the adapter class in your configuration.
Supported Adapters
- LibSQLAdapter (
@project-karin/drizzle/adapters/libsql): For Turso and Edge environments. Uses HTTP or WebSocket. - PostgresAdapter (
@project-karin/drizzle/adapters/postgres): Standardpgdriver for Node.js. - NeonAdapter (
@project-karin/drizzle/adapters/neon): Optimized for Neon Serverless Postgres (HTTP). - MysqlAdapter (
@project-karin/drizzle/adapters/mysql): Usesmysql2pool.
Dependency Injection
The plugin registers the Database instance in the container. Because the type of the database depends on the driver (e.g., LibSQLDatabase vs PostgresJsDatabase), we provide the @InjectDrizzle() decorator to abstract the token retrieval.
import { Service } from "@project-karin/core";
import { InjectDrizzle } from "@project-karin/drizzle";
// Import the specific type for your driver to get IntelliSense
import { LibSQLDatabase } from "drizzle-orm/libsql";
import * as schema from "../entities/schema";
@Service()
export class UserService {
constructor(
@InjectDrizzle() private readonly db: LibSQLDatabase<typeof schema>,
) {}
async create(email: string) {
// 100% Type-Safe Insert
return this.db
.insert(schema.users)
.values({
id: crypto.randomUUID(),
email,
})
.returning();
}
}Schema Management
In Drizzle, you define schemas as exported constants, not classes. However, to maintain Karin's folder structure conventions, we recommend grouping them in entities files.
File: src/users/entities/users.entity.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: text("id").primaryKey(),
email: text("email").notNull().unique(),
role: text("role", { enum: ["admin", "member"] }).default("member"),
createdAt: integer("created_at", { mode: "timestamp" }).defaultNow(),
});Aggregation: You must pass a consolidated schema object to the adapter. This allows the Drizzle query builder to know about relations.
// main.ts
import * as userSchema from "./users/entities/users.entity";
import * as postSchema from "./posts/entities/posts.entity";
const adapter = new LibSQLAdapter(
{ url: "..." },
{ schema: { ...userSchema, ...postSchema } },
);Migrations Strategy
Karin does NOT run migrations at runtime.
Running DDL (Schema changes) during application boot is a dangerous anti-pattern that can lead to race conditions in distributed systems (multiple instances trying to alter the same table).
Instead, treat migrations as a Deployment Step.
- Use
drizzle-kit generateto create SQL files locally. - Use
drizzle-kit pushor a CI/CD pipeline step to apply them before the new application version starts.
Serverless Optimization
Drizzle is lightweight and perfect for Serverless (0 dependencies at runtime if using HTTP). The plugin automatically handles the nuances of connection reuse.
For Cloudflare Workers or Vercel Edge, ensure you are using the LibSQLAdapter or NeonAdapter, as standard TCP drivers (pg, mysql2) often fail in these environments due to socket limitations (though pg works in Workers via pg-native polyfills, HTTP is often safer).
