What you keep and where: money, things, notes. End-to-end encrypted; decrypts on your computer.
Runs on your machine from its package; the steps are below.

At most: creates or changes data: records, files, settings.
Odel charges nothing per call. The publisher may charge for its own service.
Runs locally over stdio
This server isn't hosted — your MCP client launches it from a package registry. Use one of the commands below, or drop the config into your client (e.g. Claude Desktop).
Distributed as an MCP bundle (`https://github.com/WawRepo/petty/releases/download/v1.5.19/petty.mcpb`) — download it and import the bundle in your MCP client.
Everything you keep, in its place. Petty is a private record of what you keep and where: cash in a tin, tools in the basement, passports in the safe, the drill you lent out. Each drawer sits in a place, holds items — money, counted things or notes — and is shared only with the people you choose. Everything is encrypted on your device before it is sent.
Try it at petty.kropka.studio — nothing to install, it works in your browser. Or run your own.
io.github.WawRepo/petty. See
docs/agent.md.petty signs in through your browser, like gh auth login, then reads and
writes drawers from a terminal, scripts or AI agents, with --json and Tab completion. See
docs/cli.md.| For | Read |
|---|---|
| Users | this README, the in-app privacy page, docs/agent.md (use Petty from Claude Desktop or another AI app), docs/cli.md (the petty command line) |
| Operators (self-hosting) | docs/deploy.md · docs/monitoring.md · docs/auth-clerk.md |
| Contributors | CONTRIBUTING.md · docs/decisions.md · CLAUDE.md — the engineering rules; plain text, no AI tool needed to read or follow them |
| Security reviewers | SECURITY.md · docs/threat-model.md · docs/security-review-2026-09.md · docs/README.md (how keys and tokens work, with diagrams) |
| History, not maintained | petty-app-spec.md (frozen 2026-09-25), petty-spec-review.md, SPEC-ISSUES.md — the original specification, its review and the reasoning behind the first decisions · petty.html — the pre-rewrite prototype |
Ticket IDs such as PETTY-123 in docs and commit messages refer to the maintainers' internal tracker.
The public trail for changes is this repository's issues, pull requests and CHANGELOG.md.
The threat model is docs/threat-model.md; the settled design decisions are docs/decisions.md. The latest security review is
docs/security-review-2026-09.md. Report problems privately as described in SECURITY.md.
cp deploy/compose/.env.example deploy/compose/.env # fill in the values
docker compose -f deploy/compose/docker-compose.yml --env-file deploy/compose/.env up -d
Make the database passwords letters and digits only (openssl rand -hex 32). Then put an HTTPS
reverse proxy in front of 127.0.0.1:3000. docs/deploy.md has the details,
and docs/monitoring.md covers metrics, logs and traces.
Copyright (C) 2026 Petty contributors. Petty is free software under the GNU Affero General Public
License v3.0 (LICENSE, and NOTICE). If you run a changed version for other people, you must
offer them its source. Contributions are welcome; see CONTRIBUTING.md and CODE_OF_CONDUCT.md.
This part covers running Petty locally. Nothing here needs a cloud account or network access
after pnpm install.
make demo # pulls the published image (PETTY_TAG=<tag> for another), migrates, seeds two people and six drawers
make demo-local # same, but builds the image from your working tree (no CI, no registry)
make demo-down # stop it
make demo-reset # stop it and drop the data (shares the local dev database volume)
Open http://localhost:3300 (mail at http://localhost:8025). Logins are printed by the
seed: ania@petty.local / password-ania / vault passphrase vault ania 2026 drawer;
bartek@petty.local / password-bartek / vault bartek 2026 drawer.
.env at the rootCopy .env.example to .env (gitignored). The API dev server, the tests, Playwright and
docker compose all read that one file; the web app needs none (it asks the API). Anything
already in the environment wins over the file. The production image carries no file: a
deployment passes the same names as environment variables or secrets.
.node-version says 24 — the image and CI use it too)corepack enable && corepack prepare pnpm@11.28.2 --activate (the version in package.json)apt install make on Debian/Ubuntu)pnpm install
make dev
make dev starts Postgres 16 and Mailpit in Docker, waits for the database, runs the
migrations, then starts the API on http://127.0.0.1:3000 and the web app on
http://localhost:5173. Stop everything with Ctrl-C and make stop.
Then, in a second terminal:
make seed # three test users
curl localhost:3000/health # {"ok":true,"db":"up"}
| Login password | Vault passphrase (from Phase 2 on) | |
|---|---|---|
| alice@petty.local | password-alice | vault alice 2026 drawer |
| bob@petty.local | password-bob | vault bob 2026 drawer |
| carol@petty.local | password-carol | vault carol 2026 drawer |
Mailpit catches every email the app sends (join links, invitations, removals, ownership
offers): http://localhost:8025. Set SMTP_HOST/SMTP_PORT/MAIL_FROM/APP_URL for a real provider.
| Command | Does |
|---|---|
make dev | db + mailpit + migrate + api + web |
make seed | three users + a shared "Kitchen" drawer with real ciphertext (skips if alice exists; make reset first to reseed) |
make test | every vitest suite (API tests need the db) |
make lint | typecheck + eslint |
make reset | drop the database volume, migrate again |
pnpm --filter @petty/api migrate:create <name> | new empty SQL migration |
The web app is an installable PWA. The service worker caches the app shell only; API responses are never HTTP-cached. Ciphertext (vault blob, bootstrap) and non-extractable key handles live in IndexedDB, so unlock and reading work offline. Entries and document edits made offline wait in an encrypted outbox and are sent on reconnect (idempotent by client id); the home screen then reports what changed while you were away and which queued changes were refused.
pnpm e2e runs two Playwright projects: dev (Vite dev server) and prod
(the API serving the production build on :3100 under the strict security headers;
fails on any CSP violation; service worker active). A third, clerk-prod, runs the Clerk sign-in
against a Clerk development instance, only with CLERK_E2E=1 and its keys.
apps/api Fastify API. All writes go through here. Connects as petty_api;
petty_maint only inside rotation and deletion.
apps/web Vite + React SPA. Holds keys in memory; never writes plaintext to disk.
packages/crypto Envelope encryption, AAD binding, signatures, compatibility corpus.
packages/ledger Pure domain: balance fold, chain verification, money formatting,
line extras (icon, tags, counted) and place paths.
packages/protocol zod schemas shared by api and web.
packages/agent The drawer tools shared by the MCP server and the command line.
apps/mcp The MCP server for AI apps (petty-mcp.mjs, the petty.mcpb add-on).
apps/cli petty, the command line (petty.mjs).
apps/api/migrations Version-controlled SQL migrations (node-pg-migrate).
tests/integration The production image under deploy/compose, over real HTTP.
deploy/compose The production compose file; deploy/backup the pg_dump + age image.
docker/postgres/init.sql Dev-only: creates petty_api / petty_maint with LOGIN.
A drawer sits in one place of a tree (Home › Kitchen › shelf): the path of
names is stored in the drawer's encrypted document, the tree itself in the
person's encrypted user document (apps/web/src/lib/places.ts, edited on
/places). Renaming or moving a place rewrites the path on the drawers you can
write. Drawers and lines carry an optional Lucide icon slug; lines carry tags
and a "part of the total" flag, all inside the document. Details and the
reasoning: docs/decisions.md (rows Places, Icons, Line tags); background in the frozen petty-app-spec.md, section "Places, icons and line tags".
All bodies are JSON; bytes travel as base64; the server never opens ciphertext.
Session = httpOnly cookie petty_session (__Host-petty_session over HTTPS, as in the image).
In the image every route sits under /api (/api/me). Access tokens and Clerk session tokens come
as Authorization: Bearer …. Non-members get 404, never 403.
| Route | Who | Does |
|---|---|---|
GET /config GET /health GET /health/live | – | sign-in mode, contact address, open sign-up, version; health with the database; the process is up |
POST /auth/signup POST /auth/login POST /auth/logout | – | invite-only signup with vault blobs and public keys; login; logout |
POST /auth/forgot POST /auth/reset | – | local mode: a one-hour reset link by email; a new login password (the vault is untouched) |
POST /auth/provision | Clerk session | Clerk mode: the first sign-in creates the Petty account with its vault blobs |
POST /join-links GET /join-links/:token | user | create / inspect a join link (7 days) |
GET /me PATCH /me GET /users/lookup?email= GET /users/:id/keys | user | profile, locale, exact-email lookup for inviting, key history |
GET /me/doc PUT /me/doc | user | the user's own encrypted document (key pins), version-checked |
GET /bootstrap | user | all drawers, documents, my wraps, members, entries since each line's last Adjust, invitations, transfers |
POST /drawers GET /drawers/:id DELETE /drawers/:id | user / read / owner | create; read; permanent delete via delete_drawer() |
PUT /drawers/:id/document | write | optimistic lock on base_version → 409 VersionConflict; verification: true stamps last_verified_at |
GET/PUT/DELETE /drawers/:id/photo | read / write | own encrypted row |
POST /drawers/:id/lines/:lineId/delete | write | new document + entries removed via delete_line() |
POST /drawers/:id/entries | write | append; idempotent on client id; Adjust needs expected_head_seq → 409 RecountRequired; Reverse rules → 409 ReverseRefused |
GET /drawers/:id/lines/:lineId/entries?before=&limit= | read | older pages |
GET /drawers/:id/members POST /drawers/:id/invitations GET /invitations `POST /invitations/:id/accept | decline DELETE /invitations/:id` | read / owner / invitee |
DELETE /drawers/:id/members/:userId POST /drawers/:id/leave | owner / member | removes wraps, sets rotation_needed |
POST /drawers/:id/transfer POST /drawers/:id/transfer/accept DELETE /drawers/:id/transfer | owner / target | ownership moves only on acceptance |
POST /drawers/:id/rotation PUT /drawers/:id/rotation/batch GET /drawers/:id/rotation | write | publish new key + wraps first; re-seal in batches (petty_maint); status |
GET /drawers/:id/export | owner, write | everything for the drawer; read members get 403 |
GET /me/delete POST /me/delete | user | preview; delete with per-drawer decisions (hand over / delete), password re-entered |
GET /me/recovery-vault PUT /me/recovery-vault PUT /me/vault | user | recovery-code copy; a new recovery code; replace the passphrase wrap of the same keys |
POST /me/custody-challenge | user | a fresh challenge for a custody proof (docs/threat-model.md) |
POST /me/passkeys DELETE /me/passkeys/:id | user | add / remove a passkey that opens the vault |
GET /me/storage | user | storage used, against STORAGE_QUOTA_MB |
GET /drawers/:id/document/history POST /drawers/:id/document/restore | owner | the drawer document's 30-day history; restore an earlier version |
GET /me/tokens POST /me/tokens DELETE /me/tokens/:id GET/POST /me/tokens/:id/keys | user | access tokens: list, make (with a custody proof), revoke, their key bundle |
GET /me/token GET /me/token/bootstrap GET /me/token/keys DELETE /me/token | token | a token about itself, its drawers and its key bundle; a token ends itself |
GET /users/:id/delegations | user | the tokens a person's key vouched for, to check their signatures |
POST /device/code POST /device/token | – | device login: a tool asks for a code, then polls for its sealed token |
GET /device/:code POST /device/:code/approve POST /device/:code/deny | user | the /device page (signed in, never a token): see the request, allow it (the page seals a new token) or deny it |
GET /admin/users POST /admin/users/:id/block /unblock /revoke-sessions /restore-vault /admin | admin | accounts: list, block, unblock, end sessions, restore a vault from its history, make or remove an admin |
| Role | Used by | May |
|---|---|---|
petty | migrations, seed | everything (owner) |
petty_api | every API request | SELECT/INSERT on entries; no UPDATE/DELETE there |
petty_maint | rotation, line/drawer/account deletion | column-restricted UPDATE, DELETE via functions |
Connection strings live in .env.example; copy to .env to override.
pnpm install says "Ignored build scripts: esbuild" → pnpm-workspace.yaml already
allows it; run pnpm install again.brew services stop postgresql) or change the
port in docker-compose.yml and .env.