Forms & Data

Database

PostgreSQL via pgx, with embedded migrations and a repository pattern.

HAMR uses pgx as the database driver, sqlx for typed queries, and golang-migrate for migrations. The connection layer is in pkg/db and handles retries, pooling, and migration runs for you.

Connecting

internal/db/db.go
package db

import (
    "context"
    "github.com/FyrmForge/hamr/pkg/db"
)

func ConnectContext(ctx context.Context, url string) (*db.DB, error) {
    return db.Connect(ctx, db.Config{
        URL:           url,
        MaxOpenConns:  25,
        MaxIdleConns:  5,
        RetryAttempts: 5,
    })
}

db.Connect retries with exponential backoff so the app can wait for Postgres to come up — perfect for Docker Compose setups where the database might not be ready yet.

Migrations

SQL migrations live in internal/db/migrations/ with a sequential numbering scheme. Each migration has matching .up.sql and .down.sql files.

0001_create_users.up.sql
CREATE TABLE users (
    id          TEXT PRIMARY KEY,
    email       TEXT UNIQUE NOT NULL,
    password    TEXT NOT NULL,
    name        TEXT NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
0001_create_users.down.sql
DROP TABLE users;

Migrations are embedded into the binary via embed.FS and run automatically on startup:

internal/db/db.go
import "embed"

//go:embed migrations/*.sql
var migrationFS embed.FS

func AutoMigrate(database *db.DB) error {
    return database.MigrateFS(migrationFS, "migrations")
}

Repository pattern

Data access lives in internal/repo/. The package defines a Store interface, and concrete implementations live in subpackages (e.g. internal/repo/postgres).

internal/repo/repo.go
package repo

import "context"

type Store interface {
    BookRepo
    UserRepo
}

type BookRepo interface {
    All(ctx context.Context) ([]Book, error)
    Get(ctx context.Context, id string) (*Book, error)
    Create(ctx context.Context, book *Book) error
    Update(ctx context.Context, book *Book) error
    Delete(ctx context.Context, id string) error
}
internal/repo/postgres/books.go
package postgres

import (
    "context"
    "github.com/yourname/myapp/internal/repo"
)

func (s *Store) All(ctx context.Context) ([]repo.Book, error) {
    var books []repo.Book
    err := s.db.SelectContext(ctx, &books,
        "SELECT id, title, author, created_at FROM books ORDER BY created_at DESC")
    return books, err
}
Why an interface?
The interface lets you swap implementations for tests (in-memory) and lets you build other backends (SQLite, distributed stores) without touching handlers or services.