I was tired of paying for Excalidraw, so I made my own, and made it better.
I lived in that app. But the core loop I actually used, draw, connect, export, was wrapped in something that kept growing, and every board sat on someone else's server behind a plan. A whiteboard is a thinking tool. It shouldn't need an account, a network, or a subscription.
Squigl is that loop and nothing else: a small, private whiteboard that runs entirely in the browser. Better here means it stays local and private with no account, exports hand-drawn files that look the same wherever they're opened, and has Squiggy on hand to draw a diagram for me.
Rectangles, diamonds, ellipses, lines, arrows, freehand ink, text, embedded images, and connectors that follow the shapes they're attached to. Boards live in tabs and autosave separately. Undo works at the gesture level, so a whole drag is one step rather than forty. A ⌘K palette covers every tool, toggle, export, and view action, and the eraser, the hand tool, and a read-only view mode are all one key away.
Hand-drawn rendering is on by default and carries through to SVG and PNG exports, with Excalidraw's own typeface bundled locally so files look the same anywhere they're opened. Everything autosaves to IndexedDB. No accounts, no analytics, no telemetry, nothing fetched at runtime.

The one exception to the offline rule is Squiggy, a small morphing blob in the corner who can draw for you. He drifts and blinks at rest, winks when you open him, morphs while he's thinking, and sleeps when no key is configured. Ask for a flowchart or a multi-screen wireframe and it lands on the canvas as ordinary editable shapes, in a single undo step. He is also the only AI surface in the app: a separate generate-diagram dialog was built, shipped, and retired the same day, because two entry points meant typing a diagram request into the chat and watching nothing happen.
The model only ever returns a graph. Layout is done locally, which keeps cheap models usable and the output deterministic, and means a bad answer is a bad diagram rather than a broken board. Everything downstream of that decision follows from it: the layered layout, the grid snapping, the sizing, the lane and tier geometry for kanban and architecture views are all local code that a model never sees, so the same request produces the same arrangement twice.
Getting updates right took longer than getting generation right. Asked to change one box, a cheap model cheerfully re-emits the entire graph, which used to duplicate the whole diagram. Three defences now stack: a new node whose label matches exactly one existing shape is treated as that shape, the chat rewrites each applied block into a note mapping node names to real canvas ids so the next turn can reference them, and the board summary hands the model short per-request handles instead of long ids, because long look-alike ids are the thing small models garble. The rule underneath all three is that the fix has to hold for the cheapest model available, not the best one.
Which model that is was settled by measurement rather than reputation: the real prompt was run through the candidates and scored on how much structure came back, and the winner ships as the default. Chat and diagrams run on different models, chosen per turn, because holding a thirty-node graph and its groupings in one JSON object is a different job from answering "hey, what can you do". After shapes land, the camera eases over to frame them, but only when they are not already on screen, so a centred insert never nudges the view.

React, TypeScript, and Vite, with IndexedDB underneath and no server of its own. Rough.js draws the sketchy outlines and perfect-freehand does the variable-width pen ink, which is the same pairing Excalidraw uses. The renderer is one function shared by the live canvas and the file export, so what you see is exactly what ships out.
On the hosted build the OpenRouter key is never embedded: a small serverless relay holds it and gives each visitor a budget of twelve messages every thirty minutes. The relay also sets its own reply length cap rather than trusting the browser, because an uncapped reply is an uncapped bill. Run it locally with your own key and there is no relay at all.
