Schema and migrations — the users table, embedding, config
First, the new dependencies:
go get github.com/jackc/pgx/v5 github.com/pressly/goose/v3 github.com/joho/godotenv
Three files as tabs, in dependency order: 00001_users.sql → db/embed.go → config.go (the schema, then embedding it, then teaching config where the database lives). The last is a modify of your lesson-1 config.
1. db/migrations/00001_users.sql — the first migration. The -- +goose markers tell goose where each direction begins:
-- +goose Up
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- +goose Down
DROP TABLE users;
Up builds it, Down reverses it. gen_random_uuid() (from pgcrypto) gives every row a UUID primary key — no guessable sequential IDs, which matters once documents belong to users. The email is UNIQUE; password_hash is filled in lesson 4.
2. db/embed.go — bake the migrations into the binary:
//go:embed migrations/*.sql
var FS embed.FS
This is what makes deployment self-migrating: the .sql files travel inside the compiled binary, and startup runs any that haven't been applied.
3. internal/config/config.go (modify) — config learns where the database is. The change: a DatabaseURL field, godotenv to read a local .env, and a production guard.
DatabaseURL: getenv("DATABASE_URL", "postgres://postgres@localhost:5432/signflow?sslmode=disable"),
The dev default connects to a local Postgres with no password (trust auth), so go run works with zero config. Everything from lesson 1 — Port, Env, BaseURL, IsProd — is carried forward unchanged; the diff is just the new field and the .env loading.
.envin dev.godotenv.Load()reads a.envfile if present (and shrugs if not), so you can keepDATABASE_URLin a gitignored.envinstead of exporting it every shell. In prod, Railway sets the real environment.