dbkit

Laravel-style migrations, schema builder, factories and seeders for Go, driven by a command-line tool you install once with go install. You describe tables in Go code and dbkit generates the SQL for PostgreSQL, MySQL/MariaDB or SQLite. No .sql files and no hand-written SQL.
go install github.com/saeedjhn/dbkit/cmd/dbkit@latest
cd my-go-project # any directory with a go.mod
dbkit init # creates db/, dbkit.json and asks for your database settings
dbkit make:migration create_users_table
dbkit migrate
Persian guide ("zero to hundred"): docs/GUIDE.fa.md · Deep reference (schema builder, factories, seeders, architecture): docs/REFERENCE.md
Contents
- Install
- How it works
- Quick start
- Project layout
- The
.env file
dbkit.json
- Command reference
- Example: the users table, end to end
- Everyday workflows
- Integrating into your framework
- Troubleshooting
- Limitations
- Contributing and license
Install
Requirements: Go 1.26+ (also needed at run time, see How it works).
go install github.com/saeedjhn/dbkit/cmd/dbkit@latest
dbkit version
go install puts the binary in $(go env GOPATH)/bin (usually ~/go/bin). If dbkit: command not found, add that directory to your PATH:
export PATH="$PATH:$(go env GOPATH)/bin" # add to ~/.bashrc or ~/.zshrc
Shell completion (bash, zsh, fish, powershell), generated by Cobra:
dbkit completion zsh > "${fpath[1]}/_dbkit"
How it works
Migrations are Go code, so they must be compiled into something. A single globally installed binary cannot contain your project's migrations, so dbkit has two parts:
| Part |
Where |
What it does |
| launcher |
the dbkit binary you installed |
finds your project, reads dbkit.json and .env, runs setup commands itself, and forwards database commands |
| runner |
db/main.go in your project (generated by dbkit init) |
imports your db/migrations and db/seeders and runs the real commands |
When you type dbkit migrate, the launcher runs go run ./db migrate in your project root with the right environment file. You never call the runner by hand. The cost: the Go toolchain must be installed, and the first run compiles your project (a few seconds; later runs use the build cache).
dbkit help lists commands handled by the installed launcher. In a project initialized with dbkit init, it also prints the project runner's help, which lists database commands such as migrate, migrate:rollback, db:seed, and db:ping. The launcher accepts these as dbkit migrate and dbkit db:seed; when mounting the command tree in another Cobra app, setting db.Use = "db" instead exposes them as myapp db migrate and myapp db db:seed.
Commands handled by the launcher itself (no compile needed): init, env:setup, env:show, env:path, config:show, config:set, doctor, version, make:*, completion.
Quick start
mkdir blog && cd blog
go mod init example.com/blog
dbkit init
dbkit init does the following and prints each step:
- creates
db/migrations, db/factories, db/seeders and the runner db/main.go;
- writes
dbkit.json and .env.example, and adds .env to .gitignore;
- runs
go get github.com/saeedjhn/dbkit@latest and go mod tidy (skip with --skip-get);
- asks whether to create
.env and walks you through it:
Environment file: /home/me/blog/.env
? Which database do you use?
1) postgres
2) mysql
* 3) sqlite
choice [sqlite]: 1
? Host [127.0.0.1]:
? Port [5432]:
? Database name: blog
? Username [postgres]:
? Password (input is hidden; leave empty to keep the current one):
? SSL mode: (menu, default disable)
? Application environment (APP_ENV): (menu, default local)
Summary:
APP_ENV local
DB_DRIVER postgres
...
Write these values? [Y/n]
Then:
dbkit doctor # checks Go, files, .env and pings the database
dbkit make:migration create_users_table
# edit db/migrations/<timestamp>_create_users_table.go
dbkit migrate
dbkit migrate:status
For scripts and CI, nothing is asked when you pass --yes or when input is not a terminal:
dbkit init --yes --driver sqlite --database dev.db
dbkit init --yes --driver postgres --host db --database app --username app --password "$DB_PASS"
Project layout
After dbkit init (default directory db):
my-project/
├── go.mod
├── dbkit.json # dbkit settings (commit this)
├── .env # your database settings (git-ignored)
├── .env.example # template without secrets (commit this)
└── db/
├── main.go # runner, generated; you rarely touch it
├── migrations/ # one Go file per migration
├── factories/ # fake-data factories
└── seeders/ # seeders (DatabaseSeeder is the root)
Change the directory at any time (the folder is renamed and the runner regenerated):
dbkit config:set dir database # db/ -> database/
dbkit init --dir internal/db # or choose it at init time
The directory must be a relative path inside the project, so ../x and absolute paths are rejected.
The .env file
dbkit reads database settings from a .env file in the project root (the directory with dbkit.json or go.mod).
APP_ENV=local # local | production
DB_DRIVER=postgres # postgres | mysql | sqlite
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=blog # for sqlite: the file path, e.g. dev.db
DB_USERNAME=postgres
DB_PASSWORD=secret
DB_SSLMODE=disable # PostgreSQL only
dbkit builds the connection string for you (postgres://…, user:pass@tcp(host:port)/db?parseTime=true, file:dev.db?_pragma=foreign_keys(1)), escaping special characters in the password.
Need full control? Set DB_DSN (or DATABASE_URL); it wins over the split variables:
DB_DRIVER=postgres
DB_DSN=postgres://user:pass@localhost:5432/blog?sslmode=disable
Rules:
KEY=VALUE lines; # comments; optional export prefix; values may be quoted.
- Real environment variables win over the file (so CI and servers can override).
- The file is written with mode
0600, and dbkit env:setup keeps lines it does not manage.
- Precedence for the connection:
--driver/--dsn flags, then DB_* variables, then cli.Options (library use).
Where is the file? Create or move it
Order of precedence: --env-file flag, then the DBKIT_ENV_FILE variable, then envFile in dbkit.json, then .env.
dbkit env:path # which file is used, why, and whether it exists
dbkit env:path config/.env.local # use another file (saved in dbkit.json)
dbkit env:setup # create or update it by answering questions
dbkit env:setup --driver mysql --host db --database app --username app # answer some, be asked the rest
dbkit env:show # show settings (password masked) and the resulting connection
dbkit migrate --env-file .env.test # one-off override for a single command
dbkit.json
{
"dir": "db",
"envFile": ".env"
}
| Key |
Default |
Meaning |
dir |
db |
directory holding migrations/, factories/, seeders/ and the runner |
envFile |
.env |
environment file, relative to the project root (or absolute) |
Edit it by hand or with dbkit config:set dir <path> / dbkit config:set envFile <path>; view with dbkit config:show. If dbkit.json is missing, dbkit uses the defaults and finds the project root by the nearest go.mod.
Command reference
Global flags (every command): --env-file <path>, --db-dir <dir>. Database commands also accept --driver and --dsn.
Project and environment
| Command |
Purpose |
dbkit init [--dir D] [--skip-get] [--skip-env] [--force] [--yes] [env flags] |
scaffold the project |
dbkit env:setup [--yes] [--driver … --host … --port … --database … --username … --password … --sslmode … --app-env …] |
create or update the env file interactively |
dbkit env:show |
print settings with secrets masked |
dbkit env:path [file] |
show, or set, the env file location |
dbkit config:show |
show root, config, env file and runner status |
dbkit config:set dir|envFile <value> |
change a setting (dir also moves the folder) |
dbkit doctor |
check Go, project files, go.mod, .env permissions and the DB connection; exit code 1 on problems |
dbkit version |
print the version |
dbkit completion <shell> |
shell completion script |
Migrations
| Command |
Purpose |
dbkit migrate [--step N] [--pretend] [--force] |
run pending migrations; --pretend prints the SQL only |
dbkit migrate:rollback [--step N] |
revert the last N (default 1) |
dbkit migrate:reset |
revert everything |
dbkit migrate:refresh [--seed] |
reset, then migrate |
dbkit migrate:fresh [--seed] |
drop all tables, then migrate |
dbkit migrate:status |
list ran/pending migrations |
Seeding
| Command |
Purpose |
dbkit db:seed [--class UserSeeder] [--seed-value N] |
run DatabaseSeeder or one seeder, in one transaction; --seed-value makes fake data reproducible |
Database helpers
| Command |
Purpose |
dbkit db:ping |
test the connection |
dbkit db:tables |
list tables |
dbkit db:wipe [--force] |
drop every table (including migrations) |
Generators
| Command |
Creates |
dbkit make:migration create_users_table |
db/migrations/<timestamp>_create_users_table.go (table inferred from the name; override with --create T / --table T) |
dbkit make:migration add_phone_to_users_table |
an alter-table migration |
dbkit make:seeder UserSeeder |
db/seeders/user_seeder.go, registered automatically |
dbkit make:factory User --table users |
db/factories/user_factory.go |
Production guard
When APP_ENV=production, destructive commands (migrate, migrate:rollback|reset|refresh|fresh, db:seed, db:wipe) refuse to run without --force. Review first with dbkit migrate --pretend.
Example: the users table, end to end
1. Create the migration
dbkit make:migration create_users_table
Edit the generated file:
package migrations
import (
"github.com/saeedjhn/dbkit/migration"
"github.com/saeedjhn/dbkit/schema"
)
func init() {
migration.Register("20261031120000_create_users_table",
func(s *schema.Builder) error {
return s.Create("users", func(t *schema.Blueprint) {
t.ID()
t.String("name")
t.String("email", 191).Unique()
t.Timestamp("email_verified_at").Nullable()
t.String("password")
t.Boolean("is_active").Default(true)
t.Enum("role", "admin", "editor", "user").Default("user")
t.Timestamps()
t.SoftDeletes()
})
},
func(s *schema.Builder) error { return s.DropIfExists("users") },
)
}
2. Preview and run
dbkit migrate --pretend # prints the CREATE TABLE for your database
dbkit migrate
3. Model and factory (db/factories/user_factory.go)
package factories
import (
"time"
"github.com/brianvoe/gofakeit/v7"
"github.com/saeedjhn/dbkit/factory"
)
type UserRow struct {
ID int64 `db:"id" goqu:"skipinsert"`
Name string `db:"name"`
Email string `db:"email"`
Password string `db:"password"`
Role string `db:"role"`
IsActive bool `db:"is_active"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
func User() *factory.Factory[UserRow] {
return factory.New("users", func(f *gofakeit.Faker, i int) UserRow {
now := time.Now().UTC()
return UserRow{
Name: f.Name(), Email: f.Username() + "@example.com",
Password: "x", Role: "user", IsActive: true,
CreatedAt: now, UpdatedAt: now,
}
})
}
func AsAdmin(u *UserRow) { u.Role = "admin" }
4. Seeder (dbkit make:seeder UserSeeder, then fill Run)
func (UserSeeder) Run(ctx context.Context, c *seeder.Context) error {
if _, err := factories.User().State(factories.AsAdmin).CreateOne(ctx, c); err != nil {
return err
}
_, err := factories.User().Count(50).Create(ctx, c)
return err
}
DatabaseSeeder is the root; call yours from it with c.Call(ctx, UserSeeder{}).
5. Run everything
dbkit migrate:fresh --seed --seed-value 42 # same fake data every time
dbkit db:tables
dbkit migrate:rollback
A full runnable blog example (users, a phone column added later, posts with a foreign key, factories, seeders and an end-to-end test) is in examples/blog.
Everyday workflows
Development
dbkit make:migration create_comments_table
dbkit migrate --pretend
dbkit migrate
dbkit migrate:rollback && dbkit migrate # test the down()
dbkit migrate:fresh --seed # rebuild the dev database
Tests: use SQLite and an env file just for tests:
dbkit migrate:fresh --seed --env-file .env.test
CI
go install github.com/saeedjhn/dbkit/cmd/dbkit@latest
dbkit migrate:fresh --seed --seed-value 1 --driver postgres \
--dsn "postgres://postgres:postgres@localhost:5432/test?sslmode=disable"
go test ./...
Production
export APP_ENV=production
dbkit migrate --pretend # review the SQL
dbkit migrate --force
Back up first. Never run fresh, reset, refresh or db:wipe in production. Ship destructive changes (dropping a column) in two releases. To avoid needing the Go toolchain on a server, build the runner once and ship it: go build -o migrate ./db && ./migrate migrate --force (it reads DBKIT_ENV_FILE or .env).
Integrating into your framework
The runner (db/main.go) is plain Go using the public cli package, so you can edit it or mount the same commands in your own CLI:
root := &cobra.Command{Use: "myapp"}
db := cli.NewRootCommand(cli.Options{
MigrationsDir: "db/migrations", SeedersDir: "db/seeders", FactoriesDir: "db/factories",
})
db.Use = "db" // myapp db migrate, myapp db migrate:fresh --seed
root.AddCommand(db)
You can also run migrations from code (migration.New(...).Up(ctx, 0)), use your own connection (cli.Options.Open), and keep several registries for several databases. All of it is covered in docs/REFERENCE.md section 12.
Supported databases:
| Database |
Driver |
Notes |
| PostgreSQL |
postgres |
optional advisory lock (migration.Options{Lock: true}) |
| MySQL / MariaDB |
mysql |
DDL is not transactional (a MySQL limitation) |
| SQLite 3.35+ |
sqlite |
cannot add or drop foreign/primary keys on existing tables |
Troubleshooting
Start with dbkit doctor; it checks the usual causes in one go.
| Symptom |
Cause |
Fix |
dbkit: command not found |
GOPATH/bin not in PATH |
export PATH="$PATH:$(go env GOPATH)/bin" |
not inside a Go project |
no go.mod found upward |
go mod init <module> then dbkit init |
this project has no runner at db/main.go |
dbkit init not run (or dir changed by hand) |
dbkit init or dbkit config:set dir <dir> |
no required module provides package …dbkit… |
the library is not in go.mod |
go get github.com/saeedjhn/dbkit@latest && go mod tidy |
database driver is not set / DB_DATABASE missing |
no .env or incomplete |
dbkit env:setup |
.env ignored |
wrong file |
dbkit env:path shows the file in use |
Nothing to migrate after adding a file |
file not in the migrations directory, or wrong package |
check the path shown by dbkit config:show, and package migrations |
seeder "X" is not registered |
missing seeder.Register in init() |
see make:seeder output |
APP_ENV is production: pass --force … |
APP_ENV=production |
review with --pretend, then add --force |
| first command is slow |
the runner is compiling |
later runs use the Go build cache |
Limitations
- The Go toolchain is required where you run
dbkit (it compiles your migrations via go run).
- MySQL cannot roll back a failed DDL migration (no transactional DDL).
- SQLite cannot add/drop foreign keys or primary keys on an existing table.
- Windows paths work, but are not covered by CI yet.
Contributing and license
Issues and pull requests are welcome. Run gofmt, go vet ./... and go test ./... before submitting. See CHANGELOG.md.
MIT © 2026 saeedjhn