P99 turns issues and feedback on any website into cards, part of PITO MD. The people you invite see a small "+" on your site; it opens a panel where they file a card (a bug, an idea, an improvement, a question or a task) with a capture of the screen, marked regions and files, and follow the replies. You and your agents answer from the terminal: p99 alone opens your cards in the terminal (also p99 tui), and its commands are a CLI with --json for scripts and agents.
Cloudflare is the whole infrastructure: one Worker, one D1 database and one R2 bucket per Cloudflare account, mounted on each site's own domain at /_p99/*. The page talks only to its own origin. There is no other backend, no CORS, no framework, no build step and nothing from your stack.
Works on Linux and macOS (Windows through WSL). The widget runs in any modern browser.
P99: got questions or doubts? Ping me on Discord.
| What every page downloads | 1,954 bytes gzipped: the loader as served, nothing else |
| Loader start to the "+" painted | 10.1 ms cold, 2.4 ms warm |
| Page navigation to the "+" painted | 26.5 ms cold, 10.3 ms warm |
| Click to the panel's form on screen | 34.2 ms cold: panel and styles load only then (37.8 KB gzipped), and the capture code right after the form shows; 23.5 ms warm |
| An Owner's click on "Board" to the board with its cards | 132 ms cold, 180 cards on it: the board's code loads only then (12.1 KB gzipped), with the board from the Worker |
| The web board while a card is dragged | 0.7 ms a frame at the median, 2.7 ms at p95, 6.2 ms at p99 across the seven lanes (input, scripts, style and layout): 120 fps leaves 8.33 ms |
| The P99 page's first paint on a mid-range phone | 204 ms cold signed out, 196 ms signed in (150 ms round trips, 1.6 Mbps, a 4x slower CPU) |
| Worker, p50 and p99 per request | 17.9 / 30.4 ms the loader, 26.1 / 42.1 ms a card with its thread, 33.8 / 50.6 ms the board, 49.1 / 103 ms a list of 50, 35.6 / 72.4 ms a new card, 77.5 / 159 ms a move, on a local wrangler dev, not Cloudflare's edge |
| CLI start-up | 0.45 ms p99 --version, 0.65 ms p99 --demo list |
| TUI start to its first frame at 120×34 | 1.4 ms |
| TUI frame time at 120×34 | 0.05 to 0.17 ms average; the board's worst frame while a card is dragged and slides 0.25 ms: 120 fps leaves 8.33 ms |
| Backend | 0 servers: one Cloudflare Worker, D1 and R2 per space |
| Worker runtime dependencies | 0 (wrangler only to run its tests) |
| p99 binary | 8.9 MB (3.9 MB gzipped), 14 direct crates, the Worker and widget embedded |
Measured on 2026-10-30 for 1.13.8 on an AMD Ryzen 9 9950X (32 threads), with the load average of the machine's other work noted for each:
- Sizes with
gzip -9: the loader as a host with no placement settings gets it (bin/budget), the panel, styles, capture and board code as the Worker serves them. - Browser times in headless Chromium 154 on localhost against a local Worker on
wrangler dev(render/web/serve.mjs) with 360 cards on 2 hosts, each the median of 5 runs of 10 page loads at a load average of 3 to 9, where "painted" is the first animation frame with the button (so times snap to 16.7 ms frames). Cold is a fresh browser context, warm a reload in it, and the panel's warm open is the click after that reload. The board time is from the pointer going down on "Board" to the board on screen with all its cards; it opens on the site's own cards, 180 of the 360. - The web board's frames with
render/web/drag-bench.mjs: a mouse drag across the seven lanes and back, 8 drags of 214 frames with 70 cards in a lane, at a load average of 4 to 6; each frame from its first input event to the end of style and layout, which leaves out the paint and compositing of headless Chromium's software renderer. No frame passed 8.33 ms; the worst took 7.4 ms. - The P99 page: a fresh browser context each time at 412×823, throttled through the DevTools protocol (150 ms of latency, 1.6 Mbps down, 750 kbps up, a 4x slower CPU), 50 loads each, first paint as Chromium records it, at a load average of 10 to 13.
- Worker times on
wrangler dev(workerd and local D1 on this machine, not Cloudflare's edge), one request at a time over a kept-alive connection, 400 of each after 30 to warm up, signed as the CLI signs them, on 360 cards with 120 comments on 2 hosts (the new-card runs add 430 more before the moves), at a load average of 4 to 13. - CLI and TUI start-up: the whole process, the median of 300 runs after 20 to warm up (
p99 tui --demo --dump 120x34for the first frame), timed from a harness whose own process start costs 0.15 ms; TUI frame times withp99 tui --demo --bench 200, the median of 3 runs; both on a release build, at a load average of 4.
One tag, every page. The loader is a classic script of about 2 KB that draws the "+" in its own closed Shadow DOM, only for people holding a personal code. For everyone else it reads nothing and draws nothing. It adds one element and one global, P99, and works under a strict Content-Security-Policy: no 'unsafe-inline', no eval, no data: images, nothing from another origin; default-src 'self' alone is enough.
<script src="/_p99/p99.js" async></script>The panel loads on the first click. Its New card form files a bug, an idea, an improvement, a question or a task; the panel lists the cards the person filed with what's new for them, and shows each thread with its replies, files, status changes and where its fix has landed. It follows the page's light or dark scheme and takes the site's colours through CSS variables.
A board for the people who run the site. An Owner opens "Board" in the panel and gets the one board over the page: seven lanes in order (New, Planned, In progress, Needs you, Stuck, Landed, Done, each one renameable), cards to drag with the mouse, a finger or the keyboard, and a filter that narrows it to projects, to what needs you, to what has landed, or to the last change by people or by agents. A move made anywhere, in the browser, the terminal or by an agent, slides into place on every open board within about a second. The ⋯ button carries two counts: at its top right, in blue, the cards in Needs you plus the replies someone else left on your cards; at its top left, in red-purple, the open cards filed on the page you are on, which its list then shows first. The board's code loads only when an Owner opens it.
Captures that keep secrets. The visible screen, the whole page or regions the person marks go with the card. Elements marked data-p99-redact, everything inside them, the site's own mask selectors and every password field are painted over as solid blocks before a capture leaves the browser, so the person filing sees the masked preview, and can uncheck "Hide this site's private areas" to show them when the bug is in them (passwords stay blank); a site can also set capture = "off" and take cards with text only, the address is kept without its query string and hash, and a site can switch P99 off on any page, such as a checkout's receipt.
This is P99 on PITO MD's own site, pitomd.com, recorded on its dev server with a local P99 Worker and sample people. The site added one tag and nothing else; its host config keeps the "+" clear of the site's own back-to-top button.
The Cards screen. p99 (or p99 tui) shows every card across your sites and Cloudflare accounts, or one site at a time, with its host and its label on every row. Open a thread to read it, reply, add an internal note, move it to another lane with m, or assign it. w lists the cards that need you, and the header counts them (3 waiting). People are usernames, made from their names and changed by each person for themselves, so an assignee shows as @grace.hopper with a disc drawn from it, never an e-mail.
The board. Each space has one board, and every card sits in one of its seven lanes, in that order. A lane's order is its priority: the top matters most, and there is no priority field to fill. Any Owner moves any card, and a filter narrows what shows (projects, hosts, who changed it last). An agent that needs a person's call moves its card to Needs you, and an Owner's reply sends it back to the lane it came from. p99 card move BUG-12 --to in-progress (or --to "In progress", with --before or --after another card) does it from a script or an agent; p99 board lists the lanes and their cards, p99 cards --waiting what needs you, and p99 labels the lanes' names, each with --json.
A CLI for people and agents. Every command answers --json. An agent acting for you runs with --agent (or P99_AGENT=1), and its cards, comments and changes show as your name with a robot, Ada Lovelace 🤖, so your words and your agent's never look alike.
p99 list --all --status open
p99 show BUG-12
p99 card new --host shop.example.com --kind bug --title "The total does not update"
p99 card comment BUG-12 "Fixed on dev, can you check?"
p99 card move BUG-12 --to Done
The cards' own commands sit under p99 card: new files one (with --project or --host, --kind, --title, --body, --page and --attach), comment answers one (--attach, --internal), status sets its lane or closes it as abandoned or a duplicate, and move puts it in a lane or beside another card. A card filed from the terminal or by an agent without --page is on no page. p99 file, p99 comment, p99 status and p99 move still run for one release and print their new name.
Changes arrive live. A new card, a comment, a change of label, assignee, title or details, or a new file shows in an open p99 tui within about a second, without polling: the Worker tells a Durable Object, which holds one WebSocket for each open p99 tui and p99 watch. p99 watch prints each change as it happens, and p99 watch --json prints one JSON event per line, so an agent can react to new cards as they come.
$ p99 watch --host shop.example.com
BUG-12 card shop.example.com Grace Hopper: The total does not update
BUG-12 comment shop.example.com Ada Lovelace 🤖: The total does not update
BUG-12 change shop.example.com Ada Lovelace: The total does not update
Where a fix landed. p99 announce reads your commits' Fixes: BUG-12 and Refs: FEAT-3 trailers and tells P99 which environment a build reached. Each card then shows when its fix is on dev and in production, and the person who filed it can be told when it ships. p99 feed on <host> also publishes the fixed cards as an Atom or JSON feed on the site, with keys, kinds, titles and dates only.
One card for the same thing. p99 merge BUG-14 BUG-15 --into BUG-12 folds duplicates into one card. The people who filed them follow it: they read its public thread in the panel and get its replies, its closing and its landings by mail.
Delete for good. p99 delete BUG-12, d in the TUI or the panel's Delete under Manage removes a card with its comments, notes and every stored file; Owners only.
GitHub, kept small. p99 github set example.com --repo acme/shop --token-ref env:GITHUB_TOKEN links a site to a repository: p99 github open BUG-12 opens a GitHub issue for the card, and when a pull request carrying Fixes: BUG-12 merges, or that issue is closed, the card is marked fixed (the GitHub guide).
Slack and Discord. p99 hooks set example.com --slack <webhook URL> posts the site's new cards, comments, closed cards and shipped fixes to a channel, with no app to install; the URL is kept sealed and never shown again (docs/hosts.md).
Mail for the people who file. With p99 email set --provider resend --key-ref env:RESEND_KEY --from p99@example.com, the person who filed a card gets a mail when someone replies, when the card is closed or reopened and when the fix ships, in the site's colours with a plain-text part. Each mail's link opens the card straight in the panel, and one click stops mail about that card.
Setup that explains itself. p99 setup says which Cloudflare API token to create, where, and why each permission is needed. It checks the token and every permission, then creates the D1 database, the R2 bucket, the key, the Worker and a route for each site, showing its progress. For each secret it asks where it comes from: pasted, a file only you can read, an environment variable, or a command that prints it (pass show …, bw get …), and the config keeps the source, never the value. It ends with what exists and the command to run next. It is safe to run again, and a second Cloudflare account goes in beside the first. p99 deploy uploads a new version later, keeping every secret.
A space is a Cloudflare account. It holds one Worker, one D1 database and one R2 bucket, and the Worker sits on a route, <host>/_p99/*, in front of each of your sites. A route runs before a Pages project, a tunnel or another Worker on that host, so P99 never touches the site itself. The p99 binary carries the Worker, the loader, the panel and the database migrations, and uploads that same build to every space.
Invites are personal codes rather than passwords. p99 invite --space --name "Ada Lovelace" --email ada@example.com grants a person the whole space, --project "Example site" only some of its projects, and either mints a 12-character code and a link, https://example.com/#p99=<code>. The code travels in the link's fragment, which browsers never send to a server, so it stays out of every log. Codes are revocable and rate-limited, and a revoked one can be deleted for good (p99 invites delete <id>). There are two roles: an Owner runs P99 and sees everything (p99 invite … --owner), and a User, any other code holder, sees their own cards.
A second factor, when you want one. p99 invite … --totp adds an authenticator to the code, and p99 invites totp <id> adds one to a code someone already holds, keeping their link. P99 mails the setup to the person separately, apart from the link: a QR code to scan, the key in groups of four, and a link that adds it in one tap on a phone. With --no-mail, it prints the setup once instead: a key to paste as the one-time password in your password manager, or as the setup key in Google Authenticator or Authy, and the same secret as an otpauth:// link for apps that take one, for you to send apart from the link. The first time a browser uses the code, the panel asks for the 6-digit code; after that the browser is remembered, and a forwarded link alone opens nothing anywhere else. The Worker keeps the secret encrypted, refuses a code already used, and locks the authenticator after 10 wrong tries. Nothing changes on the site: the prompt lives in the panel.
When the space has mail on (p99 email set), p99 invite mails the person their link with a few numbered steps a newcomer can follow alone: one "Open P99 on " button per host the code covers, the busiest site first, and the plain https://<host>/#p99=<code> lines in the text part, with replies going to whoever sent it; with --totp, the authenticator setup follows in a second mail of its own. Without mail, or with --no-mail, the terminal prints the links once. p99 revoke <id> ends a code at once, and p99 relink <id|email> gives someone a new code in place of the one they hold: the old code stops working in the same request, the new link is mailed, and their authenticator, cards and history carry over, since those follow the e-mail. With an authenticator on the code, a forwarded link alone opens nothing in another browser. Your own links are printed in the terminal, never mailed unless you add --mail-me; P99 writes them nowhere, so save them in your password manager if you keep them there. p99 link [host] opens your link in the browser from the code your config keeps (inline, or a file, an environment variable or a command that prints it, read at that moment), and --print prints it. More in the invite guide.
Each space's Worker also answers on an address of its own, so P99 stays reachable when a site is down or its tag is off. It is the space's workers.dev address (https://p99.<subdomain>.workers.dev/), or a host you set once as app = "p99.example.com" for the space in the config, and p99 deploy sets it up; p99 app prints it. The page opens on the space under one fixed header: on the left the P99 mark, which leads back to the board from every screen, with the space's licence badge beside it; on the right the space's name beside your picture. Owners see the board, narrowed by the space's projects, each listing its sites; a card opens in the side panel. Everyone else sees their cards. Profile (back link or Esc to leave) holds your username, your picture (cropped square and resized to 256 × 256 in the page, or the generated disc) and, for Owners, the licence key field. The badge names the space's licence (Free, Studio, Agency or F&F, each in its own colour; the masters are in assets/badges/), as an Owner last pasted it on Profile or p99 deploy last set it from p99 license; the panel's head on your sites shows the same badge under the P99 lockup, so everyone with a code sees the space's licence word (never the key).
Signing in takes your e-mail and the 6-digit code from the authenticator on your P99 code; there are no passwords and no mailed links, so only codes with an authenticator sign in here (p99 invites totp <id> adds one). The session is a cookie that lasts 30 days and ends at sign-out, or the moment its code is revoked or relinked. On the page a person sees exactly what their codes see on the sites. More in the security notes.
Each release carries a Linux (x86_64) and a macOS (Apple Silicon) binary with a SHA256SUMS file. The macOS binary is built on an Apple Silicon Mac.
Linux and WSL:
v=$(basename "$(curl -fsSLo /dev/null -w '%{url_effective}' https://github.com/gmrdad82/p99/releases/latest)")
mkdir -p ~/.local/bin && curl -fsSL "https://github.com/gmrdad82/p99/releases/download/$v/p99-$v-linux-x86_64.tar.gz" | tar -xz -C ~/.local/bin p99
macOS:
v=$(basename "$(curl -fsSLo /dev/null -w '%{url_effective}' https://github.com/gmrdad82/p99/releases/latest)")
mkdir -p ~/.local/bin && curl -fsSL "https://github.com/gmrdad82/p99/releases/download/$v/p99-$v-macos-arm64.tar.gz" | tar -xz -C ~/.local/bin p99
~/.local/bin needs to be on your PATH. From source, with Rust 1.97 or newer: cargo install --locked --git https://github.com/gmrdad82/p99 --tag vX.Y.Z p99.
First use:
p99 tui --demo # your cards on sample data, nothing to set up
p99 setup # your Cloudflare account, step by step
p99 invite --space --name "Ada Lovelace" --email ada@example.com
p99 # your cards
Then add the tag to your site's <head> and open the link p99 invite printed for you.
- docs/hosts.md: placement, themes, Content-Security-Policy, Cloudflare Access and the pages P99 stays off.
- docs/config.md:
~/.config/p99/config.toml, several spaces and hosts, each secret inline (mode 0600), or a file, an environment variable or a command that prints it. A script setsP99_TOKENrather than passing--token, which would land in shell history; every command that talks to Cloudflare takes it over the configured token, sop99 deployruns unattended. - docs/import.md: bring cards over from another tracker with
p99 import, keeping their keys and numbers. - docs/api.md: the Worker's HTTP API, signing and its test vector.
- docs/guides/: one task a page, also on docs.pitomd.com.
P99 is one of PITO MD's open tools. Its terminal UI is built on pito-tui, the shared terminal app shell. Its clips and stills are recorded with prec and made GIFs with pcod from pito-codecs. Its charts are drawn with pito-compare.
bin/gate --fast # fmt, clippy -D warnings, the unit tests
bin/gate # the same plus the release build
bin/budget # the loader's 2,048-byte gzipped budget
cd worker && npm ci && npm test # the Worker's tests on a local wrangler dev
Issues and pull requests are welcome on GitHub. A pull request is merged only once its author has signed the Contributor Licence Agreement; CONTRIBUTING.md says how, and what a change needs. Keep changes small, keep the loader under its budget, and run the gate before you push. Please follow the code of conduct; report security issues privately as SECURITY.md says.
© 2026 Catalin Ilinca. AGPL-3.0, with a commercial licence for closed or paid use.
P99 is free software under the GNU Affero General Public License, version 3 only (LICENSE): use it, study it, change it and share it as that licence allows. Closed or paid use takes a commercial licence instead; p99 license set stores its key, which is checked offline (docs/config.md). Versions up to 1.7.x were released under the MIT licence and stay under it as published.
The AGPL-3.0 and the commercial licence cover the code only: the P99 and PITO MD names and logos are © Catalin Ilinca, all rights reserved (TRADEMARKS.md). Third-party code and its licence are listed in NOTICE.






















