Skip to content

External Databases

Every Aerostack Function comes with a built-in database (env.DB — SQLite-compatible, zero config). But you can also connect to any external database — Neon (Postgres), PlanetScale (MySQL), Turso, Supabase, MongoDB, Redis, and more.


Aerostack uses Drizzle ORM as the default ORM. The CLI has built-in support for Drizzle schema generation and migrations.

  • Edge-native — designed to run on Cloudflare Workers (no Node.js-only dependencies)
  • Type-safe — full TypeScript inference from your schema
  • Lightweight — zero runtime overhead, compiles to raw SQL
  • Multi-database — same API for SQLite (D1), Postgres (Neon), MySQL (PlanetScale)
  1. Install Drizzle

    Terminal window
    npm install drizzle-orm
    npm install -D drizzle-kit
  2. Define your schema

    Create src/schema.ts:

    import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
    export const users = sqliteTable('users', {
    id: integer('id').primaryKey({ autoIncrement: true }),
    email: text('email').notNull().unique(),
    name: text('name').notNull(),
    createdAt: text('created_at').default('(datetime(\'now\'))'),
    })
    export const posts = sqliteTable('posts', {
    id: integer('id').primaryKey({ autoIncrement: true }),
    userId: integer('user_id').references(() => users.id),
    title: text('title').notNull(),
    content: text('content').notNull(),
    publishedAt: text('published_at'),
    })
  3. Generate types

    Terminal window
    aerostack generate types
  4. Use in your function

    import { drizzle } from 'drizzle-orm/d1'
    import { eq } from 'drizzle-orm'
    import * as schema from './schema'
    interface Env {
    DB: Database
    }
    export default {
    async fetch(request: Request, env: Env): Promise<Response> {
    const db = drizzle(env.DB, { schema })
    // Type-safe queries
    const allUsers = await db.select().from(schema.users)
    const userWithPosts = await db.query.users.findFirst({
    where: eq(schema.users.email, 'alice@example.com'),
    with: { posts: true }
    })
    // Insert
    const newUser = await db.insert(schema.users).values({
    email: 'bob@example.com',
    name: 'Bob'
    }).returning()
    return Response.json({ users: allUsers })
    }
    }

Neon provides serverless Postgres with a HTTP driver that works on Cloudflare Workers.

  1. Create a Neon database

    Sign up at neon.tech and create a project. Copy the connection string.

  2. Install dependencies

    Terminal window
    npm install drizzle-orm @neondatabase/serverless
    npm install -D drizzle-kit
  3. Define your schema

    Create src/schema.ts:

    import { pgTable, serial, text, timestamp, integer } from 'drizzle-orm/pg-core'
    export const users = pgTable('users', {
    id: serial('id').primaryKey(),
    email: text('email').notNull().unique(),
    name: text('name').notNull(),
    createdAt: timestamp('created_at').defaultNow(),
    })
    export const posts = pgTable('posts', {
    id: serial('id').primaryKey(),
    userId: integer('user_id').references(() => users.id),
    title: text('title').notNull(),
    content: text('content').notNull(),
    })
  4. Set your connection string

    Terminal window
    # Local development
    echo "DATABASE_URL=postgres://user:pass@ep-xxx.us-east-2.aws.neon.tech/mydb" >> .dev.vars
    # Production
    aerostack secrets set DATABASE_URL
  5. Use in your function

    import { neon } from '@neondatabase/serverless'
    import { drizzle } from 'drizzle-orm/neon-http'
    import * as schema from './schema'
    interface Env {
    DATABASE_URL: string
    DB: Database // You can still use the built-in DB alongside Neon
    }
    export default {
    async fetch(request: Request, env: Env): Promise<Response> {
    const sql = neon(env.DATABASE_URL)
    const db = drizzle(sql, { schema })
    const users = await db.select().from(schema.users)
    return Response.json({ users })
    }
    }

Turso provides distributed SQLite with edge replicas — similar to env.DB but with multi-region write support.

Terminal window
npm install @libsql/client drizzle-orm
import { createClient } from '@libsql/client/web'
import { drizzle } from 'drizzle-orm/libsql'
import * as schema from './schema'
interface Env {
TURSO_URL: string
TURSO_AUTH_TOKEN: string
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const client = createClient({
url: env.TURSO_URL,
authToken: env.TURSO_AUTH_TOKEN,
})
const db = drizzle(client, { schema })
const users = await db.select().from(schema.users)
return Response.json({ users })
}
}

Terminal window
npm install @supabase/supabase-js
import { createClient } from '@supabase/supabase-js'
interface Env {
SUPABASE_URL: string
SUPABASE_ANON_KEY: string
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY)
const { data: users, error } = await supabase
.from('users')
.select('*')
.eq('active', true)
if (error) return Response.json({ error: error.message }, { status: 500 })
return Response.json({ users })
}
}

Terminal window
npm install mongodb
import { MongoClient } from 'mongodb'
interface Env {
MONGODB_URI: string
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const client = new MongoClient(env.MONGODB_URI)
try {
const db = client.db('myapp')
const users = await db.collection('users').find({ active: true }).toArray()
return Response.json({ users })
} finally {
await client.close()
}
}
}

For caching and real-time data beyond what env.CACHE provides (pub/sub, sorted sets, streams):

Terminal window
npm install @upstash/redis
import { Redis } from '@upstash/redis'
interface Env {
UPSTASH_REDIS_REST_URL: string
UPSTASH_REDIS_REST_TOKEN: string
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const redis = new Redis({
url: env.UPSTASH_REDIS_REST_URL,
token: env.UPSTASH_REDIS_REST_TOKEN,
})
// Sorted set for leaderboard
await redis.zadd('leaderboard', { score: 100, member: 'alice' })
const top10 = await redis.zrange('leaderboard', 0, 9, { rev: true, withScores: true })
return Response.json({ leaderboard: top10 })
}
}

You don’t have to choose one or the other. Use env.DB for fast, local data and an external database for specialized needs:

export default {
async fetch(request: Request, env: Env): Promise<Response> {
const sql = neon(env.DATABASE_URL)
const pgDb = drizzle(sql)
// Fast: session lookup from built-in DB
const session = await env.DB
.prepare('SELECT * FROM sessions WHERE token = ?')
.bind(token)
.first()
// Specialized: complex Postgres query for analytics
const analytics = await pgDb.execute(
sql`SELECT date_trunc('day', created_at) as day, count(*) FROM events GROUP BY 1`
)
// Cache the result
await env.CACHE.put('analytics:daily', JSON.stringify(analytics), { expirationTtl: 3600 })
return Response.json({ session, analytics })
}
}

External database calls are 50-200x slower than env.DB. Always cache frequent reads:

async function getUser(env: Env, userId: string) {
// Check cache first
const cached = await env.CACHE.get(`user:${userId}`, 'json')
if (cached) return cached
// External DB query
const sql = neon(env.DATABASE_URL)
const result = await sql`SELECT * FROM users WHERE id = ${userId}`
const user = result[0]
// Cache for 5 minutes
if (user) {
await env.CACHE.put(`user:${userId}`, JSON.stringify(user), { expirationTtl: 300 })
}
return user
}

Never put database credentials in aerostack.toml or source code:

Terminal window
# Local development
echo "DATABASE_URL=postgres://..." >> .dev.vars
# Production
aerostack secrets set DATABASE_URL

Add .dev.vars to .gitignore.


DatabasePackageEdge CompatibleDrizzle Support
Built-in (env.DB)None neededNativeYes (d1)
Neon (Postgres)@neondatabase/serverlessYesYes (neon-http)
Turso (SQLite)@libsql/clientYesYes (libsql)
PlanetScale (MySQL)@planetscale/databaseYesYes (planetscale)
Supabase (Postgres)@supabase/supabase-jsYesVia Neon driver
MongoDB AtlasmongodbPartialNo
Upstash Redis@upstash/redisYesNo (not SQL)
CockroachDB@neondatabase/serverlessYesVia pg driver