bionicjs.config.ts
The file, the plugins, and the runtime exports it drives.
The file
Every BionicJS project has a bionicjs.config.ts at its root. This is the single configuration surface for the framework. There is no vite.config.ts, no nitro.config.ts, and no tooling configuration the developer needs to manage.
1import { defineConfig } from "@bionicjs/core";
2import { betterAuth } from "@bionicjs/better-auth";
3import { drizzle } from "@bionicjs/drizzle";
4import { anthropic } from "@bionicjs/anthropic";
5import { celery } from "@bionicjs/celery";
6
7export default defineConfig({
8 auth: betterAuth({ emailAndPassword: { enabled: true } }),
9 database: drizzle({ provider: "sqlite", url: "file:./db.sqlite" }),
10 ai: anthropic({ model: "claude-sonnet-4-5" }),
11 jobs: celery({ broker: "redis://localhost:6379" }),
12});
Each top-level key - auth, database, ai, jobs - is a plugin. The provider packages ( @bionicjs/better-auth, @bionicjs/drizzle, @bionicjs/anthropic, @bionicjs/celery, and the rest) export the factory you call with your options. The framework reads the file at startup and wires each plugin into the dev server, the build, and the runtime exports.
Plugins
Every first-party plugin is a thin add-on package built on one of four factories exported from bionicjs - one per capability:
| Capability | Factory | Key | Packages |
|---|---|---|---|
auth | createAuthPlugin | auth | better-auth · clerk · firebase · supabase · workos |
database | createDatabasePlugin | database | prisma · drizzle · kysely · sql |
ai | createAiPlugin | ai | anthropic · openai · google |
jobs | createJobsPlugin | jobs | celery · rq · dramatiq |
The provider package stays thin and the shared logic lives in the factory. The factory takes options (the provider, model, path, and feature flags) plus optional setup and generateExports hooks.
1import { createAuthPlugin } from "@bionicjs/core";
2
3export function betterAuth(options: BetterAuthConfig) {
4 return createAuthPlugin({
5 name: "better-auth",
6 options,
7 path: "/api/auth/[...auth]",
8 generateExports: () =>
9 `export const auth = {
10 getSession: async () => null,
11 requireAuth: async () => null,
12};`,
13 });
14}
defineConfig
defineConfig is a typed helper exported from bionicjs. It returns its input unchanged - its value is TypeScript autocompletion and type safety, not runtime transformation.
1export interface BionicJSPlugin<TName extends string = string, TExports = any> {
2 name: TName;
3 setup: (app: any) => void | Promise<void>;
4 generateExports?: () => string | Promise<string>;
5}
6
7export type BionicJSConfig = {
8 [key: string]: BionicJSPlugin | any;
9};
10
11export function defineConfig(config: BionicJSConfig): BionicJSConfig {
12 return config;
13}
This is the same pattern used by Vite's own defineConfig, Nitro's defineNitroConfig, and virtually every modern TypeScript tool. The developer gets IDE help; the framework reads the file at startup. The config object is deliberately permissive - any key may hold a plugin or a plain value (a port, a flag), so additions don't require a config type change.
Runtime exports
Plugins exist to make capabilities importable from real code. The client-side RPC helper is api, exported from bionicjs:
1import { api } from "@bionicjs/core";
2
3const result = await api.users.sayHello({ name: "bionicjs" });
The server-side handles - auth, the database client, the AI connection, and the jobs client - come from a single import, @bionicjs/core/server. Their implementations are generated at dev/build time from the generateExports return value of whichever plugins you composed:
1import { auth, db, ai, jobs } from "@bionicjs/core/server";
2
3const session = await auth.getSession();
4const row = await db.query.users.findFirst();
5const reply = await ai.chat("hello");
6await jobs.enqueue("send-email", { to: "you@bionicjs.dev" });
@bionicjs/core/server is a generated boundary: import it in your server-side code, never from the client. Compose only the plugins you use, and only those exports exist.12 / 25