prefect server in zig

add docs for server internals

concise overview of implemented components:
- web server: route dispatch, resource handlers
- database: sqlite layer, query patterns
- services: background worker registry
- messaging: bounded channel, backpressure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+158
+14
docs/README.md
··· 1 + # prefect-zig internals 2 + 3 + zig implementation of a prefect-compatible server. 4 + 5 + ## components 6 + 7 + - [web server](./web-server.md) - http layer, route dispatch 8 + - [database](./database.md) - sqlite persistence 9 + - [services](./services.md) - background workers 10 + - [messaging](./messaging.md) - inter-component communication 11 + 12 + ## see also 13 + 14 + - [roadmap](../ROADMAP.md) - implementation status vs python prefect
+39
docs/database.md
··· 1 + # database 2 + 3 + sqlite via [zqlite](https://github.com/zigzap/zqlite). 4 + 5 + ## connection 6 + 7 + single global connection with mutex for thread safety. pragmas: 8 + - `journal_mode=WAL` 9 + - `busy_timeout=5000` 10 + - `foreign_keys=ON` 11 + 12 + ## tables 13 + 14 + - `flow` - flow definitions 15 + - `flow_run` - flow run instances 16 + - `flow_run_state` - state history (FK cascade delete) 17 + - `task_run` - task run instances 18 + - `events` - persisted events 19 + 20 + ## query pattern 21 + 22 + functions take allocator, return arena-owned structs: 23 + 24 + ```zig 25 + pub fn getFlowById(alloc: Allocator, id: []const u8) !?FlowRow 26 + ``` 27 + 28 + caller manages arena lifetime - typically request-scoped. 29 + 30 + ## transactions 31 + 32 + explicit for multi-statement ops: 33 + 34 + ```zig 35 + conn.transaction() catch |err| { ... }; 36 + errdefer conn.rollback(); 37 + // ... statements ... 38 + conn.commit() catch |err| { ... }; 39 + ```
+39
docs/messaging.md
··· 1 + # messaging 2 + 3 + inter-component communication via `messaging.zig`. 4 + 5 + ## bounded channel 6 + 7 + generic thread-safe queue with backpressure: 8 + 9 + ```zig 10 + pub fn BoundedChannel(comptime T: type, comptime capacity: usize) type 11 + ``` 12 + 13 + operations: 14 + - `trySend()` - non-blocking, returns false if full 15 + - `receiveTimeout()` - blocking with timeout 16 + - `drain()` - batch receive up to N items 17 + - `close()` - signal shutdown, wake waiters 18 + 19 + ## event channel 20 + 21 + global channel for event ingestion: 22 + 23 + ```zig 24 + pub const EventChannel = BoundedChannel(StoredEvent, 50000); 25 + ``` 26 + 27 + 50k capacity matches python prefect's backpressure limit. dropped events logged with periodic sampling. 28 + 29 + ## usage 30 + 31 + producers call `publishEvent()`, consumers get channel via `getEventChannel()`. 32 + 33 + ## future 34 + 35 + current implementation is in-memory. interface designed to support: 36 + - redis streams 37 + - kafka 38 + 39 + swap implementation without changing producer/consumer code.
+32
docs/services.md
··· 1 + # services 2 + 3 + background workers managed by `services/mod.zig`. 4 + 5 + ## registry 6 + 7 + simple array of function pointers: 8 + 9 + ```zig 10 + pub const Service = struct { 11 + name: []const u8, 12 + start: *const fn () anyerror!void, 13 + stop: *const fn () void, 14 + }; 15 + 16 + const all = [_]Service{ 17 + .{ .name = "event_persister", .start = event_persister.start, .stop = event_persister.stop }, 18 + }; 19 + ``` 20 + 21 + `startAll()` iterates forward, `stopAll()` iterates reverse. 22 + 23 + ## event_persister 24 + 25 + drains events from the messaging channel and writes to sqlite. 26 + 27 + - batch size: 100 events 28 + - flush interval: 1 second 29 + - deduplication: `INSERT OR IGNORE` by event id 30 + - retention: 7 days, trimmed hourly 31 + 32 + lifecycle: spawns worker thread on `start()`, signals and joins on `stop()`.
+34
docs/web-server.md
··· 1 + # web server 2 + 3 + uses [zap](https://github.com/zigzap/zap) (facil.io bindings) for http. 4 + 5 + ## route dispatch 6 + 7 + single `routes.handle()` function dispatches by path prefix: 8 + 9 + ```zig 10 + if (std.mem.startsWith(u8, target, "/api/flows")) 11 + try flows.handle(r); 12 + ``` 13 + 14 + each resource module handles its own method/path matching internally. 15 + 16 + ## resource handlers 17 + 18 + pattern from python prefect: each resource (flows, flow_runs, task_runs) has: 19 + - `handle(r)` - top-level dispatch 20 + - internal functions for create/read/update operations 21 + - arena allocator per request for response formatting 22 + 23 + ## response helpers 24 + 25 + common pattern across handlers: 26 + 27 + ```zig 28 + fn sendJson(r: zap.Request, body: []const u8) void { ... } 29 + fn sendJsonStatus(r: zap.Request, body: []const u8, status: StatusCode) void { ... } 30 + ``` 31 + 32 + ## cors 33 + 34 + preflight handled at route dispatch level - returns 204 with allow headers.