"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, Authorization header 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

ActivityWhere it runsWhat leaves the machine
Spec editing and AI patchesLocal file + your configured modelOnly the prompt payload to your model endpoint
Sending API requestsNode main processOnly the request to the target host you chose
Mock serverLocal main processNothing
Scenario reportsLocal engine, local HTML exportNothing; credentials redacted in the file
Database comparisonBounded read-only SELECT over your saved profileOnly the query to your database
LicensingOffline verification on the deviceNo 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.