The AdonisJS Starter Kit uses Lucid ORM with PostgreSQL for database management. This guide covers database configuration, migrations, and seeders.
Database Configuration
The database configuration is located in config/database.ts:
import env from '#start/env'
import { defineConfig } from '@adonisjs/lucid'
import app from '@adonisjs/core/services/app'
const dbConfig = defineConfig({
connection: 'postgres',
prettyPrintDebugQueries: app.inDev,
connections: {
postgres: {
client: 'pg',
connection: {
host: env.get('DB_HOST'),
port: env.get('DB_PORT'),
user: env.get('DB_USER'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
migrations: {
naturalSort: true,
paths: ['app/users/database/migrations'],
},
seeders: {
paths: ['app/users/database/seeders'],
},
debug: app.inDev,
},
},
})
export default dbConfig
The starter kit uses a modular structure where migrations and seeders are organized by feature (e.g., app/users/database/migrations).
Environment Variables
Configure your database connection in the .env file:
DB_HOST=127.0.0.1
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_DATABASE=adonis_starter
Creating Migrations
Generate a new migration using the Ace CLI:
node ace make:migration create_posts_table
Migration Example: Roles Table
Here’s a real migration from the starter kit that creates the roles table:
app/users/database/migrations/1737139066940_create_roles_table.ts
import { BaseSchema } from '@adonisjs/lucid/schema'
import Roles from '#users/enums/role'
export default class extends BaseSchema {
protected tableName = 'roles'
async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.string('name', 50).notNullable()
table.string('description', 255).nullable()
table.timestamp('created_at')
table.timestamp('updated_at')
})
this.defer(async (db) => {
await db.table(this.tableName).insert([
{
id: Roles.USER,
name: 'User',
description: 'Authenticated User',
},
{
id: Roles.ADMIN,
name: 'Admin',
description: 'Super User with full access',
},
])
})
}
async down() {
this.schema.dropTable(this.tableName)
}
}
Use the defer method to insert seed data after the table is created. This is useful for lookup tables and default values.
Migration Example: Users Table with Foreign Keys
app/users/database/migrations/1737139066942_create_users_table.ts
import { BaseSchema } from '@adonisjs/lucid/schema'
import Roles from '#users/enums/role'
export default class extends BaseSchema {
protected tableName = 'users'
async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id').notNullable()
table
.integer('role_id')
.unsigned()
.references('id')
.inTable('roles')
.notNullable()
.defaultTo(Roles.USER)
table.string('full_name').nullable()
table.string('email', 254).notNullable().unique()
table.string('password').nullable()
table.string('avatar_url').nullable().defaultTo(null)
table.json('avatar').nullable()
table.timestamp('created_at').notNullable()
table.timestamp('updated_at').nullable()
})
}
async down() {
this.schema.dropTable(this.tableName)
}
}
Running Migrations
Execute migrations to update your database schema:
Database Seeders
Seeders populate your database with test or default data.
Creating a Seeder
node ace make:seeder user
Seeder Example
Here’s the actual user seeder from the starter kit:
app/users/database/seeders/user_seeder.ts
import { BaseSeeder } from '@adonisjs/lucid/seeders'
import User from '#users/models/user'
import Roles from '#users/enums/role'
export default class UserSeeder extends BaseSeeder {
async run() {
const uniqueKey = 'email'
await User.updateOrCreateMany(uniqueKey, [
{
email: '[email protected]',
fullName: 'Administrador',
password: '123',
roleId: Roles.ADMIN,
},
])
}
}
Using updateOrCreateMany ensures the seeder is idempotent - it won’t create duplicates when run multiple times.
Running Seeders
Database Queries
Use Lucid ORM to query your database:
const users = await User.all()
Common Commands
Create Migration
node ace make:migration table_name
Create Seeder
node ace make:seeder name
Best Practices
Use descriptive migration names
Name your migrations clearly to describe what they do:
create_users_table.ts
add_avatar_to_users_table.ts
create_posts_comments_table.ts
Never modify existing migrations
Once a migration has been run in production, create a new migration to modify the schema instead of editing the existing one.
Use transactions for complex migrations
Wrap complex migrations in transactions to ensure atomicity:async up() {
this.schema.createTable('posts', (table) => {
// table definition
})
this.defer(async (db) => {
await db.transaction(async (trx) => {
// Complex operations
})
})
}
Use updateOrCreateMany or check for existence before creating records to prevent duplicates when running seeders multiple times.
Next Steps
Models
Learn how to create and use Lucid ORM models
Routes
Define routes to handle HTTP requests