Architecture
Three processes and one SQLite file.
Workers pull, the control plane never pushes
This is the single decision the rest of the design falls out of.
A worker opens outbound HTTPS to the control plane to heartbeat, lease, and complete. The control plane holds no connection to any worker and cannot initiate contact. So a Mac behind home NAT, on hotel Wi-Fi, or on a phone hotspot joins the pool with no port forwarding, no VPN, and no static address.
The cost is that the control plane cannot dispatch. It can only wait to be asked, which is why leasing is greedy and why a fast worker can starve a slow one.
Control plane
One FastAPI app, macbatch.control.app. Endpoints are thin; all state lives in SQLite.
| Table | Holds |
|---|---|
jobs | One row per submitted job, with meta_json carrying item and shard counts |
tasks | One row per shard: payload, status, attempts, lease, result, error |
workers | Registry keyed by worker id, with heartbeat time and cumulative counters |
task_events | Append-only audit: leased, done, failed, requeued, lease_expired |
There is no scheduler thread. Expired leases are reclaimed inside request handlers — reclaim_expired_leases() runs at the top of lease, heartbeat, job read, and worker list. An idle control plane reclaims nothing, which is fine, because the next poll does it.
Job status is likewise derived rather than tracked: reading GET /v1/jobs/{id} counts task statuses and flips the job to completed or partial when nothing is pending or leased.
Worker
macbatch serve runs WorkerAgent. Per cycle: heartbeat, lease up to lease_limit shards, execute them (optionally across max_concurrent threads), write each result to local disk, then bulk-post everything in one complete_batch request.
The local write before upload is deliberate. Compute on a consumer laptop is expensive enough that losing a finished shard to a dropped connection is worth avoiding, even though the current release does not yet replay those files automatically.
Execution dispatches on a fixed allowlist — embed, ocr, classify, generate — each mapping to a specific Ollama call. There is no path by which a control plane can make a worker run arbitrary code.
Runtime boundary
macbatch schedules; Ollama infers. The worker talks to http://127.0.0.1:11434 and knows only two shapes of call: /api/embeddings for embed work and /api/generate (optionally with base64 images) for everything else.
Swapping in MLX or another runtime means implementing those two calls behind the same interface. Nothing above the worker changes.
What is deliberately absent
| Missing | Why |
|---|---|
| Authentication | Not built yet. A token config field exists and is unused. |
| Migrations | Schema is CREATE TABLE IF NOT EXISTS; existing databases are never altered. |
| Connection pooling | One SQLite connection per request, WAL mode. |
| Task priorities | Tasks are leased in rowid order — strict FIFO across all jobs. |
| Scheduling fairness | First worker to ask gets the work. This produces measurable skew. |
| Result replay | Locally-stored shard results are not re-uploaded after a failed gather. |
Read next
- Jobs, shards and tasks — how work is packed
- Leases and failure — the state machine
- Runtimes and models — what actually runs the model