Use when working with databases in a Stacks application — configuring connections, running queries, migrations, seeding, SQL helpers, or using SQLite/MySQL/PostgreSQL/DynamoDB. Covers @stacksjs/database, bun-query-builder, config/database.ts, and the database/ migrations directory.
storage/framework/core/database/src/config/database.tsconfig/qb.ts (re-exported via config/query-builder.ts)database/migrations/ (96+ migration files, .sql format).qb/storage/framework/orm/database/src/
├── database.ts # Database class + factory functions
├── driver-config.ts # Driver types, defaults, validation, env detection
├── defaults.ts # DB_HOST_DEFAULT, DB_PORTS, DB_NAMES, DB_USERS constants
├── utils.ts # Lazy `db` proxy (main query builder entry point)
├── types.ts # sql template tag, Generated/Insertable/Updateable types
├── sql-helpers.ts # Cross-dialect helpers (now/boolTrue/param/etc.)
├── migrations.ts # runDatabaseMigration, resetDatabase, generateMigrations
├── seeder.ts # seed, seedModel$, freshSeed, listSeedableModels
├── validators.ts # Column type inference from validator types
├── column.ts # Column definition helpers
├── schema.ts # Schema definition helpers
├── table.ts # Table definition helpers
├── query-parser.ts # Query parsing utilities
├── query-logger.ts # Query logging/monitoring
├── auth-tables.ts # Auth-related table migrations (OAuth, passkeys)
├── custom/ # Custom migrations (jobs.ts, errors.ts)
├── drivers/ # sqlite.ts, mysql.ts, postgres.ts, dynamodb.ts
│ └── defaults/ # Default migration helpers (traits.ts, passwords.ts)
└── index.ts # Re-exports everything
const db = new Database(options: DatabaseOptions)
db.driver // 'sqlite' | 'mysql' | 'postgres'
db.connection // DatabaseConnectionConfig
db.isInitialized
db.query // QueryBuilder (lazy-initialized via createQueryBuilder())
db.initialize() // Calls setConfig() + createQueryBuilder() from bun-query-builder
db.switchDriver(driver, connection) // Close + reinitialize with new driver
db.close() // Close connection, reset state
// Static factories
Database.fromConfig(config, env?) // From stacks config object
Database.fromEnv() // From env vars (DB_CONNECTION, DB_DATABASE, etc.)
createDatabase(options: DatabaseOptions): DatabasecreateSqliteDatabase(database: string, options?): DatabasecreatePostgresDatabase(connection: DatabaseConnectionConfig, options?): DatabasecreateMysqlDatabase(connection: DatabaseConnectionConfig, options?): DatabasedetectDriver(): SupportedDialect -- checks DB_CONNECTION env, then DATABASE_URL prefix, defaults to 'sqlite'validateDriverConfig(driver, config): { valid: boolean, errors: string[] }mergeWithDefaults(driver, config): ConfiggetConfigFromEnv(driver): Config -- reads DB_DATABASE, DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD, DB_PREFIX, DB_SCHEMAgetConnectionString(driver, config): string -- builds sqlite://, mysql://, postgres:// URLdb Instance (utils.ts)The db export is a lazy Proxy that auto-initializes on first property access:
@stacksjs/env, calls setConfig() on bun-query-builderimport('@stacksjs/config') to override with app configcreateQueryBuilder() from bun-query-builderimport { db } from '@stacksjs/database'
// db auto-initializes here
const users = await db.selectFrom('users').where('active', '=', true).get()
initializeDbConfig(config) can be called to update the backing config at runtime.
import { sql } from '@stacksjs/database'
const query = sql`SELECT * FROM users WHERE id = ${userId}`
// Returns: { sql: 'SELECT * FROM users WHERE id = ?', parameters: [userId] }
sql.raw('NOW()') // Raw SQL (NOT parameterized) -- returns { raw: 'NOW()' }
sql.ref('users.name') // Column reference -- returns { raw: 'users.name' }
How it works: template values are replaced with ? placeholders and collected into parameters[]. Values wrapped in sql.raw() or sql.ref() are inlined directly into the SQL string.
import { sqlHelpers } from '@stacksjs/database'
const h = sqlHelpers('sqlite') // or 'mysql' or 'postgres'
h.isPostgres // false
h.isMysql // false
h.isSqlite // true
h.now // "datetime('now')" for sqlite, 'NOW()' for mysql/postgres
h.boolTrue // '1' for sqlite/mysql, 'true' for postgres
h.boolFalse // '0' for sqlite/mysql, 'false' for postgres
h.autoIncrement // 'INTEGER' for sqlite, 'SERIAL' for postgres
h.primaryKey // 'PRIMARY KEY AUTOINCREMENT' | 'PRIMARY KEY AUTO_INCREMENT' | 'PRIMARY KEY'
h.param(1) // '?' for sqlite/mysql, '$1' for postgres
h.params('a', 'b') // { sql: '?, ?', values: ['a', 'b'] } or { sql: '$1, $2', values: ['a', 'b'] }
DB_HOST_DEFAULT = '127.0.0.1'
DB_PORTS = { mysql: 3306, postgres: 5432, sqlite: 0 }
DB_NAMES = { default: 'stacks', sqlitePath: 'database/stacks.sqlite', sqliteTestingPath: 'database/stacks_testing.sqlite' }
DB_USERS = { mysql: 'root', postgres: 'postgres', sqlite: '' }
REDIS_DEFAULTS = { host: 'localhost', port: 6379 }
AWS_DEFAULTS = { region: 'us-east-1' }
getConnectionDefaults(driver: string, envProxy?): ConnectionDefaults
interface DatabaseOptions {
driver: 'sqlite' | 'mysql' | 'postgres'
connection: { database: string, host?: string, port?: number, username?: string, password?: string, url?: string }
verbose?: boolean
timestamps?: { createdAt?: string, updatedAt?: string, defaultOrderColumn?: string }
softDeletes?: { enabled?: boolean, column?: string, defaultFilter?: boolean }
hooks?: QueryBuilderConfig['hooks']
}
interface SqliteConfig { database: string, prefix?: string }
interface MysqlConfig { name: string, host?: string, port?: number, username?: string, password?: string, prefix?: string, charset?: string, collation?: string }
interface PostgresConfig { name: string, host?: string, port?: number, username?: string, password?: string, prefix?: string, schema?: string, sslMode?: 'disable' | 'require' | 'verify-ca' | 'verify-full' }
interface DynamoDbConfig { key: string, secret: string, region?: string, prefix?: string, endpoint?: string, tableName?: string, singleTable?: { enabled?, pkAttribute?, skAttribute?, entityTypeAttribute?, keyDelimiter?, gsiCount? } }
runDatabaseMigration(): Promise<Result<string, Error>> -- ensures DB exists (postgres/mysql), configures QB, preprocesses SQLite migrations, then calls qbExecuteMigration()resetDatabase(): Promise<Result<string, Error>> -- drops framework tables (OAuth, passkeys, jobs, etc.) then calls qbResetDatabase()generateMigrations(): Promise<Result<string, Error>> -- compares models to DB state, generates .sql diff filesgenerateMigrations2(): Promise<Result<string, Error>> -- full regeneration ignoring previous state ({ full: true })Before running migrations on SQLite, preprocessSqliteMigrations():
ALTER TABLE ADD CONSTRAINT to no-ops (SQLite does not support this)CREATE UNIQUE INDEX to no-ops (redundant when table already has inline UNIQUE)DROP COLUMN for non-existent columns (checks via PRAGMA table_info)oauth_refresh_tokens, oauth_access_tokens, oauth_clients, passkeys, failed_jobs, jobs, notifications, password_reset_tokens
seed(config?: SeederConfig): Promise<SeedSummary> -- loads models from both storage/framework/defaults/models/ (recursive) and app/Models/ (flat), user models override defaults by nameseedModel$(modelName, options?): Promise<SeedResult> -- seed one model by namefreshSeed(config?): Promise<SeedSummary> -- calls seed({ ...config, fresh: true }) (truncates before seeding)listSeedableModels(): Promise<Array<{ name, table, count, source: 'default' | 'user' }>> -- list without seedinginterface SeederConfig {
modelsDir?: string // defaults to path.userModelsPath()
defaultCount?: number // default 10
verbose?: boolean // default true
fresh?: boolean // truncate tables first
only?: string[] // specific models to seed
except?: string[] // models to exclude
}
traits.useSeeder (or traits.seedable) set to true or { count: N }factory: (faker) => ... generate fake data via @stacksjs/fakerhidden: true + name includes "pass") and hashed with bcryptinterface SeedResult { model: string, table: string, count: number, success: boolean, error?: string, duration: number }
interface SeedSummary { total: number, successful: number, failed: number, results: SeedResult[], duration: number }
isStringValidator, isNumberValidator, enumValidator, isBooleanValidator, isDateValidator, isUnixValidator, isFloatValidator, isDatetimeValidator, isTimestampValidator, isTimestampTzValidator, isDecimalValidator, isSmallintValidator, isIntegerValidator, isBigintValidator, isBinaryValidator, isBlobValidator, isJsonValidator
checkValidator(validator, driver): string -- converts validator type to SQL column type string (e.g., 'integer', 'text', 'varchar(255)')'text' for all strings, 'integer' for numbers; MySQL uses 'varchar(N)', native enum()Entity-centric API for single-table design:
createDynamo(config), dynamo (default instance)EntityQueryBuilder -- query builder for DynamoDB entitiesgenerateKeyPattern, parseKeyPattern, buildKey -- key pattern utilitiesmarshall, unmarshall -- DynamoDB data type conversioncreateQueryBuilder, setConfig -- core QB functionsQueryBuilder, QueryBuilderConfig, Seeder, SupportedDialect -- typesGenerated<T>, GeneratedAlways<T> -- column generation markers (both alias to T)Insertable<T>, Selectable<T>, Updateable<T> -- CRUD type utilitiesRawBuilder<T>, Sql -- raw SQL expression typesbuddy migrate -- run pending migrationsbuddy migrate:fresh -- drop all + re-migrate (add --seed to also seed)buddy make:migration <name> -- create migration filebuddy seed -- seed databasebuddy generate:migrations -- generate migration diffs from models{
default: env.DB_CONNECTION || 'mysql',
connections: { sqlite, mysql, postgres, dynamodb },
migrations: 'migrations',
migrationLocks: 'migration_locks',
queryLogging: {
enabled: true,
slowThreshold: 100, // ms
retention: 7, // days
pruneFrequency: 24, // hours
excludedQueries: ['query_logs'],
analysis: { enabled: true, analyzeAll: false, explainPlan: true, suggestions: true }
}
}
{
verbose: true,
dialect: env.DB_CONNECTION || 'sqlite',
database: { database, username?, password?, host?, port? },
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at', defaultOrderColumn: 'created_at' },
pagination: { defaultPerPage: 25, cursorColumn: 'id' },
aliasing: { relationColumnAliasFormat: 'table_column' },
relations: { foreignKeyFormat: 'singularParent_id', maxDepth: 10, maxEagerLoad: 50, detectCycles: true },
transactionDefaults: { retries: 2, isolation: 'read committed', sqlStates: ['40001', '40P01'], backoff: { baseMs: 50, factor: 2, maxMs: 2000, jitter: true } },
sql: { randomFunction: 'RANDOM()', sharedLockSyntax: 'FOR SHARE', jsonContainsMode: 'operator' },
features: { distinctOn: true },
debug: { captureText: true },
softDeletes: { enabled: false, column: 'deleted_at', defaultFilter: true }
}
config/database.ts is 'mysql' (not 'sqlite'), but utils.ts and driver-config.ts fall back to 'sqlite' when DB_CONNECTION is unsetdb export is a lazy Proxy -- it auto-initializes on first property access, which means errors are deferred until first useconfig/query-builder.ts re-exports from config/qb.ts.qb/ directory at project root stores query builder state for migration diffingresetDatabase() drops ALL tables including framework tables (OAuth, passkeys, jobs, etc.) -- only use in developmentfreshSeed() truncates tables before seeding using deleteFrom() (not DROP TABLE).sql files in-place (rewrites them to no-ops)ensureDatabaseExists() function connects to admin DB (postgres or mysql) to run CREATE DATABASE before switching to the target DBDatabase.fromConfig() appends _testing to database name/path when env === 'testing'enabled: false)read committed isolation, with exponential backoff + jitterstorage/framework/core/orm/ (package) and storage/framework/orm/ (implementation)