Public: anyone can read this; the crew can change it.

A shared board cannot be used to fill the disk: bound the undo log, a board's size, and board creation #16

shipped production is serving it opened this has it
opened this

A board anyone can open and change has nothing standing between it and the disk: the action log grows by a row for every change and is only ever shortened by undo, a board can hold unlimited columns and cards, and POST /boards will create boards as fast as it is asked. Give the app a floor it can defend before identity and an MCP agent make writing to it even easier.

Done looks like

  • [ ] src/limits.ts (new) holds every limit as one named constant with a sentence saying why, and nothing else: undo depth, columns per board, cards per board, boards created per minute, and the byte cap for a request body.
  • [ ] The undo log keeps at most the newest N actions per board (200 is a sensible N), trimmed inside the same transaction as the write that added one. Undoing past the horizon takes the existing "nothing to undo" path — the page and src/tty.ts already handle a false.
  • [ ] A board holds at most 50 columns and 500 cards. The limit is enforced in src/db.ts, not only in src/routes.ts, so the terminal and any later caller (MCP) get it too: the create returns null/false and the route answers 400 with a sentence a person can act on ("this board is full — 500 cards"). The terminal shows the same sentence in its message line instead of doing nothing.
  • [ ] Board creation is bounded: a token bucket in src/limits.ts, in memory, for the whole process, refuses more than the per-minute cap with 429 and a retry-after header, and recovers on its own. It is keyed by nothing a caller controls, so it cannot itself become an unbounded map — say plainly in the README that this is a crude, app-wide brake, not per-person fairness.
  • [ ] The README lists the limits under "What's simulated" or beside it, so a person who hits one is not surprised.
  • [ ] Tests (src/limits.test.ts new, plus cases in src/server.test.ts): the 51st column is refused with 400 and the board is unchanged; the undo log stops growing at N and the oldest action is the one dropped, while undo still works for the newest change; the bucket refuses after the cap and allows again once refilled (inject the clock, do not sleep); a refused create writes no row.
  • [ ] No new dependency, no behaviour change under the limits, bun test passes.

Files

src/limits.ts (new), src/limits.test.ts (new), src/db.ts, src/routes.ts, src/tty.ts, src/server.test.ts, README.md.

Risks

A limiter keyed by anything a caller sends (ip, header, board id) is a memory leak wearing a safety vest — keep it a single bucket. Trimming the log in a different transaction from the write leaves undo reading a row that is gone. A limit enforced only in the route silently leaves the terminal and the future /mcp unbounded. Numbers chosen too low turn a legitimate busy board into a broken one; say the number in the error, always.

Blocked by the transactional write path, because the trim belongs inside that transaction.

Suggested brief: scout (small, tested, says what it left out); sentinel to review the pull — denial of service and unbounded state are its beat.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs