Skip to main content
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:
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:
.env
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:
node ace migration:run

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

node ace db:seed

Database Queries

Use Lucid ORM to query your database:
const users = await User.all()

Common Commands

Create Migration

node ace make:migration table_name

Run Migrations

node ace migration:run

Create Seeder

node ace make:seeder name

Run Seeders

node ace db:seed

Best Practices

Name your migrations clearly to describe what they do:
  • create_users_table.ts
  • add_avatar_to_users_table.ts
  • create_posts_comments_table.ts
Once a migration has been run in production, create a new migration to modify the schema instead of editing the existing one.
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

Build docs developers (and LLMs) love