TRAINYOURAGENT

Sovereign: a local-first macOS desktop app with a tamper-evident audit log

We built a local-first macOS desktop application in Rust and React: 115 Rust modules behind a Tauri 2 shell, an append-only audit log chained with SHA-256, secrets in the macOS Keychain rather than a dotfile, and OAuth clients written against six real platform APIs. CI blocks on gitleaks and cargo audit.

The problem

Every tool in the operator-automation category is a hosted SaaS that wants your platform tokens. You paste an OAuth credential into a web form, and from that moment a company you do not control can post as you, and can read everything it posts on your behalf. For a single-operator business running multiple brands, that is a concentration of risk with no upside: you are paying rent for a database that could have lived on your laptop. The second problem is evidentiary. When an automated system acts on your behalf — publishes, spends, schedules — you eventually need to answer 'what did it do, and when, and did anyone change the record afterwards'. Most tools give you a mutable activity feed, which answers none of that. Sovereign is the answer to both: a desktop application where the SQLite file, the Keychain entries and the audit chain all live on the operator's machine, and where the audit log is structurally hard to edit after the fact.

Tauri 2, Rust backend, React front end

The application is a Tauri 2 binary. The frontend is React 18 on Vite; the backend is 115 Rust source files totalling 38,857 lines under `src-tauri/src`, exposed to the webview as Tauri commands. Persistence is `rusqlite` with the bundled SQLite in WAL mode, so there is no server process and no network dependency for anything that is not explicitly an outbound API call. The module list reads as a map of the problem domain: `oauth2_client.rs` and `x_client.rs` for authentication, `publish.rs` and `publish_scheduler.rs` for outbound posting, `token_refresh.rs` for credential lifetime, `keychain.rs` and `crypto.rs` and `security.rs` for secrets, `audit.rs` for the record, `breaker.rs` for circuit-breaking, `isolation.rs` and `spend_caps.rs` for blast radius, and a set of `*_scheduler.rs` modules for background work. There is also an internal HTTP server bound locally, which is what lets the app expose a small surface to other local tools without opening anything to the network.

The audit log is a hash chain, not a table of rows

`src-tauri/src/audit.rs` implements an append-only log where each entry's SHA-256 digest is computed over the previous entry's hash concatenated with the current entry's timestamp, kind, profile and payload. The row stores both `prev_hash` and `hash`. The property this buys is specific and worth stating exactly: you cannot alter or delete a historical entry without invalidating every hash after it. It does not make the log immutable — anyone with the SQLite file can rewrite the whole chain — but it makes silent, partial tampering detectable, which is the realistic threat. It is the difference between an activity feed and a record. The file's second line is a rule rather than a comment: payloads are redacted JSON, never full tokens or request bodies. An audit log that captures the credential it was auditing is a liability, not a control.

Secrets in the Keychain, OAuth against the real endpoints

Credentials go to the OS keystore via the `keyring` crate with the `apple-native` feature, so on macOS they land in the login Keychain and inherit its access control. `token_refresh.rs` handles expiry on the same path. Nothing lands in a plaintext dotfile, and `security.rs` plus the CI secret scan exist to keep it that way. The OAuth and publishing code is written against the real production APIs, not a wrapper service. X uses OAuth 1.0a with the PIN flow (`https://api.x.com/oauth/request_token` → `authorize` → `access_token`) and posts to `https://api.x.com/2/tweets`. LinkedIn uses OAuth 2.0 against `https://www.linkedin.com/oauth/v2/` and posts to `https://api.linkedin.com/v2/ugcPosts`. Facebook, Instagram and Threads go through the Meta Graph v19 OAuth dialog, and the app discovers the operator's Pages via `GET /me/accounts` so it can hold a per-Page token. Pinterest uses the v5 OAuth endpoints and enumerates boards via `GET /v5/boards`. TikTok and Google/YouTube clients are present on the same pattern. So the honest description of the publishing capability is: the credential exchange and the publish calls are implemented end-to-end against the real endpoints, and the X OAuth flow has been verified generating a valid request token and authorize URL. What has not happened is the last step. See below.

CI that blocks on secrets and dependency advisories

The single CI workflow runs three jobs. `secret-scan` runs gitleaks over the full history with `fetch-depth: 0` and a repository-specific `.gitleaks.toml`. `frontend` runs `tsc --noEmit`, a Vite build, and `pnpm audit --prod --audit-level=high`. `backend` runs `cargo fmt --check`, clippy, and `cargo audit`. Two of those are unusual to see actually enforced rather than aspirational: a full-history secret scan on every pull request, and a production-only dependency audit that fails at high severity. For an application whose entire value proposition is 'your tokens stay on your machine', a leaked credential in git history would not be a bug, it would be a refutation.

The numbers, and the command behind each one

What is genuinely hard about this

Desktop OAuth is meaningfully harder than server OAuth and almost nobody says so. There is no stable redirect URI you control, so X requires the PIN flow — request token, user authorises in a browser, user pastes a numeric PIN back into the app — and OAuth 1.0a request signing has to be implemented correctly by hand because the convenient libraries assume a web server. Meta then adds a second hop: the user token is not what you post with; you have to enumerate the Pages the user administers and hold a per-Page token, and handle the case where they administer none. The hash chain is simple to write and hard to keep correct once concurrency exists, because 'read the last hash, then insert' is a race. Serialising audit appends is a constraint on the whole application's threading model, and it is the kind of thing that is trivial to get right on day one and easy to break in month six by adding one background scheduler that writes directly. The largest problem with this codebase is not technical, and the repository says so itself in its ground-truth document: 73 tab components, each built to the same template, with no single flagship workflow. It is a breadth-first build that needed to be depth-first. We are including it because the security architecture is genuinely good and independently verifiable, not because the product shape is right.

What this entry does not claim

Stack and status

Built with Tauri 2, Rust (edition 2021, rust-version 1.77), React 18 + Vite, rusqlite 0.32 (bundled SQLite, WAL), keyring 3 (apple-native), tokio, reqwest (rustls-tls), sha2, OAuth 1.0a + OAuth 2.0, gitleaks and cargo-audit. Relationship: Internal build. Stage: Built, not launched. Period: 2026. Our role: Rust backend, Tauri shell, security model, CI Figures re-derived on 2026-08-23.