Skip to content
Open
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
69 changes: 55 additions & 14 deletions docs/content/docs/2.database/1.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,51 @@ Install Drizzle ORM, Drizzle Kit, and the appropriate driver(s) for the database

::tabs{sync="database-dialect"}
:::tabs-item{label="PostgreSQL" icon="i-simple-icons-postgresql"}
:pm-install{name="drizzle-orm drizzle-kit postgres @electric-sql/pglite"}
::callout
NuxtHub automatically detects your database connection using environment variables:
- Uses `PGlite` (embedded PostgreSQL) if no environment variables are set.
- Uses `postgres-js` driver if you set `DATABASE_URL`, `POSTGRES_URL`, or `POSTGRESQL_URL` environment variable.
- Use `neon-http` driver with `@neondatabase/serverless` for [Neon](https://neon.com) serverless PostgreSQL.
::
::::tabs{sync="postgres-driver"}
:::::tabs-item{label="postgres-js" icon="i-simple-icons-postgresql"}
:pm-install{name="drizzle-orm drizzle-kit postgres @electric-sql/pglite"}

::::::callout
Install this for NuxtHub's default PostgreSQL auto-detection. NuxtHub uses `postgres-js` when `DATABASE_URL`, `POSTGRES_URL`, or `POSTGRESQL_URL` is set, and uses `@electric-sql/pglite` as the local fallback when no connection URL is set.
::::::
:::::

:::::tabs-item{label="node-postgres" icon="i-simple-icons-nodedotjs"}
:pm-install{name="drizzle-orm drizzle-kit pg"}
:pm-install{name="@types/pg" dev}

::::::callout
Install this when you explicitly set `driver: 'node-postgres'`. This driver uses `pg` instead of `postgres` and does not use the PGlite fallback.
::::::
:::::

:::::tabs-item{label="neon-http" icon="i-logos-neon-icon"}
:pm-install{name="drizzle-orm drizzle-kit @neondatabase/serverless"}

::::::callout
Use this with `driver: 'neon-http'` for Neon serverless PostgreSQL over HTTP.
::::::
:::::
::::
:::
:::tabs-item{label="MySQL" icon="i-simple-icons-mysql"}
:pm-install{name="drizzle-orm drizzle-kit mysql2"}
::callout
::::callout
NuxtHub automatically detects your database connection using environment variables:
- Uses `mysql2` driver if you set `DATABASE_URL` or `MYSQL_URL` environment variable.
- Requires environment variable (no local fallback).
::
::::
:::
:::tabs-item{label="SQLite" icon="i-simple-icons-sqlite"}
:pm-install{name="drizzle-orm drizzle-kit @libsql/client"}
::callout
::::callout
NuxtHub automatically detects your database connection using environment variables:
- Uses `libsql` driver for [Turso](https://turso.tech) if you set `TURSO_DATABASE_URL` and `TURSO_AUTH_TOKEN` environment variables.
- Uses `libsql` locally with file at `.data/db/sqlite.db` if no environment variables are set.
::
::tip{to="/docs/getting-started/deploy#cloudflare"}
::::
::::tip{to="/docs/getting-started/deploy#cloudflare"}
For Cloudflare D1, configure the database ID in your `nuxt.config.ts` and NuxtHub auto-generates the wrangler bindings.
::
::::
:::
::

Expand Down Expand Up @@ -326,6 +345,28 @@ This driver requires the following environment variable:
You can find your Neon connection string in the [Neon dashboard](https://console.neon.tech). The connection string format is `postgresql://user:password@hostname/database`.
::

### Node Postgres

Use the `node-postgres` driver to connect with Drizzle's [`drizzle-orm/node-postgres`](https://orm.drizzle.team/docs/connect-planetscale-postgres#node-postgres) driver. This is useful for standard PostgreSQL clients, including PlanetScale Postgres.

```ts [nuxt.config.ts]
export default defineNuxtConfig({
hub: {
db: {
dialect: 'postgresql',
driver: 'node-postgres'
}
}
})
```

Install the required dependencies:

:pm-install{name="drizzle-orm drizzle-kit pg"}
:pm-install{name="@types/pg" dev}

NuxtHub does not auto-detect `node-postgres`; set `driver: 'node-postgres'` explicitly. When `DATABASE_URL`, `POSTGRES_URL`, or `POSTGRESQL_URL` is set, NuxtHub passes it to Drizzle as the `pg` `connectionString`.

### Docker/Kubernetes Deployments

Build container images without database credentials by deferring environment resolution to runtime. Set the driver explicitly and disable automatic migrations during build and dev:
Expand All @@ -351,7 +392,7 @@ Build container images without database credentials by deferring environment res
hub: {
db: {
dialect: 'postgresql',
driver: 'postgres-js', // or 'neon-http'
driver: 'postgres-js', // or 'neon-http' or 'node-postgres'
applyMigrationsDuringBuild: false,
applyMigrationsDuringDev: false
}
Expand Down
11 changes: 11 additions & 0 deletions src/db/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ export async function createDrizzleClient(config: ResolvedDatabaseConfig, hubDir
pkg = 'drizzle-orm/postgres-js'
const { drizzle } = await import(pkg)
return drizzle({ connection: { ...connection, onnotice: () => {} }, casing })
} else if (driver === 'node-postgres') {
pkg = 'drizzle-orm/node-postgres'
const { drizzle } = await import(pkg)
const { url, ...connectionOptions } = connection
return drizzle({
connection: {
...connectionOptions,
connectionString: connection.connectionString || url
},
casing
})
} else if (driver === 'neon-http') {
const clientPkg = '@neondatabase/serverless'
const { neon } = await import(clientPkg)
Expand Down
28 changes: 26 additions & 2 deletions src/db/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,15 @@ export async function resolveDatabaseConfig(nuxt: Nuxt, hub: HubConfig): Promise
}
config.connection = defu(config.connection, { url: process.env.POSTGRES_URL || process.env.POSTGRESQL_URL || process.env.DATABASE_URL || '' })
// Only error at build time if migrations need to run
if (config.applyMigrationsDuringBuild && config.driver && ['neon-http', 'postgres-js'].includes(config.driver) && !config.connection.url) {
const hasPostgresConnectionUrl = config.connection.url || (config.driver === 'node-postgres' && config.connection.connectionString)
if (config.applyMigrationsDuringBuild && config.driver && ['neon-http', 'postgres-js', 'node-postgres'].includes(config.driver) && !hasPostgresConnectionUrl) {
throw new Error(`\`${config.driver}\` driver requires \`DATABASE_URL\`, \`POSTGRES_URL\`, or \`POSTGRESQL_URL\` environment variable when \`applyMigrationsDuringBuild\` is enabled`)
}
if (config.connection.url) {
if (hasPostgresConnectionUrl) {
config.driver ||= 'postgres-js'
break
}
if (config.driver === 'node-postgres') break
config.driver ||= 'pglite'
config.connection = defu(config.connection, { dataDir: join(hub.dir, 'db/pglite') })
await mkdir(join(hub.dir, 'db/pglite'), { recursive: true })
Expand Down Expand Up @@ -166,6 +168,8 @@ export async function setupDatabase(nuxt: Nuxt, hub: HubConfig, deps: Record<str
}
if (driver === 'postgres-js' && !deps['postgres']) {
logWhenReady(nuxt, 'Please run `npx nypm i postgres` to use PostgreSQL as database.', 'error')
} else if (driver === 'node-postgres' && (!deps.pg || !deps['@types/pg'])) {
logWhenReady(nuxt, 'Please run `npx nypm i pg` and `npx nypm i -D @types/pg` to use node-postgres as database.', 'error')
} else if (driver === 'neon-http' && !deps['@neondatabase/serverless']) {
logWhenReady(nuxt, 'Please run `npx nypm i @neondatabase/serverless` to use Neon serverless database.', 'error')
} else if (driver === 'pglite' && !deps['@electric-sql/pglite']) {
Expand Down Expand Up @@ -344,6 +348,15 @@ async function setupDatabaseClient(nuxt: Nuxt, hub: ResolvedHubConfig) {
return Object.keys(rest).length ? `{ onnotice: () => {}, ...${JSON.stringify(rest)} }` : '{ onnotice: () => {} }'
})()

const nodePostgresConnection = (() => {
if (driver !== 'node-postgres' || !connection) return undefined
const { url, ...connectionOptions } = connection as Record<string, unknown>
return {
...connectionOptions,
connectionString: connection.connectionString || url
}
})()

// For types, d1-http uses sqlite-proxy
const driverForTypes = driver === 'd1-http' ? 'sqlite-proxy' : driver

Expand Down Expand Up @@ -454,6 +467,17 @@ export { db, schema }
_db = drizzle(sql, { schema${casingOption} })`
)
}
if (driver === 'node-postgres') {
const urlExpr = connection.connectionString || connection.url ? JSON.stringify(connection.connectionString || connection.url) : `process.env.POSTGRES_URL || process.env.POSTGRESQL_URL || process.env.DATABASE_URL`
const { connectionString: _, ...connectionOptions } = (nodePostgresConnection || {}) as Record<string, unknown>
const connectionOptionsExpr = JSON.stringify(connectionOptions)
drizzleOrmContent = generateLazyDbTemplate(
`import { drizzle } from 'drizzle-orm/node-postgres'`,
` const connectionString = ${urlExpr}
if (!connectionString) throw new Error('DATABASE_URL, POSTGRES_URL, or POSTGRESQL_URL required')
_db = drizzle({ connection: { ...${connectionOptionsExpr}, connectionString }, schema${casingOption} })`
)
}
if (driver === 'd1') {
drizzleOrmContent = generateLazyDbTemplate(
`import { drizzle } from 'drizzle-orm/d1'`,
Expand Down
6 changes: 3 additions & 3 deletions src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,10 @@ export type DatabaseConfig = {
* Database driver (optional, auto-detected if not provided)
*
* SQLite drivers: 'better-sqlite3', 'libsql', 'bun-sqlite', 'd1', 'd1-http'
* PostgreSQL drivers: 'postgres-js', 'pglite', 'neon-http'
* PostgreSQL drivers: 'postgres-js', 'node-postgres', 'pglite', 'neon-http'
* MySQL drivers: 'mysql2'
*/
driver?: 'better-sqlite3' | 'libsql' | 'bun-sqlite' | 'd1' | 'd1-http' | 'postgres-js' | 'pglite' | 'neon-http' | 'mysql2'
driver?: 'better-sqlite3' | 'libsql' | 'bun-sqlite' | 'd1' | 'd1-http' | 'postgres-js' | 'node-postgres' | 'pglite' | 'neon-http' | 'mysql2'
/**
* Database connection configuration
*/
Expand Down Expand Up @@ -228,7 +228,7 @@ export type DatabaseConfig = {

export type ResolvedDatabaseConfig = DatabaseConfig & {
dialect: 'sqlite' | 'postgresql' | 'mysql'
driver: 'better-sqlite3' | 'libsql' | 'bun-sqlite' | 'd1' | 'd1-http' | 'postgres-js' | 'pglite' | 'neon-http' | 'mysql2'
driver: 'better-sqlite3' | 'libsql' | 'bun-sqlite' | 'd1' | 'd1-http' | 'postgres-js' | 'node-postgres' | 'pglite' | 'neon-http' | 'mysql2'
connection: DatabaseConnection
migrationsDirs: string[]
queriesPaths: string[]
Expand Down
68 changes: 68 additions & 0 deletions test/database.client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createDrizzleClient } from '../src/db/lib/client'
import type { ResolvedDatabaseConfig } from '../src/types'

const { nodePostgresDrizzle } = vi.hoisted(() => ({
nodePostgresDrizzle: vi.fn(() => ({ driver: 'node-postgres' }))
}))

vi.mock('drizzle-orm/node-postgres', () => ({
drizzle: nodePostgresDrizzle
}))

describe('createDrizzleClient', () => {
beforeEach(() => {
nodePostgresDrizzle.mockClear()
})

it('should create node-postgres clients with drizzle-orm/node-postgres', async () => {
const config: ResolvedDatabaseConfig = {
dialect: 'postgresql',
driver: 'node-postgres',
connection: {
connectionString: 'postgresql://user:pass@localhost:5432/db',
ssl: true
},
migrationsDirs: [],
queriesPaths: [],
applyMigrationsDuringBuild: true,
applyMigrationsDuringDev: true,
casing: 'snake_case'
}

const client = await createDrizzleClient(config, '/tmp/hub')

expect(client).toEqual({ driver: 'node-postgres' })
expect(nodePostgresDrizzle).toHaveBeenCalledWith({
connection: {
connectionString: 'postgresql://user:pass@localhost:5432/db',
ssl: true
},
casing: 'snake_case'
})
})

it('should map connection url to node-postgres connectionString', async () => {
const config: ResolvedDatabaseConfig = {
dialect: 'postgresql',
driver: 'node-postgres',
connection: {
url: 'postgresql://user:pass@localhost:5432/db',
ssl: true
},
migrationsDirs: [],
queriesPaths: [],
applyMigrationsDuringBuild: true,
applyMigrationsDuringDev: true
}

await createDrizzleClient(config, '/tmp/hub')

expect(nodePostgresDrizzle).toHaveBeenCalledWith({
connection: {
connectionString: 'postgresql://user:pass@localhost:5432/db',
ssl: true
}
})
})
})
54 changes: 54 additions & 0 deletions test/database.config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,44 @@ describe('resolveDatabaseConfig', () => {
})
})

it('should use node-postgres driver when explicitly set', async () => {
process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/db'

const nuxt = createMockNuxt()
const hub = createBaseHubConfig({
dialect: 'postgresql',
driver: 'node-postgres'
})

const result = await resolveDatabaseConfig(nuxt, hub)

expect(result).toMatchObject({
dialect: 'postgresql',
driver: 'node-postgres',
connection: {
url: 'postgresql://user:pass@localhost:5432/db'
}
})
})

it('should allow node-postgres without DATABASE_URL when applyMigrationsDuringBuild is false', async () => {
const nuxt = createMockNuxt()
const hub = createBaseHubConfig({
dialect: 'postgresql',
driver: 'node-postgres',
applyMigrationsDuringBuild: false
})

const result = await resolveDatabaseConfig(nuxt, hub)

expect(result).toMatchObject({
dialect: 'postgresql',
driver: 'node-postgres',
connection: { url: '' },
applyMigrationsDuringBuild: false
})
})

it('should not use neon-http driver automatically', async () => {
process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/db'

Expand Down Expand Up @@ -307,6 +345,22 @@ describe('resolveDatabaseConfig', () => {
)
})

it('should not treat connectionString as DATABASE_URL for neon-http', async () => {
const nuxt = createMockNuxt()
const hub = createBaseHubConfig({
dialect: 'postgresql',
driver: 'neon-http',
connection: {
connectionString: 'postgresql://user:pass@localhost:5432/db'
},
applyMigrationsDuringBuild: true
})

await expect(resolveDatabaseConfig(nuxt, hub)).rejects.toThrow(
'`neon-http` driver requires `DATABASE_URL`, `POSTGRES_URL`, or `POSTGRESQL_URL` environment variable when `applyMigrationsDuringBuild` is enabled'
)
})

it('should allow neon-http without DATABASE_URL when applyMigrationsDuringBuild is false', async () => {
const nuxt = createMockNuxt()
const hub = createBaseHubConfig({
Expand Down
Loading