Apiovnia Apiovnia alpha
Local-first · Alpha · PolyForm NC

An API client that
minds its own business.

One SQLite file on your machine. Environments as per-request overrides, not variable soup. Any environment can be sealed behind a master password. No cloud, no accounts, no roadmap toward either.

182 tests behind the resolver, the crypto, the snippets, and the GraphQL body · Linux · macOS · Windows · Free, forever
Apiovnia — the three-panel shell with projects, requests, the request editor and the response viewer

Three panels. Projects, requests, editor + response. The JSON viewer is custom — not a re-skinned tree.

Why Apiovnia exists

Every tool in this category starts the same way. Small, sharp, written by someone who needed it themselves. Postman in 2014. Insomnia in 2016. They were good. I remember.

Then comes the second act. An investor. A strategic partner. A pivot toward "the enterprise." The roadmap fills with features nobody asked for and empties of the ones that mattered. Cloud sync arrives, and with it an account you didn't want. The free tier shrinks. A "Team" tab appears. Five years in, the tool that solved your problem is now a problem of its own.

This isn't a story about bad companies. It's a story about a structure. Venture funding has a clock, and that clock has a sound, and the sound is grow or die. A REST client that stays small forever is a failed investment. So it doesn't stay small. It can't.

Apiovnia is built next to that structure, not against it. There's no investor to satisfy. There's no growth curve to bend. The whole thing is one SQLite file on your machine — yours to back up, inspect, copy, or delete. No account because there's nothing to log into. No cloud because there's nowhere your data needs to go.

The price is honest. Free, forever. No team workspaces, no sync, no browser version. If the work needs those things, Postman exists and it's still good at them. Apiovnia is for the other case — the request you'll send again in three years, the secret you don't want on someone else's server, the collection that should outlive the company that produced it.

This is a small idea. It used to be the default.

— Sebastian, somewhere with good coffee and bad Rust

Six positions, not features

What "small forever" actually means.

Your data, in a file you can rename.

Everything is one SQLite file in your XDG data dir. cp it for backup. sqlite3 into it for inspection. Move it between machines on a USB stick. Delete it for a fresh start. The database isn't an API — it's a file. The way files used to be.

Environments without the variable maze.

Define a request once. Per (request, env), patch only what differs — URL, auth, a single header. An amber dot marks any tab that carries an override. Resolution order is fixed: request > env override > base. Explicit beats clever, every time.

Secrets that live where you put them.

Mark an env as encrypted, set a master password once. AES-256-GCM seals every variable value and every secret-bearing override field; Argon2id (OWASP 2024 baseline) derives the key. The key never crosses the IPC boundary. Idle auto-lock at 10 minutes. The crypto is boring on purpose.

Built for the keyboard you already own.

⌘ P palette · ⌘ K sidebar filter · ⌘ ↵ send · ⌘ N new request · ⌘ 1/2/3 focus panel · ⌘ , settings. The mouse is an option, not the path.

{{var}} that resolves before it leaves Rust.

Reference env variables anywhere — URL, headers, body, auth. The resolver is a pure function in apiovnia-core with 33 unit tests. Decryption happens in-process before the wire request is built. The frontend never sees a plaintext secret.

A roadmap that ends.

No pre-request scripts. No test assertions. No "workspaces." No sync. No mobile. The list of things Apiovnia won't become is longer than the list of things it will — and that's the whole point. See where it stops →

What works on first launch

The alpha is not a demo.

Every line below is wired to a Rust crate with tests. Apiovnia is in active development; it is not a slide deck. You can clone it, build it, and lose data in it today.

  • Projects → Collections → Requests, stored in one SQLite file you can hold in your hand
  • Method, URL, params, headers, body (JSON, GraphQL, Form, Multipart, Raw), auth (Bearer, Basic, API key, none)
  • HTTP execution with reqwest + rustls, gzip, redirects, 30 s timeout, 2 MiB body cap
  • Response viewer with a custom JSON tree — collapsible nodes, ⌘F search, hover-copy per value, Pretty / Headers / Request / Raw tabs
  • Environments + per-(request, env) overrides + {{var}} interpolation, resolved in Rust before the wire request is built
  • Master-password encrypted environments — Argon2id + AES-256-GCM, 10 min idle auto-lock, zxcvbn meter with an explicit pro-user bypass
  • Command palette (⌘P) — fuzzy ranking across requests, collections, projects, envs, and actions
  • Copy as… curl / Python (requests) / HTTPie / JavaScript (fetch) / PowerShell — env-resolved, secrets-decrypted, idiomatic per language
  • OpenAPI 3.x import + export — $ref resolution, schema inference, secret scrubbing, persistent OpLog audit panel
  • History panel — last ~200 executions, filterable, click to navigate and rehydrate the saved response
  • Five themes — apiovnia (amber) / atomic-dark / tokyo-night / monokai / light, applied live without a reload
  • GraphQL — split query + variables editor, POST (JSON body) or GET (query string) per the GraphQL-over-HTTP spec
Spotlight · Copy as…

One request leaves in five languages.

Right-click any request, or invoke from the palette. Copy as curl, Python requests, HTTPie, JavaScript fetch, or PowerShell Invoke-RestMethod. Full env resolution. Full {{var}} interpolation. Full decryption of encrypted values. Multipart bodies rewritten to each language's native idiom — not the lowest common denominator.

If the env is locked, the unlock modal pops up and the copy retries when you've entered the password. You shouldn't have to think about that flow. You don't.

# curl
curl -X POST 'https://api.acme.dev/auth/login' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer eyJhbGciOi…' \
  --data-raw '{"email":"{{email}}","password":"<your-password>"}'
# Python (requests)
import requests

requests.post(
    "https://api.acme.dev/auth/login",
    headers={"Authorization": "Bearer eyJhbGciOi…"},
    json={"email": "alice@acme.dev", "password": "<your-password>"},
)
# JavaScript (fetch)
await fetch("https://api.acme.dev/auth/login", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer eyJhbGciOi…",
  },
  body: JSON.stringify({ email: "alice@acme.dev", password: "<your-password>" }),
});

Try it on the work you already have.

One download. The database is a file. You can delete it any time. Nothing follows you.