Appearance
Database: Getting Started
Holo-JS's database layer is built around configurable runtime setup, typed model definitions, and a fluent query surface.
Typical flow
Most applications follow this path:
- configure a connection in
config/database.ts - write migrations in
server/db/migrations - run
npx holo migrate - let Holo-JS refresh the internal generated schema metadata under
.holo-js/generated - define models under
server/models - query through
DB.table(...)or the ORM
Database config
Put database setup in config/database.ts.
ts
import { defineDatabaseConfig } from '@holo-js/db'
import { env } from '@holo-js/config'
export default defineDatabaseConfig({
defaultConnection: 'main',
connections: {
main: {
driver: 'sqlite',
url: './storage/database.sqlite',
},
analytics: {
driver: 'postgres',
url: env('ANALYTICS_DATABASE_URL'),
},
},
})Use SQLite for local development or switch to MySQL and Postgres by changing the named connection definitions.
Install every concrete driver referenced by the configuration:
bash
bun add @holo-js/db-sqlite
# or: bun add @holo-js/db-postgres
# or: bun add @holo-js/db-mysqlWhen constructing an adapter directly, import it from its concrete package:
ts
import { createPostgresAdapter } from '@holo-js/db-postgres'
const adapter = createPostgresAdapter({
connectionString: process.env.DATABASE_URL,
})The same application-level query and model code should stay stable while the configured database driver changes underneath it.
CLI workflow
For day-to-day database work, use the Holo-JS CLI:
bash
npx holo list
npx holo migrate
npx holo migrate:fresh --seed
npx holo seednpx holo list shows internal database commands plus app commands auto-discovered from server/commands.
Multiple connections
One application can use more than one connection.
ts
const events = await DB.table('audit_events', 'analytics').latest().get()Models can target a named connection with connectionName.
Defining models
ts
import { defineModel } from '@holo-js/db'
export const User = defineModel('users', {
fillable: ['name', 'email'],
})Use the query builder directly for reporting, maintenance, and table-shaped responses. Use models when the query belongs to one domain record type.
Transactions
Transactions are explicit and context-scoped:
ts
await DB.transaction(async (tx) => {
await tx.table('users').insert({
name: 'Ava',
email: '[email protected]',
})
})