nself uses a schema-first approach with DBML (Database Markup Language) for designing, managing, and evolving your database schema with automatic migration generation.
Schema management in nself is built around DBML files that serve as the single source of truth for your database structure. Changes to your schema are tracked, and migrations are automatically generated to apply those changes safely.
Your schema is defined in schema.dbml
file:
// Example schema.dbml
Project nself_project {
database_type: 'PostgreSQL'
Note: 'nself project database schema'
}
Table users {
id uuid [pk, default: `gen_random_uuid()`]
email varchar(255) [unique, not null]
name varchar(255) [not null]
password_hash varchar(255) [not null]
email_verified boolean [default: false]
created_at timestamp [default: `now()`, not null]
updated_at timestamp [default: `now()`, not null]
Note: 'User accounts and authentication'
}
Table profiles {
id uuid [pk, default: `gen_random_uuid()`]
user_id uuid [ref: > users.id, not null]
first_name varchar(100)
last_name varchar(100)
avatar_url varchar(500)
bio text
created_at timestamp [default: `now()`, not null]
updated_at timestamp [default: `now()`, not null]
}
Table posts {
id uuid [pk, default: `gen_random_uuid()`]
user_id uuid [ref: > users.id, not null]
title varchar(255) [not null]
content text
status post_status [default: 'draft']
published_at timestamp
created_at timestamp [default: `now()`, not null]
updated_at timestamp [default: `now()`, not null]
indexes {
user_id
status
(user_id, status)
published_at [where: 'published_at IS NOT NULL']
}
}
Enum post_status {
draft
published
archived
}
Modify your schema.dbml
file to add tables, columns, or relationships.
Run the schema analysis and migration generation:
# Analyze schema and generate migrations
nself db run
# Example output:
# ✓ Schema changes detected!
# ✓ Generating migration: 20250806143000_add_posts_table
# ✓ Migration created: hasura/migrations/default/20250806143000_add_posts_table
#
# Review the migration files before applying:
# - hasura/migrations/default/20250806143000_add_posts_table/up.sql
# - hasura/migrations/default/20250806143000_add_posts_table/down.sql
Always review the generated SQL before applying:
# up.sql - Forward migration
CREATE TABLE posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
title VARCHAR(255) NOT NULL,
content TEXT,
status post_status DEFAULT 'draft',
published_at TIMESTAMP,
created_at TIMESTAMP DEFAULT now() NOT NULL,
updated_at TIMESTAMP DEFAULT now() NOT NULL
);
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_status ON posts(status);
CREATE INDEX idx_posts_user_status ON posts(user_id, status);
CREATE INDEX idx_posts_published_at ON posts(published_at) WHERE published_at IS NOT NULL;
Apply the migrations to your database:
# Safe migration apply (recommended for teams)
nself db update
# Or direct migration apply (for lead developers)
nself db migrate:up
Table table_name {
id uuid [pk, default: `gen_random_uuid()`]
name varchar(255) [not null, unique]
email varchar(255) [not null, note: 'User email address']
age integer [default: 0]
created_at timestamp [default: `now()`]
// Table-level note
Note: 'Description of this table'
}
pk
- Primary keynot null
- Not null constraintunique
- Unique constraintdefault: value
- Default valueincrement
- Auto incrementnote: 'description'
- Column documentationTable users {
id uuid [pk]
email varchar(255)
}
Table posts {
id uuid [pk]
user_id uuid [ref: > users.id] // Many-to-one relationship
title varchar(255)
}
// Alternative relationship syntax
Ref: posts.user_id > users.id
// One-to-many relationship
Ref: users.id < posts.user_id
// Many-to-many relationship
Ref: posts.id <> tags.id
Table posts {
id uuid [pk]
user_id uuid
title varchar(255)
status varchar(50)
created_at timestamp
indexes {
user_id // Simple index
(user_id, status) // Composite index
title [type: hash] // Hash index
created_at [type: btree] // B-tree index
status [where: 'status != "draft"'] // Partial index
title [unique] // Unique index
}
}
Enum user_role {
admin
moderator
user
guest
}
Enum post_status {
draft [note: 'Draft posts not visible to public']
published
archived
deleted
}
Table users {
role user_role [default: 'user']
status user_status [not null]
}
nself integrates with dbdiagram.io for visual schema design:
# Set dbdiagram.io URL in .env.local
DBDIAGRAM_URL=https://dbdiagram.io/d/myproject-12345
# Sync schema from dbdiagram.io
nself db sync
# This will:
# 1. Download schema from dbdiagram.io
# 2. Update local schema.dbml
# 3. Generate migrations for changes
nself db sync
nself db run
nself db update
// Before
Table users {
id uuid [pk]
email varchar(255)
name varchar(255)
}
// After - Adding phone column
Table users {
id uuid [pk]
email varchar(255)
name varchar(255)
phone varchar(20)
created_at timestamp [default: `now()`]
}
// Before
Table users {
name varchar(100)
}
// After - Increase length and add constraint
Table users {
name varchar(255) [not null]
}
// Before - Standalone tables
Table users {
id uuid [pk]
}
Table posts {
id uuid [pk]
title varchar(255)
}
// After - Adding relationship
Table posts {
id uuid [pk]
user_id uuid [ref: > users.id]
title varchar(255)
}
Table tenants {
id uuid [pk]
name varchar(255) [not null]
subdomain varchar(50) [unique, not null]
created_at timestamp [default: `now()`]
}
Table users {
id uuid [pk]
tenant_id uuid [ref: > tenants.id, not null]
email varchar(255) [not null]
name varchar(255)
// Ensure email uniqueness within tenant
indexes {
(tenant_id, email) [unique]
}
}
Table posts {
id uuid [pk]
tenant_id uuid [ref: > tenants.id, not null]
user_id uuid [ref: > users.id, not null]
title varchar(255)
// Row Level Security policies will be applied
Note: 'Posts are isolated by tenant'
}
Table audit_log {
id uuid [pk, default: `gen_random_uuid()`]
table_name varchar(100) [not null]
record_id uuid [not null]
action audit_action [not null]
old_values jsonb
new_values jsonb
user_id uuid [ref: > users.id]
timestamp timestamp [default: `now()`, not null]
indexes {
table_name
record_id
user_id
timestamp
(table_name, record_id)
}
}
Enum audit_action {
INSERT
UPDATE
DELETE
}
nself db status
Check current schema and database status:
nself db status
# Example output:
# Database Status
#
# Schema file: schema.dbml
# Hash: b4dc5d616f59...
# Migrations: 5
# Latest migrations:
# - 20250806143000_add_posts_table
# - 20250806142000_add_profiles_table
# - 20250806141000_create_users_table
# Backups: 3
# PostgreSQL: Running
# Validate DBML syntax
nself db run --validate
# Common syntax issues:
# - Missing closing brackets
# - Invalid column types
# - Malformed references
# - Typos in enum values
# If migrations conflict:
# 1. Check current database state
nself db status
# 2. Review pending migrations
ls hasura/migrations/default/
# 3. Manually resolve conflicts in schema.dbml
# 4. Regenerate migrations
nself db run --force
# Rollback last migration
nself db migrate:down
# Rollback specific number of migrations
nself db migrate:down 2
# Restore from backup if needed
nself db revert