Two ways to use this template
- 1. Click "Copy prompt" below
- 2. Paste into Cursor, Claude Code, Codex, or any coding agent
- 3. Your agent builds the app — it asks questions along the way so the result is exactly what you want
Follow the steps below to set things up manually, at your own pace.
Lakebase Off-Platform
Use Lakebase from apps hosted outside Databricks App Platform (for example on AWS, Vercel, or Netlify) with portable env, token, and Drizzle patterns.
A connection from an app hosted outside the Databricks Apps platform (for example on AWS, Vercel, or Netlify) to Lakebase Postgres. The app uses portable environment configuration, token management with automatic credential refresh, and Drizzle ORM for type-safe database access.
Components
- Lakebase Environment Management — set up a Zod-validated environment configuration for secure Lakebase connection values.
- Lakebase Token Management — implement token fetch, cache, and automatic refresh for Lakebase Postgres credentials.
- Drizzle ORM with Lakebase — configure a Drizzle ORM pool with auto-refreshing credentials and migration support.
Prerequisites
Verify these Databricks workspace features are enabled before starting. If any check fails, ask your workspace admin to enable the feature.
- Databricks CLI authenticated. Run
databricks auth profilesand confirm at least one profile showsValid: YES. If none do, authenticate withdatabricks auth login --host <workspace-url> --profile <PROFILE>. - Lakebase Postgres available in the workspace. Run
databricks postgres list-projects --profile <PROFILE>and confirm the command succeeds (an empty list is fine — you are about to create the first project). Anot enabledor permission error means Lakebase is not available to this identity.
This template collects the environment variables needed to reach Lakebase from an app running outside Databricks App Platform. Verify these Databricks workspace features are enabled before starting.
- Databricks CLI authenticated. Run
databricks auth profilesand confirm at least one profile showsValid: YES. If none do, authenticate withdatabricks auth login --host <workspace-url> --profile <PROFILE>. - Lakebase Postgres available. Run
databricks postgres list-projects --profile <PROFILE>and confirm the command succeeds. Anot enablederror means Lakebase is not available to this identity. - A provisioned Lakebase project. Complete the Create a Lakebase Instance template first. You will read connection values from its branch, endpoint, and database.
- Machine-to-machine OAuth for production (optional). If you plan to run in production with a service principal, have
DATABRICKS_CLIENT_ID/DATABRICKS_CLIENT_SECRETready for that service principal. For local development, a workspace token fromdatabricks auth token --profile <PROFILE>is sufficient.
This template fetches and caches Lakebase Postgres credentials from a Node.js process. Verify these Databricks workspace features are enabled before starting.
- Databricks CLI authenticated. Run
databricks auth profilesand confirm at least one profile showsValid: YES. If none do, authenticate withdatabricks auth login --host <workspace-url> --profile <PROFILE>. - Lakebase Postgres available. Run
databricks postgres list-projects --profile <PROFILE>and confirm the command succeeds. Anot enablederror means Lakebase is not available to this identity. - A provisioned Lakebase project. Complete the Create a Lakebase Instance template first so you have a
LAKEBASE_ENDPOINTresource path to pass to the credentials API. - An env management setup. Complete the Lakebase Env Management for Off-Platform Apps template first — this template imports the validated
envmodule and expectsDATABRICKS_HOST,LAKEBASE_ENDPOINT, and eitherDATABRICKS_TOKENorDATABRICKS_CLIENT_ID+DATABRICKS_CLIENT_SECRETto be set.
This template connects an off-platform Node.js app (e.g. AWS, Vercel, Netlify) to Lakebase Postgres. Verify these Databricks workspace features are enabled before starting.
- Databricks CLI authenticated. Run
databricks auth profilesand confirm at least one profile showsValid: YES. If none do, authenticate withdatabricks auth login --host <workspace-url> --profile <PROFILE>. - Lakebase Postgres available. Run
databricks postgres list-projects --profile <PROFILE>and confirm the command succeeds. Anot enablederror means Lakebase is not available to this identity. - A provisioned Lakebase project. Complete the Create a Lakebase Instance template first so you have an endpoint host, database, and endpoint resource path available as
PGHOST,PGDATABASE, andLAKEBASE_ENDPOINT. - An env management setup for off-platform auth. Complete the Lakebase Env Management for Off-Platform Apps and Lakebase Token Management templates first — this template imports
envandgetLakebasePostgresTokenfrom those modules.
Create a Lakebase Instance
Provision a managed Lakebase Postgres project on Databricks and collect the connection values needed by downstream templates.
1. Create a Lakebase project
Create a new Lakebase Postgres project. This provisions a managed Postgres cluster with a default branch and endpoint:
databricks postgres create-project <project-name> --profile <PROFILE>
2. Verify the project resources
Confirm the branch, endpoint, and database were created:
databricks postgres list-branches \
projects/<project-name> \
--profile <PROFILE> -o json
databricks postgres list-endpoints \
projects/<project-name>/branches/production \
--profile <PROFILE> -o json
databricks postgres list-databases \
projects/<project-name>/branches/production \
--profile <PROFILE> -o json
3. Note the connection values
Record these values from the command output above. They are required by the Lakebase Data Persistence template and other Lakebase-dependent templates:
| Value | JSON path | Used for |
|---|---|---|
| Endpoint host | ...status.hosts.host | PGHOST, lakebase.postgres.host |
| Endpoint resource path | ...name | LAKEBASE_ENDPOINT, lakebase.postgres.endpointPath |
| Database resource path | ...name | lakebase.postgres.database |
| PostgreSQL database name | ...status.postgres_database | PGDATABASE, lakebase.postgres.databaseName |
References
Lakebase Environment Management for Off-Platform Apps
Define and validate the environment variables needed to connect to Lakebase from apps deployed outside Databricks App Platform (for example on AWS, Vercel, or Netlify).
1. Collect connection values via the Databricks CLI
Every value below can be obtained from the CLI. Run each command and record the result.
Workspace host (DATABRICKS_HOST):
databricks auth profiles
Use the Host column for your profile (e.g. https://dbc-xxxxx.cloud.databricks.com).
Lakebase endpoint and Postgres host (LAKEBASE_ENDPOINT, PGHOST):
databricks postgres list-endpoints \
projects/<project-name>/branches/production \
--profile <PROFILE> -o json
LAKEBASE_ENDPOINT= thenamefield (e.g.projects/<project>/branches/production/endpoints/primary)PGHOST= thestatus.hosts.hostfield
Postgres database name (PGDATABASE):
databricks postgres list-databases \
projects/<project-name>/branches/production \
--profile <PROFILE> -o json
Use the status.postgres_database field (typically databricks_postgres).
Postgres user (PGUSER):
For local development with token auth, this is your Databricks email:
databricks current-user me --profile <PROFILE> -o json
Use the userName field.
For production with M2M auth, this is the service principal's application ID used for DATABRICKS_CLIENT_ID.
Auth credentials:
For local development, get a short-lived workspace token:
databricks auth token --profile <PROFILE> -o json
Use the access_token field for DATABRICKS_TOKEN. This token expires after about one hour; the Token Management template covers automated refresh.
For production, use OAuth M2M credentials (DATABRICKS_CLIENT_ID + DATABRICKS_CLIENT_SECRET) from a service principal configured in your workspace.
2. Validate env at startup with Zod
Create src/lib/env.ts. Parsing process.env through a Zod schema on import ensures the app fails fast with a clear error when a variable is missing:
import { z } from "zod";
const baseSchema = z.object({
DATABRICKS_HOST: z.string().min(1),
LAKEBASE_ENDPOINT: z.string().min(1),
PGHOST: z.string().min(1),
PGPORT: z.coerce.number().default(5432),
PGDATABASE: z.string().min(1),
PGUSER: z.string().min(1),
PGSSLMODE: z.enum(["require", "prefer", "disable"]).default("require"),
DATABRICKS_TOKEN: z.string().optional(),
DATABRICKS_CLIENT_ID: z.string().optional(),
DATABRICKS_CLIENT_SECRET: z.string().optional(),
});
type AppEnv = z.infer<typeof baseSchema>;
function validateAuth(env: AppEnv): AppEnv {
const hasToken = Boolean(env.DATABRICKS_TOKEN);
const hasM2M =
Boolean(env.DATABRICKS_CLIENT_ID) && Boolean(env.DATABRICKS_CLIENT_SECRET);
if (!hasToken && !hasM2M) {
throw new Error(
"Set DATABRICKS_TOKEN or both DATABRICKS_CLIENT_ID and DATABRICKS_CLIENT_SECRET",
);
}
return env;
}
export const env = validateAuth(baseSchema.parse(process.env));
3. Commit an .env.example
Commit this file so every developer (and CI) knows which variables are required. Set the same keys in your hosting platform's secret/env configuration:
DATABRICKS_HOST=https://<workspace-host>
LAKEBASE_ENDPOINT=projects/<project>/branches/production/endpoints/primary
PGHOST=<status.hosts.host from list-endpoints>
PGPORT=5432
PGDATABASE=<status.postgres_database from list-databases>
PGUSER=<your Databricks email or service principal application ID>
PGSSLMODE=require
# Option A: local dev, token auth (expires ~1h, use refresh script)
DATABRICKS_TOKEN=
# Option B: production, M2M auth (service principal)
DATABRICKS_CLIENT_ID=
DATABRICKS_CLIENT_SECRET=
4. Import env early in your server entry point
Import env at the top of your server bootstrap file. The Zod parse runs on import, so any missing or invalid variable throws before the app starts accepting requests.
References
Lakebase Token Management
Fetch, cache, and automatically refresh the short-lived Postgres credentials that Lakebase requires. Supports both direct token auth (local dev) and M2M OAuth (production).
1. Add a token manager for workspace auth and Lakebase credentials
Create src/lib/lakebase/tokens.ts:
import { env } from "@/lib/env";
const REFRESH_BUFFER_MS = 2 * 60 * 1000;
type CachedToken = {
value: string;
expiresAt: number;
};
type AuthStrategy =
| { kind: "token"; token: string }
| { kind: "m2m"; host: string; clientId: string; clientSecret: string };
let cachedWorkspaceToken: CachedToken | null = null;
let workspaceRefreshPromise: Promise<CachedToken> | null = null;
let cachedLakebaseToken: CachedToken | null = null;
let lakebaseRefreshPromise: Promise<CachedToken> | null = null;
function isFresh(token: CachedToken | null): token is CachedToken {
return token !== null && Date.now() < token.expiresAt - REFRESH_BUFFER_MS;
}
function authStrategyFromEnv(): AuthStrategy {
if (env.DATABRICKS_TOKEN) {
return { kind: "token", token: env.DATABRICKS_TOKEN };
}
return {
kind: "m2m",
host: env.DATABRICKS_HOST.replace(/\/$/, ""),
clientId: env.DATABRICKS_CLIENT_ID!,
clientSecret: env.DATABRICKS_CLIENT_SECRET!,
};
}
async function fetchWorkspaceTokenM2M(
host: string,
clientId: string,
clientSecret: string,
): Promise<CachedToken> {
const response = await fetch(`${host}/oidc/v1/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: clientId,
client_secret: clientSecret,
scope: "all-apis",
}),
});
if (!response.ok) {
throw new Error(`M2M token request failed: ${response.status}`);
}
const data = (await response.json()) as {
access_token?: string;
expires_in?: number;
};
if (!data.access_token || !data.expires_in) {
throw new Error("Invalid M2M token response");
}
return {
value: data.access_token,
expiresAt: Date.now() + data.expires_in * 1000,
};
}
async function getWorkspaceToken(auth: AuthStrategy): Promise<string> {
if (auth.kind === "token") {
return auth.token;
}
if (isFresh(cachedWorkspaceToken)) {
return cachedWorkspaceToken.value;
}
if (!workspaceRefreshPromise) {
workspaceRefreshPromise = fetchWorkspaceTokenM2M(
auth.host,
auth.clientId,
auth.clientSecret,
)
.then((token) => {
cachedWorkspaceToken = token;
return token;
})
.finally(() => {
workspaceRefreshPromise = null;
});
}
return (await workspaceRefreshPromise).value;
}
async function fetchLakebaseCredential(
databricksHost: string,
workspaceToken: string,
): Promise<CachedToken> {
const response = await fetch(
`${databricksHost}/api/2.0/postgres/credentials`,
{
method: "POST",
headers: {
Authorization: `Bearer ${workspaceToken}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ endpoint: env.LAKEBASE_ENDPOINT }),
},
);
if (!response.ok) {
throw new Error(`Lakebase credential request failed: ${response.status}`);
}
const data = (await response.json()) as {
token?: string;
expire_time?: string;
};
if (!data.token || !data.expire_time) {
throw new Error("Invalid Lakebase credential response");
}
return {
value: data.token,
expiresAt: new Date(data.expire_time).getTime(),
};
}
export async function getLakebasePostgresToken(): Promise<string> {
if (isFresh(cachedLakebaseToken)) {
return cachedLakebaseToken.value;
}
if (!lakebaseRefreshPromise) {
lakebaseRefreshPromise = (async () => {
const auth = authStrategyFromEnv();
const workspaceToken = await getWorkspaceToken(auth);
return fetchLakebaseCredential(
env.DATABRICKS_HOST.replace(/\/$/, ""),
workspaceToken,
);
})()
.then((token) => {
cachedLakebaseToken = token;
return token;
})
.finally(() => {
lakebaseRefreshPromise = null;
});
}
return (await lakebaseRefreshPromise).value;
}
2. Add a script to refresh DATABRICKS_TOKEN for local dev
CLI-issued tokens expire after about one hour. Create scripts/refresh-lakebase-token.ts to write a fresh token into your local env file:
import { execSync } from "node:child_process";
import { readFileSync, writeFileSync, existsSync } from "node:fs";
const envFile = process.argv[2] ?? ".env.local";
const profile = process.env.DATABRICKS_CONFIG_PROFILE ?? "DEFAULT";
const raw = execSync(`databricks auth token --profile "${profile}" -o json`, {
encoding: "utf-8",
});
const parsed = JSON.parse(raw) as { access_token?: string };
if (!parsed.access_token) {
throw new Error("Failed to get access token from Databricks CLI");
}
if (!existsSync(envFile)) {
throw new Error(`Env file not found: ${envFile}`);
}
const content = readFileSync(envFile, "utf-8");
const tokenLine = `DATABRICKS_TOKEN="${parsed.access_token}"`;
const updated = content.includes("DATABRICKS_TOKEN=")
? content.replace(/^DATABRICKS_TOKEN=.*/m, tokenLine)
: `${content.trimEnd()}\n${tokenLine}\n`;
writeFileSync(envFile, updated);
console.log(`Updated DATABRICKS_TOKEN in ${envFile}`);
3. Verify token and credential flow
databricks auth token --profile <PROFILE> -o json
curl -sS -X POST "https://<workspace-host>/api/2.0/postgres/credentials" \
-H "Authorization: Bearer <workspace-access-token>" \
-H "Content-Type: application/json" \
-d '{"endpoint":"projects/<project>/branches/<branch>/endpoints/<endpoint>"}'
The response should include token and expire_time.
References
Drizzle ORM with Lakebase in an Off-Platform App
Connect Drizzle ORM to Lakebase in any Node.js server outside Databricks App Platform. Uses the @databricks/lakebase package for automatic OAuth token refresh.
1. Install Drizzle and the Lakebase package
npm install drizzle-orm @databricks/lakebase
npm install -D drizzle-kit tsx
drizzle-orm and drizzle-kit must be on the same major version. If drizzle-kit errors with "This version of drizzle-kit is outdated," check that both packages share the same major (e.g. both 0.x or both 1.x).
2. Create a Lakebase-backed pool and Drizzle client
Create src/lib/db/client.ts. createLakebasePool() reads env vars automatically (PGHOST, PGDATABASE, LAKEBASE_ENDPOINT, PGUSER, etc.) and handles OAuth token refresh with a 2-minute buffer:
import { drizzle } from "drizzle-orm/node-postgres";
import { createLakebasePool } from "@databricks/lakebase";
import * as itemsSchema from "@/lib/items/schema";
const pool = createLakebasePool();
export const db = drizzle({ client: pool, schema: { ...itemsSchema } });
@databricks/lakebaseis for Lakebase Autoscaling only (not compatible with Provisioned). See the manual alternative at the end if you need Provisioned support.
3. Define a Drizzle schema
Create src/lib/items/schema.ts with a starter table. Adapt the table name, columns, and types to your domain (e.g. products, orders, users):
import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
export const items = pgTable("items", {
id: serial("id").primaryKey(),
name: text("name").notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
});
Add more schema files under src/lib/<domain>/schema.ts as your app grows. The drizzle.config.ts glob (./src/lib/*/schema.ts) picks them all up automatically.
4. Write the migration script
Create scripts/db-migrate.ts. This uses the same createLakebasePool() with automatic credential handling — no need to build a temporary DATABASE_URL:
import { drizzle } from "drizzle-orm/node-postgres";
import { migrate } from "drizzle-orm/node-postgres/migrator";
import { createLakebasePool } from "@databricks/lakebase";
const pool = createLakebasePool();
const db = drizzle({ client: pool });
await migrate(db, { migrationsFolder: "./src/lib/db/migrations" });
await pool.end();
console.log("Migrations applied successfully");
5. Keep drizzle.config.ts minimal
Commands like generate only read schema files and never connect, so no dbCredentials are needed:
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/lib/*/schema.ts",
out: "./src/lib/db/migrations",
dialect: "postgresql",
});
6. Verify schema generation and migration
Generate reads schema files locally (no database connection):
npx drizzle-kit generate
Migrate fetches a fresh Lakebase credential and applies the generated SQL:
npx dotenv -e .env.local -- npx tsx scripts/db-migrate.ts
tsx does not load .env.local automatically (that is a Next.js-specific behavior), so use dotenv-cli or your framework's env-loading mechanism to inject the variables.
If both commands succeed, your Drizzle schema and Lakebase connection are working.
Manual alternative (Provisioned or full control)
If you cannot use @databricks/lakebase (e.g. Lakebase Provisioned, or you need full control over SSL and token refresh), build a manual pg.Pool with a password callback:
npm install drizzle-orm pg
npm install -D drizzle-kit @types/pg tsx
import { Pool, type PoolConfig } from "pg";
import { env } from "@/lib/env";
import { getLakebasePostgresToken } from "@/lib/lakebase/tokens";
function sslConfig(mode: "require" | "prefer" | "disable"): PoolConfig["ssl"] {
switch (mode) {
case "require":
return { rejectUnauthorized: true };
case "prefer":
return { rejectUnauthorized: false };
case "disable":
return false;
}
}
export function createLakebasePool(): Pool {
return new Pool({
host: env.PGHOST,
port: env.PGPORT,
database: env.PGDATABASE,
user: env.PGUSER,
password: () => getLakebasePostgresToken(),
ssl: sslConfig(env.PGSSLMODE),
max: 10,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 10_000,
});
}
For the migration script with this approach, build a temporary DATABASE_URL with a fresh credential and pass it to drizzle-kit migrate via execSync.