Development Guide

March 19, 2026 · View on GitHub

Dev Environment Setup

API Server

Three options, from most isolated to simplest:

Option 1: Docker with Air (Recommended)

Consistent environment, no local Go required for running:

# Start database + API with hot reloading
docker compose up --build app

Code changes are detected automatically and the app rebuilds inside the container.

Option 2: Local with Air

Run natively with hot reloading:

# Install Air
go install github.com/air-verse/air@latest

# Start the database
docker compose up -d db

# Run with Air
export WIMAPI_DATABASE_HOST=localhost
air

Option 3: Direct Execution

No hot reloading, simplest setup:

docker compose up -d db
export WIMAPI_DATABASE_HOST=localhost
go run cmd/api/main.go

Crawler

The crawler requires mock data and a mock HTTP server to serve it:

Docker (recommended):

# Generate mock data, then run everything
go run ./cmd/generate-mock -count 100
docker compose up -d db mock-server
docker compose run --rm crawler

Local:

# Generate mock data
go run ./cmd/generate-mock -count 100

# Start DB, serve mock data locally
docker compose up -d db
cd data && python3 -m http.server 8081 &

# Update config.yaml source URL to: http://localhost:8081/mock-crawler-data.json
WIMAPI_DATABASE_HOST=localhost go run ./cmd/crawler

See tools.md for details on the mock data generator and seed tool.

IPFS Publisher

The publisher requires Storacha credentials (ipfs.key, proof.ucan) and a configured space DID.

Docker:

# Run as a one-shot command
docker compose run --rm ipfs-publisher

# Or start with a profile
docker compose --profile ipfs-publisher up

Local:

docker compose up -d db
WIMAPI_DATABASE_HOST=localhost go run ./cmd/ipfs-publisher

See ipfs-publisher.md for credential setup, configuration, and architecture details.

Stopping

# Stop containers
docker compose down

# Stop and delete all data (full reset)
docker compose down -v

Docker Compose Services

ServiceDescriptionPortProfile
dbPostgreSQL 155432default
appAPI server with Air hot reloading8080default
mock-serverNginx serving mock JSON files8081default
crawlerCrawler (one-shot)crawler
ipfs-publisherIPFS publisher (one-shot)ipfs-publisher

The crawler uses the crawler profile, so it only starts when explicitly requested:

# Start everything including crawler
docker compose --profile crawler up

# Or run crawler as a one-shot command
docker compose run --rm crawler

Volumes:

  • postgres_data — Persistent database storage
  • app_tmp — Temporary build artifacts for Air

Configuration

Precedence (highest to lowest)

  1. CLI flag -config (config file location only)
  2. Environment variable WIMAPI_CONFIG_FILE (config file location)
  3. Individual environment variables (e.g., WIMAPI_DATABASE_HOST)
  4. Config file values (config.yaml)
  5. Built-in defaults

Environment Variable Naming

Prefix with WIMAPI_, use underscores for nesting:

  • database.hostWIMAPI_DATABASE_HOST
  • server.portWIMAPI_SERVER_PORT
  • crawler.orchestrator.batch_sizeWIMAPI_CRAWLER_ORCHESTRATOR_BATCH_SIZE

All Settings

Server

SettingEnv VarDefault
server.portWIMAPI_SERVER_PORT8080
server.hostWIMAPI_SERVER_HOSTlocalhost
server.read_timeoutWIMAPI_SERVER_READ_TIMEOUT30 (seconds)
server.write_timeoutWIMAPI_SERVER_WRITE_TIMEOUT30 (seconds)

Database

SettingEnv VarDefault
database.hostWIMAPI_DATABASE_HOSTdb (Docker) / localhost (local)
database.portWIMAPI_DATABASE_PORT5432
database.userWIMAPI_DATABASE_USERwimapi
database.passwordWIMAPI_DATABASE_PASSWORDwimapi_password
database.nameWIMAPI_DATABASE_NAMEwimapi_db
database.ssl_modeWIMAPI_DATABASE_SSL_MODEdisable

Logging

SettingEnv VarDefault
log.levelWIMAPI_LOG_LEVELinfo
log.fileWIMAPI_LOG_FILEwimapi.log

Crawler Orchestrator

SettingEnv VarDefault
crawler.orchestrator.batch_sizeWIMAPI_CRAWLER_ORCHESTRATOR_BATCH_SIZE100
crawler.orchestrator.checkpoint_intervalWIMAPI_CRAWLER_ORCHESTRATOR_CHECKPOINT_INTERVAL10 (batches)
crawler.orchestrator.debug_sample_rateWIMAPI_CRAWLER_ORCHESTRATOR_DEBUG_SAMPLE_RATE0.01 (1%)
crawler.orchestrator.max_retriesWIMAPI_CRAWLER_ORCHESTRATOR_MAX_RETRIES3
crawler.orchestrator.retry_backoff_baseWIMAPI_CRAWLER_ORCHESTRATOR_RETRY_BACKOFF_BASE1s
crawler.orchestrator.concurrencyWIMAPI_CRAWLER_ORCHESTRATOR_CONCURRENCY2
crawler.orchestrator.run_timeoutWIMAPI_CRAWLER_ORCHESTRATOR_RUN_TIMEOUT4h

Crawler HTTP Client

SettingEnv VarDefault
crawler.http_client.timeoutWIMAPI_CRAWLER_HTTP_CLIENT_TIMEOUT30s
crawler.http_client.max_idle_connsWIMAPI_CRAWLER_HTTP_CLIENT_MAX_IDLE_CONNS10

IPFS Publisher

SettingEnv VarDefaultDescription
ipfs.space_didWIMAPI_IPFS_SPACE_DID""Storacha space DID (required)
ipfs.private_key_pathWIMAPI_IPFS_PRIVATE_KEY_PATH./ipfs.keyPath to Ed25519 private key
ipfs.proof_pathWIMAPI_IPFS_PROOF_PATH./proof.ucanPath to UCAN delegation proof
ipfs.gateway_urlWIMAPI_IPFS_GATEWAY_URLhttps://w3s.linkIPFS gateway base URL
ipfs.max_retriesWIMAPI_IPFS_MAX_RETRIES3Upload retry attempts
ipfs.retry_backoff_baseWIMAPI_IPFS_RETRY_BACKOFF_BASE1sBase backoff between retries
ipfs.concurrencyWIMAPI_IPFS_CONCURRENCY2Parallel WRP processing
ipfs.batch_sizeWIMAPI_IPFS_BATCH_SIZE100WRPs claimed per batch
ipfs.run_timeoutWIMAPI_IPFS_RUN_TIMEOUT1hMaximum run duration

See ipfs-publisher.md for full documentation.

Crawler Sources

Sources are configured as a map under crawler.sources. Each source has:

SettingDescription
enabledWhether this source is active
typeSource type (currently: generic-json)
urlData URL to fetch
rate_limitRequests per second (simulated for generic-json)

See crawler.md for details on source types and adding new sources.

Example config.yaml

server:
  port: "8080"
  host: "localhost"
  read_timeout: 30
  write_timeout: 30

database:
  host: "db"
  port: "5432"
  user: "wimapi"
  password: "wimapi_password"
  name: "wimapi_db"
  ssl_mode: "disable"

log:
  level: "info"
  file: "wimapi.log"

crawler:
  orchestrator:
    batch_size: 100
    checkpoint_interval: 10
    concurrency: 2
    run_timeout: 4h
  http_client:
    timeout: 30s
    max_idle_conns: 10
  sources:
    mock-api:
      enabled: true
      type: "generic-json"
      url: "http://mock-server/data/mock-crawler-data.json"
      rate_limit: 1000.0

Database Migrations

Migrations run automatically on startup using golang-migrate with embedded SQL files.

How It Works

  1. Migration files live in internal/database/migrations/
  2. Naming convention: NNNNNN_description.up.sql / NNNNNN_description.down.sql
  3. On startup, the app:
    • Creates schema_migrations table if needed
    • Checks which migrations have been applied
    • Runs pending migrations in order
    • Reports the current schema version

Migration SQL is compiled into the binary — no need to ship files separately.

Adding a Migration

Create numbered files in internal/database/migrations/:

000003_add_new_feature.up.sql    # Forward migration
000003_add_new_feature.down.sql  # Rollback migration

Best practices:

  • Use sequential numbering (000001, 000002, ...)
  • Always provide both .up.sql and .down.sql
  • Wrap in transactions: BEGIN; ... COMMIT;
  • Never modify already-applied migration files
  • Test on a clean database before committing
  • For production: add indexes CONCURRENTLY (outside transactions) to avoid table locks

Checking Migration Version

docker exec wimapi-db psql -U wimapi -d wimapi_db -c "SELECT * FROM schema_migrations;"

Resetting the Database

docker compose down -v

This removes all volumes, including the PostgreSQL data directory.

Testing

Unit Tests

GOCACHE=$(pwd)/.cache go test -cover ./...

The explicit GOCACHE avoids sandbox permission issues on some systems (e.g., Docker-mounted volumes).

Run a single test or package:

# Single test
go test ./internal/services -run TestRelyingPartyService_Search

# Single package
go test ./internal/services -cover

Integration Tests (Smoke Tests)

./scripts/smoke-test.sh

This script:

  • Starts the PostgreSQL container if not running
  • Waits for the database to be ready
  • Runs integration-tagged tests (//go:build integration) in: ./cmd/seed, ./internal/database, ./internal/database/seed, ./internal/server
  • Stops the container after tests complete

Crawler E2E Tests

./scripts/crawler-e2e-test.sh

Tests multi-source crawling, resume after SIGTERM, idempotency, change detection, and hard crash (SIGKILL) recovery. Individual tests can be run selectively:

./scripts/crawler-e2e-test.sh test1   # Full multi-source crawl
./scripts/crawler-e2e-test.sh test2   # Resume after SIGTERM
./scripts/crawler-e2e-test.sh test3   # Idempotency
./scripts/crawler-e2e-test.sh test4   # Change detection
./scripts/crawler-e2e-test.sh test5   # Resume after SIGKILL

IPFS Publisher E2E Tests

./scripts/ipfs-e2e-test.sh              # Start DB if needed, run test
./scripts/ipfs-e2e-test.sh --skip-db    # DB already running

Performs real uploads to Storacha and verifies the full publishing pipeline. Requires ipfs.key and proof.ucan in the project root.

Performance Tests

./scripts/perf-test.sh

Seeds a large dataset, then tests pagination, retrieval, and concurrent load. Results are exported to tmp/perf-results-<timestamp>.json.

Env VarDefaultDescription
WIMAPI_PERF_RP_COUNT2000Relying parties to seed
WIMAPI_PERF_PAGE_LIMIT100Page size for pagination tests
WIMAPI_PERF_MAX_PAGES50Maximum pages to retrieve
WIMAPI_PERF_HTTP_CONCURRENCY16HTTP request concurrency
WIMAPI_PERF_STRESS_CONCURRENCY32Stress test concurrency
WIMAPI_PERF_STRESS_DURATION_SEC15Stress test duration (seconds)

Test Patterns by Layer

LayerApproachExample
RepositoriesLightweight pgx stubs (no live DB) — assert query args, scanning, errorsinternal/repositories/relying_party_repository_test.go
ServicesTestify mocks for repositories — assert business rules, pagination, filtersinternal/services/relying_party_service_test.go
HandlersGin test context, call handler methods directly — assert status codes, JSON shapeinternal/server/handlers_helper_test.go
Router integrationNewServer() + httptest.Server — verify routing, middleware, error mappinginternal/server/router_integration_test.go
Smoke testsIntegration-tagged, require live DB — exercise endpoints end-to-endinternal/server/api_smoke_test.go

Tests are colocated with implementation files and follow the Test<Function> naming convention.

Common Troubleshooting

"connection refused" to database:

  • Local development requires WIMAPI_DATABASE_HOST=localhost (the default db is the Docker hostname)

Stale data or schema issues:

  • Reset with docker compose down -v to drop all data and re-run migrations on next startup

Duplicate key errors during seeding:

  • Use the -fresh flag: go run ./cmd/seed -fresh to truncate all tables before inserting