Capabilities/Database access

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.

SQLitePostgreSQL
Prismaschema-driven, TS-safeschema-driven, TS-safe
Drizzlequery builderquery builder
Kyselytyped SQL buildertyped SQL builder
raw sqldirect driverdirect driver

The combination becomes a plugin package composed in bionicjs.config.ts under the database key:

bionicjs.config.ts
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:

A Drizzle + SQLite client
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_URL is the single environment variable; the scaffolded .env.example names 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:

server/api/health.ts
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});
ⓘ
The access layer (Prisma, Drizzle, Kysely) only changes the query code. The boundary - 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.