Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 6 additions & 14 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
# Labels applied automatically by actions/labeler@v5.
# Matching uses glob patterns; any matching file in the PR triggers the label.
# Manually-applied labels (bug, feature, breaking-change, etc.) are not listed here.
# Manually-applied labels (type, priority, needs, blocked, etc.) are not listed here.

backend:
"area: backend":
- changed-files:
- any-glob-to-any-file: ["backend/**"]

web:
"area: web":
- changed-files:
- any-glob-to-any-file: ["web/**"]

mobile:
"area: mobile":
- changed-files:
- any-glob-to-any-file: ["mobile/**"]

documentation:
- changed-files:
- any-glob-to-any-file:
- "**/*.md"
- "backend/docs/**"
- "web/docs/**"
- "mobile/docs/**"

ci:
"area: infra":
- changed-files:
- any-glob-to-any-file: [".github/**"]

dependencies:
"type: chore":
- changed-files:
- any-glob-to-any-file:
- "backend/go.mod"
Expand Down
30 changes: 26 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,30 @@ cd mobile && ./gradlew lint && ./gradlew test # mobile quality gate
cd mobile && ./gradlew connectedAndroidTest # instrumented tests (emulator/device required)
```

## Migration commands (run from backend/)
```bash
make migrate-create name=<slug> # create a new timestamped SQL migration file
make migrate-status # show applied vs. pending migrations
make migrate-up # apply all pending migrations
make migrate-up-one # apply the next pending migration only
make migrate-down # roll back the last applied migration
make migrate-reset # roll back everything to version 0
make migrate-version # print current schema version
```

## Feature development workflow — always follow this
1. **Check docs first** — delegate to `docs` agent: find relevant topic docs, verify they match the current code
2. **Fix stale docs** — if docs diverge from code, update docs before implementing
3. **Implement** — delegate to `backend` or `web` agent, passing the relevant doc content as context
4. **Update docs** — delegate to `docs` agent: update `last_verified`, add new topics if introduced
5. **Quality gate** — run `/project:check` before declaring done
3. **Migrate** — if the feature needs new or changed tables: `make migrate-create name=<slug>`, write the SQL, `make migrate-up`
4. **Implement** — delegate to `backend` or `web` agent, passing the relevant doc content as context
5. **Update docs** — delegate to `docs` agent: update `last_verified`, add new topics if introduced
6. **Quality gate** — run `/project:check` before declaring done

Use `/project:implement` to run this workflow end-to-end.

## Documentation locations
```
backend/docs/ # database, routing, testing, error-handling, environment
backend/docs/ # database, migrations, routing, testing, error-handling, environment
web/docs/ # routing, data-fetching, styling, components
mobile/docs/ # compose-conventions, architecture, testing
```
Expand All @@ -59,6 +71,8 @@ Each doc file has `last_verified` and `sources` frontmatter. The `docs` agent ma
```
backend/
cmd/api/main.go # entry point — wires layers, graceful shutdown
cmd/migrate/main.go # migration CLI — wraps goose, reads BLUEPRINT_DB_* env vars
migrations/ # SQL migration files (goose) — YYYYMMDDHHMMSS_<slug>.sql
internal/
domain/ # Layer 1: entities + repository interfaces (no external deps)
health.go # HealthStats type
Expand Down Expand Up @@ -98,6 +112,14 @@ mobile/
- Return errors up the stack. Never `log.Fatal` or `os.Exit` inside `internal/`.
- Run `go vet ./...` before committing.

## Migrations (goose)
- Any feature that introduces or changes a table **must** include a goose migration.
- `make migrate-create name=<slug>` → edit the generated SQL → `make migrate-up`.
- SQL only — no Go migrations. No DDL inside repository methods.
- Never edit or delete an applied migration; add a new one to fix it.
- Integration tests do NOT run migrations — Testcontainers starts blank and tests create their own schema via `testDB.Exec`.
- See `backend/docs/migrations.md` for the full workflow and format rules.

## TypeScript/React conventions
- App Router only. Default to Server Components.
- Add `"use client"` only when browser APIs or React hooks are required.
Expand Down
36 changes: 32 additions & 4 deletions backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ all: build test

build:
@echo "Building..."


@go build -o main.exe cmd/api/main.go

# Run the application
run:
@go run cmd/api/main.go

# Create DB container
docker-run:
@docker compose up --build
Expand All @@ -24,7 +23,8 @@ docker-down:
test:
@echo "Testing..."
@go test ./... -v
# Integrations Tests for the application

# Integration tests (requires Docker)
itest:
@echo "Running integration tests..."
@go test ./internal/repository/postgres/... -v
Expand All @@ -46,4 +46,32 @@ watch:
Write-Output 'Watching...'; \
}"

.PHONY: all build run test clean watch docker-run docker-down itest
# ---- Migrations (goose) ----

migrate-up:
@go run ./cmd/migrate up

migrate-up-one:
@go run ./cmd/migrate up-by-one

migrate-down:
@go run ./cmd/migrate down

migrate-down-to:
@go run ./cmd/migrate down-to $(version)

migrate-reset:
@go run ./cmd/migrate down-to 0

migrate-status:
@go run ./cmd/migrate status

migrate-version:
@go run ./cmd/migrate version

migrate-create:
@go run ./cmd/migrate create $(name)

.PHONY: all build run test clean watch docker-run docker-down itest \
migrate-up migrate-up-one migrate-down migrate-down-to migrate-reset \
migrate-status migrate-version migrate-create
60 changes: 60 additions & 0 deletions backend/cmd/migrate/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package main

import (
"context"
"fmt"
"log"
"os"

_ "github.com/joho/godotenv/autoload"
"github.com/pressly/goose/v3"

"backend/internal/repository/postgres"
)

const migrationsDir = "migrations"

func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "usage: migrate <command> [args]")
fmt.Fprintln(os.Stderr, "commands: up, up-by-one, down, down-to <version>, reset, status, version, create <name>")
os.Exit(1)
}

if err := goose.SetDialect("postgres"); err != nil {
log.Fatalf("migrate: set dialect: %v", err)
}

command, args := os.Args[1], os.Args[2:]

// create only touches the filesystem — connect to DB is not needed
if command == "create" {
if len(args) == 0 {
log.Fatal("migrate create: name is required (e.g. make migrate-create name=add_users)")
}
if err := goose.Create(nil, migrationsDir, args[0], "sql"); err != nil {
log.Fatalf("migrate create: %v", err)
}
return
}

cfg := postgres.DBConfig{
Host: os.Getenv("BLUEPRINT_DB_HOST"),
Port: os.Getenv("BLUEPRINT_DB_PORT"),
Database: os.Getenv("BLUEPRINT_DB_DATABASE"),
Username: os.Getenv("BLUEPRINT_DB_USERNAME"),
Password: os.Getenv("BLUEPRINT_DB_PASSWORD"),
Schema: os.Getenv("BLUEPRINT_DB_SCHEMA"),
SSLMode: os.Getenv("BLUEPRINT_DB_SSLMODE"),
}

db, err := postgres.NewPostgresDB(cfg)
if err != nil {
log.Fatalf("migrate: database: %v", err)
}
defer db.Close()

if err := goose.RunContext(context.Background(), command, db, migrationsDir, args...); err != nil {
log.Fatalf("migrate %s: %v", command, err)
}
}
1 change: 1 addition & 0 deletions backend/docs/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The `docs` agent reads this index first to locate the right file before diving i
| Topic | File | Source files covered |
|---|---|---|
| Database connection & query patterns | [database.md](database.md) | `internal/repository/postgres/db.go`, `internal/repository/postgres/health_repository.go`, `internal/domain/health.go`, `internal/usecase/health_usecase.go` |
| Schema migrations (goose) | [migrations.md](migrations.md) | `cmd/migrate/main.go`, `migrations/`, `Makefile` |
| HTTP routing & handler patterns | [routing.md](routing.md) | `internal/handler/handler.go`, `internal/handler/routes.go`, `internal/handler/hello_handler.go`, `internal/handler/health_handler.go`, `internal/server/server.go` |
| Integration testing with Testcontainers | [testing.md](testing.md) | `internal/repository/postgres/health_repository_test.go`, `internal/handler/hello_handler_test.go` |
| Error handling conventions | [error-handling.md](error-handling.md) | `internal/repository/postgres/health_repository.go`, `internal/handler/health_handler.go`, `cmd/api/main.go` |
Expand Down
72 changes: 72 additions & 0 deletions backend/docs/migrations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
topic: migrations
last_verified: 2026-06-14
sources:
- cmd/migrate/main.go
- migrations/
- Makefile
---

# Migrations

## Tool
goose v3 (`github.com/pressly/goose/v3`).
Entry point: `cmd/migrate/main.go` — a thin wrapper that reuses `postgres.NewPostgresDB` and reads the same `BLUEPRINT_DB_*` env vars as the server. No separate goose binary installation needed.

## File location
`backend/migrations/` — SQL files only. Naming: `YYYYMMDDHHMMSS_<slug>.sql`, created automatically by `make migrate-create`.

## Makefile targets
| Target | What it does |
|---|---|
| `make migrate-create name=<slug>` | Create a new timestamped SQL file in `migrations/` |
| `make migrate-status` | Show applied vs. pending migrations |
| `make migrate-up` | Apply all pending migrations |
| `make migrate-up-one` | Apply only the next pending migration |
| `make migrate-down` | Roll back the last applied migration |
| `make migrate-down-to version=N` | Roll back to a specific version number |
| `make migrate-reset` | Roll back all migrations to version 0 |
| `make migrate-version` | Print the current schema version |

## SQL migration format
Each file must have exactly one `-- +goose Up` annotation. `-- +goose Down` is optional but should always be included.

```sql
-- +goose Up
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- +goose Down
DROP TABLE users;
```

Rules:
- Every statement must end with `;`
- DDL only — no application data mutations in migrations
- For multi-statement blocks (PL/pgSQL), wrap with `-- +goose StatementBegin` / `-- +goose StatementEnd`
- Avoid `-- +goose NO TRANSACTION` unless the statement genuinely cannot run in a transaction (e.g. `CREATE INDEX CONCURRENTLY`)

## Workflow for any new table
1. `make migrate-create name=add_<table>` — generates the timestamped file
2. Fill in `CREATE TABLE` (Up) and `DROP TABLE` (Down)
3. `make migrate-up` — applies to local DB
4. Build the repository layer against the new schema
5. `make itest` to verify integration tests pass

## Go migrations — not supported
Go migrations require the migration functions to be registered and compiled into the binary. `go run ./cmd/migrate` produces a fresh binary on each invocation with no registered functions. Use SQL migrations for all schema changes.

## Testcontainers and migrations
Repository integration tests do **not** run goose migrations. Testcontainers starts a blank Postgres instance; tests create their own schema via `testDB.Exec(...)` in `TestMain` or per-test setup. This keeps tests independent of migration history and fast.

## Goose tracking table
Goose creates `goose_db_version` in the schema set by `BLUEPRINT_DB_SCHEMA` (via `search_path` in the connection string). Never modify this table manually.

## Hard rules
- No DDL inside Go code — `CREATE TABLE` belongs in a migration file, not in a repository method
- No Go migration files — SQL only
- Never edit or delete an applied migration — add a new one to correct it
20 changes: 12 additions & 8 deletions backend/go.mod
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
module backend

go 1.25.6
go 1.25.7

require (
github.com/gin-contrib/cors v1.7.7
github.com/gin-gonic/gin v1.12.0
github.com/jackc/pgx/v5 v5.10.0
github.com/joho/godotenv v1.5.1
github.com/pressly/goose/v3 v3.27.1
github.com/testcontainers/testcontainers-go v0.42.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0
)
Expand All @@ -28,7 +29,7 @@ require (
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-connections v0.7.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
Expand All @@ -53,10 +54,11 @@ require (
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.2.0 // indirect
github.com/moby/moby/api v1.54.1 // indirect
github.com/moby/moby/client v0.4.0 // indirect
github.com/moby/moby/api v1.54.2 // indirect
github.com/moby/moby/client v0.4.1 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
Expand All @@ -71,6 +73,7 @@ require (
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.60.0 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/stretchr/testify v1.11.1 // indirect
Expand All @@ -81,10 +84,11 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver/v2 v2.6.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.28.0 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/net v0.56.0 // indirect
Expand Down
Loading
Loading