Nuda Kit uses PostgreSQL as its database and Lucid ORM for database operations. Lucid provides an Active Record implementation with full TypeScript support.
Database connection is configured in config/database.ts and uses environment variables:
# backend/.env
DB_HOST=localhost
DB_PORT=5432
DB_USER=root
DB_PASSWORD=root
DB_DATABASE=app
Models are located in app/models/ and represent database tables:
| Model | Table | Purpose |
|---|---|---|
User | users | User accounts |
Team | teams | Teams/organizations |
TeamMember | team_members | Team membership (pivot) |
Invitation | invitations | Pending team invitations |
Plan | plans | Subscription plans |
Subscription | subscriptions | User subscriptions |
UserEmailVerificationToken | user_email_verification_tokens | Email verification |
UserPasswordResetToken | user_password_reset_tokens | Password reset |
Models extend BaseModel and use decorators to define columns and relationships:
import { BaseModel, column, hasMany } from '@adonisjs/lucid/orm'
import { DateTime } from 'luxon'
export default class Team extends BaseModel {
@column({ isPrimary: true })
declare id: number
@column()
declare name: string
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
}
Nuda Kit models use these relationship types:
| Type | Example |
|---|---|
hasOne | User → EmailVerificationToken |
hasMany | User → Subscriptions |
manyToMany | User ↔ Teams (through team_members) |
belongsTo | Invitation → Team |
Use @computed() for derived values:
@column({ serializeAs: null })
declare avatar: string | null
@computed()
get avatarUrl() {
if (!this.avatar) return null
return `${env.get('APP_URL')}/uploads/${this.avatar}`
}
Use serializeAs: null to exclude fields from JSON output:
@column({ serializeAs: null })
declare password: string | null
@column({ serializeAs: null })
declare isDeleted: boolean
Migrations are located in database/migrations/ and run in chronological order.
| Migration | Creates |
|---|---|
create_users_table | User accounts |
create_access_tokens_table | Auth tokens |
create_user_email_verification_tokens_table | Email verification |
create_rate_limits_table | Rate limiting |
create_user_password_reset_tokens_table | Password reset |
create_teams_table | Teams |
create_team_members_table | Team membership |
create_invitations_table | Team invitations |
create_subscriptions_table | Subscriptions |
create_plans_table | Subscription plans |
# Run all pending migrations
node ace migration:run
# Rollback last batch
node ace migration:rollback
# Fresh database (rollback all + migrate)
node ace migration:fresh
# Check migration status
node ace migration:status
node ace make:migration create_posts_table
This creates a new migration file:
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
protected tableName = 'posts'
async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.string('title').notNullable()
table.text('content')
table.integer('user_id').unsigned().references('users.id')
table.timestamp('created_at')
table.timestamp('updated_at')
})
}
async down() {
this.schema.dropTable(this.tableName)
}
}
Seeders populate the database with initial or test data.
# Run all seeders
node ace db:seed
# Run specific seeder
node ace db:seed --files=database/seeders/plan_seeder.ts
The PlanSeeder creates subscription plans:
import { BaseSeeder } from '@adonisjs/lucid/seeders'
import Plan, { PlanMode } from '#models/plan'
export default class extends BaseSeeder {
async run() {
await Plan.createMany([
{
name: 'Pro',
key: 'pro',
stripePriceId: 'price_xxx', // From Stripe
mode: PlanMode.SUBSCRIPTION,
isActive: true,
},
{
name: 'Ultimate',
key: 'ultimate',
stripePriceId: 'price_xxx', // From Stripe
mode: PlanMode.SUBSCRIPTION,
isActive: true,
},
])
}
}
node ace make:seeder Post
// Single record
const user = await User.create({
email: 'user@example.com',
firstName: 'John',
})
// Multiple records
const teams = await Team.createMany([
{ name: 'Team A' },
{ name: 'Team B' },
])
// Find by ID
const user = await User.find(1)
const user = await User.findOrFail(1) // Throws if not found
// Find by column
const user = await User.findBy('email', 'user@example.com')
// Query builder
const users = await User.query()
.where('isDeleted', false)
.orderBy('createdAt', 'desc')
.limit(10)
// Find and update
const user = await User.findOrFail(1)
user.firstName = 'Jane'
await user.save()
// Or use merge
await user.merge({ firstName: 'Jane' }).save()
const user = await User.findOrFail(1)
await user.delete()
// Load relationship
const user = await User.query()
.where('id', 1)
.preload('teams')
.firstOrFail()
// Access related records
user.teams.forEach(team => console.log(team.name))
// Create related record
await user.related('subscriptions').create({
planId: 1,
status: 'active',
})
// Attach many-to-many
await user.related('teams').attach({
[teamId]: { role: 'member' }
})
// Detach many-to-many
await user.related('teams').detach([teamId])
import db from '@adonisjs/lucid/services/db'
const team = await db.transaction(async (trx) => {
const newTeam = await Team.create({ name: 'New Team' }, { client: trx })
await user.related('teams').attach({
[newTeam.id]: { role: 'owner' }
}, trx)
return newTeam
})
┌─────────────────┐ ┌─────────────────┐
│ users │ │ teams │
├─────────────────┤ ├─────────────────┤
│ id │ │ id │
│ first_name │ │ name │
│ last_name │ │ avatar │
│ email │ │ created_at │
│ password │ │ updated_at │
│ avatar │ └────────┬────────┘
│ stripe_customer │ │
│ is_deleted │ │
│ email_verified │ ┌────────┴────────┐
│ created_at │ │ team_members │
│ updated_at │ ├─────────────────┤
└────────┬────────┘ │ user_id (FK) │
│ │ team_id (FK) │
└──────────────│ role │
│ default │
└─────────────────┘
┌─────────────────┐ ┌─────────────────┐
│ invitations │ │ plans │
├─────────────────┤ ├─────────────────┤
│ id │ │ id │
│ team_id (FK) │ │ name │
│ email │ │ key │
│ role │ │ stripe_price_id │
│ token │ │ price │
│ expires_at │ │ mode │
│ created_at │ │ is_active │
└─────────────────┘ └─────────────────┘
┌─────────────────┐
│ subscriptions │
├─────────────────┤
│ id │
│ user_id (FK) │
│ stripe_id │
│ stripe_status │
│ stripe_price_id │
│ ends_at │
└─────────────────┘