DEV Community

Luke Jones
Luke Jones

Posted on

How Teppe for Jira works under the hood

A few weeks ago I promised a technical write-up. Here it is.

The two questions I get most are: how do you render tens of thousands of Jira issues in under 10ms? And how does the AI figure out how to group things? They're related, and the answer to both starts with the same principle — do less.

The rendering problem

Jira backlogs are large. A mature project might have 50,000 issues. Naively rendering 50,000 nodes in a browser canvas is slow, and no amount of JavaScript optimisation is going to change that.

The insight is that you never need to render all of them.

The Sierpiński carpet is a fractal — it's recursive by definition. At any given moment you're looking at one level of the hierarchy, with the levels above and below rendered at progressively lower fidelity. We cap this at MAX_DEPTH + 2 levels visible at once. The result: no matter how large your backlog, a single view renders at most 584 cells. We measured layout time at ~1.4ms for a 1,000-issue project and ~1.8ms for a 10,000-issue one. It's essentially flat.

Why Rust and WASM

The geometry work — computing where every node goes, what size it should be, what fidelity level it gets — is pure arithmetic with tight loops. It's exactly the kind of work JavaScript is not well suited to, and exactly what Rust is.

We compile the layout core to WebAssembly. It runs in the browser, but it runs at near-native speed. The Rust core produces a flat buffer of RenderedNode structs — position, size, depth, status, fidelity — and the JavaScript layer is a dumb consumer of that buffer. It reads the numbers and draws rectangles. No logic lives in the rendering layer.

The canvas itself is four stacked layers: geometry, state (colours and fill), focus (the working level highlight), and presence (collaborator indicators). Only the layers that need to change repaint. A status update on one issue repaints the state layer. Navigation repaints the focus layer. The geometry layer repaints only on resize.

This separation is what keeps navigation instant. Drilling into a node is a WASM call and a canvas repaint, not a re-fetch or a DOM reconciliation.

How the AI groups your backlog

The AI grouping was the part I was most nervous about — not technically, but economically. LLM calls cost money, and a Jira backlog with 5,000 issues could get expensive fast if you sent every issue to a model.

The answer was deterministic first.

Before we touch an LLM we build as much structure as we can from what Jira already knows: existing parent/child links, epics, components, fix versions, labels, issue type. For most backlogs this resolves the majority of issues at zero inference cost.

Only then do we look at what's left — the overflow buckets where a parent has more than 8 children, and the Unsorted pile of issues that don't fit anywhere. Those go to the model, batched per bucket, not per issue. One call covers an entire overflow group. The model receives the issue keys and summaries, and returns a named partition — at most 8 thematic groups with a one-line rationale for each.

If a returned group still has more than 8 members, we recurse. The constraint is enforced all the way down.

We measured this at around $0.24 per 1,000 LLM-resolved issues, which sounds small and is small — the cost is dominated by the fixed overhead of the prompt and tool schema per call, not by the number of issues. It scales with the number of overflow buckets, not the size of the backlog, which rewards the deterministic-first approach.

One more thing worth knowing: any group you rename, move or adjust is marked as yours. A rebuild will work around your edits, never overwrite them.


The combination — a WASM geometry core that never renders more than it needs to, and an AI layer that only runs where deterministic logic falls short — is what makes Teppe feel fast and cheap to operate at the same time.

If any of this is interesting and you want to go deeper, feel free to reach out.

Email: info@teppe.io

Luke

Top comments (0)