A spatial idea tracker: every tool in this space is a spreadsheet with a
nicer front end — Ponderfish is a pond. Ideas are fish with visible state, capture
happens in conversation (Claude, ChatGPT — anything that speaks MCP),
and the web app is the living view.
0ORMs
0WebGL / canvas
2colors
22MCP tools
21migrations
1rAF loop
Core stack
Boring on purpose
Next.js 15 (App Router, React 19, TypeScript) on Vercel — git-connected, every push to main deploys production.
Postgres on Neon over the serverless WebSocket driver. Plain tagged-template SQL, no ORM.
Append-only migrations, one concern each, run by a ~60-line Node script.
Cloudflare for registrar, DNS, and inbound email routing.
Accounts & identity
Auth in the same database
Neon Auth (managed Better Auth): email + password and Google OAuth, cookie sessions.
The load-bearing detail: users live in a neon_auth schema inside the same Postgres — so projects.owner_id is a real foreign key to the user table, not a string from a third-party IdP.
Web writes go through Next server actions; there is no separate API layer to keep in sync.
Authorization
Rows, not tenants
Your own pond is simply projects where owner_id = me — no tenant tables, nothing to provision. A pond with only you in it is private; invite someone and the same pond is communal.
Communal ponds are many-to-many sharing (ponds, pond_members, idea_shares): membership grants read, ownership alone writes, sharing never moves ownership.
Open sign-up: anyone can make an account, and a pond invite is now only ever an invitation to a pond. See What the doors cost.
Machine access
Tokens shaped for every client
Per-account pf_ bearer tokens; only the sha256 is stored, plaintext shown exactly once.
Clients that speak OAuth get one by signing in: the MCP endpoint is its own OAuth 2.1 authorization server — dynamic registration, PKCE, single-use codes — and the token it hands out is an ordinary pf_ token, revocable like any other. Registered clients are never stored; a client id is a signed description of the client.
Minted and revoked in the UI; accepted four ways — URL path, Bearer, header, query — so clients that cannot sign in still fit.
Every MCP call runs as the account behind its token; tools resolve names only inside that caller's own pond.
What the doors cost
Open, with ceilings
For its first weeks Ponderfish was invite-only: sign-up stayed open and a gate sat behind it, at /welcome. The gate is gone. What replaced it is not a smaller door but a set of ceilings, because the thing an open door actually spends is our resources, not a place in a queue.
Capture never blocks. Bubbles are unlimited and uncounted on every tier; the free limit of twenty bites at keeping — hatching, planting a fry, un-archiving — and over MCP it downgrades a landing to a bubble rather than refusing it.
Invitations are metered. Anyone inside a pond may invite, as they always could, but an account mints at most twenty-five a day. A link waits to be opened; an emailed invite spends this domain's standing with a stranger's mail provider, and that is not recoverable by apologising.
Every field has a ceiling. The web always capped what it accepted; the tool schemas now do too, and a fish's notes keep their most recent hundred thousand characters rather than growing without end. Postgres text has no opinion, so the opinion lives in the schema.
Billing fails closed in production. With Stripe unconfigured anywhere else every account swims deep, because a limit with no way past it is a wall; in production the keys are always in, so their absence is a missing variable and is read as one.
The capture surface
A hand-rolled MCP server
Stateless JSON-RPC over streamable HTTP at /api/mcp — no SDK, ~19 tools: add, feed, graduate, kill, revive, archive, share, standings…
Tool descriptions are written for the model, in the product's voice — they are the real API docs, and the reason "toss that in the pond" works mid-conversation.
The lifecycle is the product: bubble → fry → fish → big fish, with belly-up as a reversible purgatory before the archive.
"add this idea" in Claude→MCP tool call + pf_ token→row in Postgres→a fish swims into the pond
The water
No WebGL — deliberately
DOM elements + CSS transforms; one requestAnimationFrame loop owns positions, CSS keyframes own tails and body flex.
Hand-drawn wobble via SVG turbulence displacement; line boil via discrete seed-stepping — the ink redraws itself like animation on paper.
State is physical: hunger drains the ink and sinks the fish, bubbles rise, dead ideas float belly-up with × eyes.
The fish
Deterministic, generative
Each fish is inline SVG derived from its idea's UUID: hash → seeded RNG → body, snout, tail, fins, pattern, eye.
Same idea = same fish, on every device, forever. No image generation, no assets to load — the one in the header is a real one.
Fry are drawn blank on purpose: patterns are earned at graduation.