Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 94 additions & 11 deletions docs/docs/plugins/lakebase.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,33 +39,116 @@ await createApp({
});
```

## Accessing the pool
## Drizzle ORM (default)

Apps scaffolded with the Lakebase plugin use [Drizzle ORM](https://orm.drizzle.team/) for type-safe database queries. The Lakebase plugin provides a built-in `drizzle()` helper that creates a fully-typed Drizzle instance backed by the connection pool.

### Schema definition

Define your tables in `server/db/schema.ts`:

```ts
import { boolean, pgSchema, serial, text, timestamp } from "drizzle-orm/pg-core";

export const appSchema = pgSchema("app");

export const todos = appSchema.table("todos", {
id: serial("id").primaryKey(),
title: text("title").notNull(),
completed: boolean("completed").notNull().default(false),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});

// Inferred types for insert and select operations
export type Todo = typeof todos.$inferSelect;
export type NewTodo = typeof todos.$inferInsert;
```

After initialization, access Lakebase through the `AppKit.lakebase` object:
### Initialization

```ts
import { createApp, lakebase, server } from "@databricks/appkit";
import * as schema from "./db/schema";

const AppKit = await createApp({
plugins: [server(), lakebase()],
async onPluginsReady(appkit) {
const db = await appkit.lakebase.drizzle(schema);

// Type-safe queries
const allTodos = await db.select().from(schema.todos);
},
});
```

### Type-safe queries

await AppKit.lakebase.query(`CREATE SCHEMA IF NOT EXISTS app`);
```ts
import { eq, desc, not } from "drizzle-orm";
import { todos } from "./db/schema";

// Select
const allTodos = await db.select().from(todos).orderBy(desc(todos.createdAt));

// Insert
const [created] = await db.insert(todos).values({ title: "New todo" }).returning();

await AppKit.lakebase.query(`CREATE TABLE IF NOT EXISTS app.orders (
id SERIAL PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`);
// Update (toggle boolean)
const [updated] = await db
.update(todos)
.set({ completed: not(todos.completed) })
.where(eq(todos.id, 1))
.returning();

// Delete
await db.delete(todos).where(eq(todos.id, 1));
```

### Migrations with drizzle-kit

The template includes [drizzle-kit](https://orm.drizzle.team/docs/kit-overview) for schema migrations:

```bash
npm run db:push # Push schema directly to database (development)
npm run db:generate # Generate SQL migration files (production)
npm run db:migrate # Apply pending migrations (production)
```

:::note
`drizzle-kit` requires password authentication. Lakebase supports it alongside OAuth — enable it in the Lakebase UI under **Branch Overview** → **Authentication**.
:::

### OBO with Drizzle

On-Behalf-Of (OBO) works transparently with Drizzle. The Drizzle instance wraps AppKit's `RoutingPool`, which automatically routes queries to the per-user pool when inside `asUser(req)`:

```ts
app.get("/api/my-todos", async (req, res) => {
const userDb = await AppKit.lakebase.asUser(req).drizzle(schema);
const myTodos = await userDb.select().from(todos);
res.json(myTodos);
});
```

## Accessing the pool

For raw SQL queries or integration with other ORMs, access the pool directly:

```ts
const AppKit = await createApp({
plugins: [server(), lakebase()],
});

// Raw SQL queries
const result = await AppKit.lakebase.query(
"SELECT * FROM app.orders WHERE user_id = $1",
[userId],
);

// Raw pg.Pool (for ORMs or advanced usage)
// Raw pg.Pool (for other ORMs or advanced usage)
const pool = AppKit.lakebase.pool;

// ORM-ready config objects
// ORM-ready config objects (for TypeORM, Sequelize, etc.)
const ormConfig = AppKit.lakebase.getOrmConfig(); // { host, port, database, ... }
const pgConfig = AppKit.lakebase.getPgConfig(); // pg.PoolConfig
```
Expand Down
2 changes: 1 addition & 1 deletion knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@
"docs/**",
".github/scripts/**"
],
"ignoreDependencies": ["json-schema-to-typescript"],
"ignoreDependencies": ["json-schema-to-typescript", "drizzle-orm"],
"ignoreBinaries": ["tarball", "appkit"]
}
11 changes: 10 additions & 1 deletion packages/appkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,16 @@
"@types/json-schema": "7.0.15",
"@types/pg": "8.16.0",
"@types/ws": "8.18.1",
"@vitejs/plugin-react": "5.1.1"
"@vitejs/plugin-react": "5.1.1",
"drizzle-orm": "0.45.2"
},
"peerDependencies": {
"drizzle-orm": ">=0.34.0"
},
"peerDependenciesMeta": {
"drizzle-orm": {
"optional": true
}
},
"overrides": {
"vite": "npm:rolldown-vite@7.1.14"
Expand Down
44 changes: 43 additions & 1 deletion packages/appkit/src/plugins/lakebase/lakebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,12 +296,54 @@ export class LakebasePlugin extends Plugin implements ToolProvider {
* Use `AppKit.lakebase.asUser(req)` to get the same API backed by a per-user pool.
*/
exports() {
const pool = this.pool!;
return {
// biome-ignore lint/style/noNonNullAssertion: pool is guaranteed non-null after setup(), which AppKit always awaits before exposing the plugin API
pool: this.pool! as LakebasePool,
pool: pool as LakebasePool,
query: this.query.bind(this),
getOrmConfig: () => getLakebaseOrmConfig(this.activePoolConfig()),
getPgConfig: () => getLakebasePgConfig(this.activePoolConfig()),
/**
* Creates a Drizzle ORM instance backed by the Lakebase connection pool.
*
* Requires `drizzle-orm` as a dependency in your project (`npm install drizzle-orm`).
* The module is loaded lazily via dynamic `import()` — zero cost if this method is never called.
*
* The returned Drizzle instance uses AppKit's {@link RoutingPool}, so queries automatically
* route to the service-principal pool or per-user pool based on `asUser(req)` context.
*
* @param schema - Optional Drizzle schema object for relational query support and type inference
* @returns A fully-typed `NodePgDatabase` instance
*
* @example
* ```ts
* import * as schema from './db/schema';
* const db = await AppKit.lakebase.drizzle(schema);
* const todos = await db.select().from(schema.todos);
* ```
*/
drizzle: async <TSchema extends Record<string, unknown>>(
schema?: TSchema,
) => {
try {
const mod = await import("drizzle-orm/node-postgres");
return mod.drizzle({
client: pool as any,
...(schema ? { schema } : {}),
});
} catch (err: unknown) {
if (
err instanceof Error &&
(err.message.includes("Cannot find module") ||
err.message.includes("ERR_MODULE_NOT_FOUND"))
) {
throw new Error(
"drizzle-orm is required for appkit.lakebase.drizzle(). Install it: npm install drizzle-orm",
);
}
throw err;
}
},
};
}
}
Expand Down
15 changes: 13 additions & 2 deletions packages/lakebase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,23 @@ When used with AppKit, logging is automatically configured - see the [AppKit Int

```typescript
import { drizzle } from "drizzle-orm/node-postgres";
import { pgSchema, serial, text, timestamp } from "drizzle-orm/pg-core";
import { createLakebasePool } from "@databricks/lakebase";

// Define schema
const appSchema = pgSchema("app");
const users = appSchema.table("users", {
id: serial("id").primaryKey(),
name: text("name").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});

// Create Drizzle instance
const pool = createLakebasePool();
const db = drizzle(pool);
const db = drizzle({ client: pool, schema: { users } });

const users = await db.select().from(usersTable);
// Type-safe queries
const allUsers = await db.select().from(users);
```

### Prisma
Expand Down
101 changes: 101 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions template/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ DATABRICKS_APP_PORT=8000
#### Lakebase Configuration

The Lakebase plugin requires additional environment variables for PostgreSQL connectivity. To learn how to configure the Lakebase plugin, see the [Lakebase plugin documentation](https://www.databricks.com/devhub/docs/appkit/v0/plugins/lakebase).

This template uses [Drizzle ORM](https://orm.drizzle.team/) for type-safe database queries. Schema definitions are in `server/db/schema.ts`. For migrations:
- **Development:** `npm run db:push` (pushes schema directly to database)
- **Production:** `npm run db:generate && npm run db:migrate` (versioned migrations)
{{- end}}

### CLI Authentication
Expand Down Expand Up @@ -190,12 +194,18 @@ databricks bundle deploy -t prod
* public/ # Static assets
* server/ # Express backend
* server.ts # Server entry point
{{- if .plugins.lakebase}}
* db/ # Database schema and client (Drizzle ORM)
{{- end}}
* routes/ # Routes
* shared/ # Shared types
{{- if .plugins.analytics}}
* config/ # Configuration
* queries/ # SQL query files
{{- end}}
{{- if .plugins.lakebase}}
* drizzle.config.ts # Drizzle Kit migration config
{{- end}}
* databricks.yml # Bundle configuration
* app.yaml # App configuration
* .env.example # Environment variables example
Expand Down
2 changes: 1 addition & 1 deletion template/client/src/pages/lakebase/LakebasePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface Todo {
id: number;
title: string;
completed: boolean;
created_at: string;
createdAt: string;
}

export function LakebasePage() {
Expand Down
Loading
Loading