# Wucher Backend Wucher is a Go backend built with GoFiber. The runtime uses MySQL for persistence and transient auth state, Amazon SQS for asynchronous email jobs, and Amazon SES v2 for email delivery. ## Prerequisites - Go 1.25+ - MySQL - LocalStack or AWS SQS/SES access for queue testing ## Project Structure - `cmd/api/` API bootstrap - `cmd/worker/` SQS worker bootstrap - `internal/app/` application wiring - `internal/resilience/` circuit breaker executor and dependency error classifiers - `internal/service/` business logic - `internal/transport/http/` GoFiber handlers and middleware - `internal/repository/mysql/` MySQL repositories and transient stores - `internal/repository/sqs/` SQS publisher/consumer adapter - `internal/queue/` queue contracts, serializer, retry policy, and worker loop ## Runtime Architecture - HTTP handlers stay thin and delegate to services. - Auth and other business writes persist to MySQL first. - API/service code never sends email synchronously. - Email jobs are written to `email_outbox_messages` when outbox is enabled, or published directly to SQS when outbox is disabled. - Worker publishes outbox rows to the SQS send queue, consumes SQS messages with long polling, and sends email through Amazon SES v2. - SES bounce, complaint, and delivery tracking should use a separate flow: - SES Configuration Set - SNS topic subscription - dedicated SQS event queue - Circuit breakers are applied only at outbound dependency boundaries: - Microsoft Entra token exchange - SES delivery - SQS publish/consume/ack calls - Transient auth state previously kept in Redis is now stored in MySQL tables: - `auth_runtime_tokens` - `queue_idempotency_records` `gorm` auto-migration creates these tables on startup. ## Getting Started Install dependencies: ```bash go mod tidy ``` Create local config from the example: ```bash cp .env.example .env ``` Run the API: ```bash go run ./cmd/api ``` Run the worker: ```bash go run ./cmd/worker ``` Worker monitor endpoints: ```bash curl http://127.0.0.1:9090/health curl http://127.0.0.1:9090/metrics curl http://127.0.0.1:9091/health curl http://127.0.0.1:9091/metrics ``` - `/metrics` exposes Prometheus format for API and worker processes. - `/health` remains JSON health/worker snapshot. ## Environment Variables Example values are in `.env` for local development. Important groups: - Email delivery - `EMAIL_PROVIDER` - `EMAIL_MOCK_DIR` - MySQL - `MYSQL_DSN` - `MYSQL_MAX_OPEN_CONNS` - `MYSQL_MAX_IDLE_CONNS` - `MYSQL_CONN_MAX_LIFETIME` - `MYSQL_CONN_MAX_IDLE_TIME` - Auth - `AUTH_EMAIL_VERIFY_URL` - `AUTH_PASSWORD_RESET_URL` - `AUTH_SECURITY_PIN_RESET_URL` - `AUTH_TOTP_CHALLENGE_TTL` - `AUTH_SSO_STATE_TTL` - `AUTH_JWT_ACCESS_SECRET` - `AUTH_JWT_REFRESH_SECRET` - `AUTH_WEBAUTHN_*` - Queue / worker - `QUEUE_MESSAGE_SCHEMA_VERSION` - `QUEUE_ENABLE_LEGACY_PAYLOAD_FALLBACK` - `QUEUE_IDEMPOTENCY_KEY_PREFIX` - `QUEUE_IDEMPOTENCY_TTL` - `QUEUE_PROCESSING_TTL` - `QUEUE_WORKER_CONCURRENCY` - `QUEUE_WORKER_MAX_BATCH_SIZE` - `QUEUE_WORKER_PROCESS_TIMEOUT` - `QUEUE_WORKER_BACKOFF_MIN` - `QUEUE_WORKER_BACKOFF_MAX` - `QUEUE_WORKER_PERMANENT_FAILURE_DELAY` - `QUEUE_OUTBOX_ENABLED` - `QUEUE_OUTBOX_POLL_INTERVAL` - `QUEUE_OUTBOX_BATCH_SIZE` - `QUEUE_OUTBOX_DISPATCH_TIMEOUT` - `QUEUE_OUTBOX_LOCK_TTL` - `QUEUE_OUTBOX_MAX_ATTEMPTS` - File queue / file worker - `FILE_QUEUE_MESSAGE_SCHEMA_VERSION` - `FILE_QUEUE_IDEMPOTENCY_KEY_PREFIX` - `FILE_QUEUE_IDEMPOTENCY_TTL` - `FILE_QUEUE_PROCESSING_TTL` - `FILE_QUEUE_WORKER_CONCURRENCY` - `FILE_QUEUE_WORKER_MAX_BATCH_SIZE` - `FILE_QUEUE_WORKER_PROCESS_TIMEOUT` - `FILE_QUEUE_WORKER_SHUTDOWN_TIMEOUT` - `FILE_QUEUE_WORKER_BACKOFF_MIN` - `FILE_QUEUE_WORKER_BACKOFF_MAX` - `FILE_QUEUE_WORKER_PERMANENT_FAILURE_DELAY` - `FILE_QUEUE_WORKER_MONITOR_ADDR` - `FILE_QUEUE_WORKER_HEALTH_ERROR_THRESHOLD` - `FILE_QUEUE_WORKER_DEPTH_POLL_INTERVAL` - `FILE_QUEUE_OUTBOX_ENABLED` - `FILE_QUEUE_OUTBOX_POLL_INTERVAL` - `FILE_QUEUE_OUTBOX_BATCH_SIZE` - `FILE_QUEUE_OUTBOX_DISPATCH_TIMEOUT` - `FILE_QUEUE_OUTBOX_LOCK_TTL` - `FILE_QUEUE_OUTBOX_MAX_ATTEMPTS` - Circuit breakers - `CB_DEFAULT_ENABLED` - `CB_DEFAULT_MAX_REQUESTS` - `CB_DEFAULT_INTERVAL` - `CB_DEFAULT_BUCKET_PERIOD` - `CB_DEFAULT_TIMEOUT` - `CB_DEFAULT_MIN_REQUESTS` - `CB_DEFAULT_FAILURE_RATIO` - `CB_DEFAULT_CONSECUTIVE_FAILURES` - `CB_MICROSOFT_SSO_ENABLED` - `CB_SES_ENABLED` - `CB_SQS_ENABLED` - AWS / SES - `AWS_REGION` - `SES_REGION` - `SES_FROM` - `SES_CONFIGURATION_SET` - `SES_SEND_TIMEOUT` - `SES_ENDPOINT` - `SES_ALLOW_INSECURE_ENDPOINT` - AWS / SQS - Email queue: - `AWS_REGION` - `AWS_ACCESS_KEY_ID` - `AWS_SECRET_ACCESS_KEY` - `SQS_ENDPOINT` - `SQS_ALLOW_INSECURE_ENDPOINT` - `SQS_QUEUE_URL` - `SQS_QUEUE_TYPE` - `SQS_MESSAGE_GROUP_ID` - `SQS_MAX_MESSAGES` - `SQS_WAIT_TIME_SECONDS` - `SQS_VISIBILITY_TIMEOUT` - File queue: - `FILE_SQS_REGION` - `FILE_SQS_ENDPOINT` - `FILE_SQS_QUEUE_URL` - `FILE_SQS_QUEUE_TYPE` - `FILE_SQS_MESSAGE_GROUP_ID` - `FILE_SQS_MAX_MESSAGES` - `FILE_SQS_WAIT_TIME_SECONDS` - `FILE_SQS_VISIBILITY_TIMEOUT` - `FILE_SQS_ALLOW_INSECURE_ENDPOINT` - File storage (S3) - `FILE_STORAGE_PROVIDER` - `FILE_S3_BUCKET` - `FILE_S3_REGION` - `FILE_S3_ENDPOINT` - `FILE_S3_FORCE_PATH_STYLE` - `FILE_S3_ALLOW_INSECURE_ENDPOINT` - `FILE_S3_AUTO_CREATE_BUCKET` - `FILE_OBJECT_KEY_PREFIX` - `FILE_S3_PRESIGN_PUT_TTL` - `FILE_S3_PRESIGN_GET_TTL` - WOPI / Collabora - `WOPI_TOKEN_SECRET` - `WOPI_PROOF_SECRET` - `WOPI_DISCOVERY_URL` - `WOPI_PUBLIC_BASE_URL` - `WOPI_EDIT_ACTION_URL` - `WOPI_TOKEN_TTL` - `WOPI_MAX_BODY_BYTES` - `WOPI_REQUIRE_PROOF` - Worker mode selector - `WORKER_JOB=email` - `WORKER_JOB=file_processing` ## WOPI / Collabora Hardening - Keep WOPI endpoints behind the dedicated `/wopi` router and do not expose them through the session-authenticated app middleware chain. - Use HTTPS end to end in production. - Restrict network access to the Collabora host or private network allowlist. - Set Collabora `aliasgroup`/host configuration so it can only call back to this backend. - Keep `WOPI_TOKEN_SECRET` and `WOPI_PROOF_SECRET` in env or a secrets manager and rotate them periodically. - Scope IAM to the minimum S3 bucket/prefix needed by file storage. - Keep direct S3 URLs disabled for editor access; the backend should proxy file bytes. - Ensure backend logs capture open/save/exit-save events for auditability. - Verify `/hosting/discovery` reachability and S3 connectivity in health checks before enabling Collabora traffic. Per-dependency overrides are supported with the same shape as the default block: - `CB_MICROSOFT_SSO_*` - `CB_SES_*` - `CB_SQS_*` Example: `CB_SES_TIMEOUT=45s` or `CB_SQS_CONSECUTIVE_FAILURES=3`. ## Circuit Breakers - Breakers are scoped per outbound dependency, not per HTTP endpoint and not global middleware. - MySQL repositories and internal business logic are intentionally not wrapped. - Only dependency-health failures count toward opening the breaker. - Microsoft SSO: network errors, timeouts, HTTP `429`, and `5xx` - SES: network errors, throttling, and AWS server faults - SQS: network errors, throttling, and AWS server faults - Client/config/input errors are excluded so they do not open the breaker. - State transitions are logged through the standard application logger. ## Queue Behavior - Long polling is enabled through `SQS_WAIT_TIME_SECONDS`. - Messages are deleted only after successful processing or explicit ack of duplicates. - Transient failures use `ChangeMessageVisibility` with backoff. - Permanent failures are left unacked so SQS redrive policy can move them to the DLQ. - The default producer schema is `v2`; legacy payload fallback remains available for older messages. - Configure DLQ on the queue itself in AWS/LocalStack. The application no longer publishes directly to a DLQ URL. ## Prometheus Metrics Scrape endpoints: - API: `http://:8080/metrics` - Email worker monitor: `http://:9090/metrics` - File worker monitor: `http://:9091/metrics` Example Prometheus scrape config: ```yaml scrape_configs: - job_name: "wucher-api" static_configs: - targets: ["127.0.0.1:8080"] - job_name: "wucher-email-worker" static_configs: - targets: ["127.0.0.1:9090"] - job_name: "wucher-file-worker" static_configs: - targets: ["127.0.0.1:9091"] ``` Metrics and labels: - `app_aws_requests_total{component,service,operation,result}` - `app_aws_request_duration_seconds{component,service,operation}` - `app_aws_errors_total{component,service,operation,error_class}` where `error_class` is `4xx`, `5xx`, or `other` - `app_aws_transferred_bytes_total{component,service,operation,direction}` - `app_queue_messages_received_total{component,queue_name}` - `app_queue_messages_deleted_total{component,queue_name}` - `app_queue_visibility_changes_total{component,queue_name,result}` - `app_queue_send_total{component,queue_name,result}` - `app_queue_retries_total{component,queue_name,result}` - `app_queue_message_bytes_total{component,queue_name,direction}` - `app_queue_depth{component,queue_name}` - `app_worker_jobs_processed_total{component,job_type,result}` - `app_worker_job_duration_seconds{component,job_type}` - `app_email_send_total{component,provider,result}` - `app_email_send_duration_seconds{component,provider}` - `app_file_upload_total{component,result}` - `app_file_delete_total{component,result}` - `app_file_head_total{component,result}` - `app_file_presign_download_total{component,result}` PromQL examples: - Error rate per AWS operation: ```promql sum by (component, service, operation) (rate(app_aws_requests_total{result="error"}[5m])) / clamp_min(sum by (component, service, operation) (rate(app_aws_requests_total[5m])), 1e-9) ``` - P95 latency per AWS operation: ```promql histogram_quantile( 0.95, sum by (le, component, service, operation) ( rate(app_aws_request_duration_seconds_bucket[5m]) ) ) ``` - Queue depth: ```promql max by (component, queue_name) (app_queue_depth) ``` - Worker success/failure count: ```promql sum by (component, job_type, result) (increase(app_worker_jobs_processed_total[15m])) ``` - SES send failure rate: ```promql sum by (component) (rate(app_email_send_total{provider="ses", result="error"}[5m])) / clamp_min(sum by (component) (rate(app_email_send_total{provider="ses"}[5m])), 1e-9) ``` - S3 4xx/5xx error rate: ```promql sum by (operation, error_class) (rate(app_aws_errors_total{service="s3"}[5m])) ``` - S3 GetObject transferred bytes (estimated from presign download flow): ```promql sum by (component) (rate(app_aws_transferred_bytes_total{service="s3",operation="getobject",direction="download"}[5m])) ``` - SQS message bytes in/out: ```promql sum by (component, queue_name, direction) (rate(app_queue_message_bytes_total[5m])) ``` - SQS retry/requeue count: ```promql sum by (component, queue_name, result) (increase(app_queue_retries_total[15m])) ``` ## File Manager - Queue separation: - Email queue: `app-queue` (+ `app-queue-dlq`) - File queue: `app-file-queue` (+ `app-file-queue-dlq`) - `FILE_SQS_QUEUE_URL` must be different from `SQS_QUEUE_URL`. - API startup validates queue separation and refuses startup on misconfiguration. - Upload writes file metadata + file outbox durably; file queue publish is handled asynchronously by the file outbox dispatcher. Local run checklist: ```bash # 1) Start localstack (queue + bucket bootstrap script will run) cd ../localstack && docker compose up -d WORKER_JOB=file_processing go run ./cmd/worker ``` ## SES Delivery Notes - `EMAIL_PROVIDER=mock` is the recommended local/dev default when you do not have real AWS SES access. - Mock delivery writes one JSON file per delivered email into `EMAIL_MOCK_DIR`. - `SES_FROM` must be a verified identity or domain in Amazon SES. - `SES_ENDPOINT` should be blank in production and only overridden for local/dev stacks such as LocalStack. - Do not point SES events to the main send queue. Use `SES_CONFIGURATION_SET` with SNS -> separate SQS event queue. - The codebase includes an SES event parser for SNS-wrapped notifications, but an event consumer is not started by default. ## Migration Notes Detailed migration notes are in [docs/queue-sqs-migration.md](docs/queue-sqs-migration.md). Data migration tracking wiki is in [docs/data-migration-wiki.md](docs/data-migration-wiki.md). Main breaking changes: - Redis runtime support has been removed from the codebase. - Redis env vars are obsolete and should be removed from deployment config. - SMTP runtime env/config has been removed in favor of SES. - Worker now relies on MySQL for queue idempotency and auth/runtime state persistence. - SQS must have a redrive policy and DLQ configured outside the application. ## CI / Build Selection The pipeline always builds and pushes the `api` image on every push to `development` or `staging`. To also build and push the worker or file-worker image, include the corresponding tag anywhere in the commit message: | Tag | Image built | Docker target | |---|---|---| | `#PUSH-WORKER` | `mybitlab/wfm:-worker` | `worker` | | `#PUSH-FILE-WORKER` | `mybitlab/wfm:-file-worker` | `file-worker` | Both tags can appear in the same commit message to build all three in one pipeline run: ``` ci: deploy all services #PUSH-WORKER #PUSH-FILE-WORKER ``` Omitting a tag skips that image entirely — it will not be built or pushed. ## Testing Run the full test suite: ```bash go test ./... ``` Generate coverage: ```bash go test ./... -coverprofile=coverage.out go tool cover -func=coverage.out go tool cover -html=coverage.out -o coverage.html ```