Database access
The provider matrix, the client, and the rules.
Matrix
BionicJS supports a matrix of database options: PostgreSQL or SQLite, combined with ORMs like Prisma, Drizzle, or Kysely. Engine and access layer are chosen independently.
| SQLite | PostgreSQL | |
|---|---|---|
| Prisma | schema-driven, TS-safe | schema-driven, TS-safe |
| Drizzle | query builder | query builder |
| Kysely | typed SQL builder | typed SQL builder |
| raw sql | direct driver | direct driver |
The combination becomes a plugin package composed in bionicjs.config.ts under the database key:
1import { defineConfig } from "@bionicjs/core";
2import { drizzle } from "@bionicjs/drizzle";
3
4export default defineConfig({
5 database: drizzle({ provider: "sqlite", url: "file:./db.sqlite" }),
6});
The client
The database client is initialized in server/db/ and is exposed at runtime as the db export of @bionicjs/core/server, making it easily accessible inside any API route handler:
1import { createClient } from "@libsql/client";
2import { drizzle } from "drizzle-orm/libsql";
3import * as schema from "./schema";
4
5const url = process.env.DATABASE_URL;
6if (!url) throw new Error("DATABASE_URL is not set");
7
8const client = createClient({ url });
9export const db = drizzle({ client, schema });
The client is constructed on the server and never imported into the client bundle. Keeping it under server/ is what guarantees the driver and connection pool stay out of the React app.
Rules
- One engine and one access layer per project - no ORM mixing.
- The client is constructed on the server and never imported into the client bundle.
- SQLite in development, PostgreSQL in production, no code changes: the URL and provider are config, not code.
DATABASE_URLis the single environment variable; the scaffolded.env.examplenames it.
Connecting at startup
Server routes can import db directly from @bionicjs/core/server. The client initializes lazily from the environment, so a missing DATABASE_URL fails fast at the first query with a clear error rather than at process start:
1import { db } from "@bionicjs/core/server";
2
3export default defineEventHandler(async () => {
4 const row = await db.query.users.findFirst();
5 return { ok: true, firstUser: row?.email ?? null };
6});
db on @bionicjs/core/server - stays the same. Swapping Drizzle for Prisma is a config change plus a rewrite of the query files, nothing else in the app moves.25 / 25