The two services
Manager
The Manager follows a hexagonal layout. HTTP adapters map requests onto services, and services reach infrastructure only through ports. Read paths and write paths live in separate service packages. It serves twelve operations across two path prefixes:
Three infrastructure dependencies back the API:
- MongoDB holds connection records and job records.
- RabbitMQ carries a new job to the Worker on the queue that
RABBITMQ_FETCHER_WORK_QUEUEnames. - Valkey or Redis backs the schema cache and the rate limiter on the connection test. The schema cache falls back to process memory when Redis is unreachable.
202 Accepted. A repeat of the same request inside a five-minute window answers 200 OK with the job that already exists.
Worker
The Worker has no primary HTTP server. It consumes the work queue and runsRABBITMQ_NUMBERS_OF_WORKERS jobs in parallel, five by default. A small health server on HEALTH_PORT carries the probes and the metrics endpoint.
For each job the Worker:
- Resolves the connections the job maps and runs the extraction through the embedded Engine.
- Signs the plaintext JSON with HMAC-SHA256, using a key derived from the master key.
- Encrypts the signed payload with AES-GCM and writes it to S3-compatible object storage.
- Publishes
job.completedorjob.failedto the exchange thatRABBITMQ_JOB_EVENTS_EXCHANGEnames.
STREAMING_ENABLED is false or the events exchange is blank, because a silent no-op emitter would swallow the contract.
The Worker speaks the S3 protocol. SeaweedFS works as a storage target through its S3-compatible gateway, which is how the local Docker Compose stack runs. Set retention on results with a lifecycle policy on the bucket.
Messages between them
Every message the Manager publishes carries an HMAC-SHA256 signature. The signed payload binds the timestamp, the signature version, the tenant ID, the job ID, the exchange, and the routing key. The body comes last. A replay of the same body under a different tenant or a different route fails verification. The signer refuses any key shorter than 32 bytes and compares signatures in constant time.The Engine underneath
The Engine owns what an extraction means. A host owns how it runs.
The Engine is a separate Go module:
github.com/LerianStudio/fetcher/pkg/engine. Its go.mod carries no require block at all, and a CI job fails the build if one appears. A second build-enforced test walks every transitive dependency and rejects anything that is neither the Go standard library nor engine-module-local. No HTTP framework, no queue client, no database driver, and no cloud SDK can enter.
That constraint is the point. The Engine adds no third-party dependency, so a host application can embed it without a single new entry in its own dependency tree.
Ports
A host wires the Engine through a small set of ports. One is always mandatory. A second is mandatory only when encrypted persistence is on. The rest are optional.
A port supplied as a typed nil counts as absent. The Engine detects that at construction, so a misconfigured host fails at startup instead of panicking on the first call.
The module also ships in-memory implementations of the storage-facing ports — the connector registry, the connection store, the schema cache, the result sink, and the execution store. That covers the infrastructure a test needs, so you can exercise the Engine with no MongoDB, no Redis, no RabbitMQ, and no object storage. It ships no
CredentialProtector, so a test that turns encrypted persistence on has to supply its own.
Failure classification
The Engine returns one of eleven stable categories:validation, not_found, unauthorized, forbidden, limit_exceeded, conflict, unavailable, connect, timeout, canceled, and internal. A host maps each category onto its own transport. The Manager maps conflict to HTTP 409, for example.
Errors that cross the Engine boundary carry fixed text. The Engine discards the driver error behind a failure, because that text can embed a DSN, a credential, or driver internals.
Health and readiness
Both services mount
/health, /readyz, /readyz/tenant/{id}, and /metrics before authentication, so Kubernetes and load balancers reach them without a token.
/healthanswers 503 until the startup self-probe succeeds. The kubelet then restarts a pod that could not reach its dependencies at boot./readyzruns every dependency probe in parallel on each request, with no cache and a per-dependency timeout. The Manager probes MongoDB, RabbitMQ, and Redis. The Worker probes MongoDB, RabbitMQ, and the storage bucket. In multi-tenant mode both services add the Tenant Manager and the multi-tenant Redis.- On
SIGTERMboth services answer/readyzwith 503 forREADYZ_DRAIN_DELAY_SECseconds, 12 by default, before they tear down connections. Kubernetes removes the pod from the Service while it still serves traffic.
Embedding instead of deploying
The Manager and the Worker are hosts, not privileged layers. Provide a connector registry and a connection store, and your application gets the same query planning, the same limits, the same tenant checks, and the same result shape in process. It needs no queue, no object storage, and no separate deployment. Lerian products embed the Engine exactly this way. Read Core concepts for the model the Engine implements.
Next steps
Connections
Register, test, update, and delete a datasource connection.
Configuration
The environment variables that shape a Fetcher deployment.

