This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Dev server (hot-reload via air)
task run
# Dev server (go run, no hot-reload)
task dev # ENV=development, LOG_LEVEL=DEBUG
task staging # ENV=staging, LOG_LEVEL=INFO
task prod # ENV=production, LOG_LEVEL=WARN
# Run tests
task test # uses gotestsum
task test -- ./internal/... # run specific package
task test-coverage # generates coverage.html
# Lint / format
task lint # golangci-lint
task fmt # go fmt + goimports
# Build
task build # outputs build/mpiper.exe
task build-prod # ENV=production
# Python worker (from project root)
poetry run python -m worker
# Python tests
poetry run pytest worker/tests/
# Docker
task docker-build && task docker-run # API
task docker-build-worker && task docker-run-worker # WorkerEnv files: development → .env.local, staging → .env.staging, production → .env.
ENV, DB_USER, DB_PASSWORD, DB_NAME, REDIS_CONNECTION_STRING, and ENCRYPTION_KEY (exactly 32 bytes) are required — the config will panic without them.
Two-service pipeline: Go API server + Python media worker, communicating via Redis Streams (media:jobs stream). Postgres is the durable source of truth; Redis is transport-only.
Entry point: cmd/server/main.go
config.InitializeConfig→config.Init— loads env file for the currentEnvbuild variable, stores singleton (config.MustGet()available everywhere after startup).pkg/logger.New— builds a*zap.Loggerwith optional OTel log export.metrics.InitTracer/metrics.InitMetrics— wires up OTel tracing + metrics exporters.database.NewPostgresDB—sqlx.DBpool; ifAUTO_MIGRATE=trueruns embedded SQL migrations on startup.server.NewServer→server.Start— Chi router with middleware stack: request-ID, logger, tracing, metrics, recovery, slow-request detector, CORS, auth.
Layer layout inside internal/:
handler/— HTTP handlers, read request → call service → write response viapkg/utils/responseservice/— business logic (AssetService); coordinates repo + queue + storagerepository/— SQL queries via sqlx (AssetRepository)router/— Chi route registration; mounts handlers onto the router returned toserver/models/— request/response structs (UploadAssetRequest,UploadAssetResponse); not DB modelsqueue/—RedisQueue.Enqueuewrites to the stream with OTel tracing + retrymetrics/— OTel metric instruments (counters, histograms);internal/metrics/metrics.godefines all instruments,otel.gohandles provider init/shutdown
Entry: worker/__main__.py → consumer/main.py
Consumer(Redis Streams, consumer group) polls withxreadgroup, processes one message at a time.- Message contains either
job_idorasset_id.job_idis canonical;asset_idtriggers an upsert into thejobstable first. _handle_jobtakes aSELECT … FOR UPDATElock, marks the rowin_progress, callsprocess_asset_dispatch, then marksdone+ acks the stream message. On failure it re-queues (up toMAX_JOB_ATTEMPTS)._recover_stuck_pendingre-addspending/in_progressjobs older than 2 min back to the stream (recovery path, called when no messages available).worker/processing/processor.py—process_asset_dispatchroutes by asset type toimages.pyorvideos.py.worker/storage/—StorageXABC;GCSStorageis the concrete impl.worker/utils/metrics.py— Prometheus metrics viaprometheus_client.
Config singleton (Go): internal/config.MustGet() — call only after config.Init(cfg) in main. Do not pass *EnvConfig via function params; use the singleton.
Logger (Go): pkg/logger wraps zap. Request-scoped logger lives in context; retrieve with applogger.FromContext(ctx) or middleware.LoggerFromContext(ctx). Base logger is constructed once in main and passed to subsystems.
Error types (Go): pkg/errors has typed API errors (NotFoundError, BadRequestError, UnauthorizedError, ConflictError, InternalServerErrorError) each embedding *ApiError (carries StatusCode). Handler layer type-asserts on these to set HTTP status. Use fmt.Errorf("op: %w", err) for internal wrapping; use errors.New* constructors (e.g. errors.NewNotFoundError) at the service/handler boundary.
Storage (pkg/utils/storagex): StorageX interface with PutObject, GetObject, GeneratePresignedURL, PublicURL, DeleteObject. Current impl: GCSStorage. S3/MinIO provider types exist in config but are not yet implemented.
OTel: Full tracing + metrics on the API side. Go instruments are in internal/metrics/metrics.go. Collector config at observability/otel-collector.yml; Grafana/Loki/Tempo/Prometheus configs in observability/. Python side uses prometheus_client (not OTel).
assets— core media record;statusenum:uploading → uploaded → processing → ready / failedvariants.image— deduplicated byvariant_hash(content+params hash); immutable once writtenjobs— processing job per asset;statusenum:pending → in_progress → done / failed;attemptstracked for retry cap
Migrations are plain SQL in db/migrations/. The Go server can auto-run them at startup (AUTO_MIGRATE=true); the Python worker also runs them via worker/consumer/migrations.py.