Database

PostgreSQL

Wire pgx and use PostgreSQL-backed GoLazy packages.

By Guillermo Alvarez - Published - Updated

Open the pool once

Open PostgreSQL in init/dependencies.go, put the pool in the application context with pg.WithPool, and let later initializers read it with pg.FromContext.

package appinit

import (
    "context"

    "github.com/jackc/pgx/v5/pgxpool"
    "golazy.dev/lazydeps"
    "golazy.dev/pg"
)

func Dependencies(deps *lazydeps.Scope) error {
    _, err := lazydeps.Service(deps, "postgres", func(ctx context.Context) (
        context.Context,
        *pgxpool.Pool,
        error,
        context.CancelFunc,
    ) {
        db, err := pg.OpenEnv(ctx, "DATABASE_URL")
        if err != nil {
            return ctx, nil, err, nil
        }
        return pg.WithPool(ctx, db), db, nil, db.Close
    })
    return err
}

The dependency graph owns shutdown. Jobs, request handlers, and other services can now read the same app-owned pool from context.

Package map

The core golazy.dev module does not import PostgreSQL drivers. Concrete implementations live in golazy.dev/pg:

  • pg: shared pgxpool helpers and context helpers.
  • pgmigrate: lazymigrate.Backend for PostgreSQL.
  • pgjobs: lazyjobs.Backend for PostgreSQL.
  • pgfiles: lazyfiles.Repository for PostgreSQL.
  • pgmedia: lazymedia.Repository for PostgreSQL.
  • pgstorage: lazystorage object backend for PostgreSQL.
  • withpg: embedded PostgreSQL helper for integration tests.

Migrations

Use golazy.dev/pg/pgmigrate as the database backend and include both app and package migration sources:

var catalog lazymigrate.Catalog

_ = catalog.Add("postgres", lazymigrate.ForDatabase(app.Migrations, "postgres"))
_ = catalog.Add("postgres", pgjobs.Migrations())
_ = catalog.Add("postgres", pgfiles.Migrations())
_ = catalog.Add("postgres", pgmedia.Migrations())
_ = catalog.Add("postgres", pgstorage.Migrations())

migrator, err := lazymigrate.New(lazymigrate.Config{
    Backend: pgmigrate.New(db),
    Sources: catalog.Sources("postgres"),
})

PostgreSQL migrations use SQL files with lazy markers:

-- +lazy Up
CREATE TABLE posts (
    id BIGSERIAL PRIMARY KEY,
    title TEXT NOT NULL
);

-- +lazy Down
DROP TABLE posts;

pgmigrate creates lazy_migrations, lists applied IDs, runs the selected section in a transaction, and uses a PostgreSQL advisory lock while applying a step. Read Migrations for the backend contract.

Jobs

Use pgjobs.New(db) when queued work must survive process restarts:

Jobs: func(ctx context.Context) (lazyjobs.Config, error) {
    db, ok := pg.FromContext(ctx)
    if !ok {
        return lazyjobs.Config{}, errors.New("postgres is not configured")
    }
    return lazyjobs.Config{
        Backend: pgjobs.New(db),
        Define:  jobs.DefinedJobs,
    }, nil
},

Include pgjobs.Migrations() in the migration catalog before starting workers. Read Background Jobs for the job backend interface and runner behavior.

Object storage

Use pgstorage.New(db) when object bytes should live in PostgreSQL:

objects := pgstorage.New(db)

info, _, err := objects.Put(
    ctx,
    "uploads/avatar.txt",
    strings.NewReader("hello"),
    lazystorage.ContentType{Value: "text/plain"},
)

pgstorage implements read, write, delete, and list interfaces. It is useful for small applications, tests, and database-only deployments. For large public files or CDN-backed delivery, prefer filesystem or S3-compatible object storage.

File catalogs

Use pgfiles.New(db) as the lazyfiles.Repository while object bytes stay in a named lazystorage backend:

files := &lazyfiles.Files{
    Repository: pgfiles.New(db),
    Storages: map[string]lazystorage.Storage{
        "postgres": pgstorage.New(db),
    },
    DefaultStorage: "postgres",
    RoutePrefix:    "/files",
    SigningKey:     []byte(os.Getenv("FILE_SIGNING_KEY")),
}

Include pgfiles.Migrations() in the migration catalog. Read Media for how lazyfiles fits between object storage and generated media.

Media variants

Use pgmedia.New(db) as the lazymedia.Repository:

media := &lazymedia.Media{
    Files:      MediaFiles{Files: files},
    Repository: pgmedia.New(db),
    Processor:  imageProcessor,
}

pgmedia stores variant relationships and status. The source and output files still go through the file service. Include pgmedia.Migrations() in the migration catalog before using the service.

Asset uploads

lazyassets does not have a separate PostgreSQL repository. Its export path uses a lazystorage.Writer, so pgstorage can be the target:

registry := lazyassets.New()
_ = registry.AddFS(app.Public)

err := registry.Upload(ctx, pgstorage.New(db), lazyassets.WithUploadPrefix("assets"))

For public production assets, object storage with CDN-friendly URLs is usually the better backend. PostgreSQL is available when a deployment needs one database-backed artifact store.

Local tests

The golazy.dev/pg integration tests read GOLAZY_PG_DATABASE_URL. golazy.dev/pg/withpg starts embedded PostgreSQL and sets both DATABASE_URL and GOLAZY_PG_DATABASE_URL for the command it runs:

go run ./pg/withpg/cmd/withpg --version 18 -- go test ./pg/...

Go tests can use withpg.Test when a package needs one subtest per PostgreSQL version.