What actually leaves your machine when you use an AI API tool? We audited ours.
"Local-first" is an easy claim and a hard audit. So instead of a manifesto, here is the concrete data-flow map of the Powerduck desktop client — where each capability runs, and what a network observer, a renderer process, or the model provider can actually see.
API requests run in the Node main process
When you send a request from the Request workspace, execution goes through the local main process, not the web view. Two consequences follow directly from that:
- there is no browser CORS wall — cross-origin calls are not blocked by the renderer’s security model;
- the request URL,
Authorizationheader and body do not appear in the renderer’s developer-tools network panel.
The same applies to model completion requests: prompts, the system prompt, the tool catalog and your model API key are sent from the main process. The gateway is deliberately not an open proxy — targets are validated against loopback and permitted hosts, request headers are sanitized, response splitting through headers is prevented, and an in-flight request can be cancelled with ai:cancel.
The model is pluggable, and yours
Configure any OpenAI-compatible Chat Completions endpoint — a built-in vendor preset or a custom base URL with its own key — and keep several profiles. Capable models get native function calling with the full tool catalog; endpoints that reject tool parameters are remembered and retried once over a content-based fallback protocol instead of failing. Tool use is bounded to three rounds by default and de-duplicated, so a model cannot spin a loop calling the same tool. If your policy says "self-hosted model only," that is just another profile.
What works with the network cable pulled
- designing and editing the spec, with full revision history on disk;
- the local mock server, including recorded-request inspection;
- rendered documentation;
- scenario runs against local targets and exported reports.
Only live requests to external hosts, Git sync, hosted models and Cloud publishing need a connection.
Database access is read-only by construction
The Data model surface stores saved connection profiles (dialect, host, port, user, database) but never returns passwords. Live queries run through database.runSelect: read-only SELECT statements with a bounded, truncated row set — writes and DDL are blocked. The migration SQL the reconciliation produces (ordered, additive ALTER steps with blockedBy dependencies, or an idempotent forward-only creation script) is generated for review and is never executed against your database from the app. The new-connection dialog is prefilled and the password is typed by you, not accepted through a chat prompt.
The small print, in one table
| Activity | Where it runs | What leaves the machine |
|---|---|---|
| Spec editing and AI patches | Local file + your configured model | Only the prompt payload to your model endpoint |
| Sending API requests | Node main process | Only the request to the target host you chose |
| Mock server | Local main process | Nothing |
| Scenario reports | Local engine, local HTML export | Nothing; credentials redacted in the file |
| Database comparison | Bounded read-only SELECT over your saved profile | Only the query to your database |
| Licensing | Offline verification on the device | No account, no session |
The cloud is a deployment target, not a home
The desktop client is sold as a one-time perpetual license and opens with no account. Cloud exists for the moments where sharing is the point — hosted docs, a managed MCP endpoint, Git sync — and the file you publish is a copy of the truth on your disk. Even the hosted demo’s sign-in exists only to meter shared cloud resources; the downloaded client never asks for it.
If your team is the kind that reads data-flow tables before approving a tool, the AI and models reference documents the same boundaries in engineering detail.