A service for accepting, validating, and processing NACHA ACH batch-payment files. Clients submit a signed ACH file over HTTP; the API validates it, persists it to PostgreSQL, and enqueues it on Redis; a worker pool then settles each entry and marks the batch complete.
Built with Go 1.26, PostgreSQL (pgx), and Redis (asynq).
batchwire splits ACH processing into two independently scalable binaries that share a PostgreSQL database and a Redis-backed work queue:
api— an HTTP server that authenticates signed requests, parses and validates submitted NACHA files, records each batch and its entries, and enqueues the batch for processing.worker— an asynq consumer that re-validates each batch, settles every entry through a pluggableSettler, and transitions the batch to its terminal state.
Every submission is content-addressed for idempotency, every request is Ed25519-signed, batches move through an explicit state machine, and the worker tolerates retries and duplicate deliveries without double-posting entries.
signed POST /v1/batches
┌────────┐ (Ed25519 headers) ┌──────────────────┐
│ client │ ─────────────────────────▶│ api │
└────────┘ │ • verify sig │
▲ │ • parse+validate│
│ GET /v1/batches/{id} │ • create batch │──┐ insert
│ GET .../entries │ • enqueue │ │
└───────────────────────────────│ │ ▼
└────────┬─────────┘ ┌──────────────┐
│ enqueue │ PostgreSQL │
│ (asynq) │ batches, │
▼ │ entries │
┌───────────────┐ └──────────────┘
│ Redis │ ▲
└───────┬───────┘ │ update
│ dequeue │
▼ │
┌───────────────┐ │
│ worker │─────────┘
│ • revalidate │
│ • settle each │
│ • complete │
└───────────────┘
- NACHA ACH parsing and validation — fixed-width file/batch/entry parsing with structural checks: service class codes, standard entry class codes (
PPD,CCD,CTX,WEB,TEL,IAT, and more), debit/credit totals, entry/addenda counts, batch hash, and ascending batch numbers. - Ed25519 request signing — every protected endpoint requires a signature over a canonical
method · path · timestamp · sha256(body)string, with a configurable timestamp window to bound replay. - Idempotent submissions — the SHA-256 of the request body is the idempotency key; resubmitting the same file returns the original batch (
200) instead of creating a duplicate (201). - Asynchronous processing — submissions are enqueued on Redis and processed by a configurable worker pool with per-queue rate limiting, bounded retries, task timeouts, and retention.
- Explicit state machine — batches move
received → queued → processing → completed | failed; entries movepending → posted | returned | failed. - Per-client rate limiting — token-bucket limiting keyed by API key id (falling back to client IP), with idle-limiter sweeping.
- Ownership-scoped reads — batches and entries are only visible to the key id that submitted them.
- Operational endpoints —
GET /healthz(liveness) andGET /readyz(database readiness), plus structured request logging, request IDs, and panic recovery. - Graceful lifecycle — startup migrations, connection-pool tuning, and signal-driven graceful shutdown.
Requires Go 1.26+.
git clone https://github.com/TerminalKyle/batchwire.git
cd batchwire
go mod download
go build ./cmd/api
go build ./cmd/worker
go build ./cmd/signbatchwire needs a reachable PostgreSQL and Redis at runtime. The defaults match:
docker run -d --name batchwire-pg \
-e POSTGRES_USER=batchwire -e POSTGRES_PASSWORD=batchwire -e POSTGRES_DB=batchwire \
-p 5432:5432 postgres:18
docker run -d --name batchwire-redis -p 6379:6379 redis:7# 1. Generate an Ed25519 client keypair into ./keys
go run ./cmd/sign keygen -id client-1 -dir keys
# 2. Generate a valid sample ACH file
go run ./cmd/sign sample -out testdata/sample.ach
# 3. Start the api (applies DB migrations on startup)
go run ./cmd/api
# 4. In another terminal, start the worker
go run ./cmd/worker
# 5. Build a signed request — prints a ready-to-run curl command
go run ./cmd/sign request \
-key keys/client-1.key -id client-1 \
-file testdata/sample.ach \
-url http://localhost:8080/v1/batchesRun the printed curl to submit; you'll get a 201 with the batch id. Fetch it back to watch it reach completed. The api loads public keys from BATCHWIRE_KEYSTORE_DIR (default keys/), so step 1 is what authorizes client-1 to submit.
All settings are read from environment variables (prefixed BATCHWIRE_). A .env file in the working directory is loaded automatically; existing environment variables take precedence.
| Variable | Default | Description |
|---|---|---|
BATCHWIRE_HTTP_ADDR |
:8080 |
API listen address |
BATCHWIRE_SHUTDOWN_TIMEOUT |
20s |
Graceful shutdown deadline |
BATCHWIRE_DATABASE_URL |
postgres://batchwire:batchwire@localhost:5432/batchwire?sslmode=disable |
PostgreSQL DSN |
BATCHWIRE_DB_MAX_CONNS |
50 |
Max pool connections |
BATCHWIRE_DB_MIN_CONNS |
5 |
Min pool connections |
BATCHWIRE_MIGRATE_ON_STARTUP |
true |
Apply migrations when the api starts |
BATCHWIRE_DB_CONNECT_TIMEOUT |
10s |
Connection timeout |
BATCHWIRE_DB_STATEMENT_TIMEOUT |
15s |
Per-statement timeout |
BATCHWIRE_REDIS_ADDR |
localhost:6379 |
Redis address |
BATCHWIRE_REDIS_DB |
0 |
Redis database index |
BATCHWIRE_REDIS_PASSWORD |
(empty) | Redis password |
BATCHWIRE_WORKER_CONCURRENCY |
64 |
Worker goroutines |
BATCHWIRE_MAX_RETRIES |
5 |
Max task retries |
BATCHWIRE_QUEUE_RATE_LIMIT |
200 |
Queue processing rate (per second) |
BATCHWIRE_KEYSTORE_DIR |
keys |
Directory of *.pub client public keys |
BATCHWIRE_KEYS_JSON |
(unset) | Inline JSON keystore (overrides the directory) |
BATCHWIRE_SIGNATURE_MAX_AGE |
5m |
Accepted clock skew for signatures |
BATCHWIRE_RATE_LIMIT_RPS |
50 |
Per-client request rate |
BATCHWIRE_RATE_LIMIT_BURST |
100 |
Per-client burst |
BATCHWIRE_MAX_FILE_BYTES |
8388608 (8 MiB) |
Max request body size |
BATCHWIRE_LOG_LEVEL |
info |
Log level |
All /v1 endpoints require the signing headers below. Reads are scoped to the submitting key id.
| Method | Path | Description |
|---|---|---|
GET |
/healthz |
Liveness probe (no auth) |
GET |
/readyz |
Readiness probe — pings the database (no auth) |
POST |
/v1/batches |
Submit a base64-encoded ACH file. 201 for a new batch, 200 if idempotent |
GET |
/v1/batches/{id} |
Fetch batch status and totals |
GET |
/v1/batches/{id}/entries |
List entries (?limit= 1–500 default 50, ?offset=) |
Submit request body:
{
"filename": "sample.ach",
"file": "<base64-encoded NACHA file>"
}Batch response:
{
"id": "15ee8b34-04bf-4699-b59d-d09d3f0b6616",
"status": "queued",
"idempotency_key": "<sha256 of the file>",
"filename": "sample.ach",
"company_name": "ACME CORP",
"sec_code": "PPD",
"entry_count": 3,
"total_debit_cents": 50000,
"total_credit_cents": 562500,
"attempts": 0,
"created_at": "2026-05-27T22:20:16Z",
"updated_at": "2026-05-27T22:20:16Z"
}Protected requests are authenticated with Ed25519 signatures over three headers:
| Header | Value |
|---|---|
X-Batchwire-Key-Id |
Client key identifier (matches a public key in the keystore) |
X-Batchwire-Timestamp |
Unix seconds; must be within BATCHWIRE_SIGNATURE_MAX_AGE of server time |
X-Batchwire-Signature |
Base64 Ed25519 signature over the canonical string |
The canonical string is newline-joined:
batchwire-ed25519-v1
<HTTP-METHOD>
<request-uri>
<unix-timestamp>
<hex sha256 of the request body>
The sign request helper builds this and prints a runnable curl. To sign from your own client, replicate crypto.CanonicalString / crypto.Sign in internal/crypto/signer.go.
sign subcommands:
keygen -id <id> -dir <dir>— generate an Ed25519 keypair (<id>.pub/<id>.key)sample -out <path>— write a valid example ACH filerequest -key <keyfile> -id <id> -file <ach> -url <endpoint>— produce a signed request body and print the curl command
# Fast, offline unit tests (ACH parsing/validation, crypto)
go test ./...
# End-to-end: boots an embedded PostgreSQL and in-process Redis, wires up the
# real api + worker constructors, and drives a signed submission to "completed".
go test -tags e2e ./internal/api/ -run E2E -vThe e2e suite is gated behind the e2e build tag so go test ./... stays fast and dependency-free. It uses embedded-postgres (downloaded and cached on first run) and miniredis, so it does not require Docker or external services.
cmd/
api/ HTTP server entrypoint
worker/ asynq worker entrypoint
sign/ keygen / sample / request CLI helper
internal/
ach/ NACHA parsing, validation, and writing
api/ routes, handlers, middleware (signing, rate limit, logging)
config/ env-based configuration loader
crypto/ Ed25519 signing, verification, and keystore
logging/ slog setup
queue/ asynq client, server, and batch processor
storage/ pgx store, models, and migrations