Engineering · Mar 23, 2026
Our Claude Code Workflow: How We Ship Software
The plans, rules, types and in-house tools that Claude Code works inside, across eight products
By Toni · 14 min read
Claude Code is Anthropic's AI coding agent, and it sits at the centre of how JT Digital builds software. The rest of our Claude Code workflow is the system around it, and that system decides how good the result is.
Eight products on one stack
JT Digital was registered in January 2026. Two months later, these were live or in active development:
- GZS Inovacije, the platform behind the national innovation awards of the Chamber of Commerce and Industry of Slovenia
- Olimpijski Kviz, a live quiz that ran on national television during the Winter Olympics, built for 10,000 concurrent players
- a gym management platform
- a financial operations tool
- Tela, an AI design tool
- a time tracker
- a project management dashboard
- fluent-html, a type-safe HTML library published on npm
Alongside them, a multi-tenant platform (one system serving many separate organisations) for a large client was in active development when this post went up.
Six days after its first commit, that platform alone stood at these numbers:
That is about 0.84 lines of test for every line of source. Like every app on the list, the platform runs on Fastify (a Node.js web server), TypeScript, fluent-html, HTMX (a small library that updates part of a page without a reload) and Tailwind CSS (styling written as short utility classes). New projects all start from the same template and share the same guidelines and tooling.
AI explains part of this pace. Claude Code is the engine, and it runs inside a chassis we designed out of project management, coding guidelines, a type-safe framework, our own tooling and deployment automation. Each layer removes one kind of friction, and together they compound.
Every phase of a project, from kickoff to iteration. What each project teaches goes back into the template and the guidelines.
Project management that lives in git
In AI-assisted development, the slow part is deciding what to write. Claude Code can produce code faster than anyone can describe what it should do.
We use no Jira, Linear or Notion. Our project management lives in plain markdown files committed to git next to the code. Requirements, tasks, bugs and architecture decisions sit in folders by epic (a large area of work) and feature.
We follow Shape Up, Basecamp's method of fixing how much time a piece of work gets and letting its scope change to fit. Tasks are discovered during the build. The task file holds only the scope we have committed to right now.
Every user story carries a hill phase. Uphill means we are still working out the approach, and downhill means every decision is made and the work is ready to build. Once a story is downhill, Claude can carry it out quickly. Stories stuck uphill show where the real delay is, which is usually an unsettled requirement or an open decision.
It reads the open bugs, then the current tasks with their priorities and hill phases, then the requirements. Within seconds it has the project's full context, and nobody has to brief it.
Architecture decisions are written down next to the code as well. When Claude reads "we chose X over Y because Z", it leaves the settled question alone and builds on the answer.
An internal PM dashboard reads these markdown files through the GitHub API. It draws progress, works out velocity (how fast tasks get done) from git history and posts a daily progress report to Google Chat. Nobody updates a PM tool by hand, because the repository already holds the status. The enterprise project's 696 tasks and its decision log are kept this way.
Everything lives in the repo. Hill phases show which stories still wait on a decision.
Written guidelines that teach Claude Code our codebase
Claude Code writes better code when its instructions are better, and our guidelines have paid back more than any other part of the system. Each new project ships with 54 guideline files in seven areas, from web development and testing to marketing and project management.
The root CLAUDE.md, the file Claude Code reads at the start of every session, is about 240 lines long. It names the stack, states the critical rules and links to the deeper files.
The full library runs to about 8,000 lines, and most sessions load only a few hundred of them. Each link in CLAUDE.md carries a trigger such as "read when implementing tracking" or "read when writing tests", so Claude opens a guide when the task calls for it.
The guidelines are written as instructions. A typical rule reads "use defineRoutes for routing, never hardcode URL strings". Most rules come with a code example marked ✓ and ✗, and each "never" gives its reason.
The guidelines live in one central repository. A script pulls them into each project and records which version it took, so an improvement written once reaches every project on its next sync.
Guidelines tell Claude how to write code, and TypeScript's compiler catches what it gets wrong. Our ESLint plugin, a code checker with 18 rules for fluent-html (10 of them fix the code automatically), catches the mistakes specific to our framework. Code has passed all three layers before a person looks at it.
Because every project reads the same guidelines, our codebases look alike: defineRoutes for routing, handle(server, ...) for controllers and setHtmx for navigation. A new project follows these conventions from its first commit.
Why a typed, server-rendered stack suits AI coding
We picked each part of our stack to leave an AI fewer ways to make a mistake. Every page is rendered on the server, with no client-side framework, and the browser loads about 14 KB of compressed JavaScript, most of it HTMX.
fluent-html is our own type-safe HTML builder, published on npm with zero dependencies. Every HTML element is a function with chainable methods, so a heading and a paragraph inside a div are written Div(H1("Title"), P("Body")) in plain TypeScript, with no JSX or template strings. We wrote about its design in What If Wrong HTML Couldn't Compile?
This matters for AI because each style has one expected way to be written. In fluent-html, .background("blue-500") sets a blue background, and nobody has to choose between styled-components, Tailwind class strings, CSS modules and inline styles. With one correct pattern to follow, Claude is more likely to pick it on the first attempt.
The Tailwind methods are typed too, as in .padding("x", "4"), .on("hover", t => t.background("blue-600")) and .at("md", t => t.textSize("lg")). A misspelled method name fails to compile, and so does a wrong text size or font weight. Colours and spacing still accepted any string in March 2026, so typos there were left to the editor's suggestions.
Type safety that catches AI mistakes at compile time
The type system turns whole kinds of mistakes into compile errors. When an AI writes the code, that is where many of its slips get caught.
Typed HTMX swaps. HTMX swap strategies are typed as a small grammar built from TypeScript template literal types. An editor offers every valid combination as you type, such as "outerMorph scroll:top settle:200ms" (update the element in place, scroll to the top, wait 200 ms to settle). In March 2026 the type still let any other string through as a fallback. Since a change that April, a typo such as "outerMorph scrolll:top" fails to compile.
Void elements reject children. Elements that can never hold content, such as Img() and Input(), take no arguments, so Img("alt text") is a compile error. No version of the function accepts content, so nothing can end up nested inside a self-closing tag.
Discriminated unions with Match. Page states are tagged unions, where one status field decides which other fields exist. Match(state, "status", { loading: () => ..., error: (s) => Alert(s.message) }) narrows the type inside each branch. Add a new state and forget to handle it, and every Match call that misses it fails to compile.
Nullable narrowing with IfThen. IfThen(user.avatar, (src) => Img().setSrc(src)) runs the callback only when the value exists and passes it in already typed as non-null. The AI never needs to write user.avatar!, the assertion that tells TypeScript to trust that a value is there.
Branded types prevent ID mixups. A UserId and a ProjectId are both strings at runtime, and the compiler still treats them as different types. Pass a project ID where a user ID belongs and it fails to compile, even though both are valid UUIDs.
Escaping by default. All text content is escaped, which blocks cross-site scripting (XSS, where text a visitor types runs as code in someone else's browser). Div(userInput) turns a typed <script> into the harmless text <script>, and raw HTML needs an explicit Raw() call. The safe path is the default, so an injection hole cannot open by accident.
Routes and element ids are typed as well. defineRoutes turns each path parameter into a required, typed argument, defineIds keeps a registry of the ids HTMX may target, and assertNever makes the compiler confirm that every case is handled. Many of the mistakes an AI can make in this code fail to compile, long before a user could meet them. Our page on how mistakes fail the build shows the same idea across our client work.
A trimmed example from our time tracker shows the routes defined once and a link that uses them.
// time-tracking.routes.ts: define once
export const timeTrackingRoutes = defineRoutes("/time-tracking", {
project: { method: "get", path: "/:projectId" },
createEntry: { method: "post", path: "/:projectId" },
removeEntry: { method: "post", path: "/:projectId/entries/:entryId/remove" },
} as const);
export const timeTrackingIds = defineIds(["time-logs-section"] as const);
// dashboard.view.ts: reference everywhere
A(project.name)
.setHtmx(timeTrackingRoutes.project(
{ projectId: project.id }, // ← typed params: omit and it won't compile
{ target: layoutIds.mainContent, // ← typed ID: typo won't compile
swap: "outerMorph show:window:top", // ← typed swap strategy
pushUrl: true },
))
.cursor("pointer")
.textColor("white").fontWeight("semibold")
.on("hover", t => t.textColor("accent"))
The route path exists in one place. Rename it and every reference either follows or fails to compile. Leave out a required parameter such as projectId and tsc, the TypeScript compiler, stops the build before anyone opens a browser, so a broken link surfaces as a compile error long before it could turn into a 404.
The same patterns hold across very different problems: a live TV quiz built for 10,000 concurrent players, a multi-tenant platform with layered user roles, and a finance tool where every calculation has to be exact.
Server rendering removes a whole class of AI bugs
With server-side rendering (SSR) there is no hydration step, where the browser re-runs the page's code to attach behaviour, and no "use client" boundary, hooks or client bundle to split. That removes the bugs AI coding assistants most often write in React and Next.js: useEffect timing issues, hydration mismatches, confusion over what runs on the client and what on the server, and state that drifts out of sync. Our stack has none of them.
The tooling backs this up. Our ESLint plugin rewrites setClass("bg-red-500 p-4") into .background("red-500").padding("4") automatically, and our Tailwind extractor teaches Tailwind to find class names inside fluent method calls. Between the framework, the linter and the build, incorrect code is hard to write.
In-house tools that feed each other
A projects template with opt-in modules
One command scaffolds a production-ready project with the modules it needs: sign-in with OAuth providers (Google, Apple, GitHub, Microsoft), payments (Stripe, Wise), AI integration (Claude, OpenAI), email, file storage (S3 or R2), Redis, job queues, analytics and monitoring (Prometheus, Grafana, Loki). Deploy scripts and the full guidelines library come with it. Going from nothing to a running app with a GitHub repo and a production deployment takes roughly five minutes.
TTL, a time tracker Claude Code writes to
TTL is our internal time tracker. It runs an MCP server (Model Context Protocol, a standard way for an AI agent to call another program's functions), so Claude Code logs time as it works by calling log_time({ userId, projectId, date, duration, description }). Nobody fills in a timesheet or forgets to clock in.
A CMS that plugs into any fluent-html project
Our CMS (content management system) is an internal package with a block editor, built on Prisma, a TypeScript database toolkit. It plugs into any fluent-html project, and when this post first went up in March, our blog ran on it.
A PM dashboard across every project
The PM dashboard puts every project on one screen. It reads the same markdown files and commit history that people and Claude produce while they work.
Each tool covers one phase of the work and feeds the others.
Tela, from design to code in one step
In the usual design workflow, a designer draws a mockup, hands it over in a meeting, and a developer rebuilds it in code. The designer reviews the result, finds where it drifted and sends it back for another round. Two people do the same work twice, and detail is lost at every handover.
Tela, our AI design tool, turns a plain-language description into production-ready HTML styled with Tailwind, using Claude Opus to generate it. It is more than 20,000 lines of hand-written TypeScript.
While Claude Code builds a feature, it can call create_project on Tela, and 30 to 60 seconds later a full design exists. It then calls export_html, takes the HTML, rewrites it as fluent-html views and fits it into the codebase. The design already uses the same Tailwind classes as the production code, so nothing is lost in translation and nobody opens a design tool.
Tela itself runs on Fastify, fluent-html and HTMX, the same stack it designs for.
Tela goes from a page described in words to production-ready HTML in 30 to 60 seconds.
Every tool feeds the others.
Projects made from the template show up in the dashboard, and the guidelines the template ships make Claude better at working in them. TTL logs time against the same projects the dashboard tracks, and sites built from the template can switch on the CMS. When a guideline improves, every project picks it up on its next sync.
Starting from the template, the enterprise project went from an empty folder to a deployed staging environment in under an hour.
What the numbers leave out
fluent-html is verbose. A Div with three Tailwind methods takes more characters than <div class="...">, so the enterprise project has more lines than the same app written in JSX would. Fluent methods trade brevity for type safety. We chose that trade and would make it again.
The template gives us a running start. Each new project begins with a shared UI component library, a test setup and the coding guidelines already in place. The enterprise project's first six days built on a foundation that took months to make, and the numbers above include that head start. We count it on purpose, because the system is the product.
Early sprints are faster than later ones. Scaffolding, CRUD screens (create, read, update, delete) and layout come first, and they produce a lot of code with few open questions. Domain logic, edge cases and integrations come later and move slower, so the first week's pace will not hold for a whole project.
This is AI-assisted work. The pace comes from people directing an AI agent inside a system built for it, deciding what to build and checking what it writes. Hiring faster developers would not reproduce it.
How the layers of our Claude Code workflow compound
Each layer makes the others more useful. These pairs do the most work:
Installing Claude Code gives you none of this on its own. The PM files, the typed framework, the time tracking, the Tela pipeline and the shared guidelines all had to be built first.
The useful question is how much of the work around the code you can remove, so the AI spends its effort on the code itself. We removed the PM tool, the design handoff, timesheets, project scaffolding, most class name typos, mismatched URL strings and status meetings.
Claude Code does its best work inside a system built for it, and every project feeds improvements back into that system.
The method has kept growing since March. Read how we ship working software in weeks for how it runs on client projects today, or see what we have built.
Co-founder
More in this vein
Type-Safe HTML in TypeScript: Wrong Markup Won't Compile
Type-safe HTML in TypeScript turns a misspelled route, a wrong HTMX target or an impossible view state into a compile error. This is how fluent-html does it.
10 min read · Toni
Olimpijski Kviz: a live TV quiz without WebSockets
Olimpijski Kviz, a live TV quiz on RTV Slovenija, ran 16 Olympic broadcasts with zero critical errors. Built for 10,000 concurrent players, with no WebSockets.
14 min read · Toni
Stay in the loop
Don't miss the next one.
One email when we publish something worth your time.