Technical overview · September 2026

How Ponderfish works

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.
How Ponderfish works · Ponderfish