dbfoundry
Renamed project: see upgrade and local installation notes for existing projects and unpublished source.

migrations, schema builder, factories and seeders for Go, available as reusable library packages and a
standalone command-line tool. You describe tables in Go code and dbfoundry generates the SQL for PostgreSQL,
MySQL/MariaDB or SQLite. No .sql files and no hand-written SQL.
go install github.com/saeedjhn/dbfoundry/cmd/dbfoundry@latest
cd my-go-project # any directory with a go.mod
dbfoundry init # creates db/, dbfoundry.json and asks for your database settings
dbfoundry make:migration create_users_table
dbfoundry migrate
Documentation
Contents
- Install
- How it works
- Quick start
- Project layout
- The
.env file
dbfoundry.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).
Install and use the CLI
Install the standalone command:
go install github.com/saeedjhn/dbfoundry/cmd/dbfoundry@latest
dbfoundry version
go install puts the binary in GOBIN if set; otherwise it uses
$(go env GOPATH)/bin (usually ~/go/bin). If dbfoundry: command not found,
add the installation directory to your PATH:
export PATH="$PATH:$(go env GOPATH)/bin" # add to ~/.bashrc or ~/.zshrc
CLI example: create a project with a local SQLite database, initialize its
runner, and run migrations:
mkdir dbfoundry-cli-demo
cd dbfoundry-cli-demo
go mod init example.com/dbfoundry-cli-demo
dbfoundry init --yes --driver sqlite --database demo.db
dbfoundry make:migration create_users_table
dbfoundry migrate
dbfoundry migrate:status
init adds the library dependency and creates the project runner. The migration
generator recognizes create_users_table and creates a migration for a users
table with an ID and timestamps, plus its rollback callback. Customize the
generated file under db/migrations/ before running migrate if you need more
columns. migrate:status then shows the applied migration.
Shell completion (bash, zsh, fish, powershell), generated by Cobra:
dbfoundry completion zsh > "${fpath[1]}/_dbfoundry"
Add and use the library
Inside an existing Go module, add the library dependency:
go get github.com/saeedjhn/dbfoundry@latest
Library example: generate SQLite schema SQL directly from Go code. This
example needs neither the CLI nor a database connection.
mkdir dbfoundry-library-demo
cd dbfoundry-library-demo
go mod init example.com/dbfoundry-library-demo
go get github.com/saeedjhn/dbfoundry@latest
Save the following as main.go:
package main
import (
"fmt"
"os"
"github.com/saeedjhn/dbfoundry/schema"
)
func main() {
statements, err := schema.ToSQL(schema.SQLite{}, "users", func(table *schema.Blueprint) {
table.ID()
table.String("name")
table.String("email").Unique()
})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
for _, statement := range statements {
fmt.Println(statement)
}
}
Run the example:
go mod tidy
go run .
It prints a CREATE TABLE statement and a unique email index. To execute schema
changes against a database, use schema.NewBuilder with a database connection,
or use the migration package.
Public packages are schema, migration, factory, seeder, driver, cli,
and project. The root package dbfoundry provides overview documentation;
functionality lives in these subpackages. internal/terminal is private.
The standalone CLI lives in cmd/dbfoundry.
Publish fixes for installation with @latest
Push the corrected source to github.com/saeedjhn/dbfoundry, then publish a new
semantic version tag with a v prefix that is higher than the existing release
tags. Updating the default branch alone does not make @latest select the fix
when a release already exists. Do not overwrite an existing release tag.
After publishing the new release, users can run the installation commands above
again to update the CLI or the library. go install targets the command under
cmd/dbfoundry; go get adds the library to the current module. The root package
is a library, so go install github.com/saeedjhn/dbfoundry@latest does not install
the CLI.
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 dbfoundry has two parts:
| Part |
Where |
What it does |
| launcher |
the dbfoundry binary you installed |
finds your project, reads dbfoundry.json and .env, runs setup commands itself, and forwards database commands |
| runner |
db/main.go in your project (generated by dbfoundry init) |
imports your db/migrations and db/seeders and runs the real commands |
When you type dbfoundry 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).
dbfoundry help lists commands handled by the installed launcher. In a project initialized with dbfoundry 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 dbfoundry migrate and dbfoundry 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
dbfoundry init
dbfoundry init does the following and prints each step:
- creates
db/migrations, db/factories, db/seeders and the runner db/main.go;
- writes
dbfoundry.json and .env.example, and adds .env to .gitignore;
- runs
go get github.com/saeedjhn/dbfoundry@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:
dbfoundry doctor # checks Go, files, .env and pings the database
dbfoundry make:migration create_users_table
# edit db/migrations/<timestamp>_create_users_table.go
dbfoundry migrate
dbfoundry migrate:status
For scripts and CI, nothing is asked when you pass --yes or when input is not a terminal:
dbfoundry init --yes --driver sqlite --database dev.db
dbfoundry init --yes --driver postgres --host db --database app --username app --password "$DB_PASS"
Project layout
After dbfoundry init (default directory db):
my-project/
├── go.mod
├── dbfoundry.json # dbfoundry 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):
dbfoundry config:set dir database # db/ -> database/
dbfoundry 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
dbfoundry reads database settings from a .env file in the project root (the directory with dbfoundry.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
dbfoundry 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 dbfoundry 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 DBFOUNDRY_ENV_FILE variable, then envFile in dbfoundry.json, then
.env.
dbfoundry env:path # which file is used, why, and whether it exists
dbfoundry env:path config/.env.local # use another file (saved in dbfoundry.json)
dbfoundry env:setup # create or update it by answering questions
dbfoundry env:setup --driver mysql --host db --database app --username app # answer some, be asked the rest
dbfoundry env:show # show settings (password masked) and the resulting connection
dbfoundry migrate --env-file .env.test # one-off override for a single command
dbfoundry.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 dbfoundry config:set dir <path> / dbfoundry config:set envFile <path>; view with
dbfoundry config:show. If dbfoundry.json is missing, dbfoundry 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 |
dbfoundry init [--dir D] [--skip-get] [--skip-env] [--force] [--yes] [env flags] |
scaffold the project |
dbfoundry env:setup [--yes] [--driver … --host … --port … --database … --username … --password … --sslmode … --app-env …] |
create or update the env file interactively |
dbfoundry env:show |
print settings with secrets masked |
dbfoundry env:path [file] |
show, or set, the env file location |
dbfoundry config:show |
show root, config, env file and runner status |
dbfoundry config:set dir|envFile <value> |
change a setting (dir also moves the folder) |
dbfoundry doctor |
check Go, project files, go.mod, .env permissions and the DB connection; exit code 1 on problems |
dbfoundry version |
print the version |
dbfoundry completion <shell> |
shell completion script |
Migrations
| Command |
Purpose |
dbfoundry migrate [--step N] [--pretend] [--force] |
run pending migrations; --pretend prints the SQL only |
dbfoundry migrate:rollback [--step N] |
revert the last N (default 1) |
dbfoundry migrate:reset |
revert everything |
dbfoundry migrate:refresh [--seed] |
reset, then migrate |
dbfoundry migrate:fresh [--seed] |
drop all tables, then migrate |
dbfoundry migrate:status |
list ran/pending migrations |
Seeding
| Command |
Purpose |
dbfoundry 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 |
dbfoundry db:ping |
test the connection |
dbfoundry db:tables |
list tables |
dbfoundry db:wipe [--force] |
drop every table (including migrations) |
Generators
| Command |
Creates |
dbfoundry make:migration create_users_table |
db/migrations/<timestamp>_create_users_table.go (table inferred from the name; override with --create T / --table T) |
dbfoundry make:migration add_phone_to_users_table |
an alter-table migration |
dbfoundry make:seeder UserSeeder |
db/seeders/user_seeder.go, registered automatically |
dbfoundry 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 dbfoundry migrate --pretend.
Example: the users table, end to end
1. Create the migration
dbfoundry make:migration create_users_table
Edit the generated file:
package migrations
import (
"github.com/saeedjhn/dbfoundry/migration"
"github.com/saeedjhn/dbfoundry/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
dbfoundry migrate --pretend # prints the CREATE TABLE for your database
dbfoundry migrate
3. Model and factory (db/factories/user_factory.go)
package factories
import (
"time"
"github.com/brianvoe/gofakeit/v7"
"github.com/saeedjhn/dbfoundry/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 (dbfoundry 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
dbfoundry migrate:fresh --seed --seed-value 42 # same fake data every time
dbfoundry db:tables
dbfoundry 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
dbfoundry make:migration create_comments_table
dbfoundry migrate --pretend
dbfoundry migrate
dbfoundry migrate:rollback && dbfoundry migrate # test the down()
dbfoundry migrate:fresh --seed # rebuild the dev database
Tests: use SQLite and an env file just for tests:
dbfoundry migrate:fresh --seed --env-file .env.test
CI
go install github.com/saeedjhn/dbfoundry/cmd/dbfoundry@latest
dbfoundry migrate:fresh --seed --seed-value 1 --driver postgres \
--dsn "postgres://postgres:postgres@localhost:5432/test?sslmode=disable"
go test ./...
Production
export APP_ENV=production
dbfoundry migrate --pretend # review the SQL
dbfoundry 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 DBFOUNDRY_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
Installing the module root fails
go install github.com/saeedjhn/dbfoundry@latest targets the library, not a
command. Older releases without a root package report "does not contain
package"; releases with the root library package report "is not a main
package". Use go get github.com/saeedjhn/dbfoundry@latest for the library or
go install github.com/saeedjhn/dbfoundry/cmd/dbfoundry@latest for the CLI.
@latest normally selects the highest released semantic version. After making
structural fixes, publish a new v0.x.y tag higher than existing release tags;
updating the default branch alone does not replace an existing release.
Start with dbfoundry doctor; it checks the usual causes in one go.
| Symptom |
Cause |
Fix |
dbfoundry: 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 dbfoundry init |
this project has no runner at db/main.go |
dbfoundry init not run (or dir changed by hand) |
dbfoundry init or dbfoundry config:set dir <dir> |
no required module provides package …dbfoundry… |
the library is not in go.mod |
go get github.com/saeedjhn/dbfoundry@latest && go mod tidy |
database driver is not set / DB_DATABASE missing |
no .env or incomplete |
dbfoundry env:setup |
.env ignored |
wrong file |
dbfoundry 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 dbfoundry 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
dbfoundry (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