Server and web
The server
The Wellfed server is an optional program you run at home, on a small Linux machine or in a container. It talks to your feeders on their behalf and gives everyone in the household one place to see and control them. This page says what it adds, how it fits beside the feeder, and where to read more.
What it is, and what it isn't#
The feeder doesn't need the server. It keeps its own feed journal and event history (and will keep its schedules, once they exist), and the app and the wellfed CLI can talk to it directly on your home network. If the server is down, the feeder keeps feeding.
The server adds what a small device can't hold:
- Remote access. Reach your feeders from outside the home, through a reverse proxy or a VPN.
- Accounts and audit. Each person logs in as themselves, with a role (owner, member or viewer). The audit log names who fed, changed a setting or adopted a feeder.
- Long history. Every event and sample the feeder reports, kept for as long as you like, with hourly rollups for charts.
- Owner data. Pets, foods, weights and refills, which the feeder doesn't track.
- Releases and installs. It fetches or takes signed firmware releases, verifies them, and installs one when an owner asks. It also keeps each feeder's stock firmware backup, so you can go back to stock.
One server serves one household, with as many feeders as you own.
Status: Experimental. Everything here is built and tested against the host-simulated feeder. No server has met a real feeder yet. See the roadmap.
Two paths, one feed#
When a server is configured, the app talks to it whenever it can reach it. That way remote access works and the audit log names the person. Each owner's or member's phone also holds its own key on every feeder. When the server is down, the phone uses that key on the home network, for reads and new requests only. A viewer's phone holds no key, so the server alone enforces a viewer's limits.
The danger is a retry that crosses paths. The server and the phone send different request ids, so a feed that timed out on one path and was retried on the other could feed twice. So an uncertain write never switches path. It is retried on its own path under its own key, or resolved from the feeder's history (a feed by its Fed event). It is never resent on the other path. The CLI follows the same rule: each command takes one path, the device API or wellfed server, with no fallback between them.
The server enforces its half:
- A write whose answer was lost is answered
504 outcome_unknownand kept unknown under itsIdempotency-Key. - A retry under that key reads the server's command journal. For a feed it sends nothing new.
- The feed is settled later from its
Fedevent. You can read the result atGET /api/v1/commands/{key}, and it arrives on the live stream as acommandevent.
So a client can resolve a lost feed on either path without resending it. Holding an uncertain write to its own path is the app's half.
This costs the feeder a client slot per phone. A feeder has 8 slots: the server, the CLI and up to six phones.
Every device operation the app can make has a server route, or a stated reason it has none, in wellfed_service::parity. It is an exhaustive match over the device API's operations, so a new operation doesn't build until it gets one. Some routes need the feeder's owner role: Wi-Fi, reboot, revoking a client, a phone's invite, the time zone rule, installs and the stock backup. They work only when the server was adopted with an owner's invite. With the server role, the feeder refuses them forbidden.
The HTTP API#
The API lives under /api/v1, as JSON. The complete reference is openapi.json, which is generated from the handlers and checked for staleness in make check. The web UI's types come from it too.
| Group | Routes | What they do |
|---|---|---|
| Setup and accounts | /setup, /session, /users, /tokens, /household |
The first owner, logins, people and their roles, phone tokens, the household's name, units and zone |
| Feeders | /feeders, /feeders/{id}, /feeders/{id}/settings, /lock, /wifi, /reboot, /play, /clients, /invites, /phones |
Adopt and remove feeders, live status, settings and administration |
| Feeding | /feeders/{id}/feed, /feeds/{feed}/cancel, /commands, /commands/{key} |
Feed, cancel, and the journal of every write |
| History | /feeders/{id}/events, /samples, /feeds/{feed}/trace, /stats, /log, /audit |
Events, samples, feed traces, statistics, the live debug log and the audit log |
| Owner data | /pets, /foods, /pets/{id}/weights, /pets/{id}/feeders/{feeder}, /feeders/{id}/food, /refills |
Pets, foods, weights, which feeder serves which pet, refills |
| Firmware | /releases, /releases/check, /feeders/{id}/install, /feeders/{id}/stock… |
Releases, installs, the stock backup and restore |
| Server data | /backups, /export, /import, /status |
Backups, a full export, import onto a fresh server, readiness |
| Live | /stream |
Server-Sent Events for every feeder you can see |
GET /health sits outside /api/v1. It needs no login and answers the version.
Some conventions hold across the API:
- Errors are RFC 9457
application/problem+json, with a typedcodefrom a closed set. There is no free-text error. - Writes carry an
Idempotency-Keyheader (a UUID). A feed requires one. - Edits carry
If-Matchwith the settings' generation. A stale one gets412with the current settings. - The stream resumes from
Last-Event-ID. When that id is too old, the server sendsresyncand the client fetches again.
Read next#
| I want to… | Read |
|---|---|
| Install and run the server | Running the server |
| Understand how it's built, or change it | Server design |
| Know what the feeder itself serves | Device API |
| See the web UI | Web UI |
| See the product the server is part of | Product architecture |
| Check what works today | Roadmap |
Evidence#
- Simulation, October 24–27, 2026: the server's crates and their tests run against
wellfed-feeder-simover real TCP and Noise. Feeds, uncertain writes, installs, the stock backup and restore, export and import were exercised this way. - Static analysis, October 27, 2026: the route groups above match the committed
openapi.json, and the role rules matchPermission::caninwellfed-service.