Use Atlas + GORM for database migrations

Atlas is a popular tool for managing database migrations. GORM is a popular ORM for Go.

Encore provides excellent support for using them together to easily manage database schemas and migrations. Encore executes database migrations using golang-migrate, which Atlas supports out-of-the-box. This means that you can use Atlas to manage your Encore database migrations.

The easiest way to use Atlas + GORM together is with Atlas's support for external schemas.

Setting up GORM

To set up your Encore application with GORM, start by installing the GORM package and associated Postgres driver:

go get -u gorm.io/gorm gorm.io/driver/postgres

Then, in the service that you want to use GORM for, add the *gorm.DB as a dependency in your service struct (create a service struct if you don't already have one).

For example, if you had a service called blog:

blog/blog.go
package blog import ( "encore.dev/storage/sqldb" "gorm.io/driver/postgres" "gorm.io/gorm" ) //encore:service type Service struct { db *gorm.DB } var blogDB = sqldb.NewDatabase("blog", sqldb.DatabaseConfig{ Migrations: "./migrations", }) // initService initializes the site service. // It is automatically called by Encore on service startup. func initService() (*Service, error) { db, err := gorm.Open(postgres.New(postgres.Config{ Conn: blogDB.Stdlib(), })) if err != nil { return nil, err } return &Service{db: db}, nil }

Finally, create the migrations directory inside the blog directory if it doesn't already exist. This is where Atlas will put your database migrations.

Setting up Atlas

First install Atlas.

Then, add an atlas.hcl file inside the blog directory:

blog/atlas.hcl
data "external_schema" "gorm" { program = ["env", "ENCORERUNTIME_NOPANIC=1", "go", "run", "./scripts/atlas-gorm-loader.go"] } env "local" { src = data.external_schema.gorm.url migration { dir = "file://migrations" format = golang-migrate } format { migrate { diff = "{{ sql . \" \" }}" } } }

Next, we need to create the atlas-gorm-loader script referenced above. It will use the atlas-provider-gorm library provided by Atlas.

Create the file as follows:

blog/scripts/atlas-gorm-loader.go
package main import ( "fmt" "io" "os" _ "ariga.io/atlas-go-sdk/recordriver" "ariga.io/atlas-provider-gorm/gormschema" "encore.app/blog" ) // Define the models to generate migrations for. var models = []any{ &blog.Post{}, &blog.Comment{}, } func main() { stmts, err := gormschema.New("postgres").Load(models...) if err != nil { fmt.Fprintf(os.Stderr, "failed to load gorm schema: %v\n", err) os.Exit(1) } io.WriteString(os.Stdout, stmts) }

Creating migrations

To wrap things up, let's create a script to automate the process of generating migrations:

blog/scripts/generate-migration
#!/bin/bash set -eu DB_NAME=blog MIGRATION_NAME=${1:-} SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) # Reset the shadow database encore db reset --shadow $DB_NAME # GORM executes Go code without initializing Encore when generating migrations, # so configure the Encore runtime to be aware that this is expected. export ENCORERUNTIME_NOPANIC=1 # Generate the migration atlas migrate diff $MIGRATION_NAME --env local --dev-url "$(encore db conn-uri --shadow $DB_NAME)&search_path=public"

Finally let's make the script executable, and generate our first migration:

$ chmod +x blog/scripts/generate-migration
$ cd blog && ./scripts/generate-migration init

This will generate a new migration file in the blog/migrations directory, which will be automatically applied when running encore run.