Code,
not files.

tcm stores each function, type and constant of your TypeScript by the hash of its code, with names as labels. Renames are free, changes carry through, nothing is built or tested twice, and server and browser connect themselves.

curl -fsSLO https://tcm.codes/tcm-ts.tgz && npm i -g ./tcm-ts.tgz See how ↓
Unison's codebase model for plain TypeScript. No new language, and Node runs it as it is.
How it works

One change to how code is kept. Everything else follows.

Follow a shop's cart from a scratch file to production.

Scroll inside the box ↓

01

Write TypeScript as usual.

You write in a scratch file: ordinary TypeScript, checked by your editor. This one holds a shop's cart: a tax rate, some functions, a React component and tests. Its first line names their namespace.

A line on its own is a watch expression: tcm check shows its value, like a REPL in your file.

02

Add it to the codebase.

Each function, type and constant becomes a definition of its own. The file was only a place to write them; once they're added, you can empty it.

$ tcm add + new TAX_RATE, subtotal, total … 9 in shop
03

Each definition is stored by its hash.

The hash of a definition's code is its identity, and a name is just a label pointing at it. Formatting and local names aren't hashed, so restyling is never a change.

04

Renaming is free.

A rename moves a label. No code is edited and nothing breaks: every use refers to the hash, so it just shows the new name.

$ tcm mv shop.total shop.cartTotal ~ renamed total → cartTotal 0 files touched
05

It knows what uses what.

Definitions refer to each other by hash, not by file, so the codebase has the whole graph.

With no imports to order, there are no circular imports. And an app bundles only what it reaches, so there's no dead code.

06

Changes carry through.

Change the tax rate and it gets a new hash. cartTotal refers to that hash, so it changes too, and so on up. tcm update re-hashes exactly what depends on the change.

What still typechecks follows along. What doesn't goes on a todo list and keeps its old version, so nothing is half-updated.

$ tcm update ~ changed TAX_RATE ~ follows cartTotal, CartView, testTotal 5 definitions unchanged
07

Only what changed is tested.

Test and lint results are cached by hash. One test reaches the change, so one test runs. Results travel with tcm push, so CI and your teammates skip it too.

$ tcm test ✓ testTotal ran 3 ms ✓ testSubtotal cached ✓ testFormat cached
08

Server and browser, inferred.

tcm works out where each definition runs from what it uses. getCart reads a table, so it runs on the server; CartView renders, so it runs in the browser.

The browser calls the server by importing it. No routes, no fetch, no encoders or decoders, no "use server".

09

Deploy by moving a name.

Builds are named by hash and kept. Deploying points production at the new one; rolling back points it back, instantly.

Open tabs keep calling the code they loaded, and returning visitors download only the chunk that changed, as a diff.

Deploying →
server
anywhere
browser
scratch.ts
// tcm: namespace shop
total([{ price: 100, qty: 2 }])
⧩ 216
names
definitions, by hash
builds, named by the app's hash
#6t0e2k
Oct 30
#a1f3c0
Nov 3
#c9e27b
today · TAX_RATE
production
●shop.example/cartopened yesterday
This tab loaded #a1f3c0 and keeps calling it: still served.
No skew: calls go by hash, so a page never calls the wrong server.
react142 KB · kept
shop38 KB → Δ 1.2 KB
ui9 KB · kept
styles6 KB · kept
Apps

A server function is just a function.

No routes, no fetch calls, no encoders and decoders at the network boundary: browser code imports a server function and calls it. tcm carries the call and traces it, whether your page is plain TypeScript and the DOM or React.

Server functions

Call the server by importing it.

A function that reads a table or a session runs on the server. Browser code calls it like any other: tcm carries the arguments across, checked against the function's types, and brings the result back. There's no transport to write, and since calls go by hash, a page can never call the wrong version.

  • Sessions, cookies and rate limits, from the server library
  • An HttpError reaches the browser; other errors stay on the server
  • An async function* streams, and break stops it on the server
  • Plain HTTP endpoints for webhooks, moved with tcm mv
shop.tsx
export async function getCart(id: string) {      // server: a session, a table
  if (!session().user) throw new HttpError(401, "sign in first");
  return carts.get(id);
}

export async function* orderStatus(id: string) { // server: streams as it goes
  yield "packed"; await shipped(id); yield "shipped";
}

export async function main() {                    // browser: it uses the DOM
  const el = document.querySelector("#cart")!;
  const cart = await getCart("tea");         // no route, no fetch
  el.textContent = formatPrice(cartTotal(cart.items));
  for await (const s of orderStatus("tea")) el.title = s;
}
$ tcm start shop.main --http shop.api serves shop.api.hooks.stripe at POST /api/hooks/stripe
Data and jobs

Tables typed by your code. Jobs queued by hash.

A table's row type is its schema. Every write is checked against it, and when the type changes, a typed step upgrades old rows as they're read. tcm update says up front what the stored rows need.

A job records the exact code that queued it, so a deploy in between doesn't change what it does.

  • Unique fields, indexes and transactions, across tables
  • Durable jobs with retries, safe with several servers
  • Schedules found in the code, with nothing to register
shop.ts
export type Cart = { user: string; items: Item[] };
export const carts = table<Cart>("carts", { unique: ["user"], migrate: [fromV1] });

export function fromV1(old: { user: string; lines: Item[] }): Cart { // a typed step
  return { user: old.user, items: old.lines };
}

export async function checkout(user: string) {
  await transaction(async () => {
    await orders.insert({ user, at: new Date() });
    enqueue(sendReceipt, user);    // on commit, as the code that queued it
  });
}

export const nightly = every("0 3 * * *", expireCarts);
$ tcm update ~ changed shop.Cart carts: 1,204 stored rows don't fit; fromV1 migrates them as they're read
Traces

And everything is traced, with nothing to set up.

tcm already answers every request, runs every job and holds every database connection, so it records what happened, by definition: from a click in the browser to the SQL it ran. No SDK, no instrumentation, no exporter.

  • When a new version runs slower, tcm names the change that did it
  • Errors say the definition that threw them
  • In the terminal, in the studio, or over OTLP to Honeycomb or any other
$ tcm traces kind calls p50 p95 shop.getCart call 412 3.1ms 9.4ms shop.CartList render 120 6.2ms 19ms carts SELECT sql 412 0.31ms 1.2ms worse than the version before shop.getCart call p95 9.4ms → 41ms (4.4× slower) since "Carts: newest first" #k4njkepv changed: shop.sortCarts
click “Checkout”one trace, browser to SQL48 ms
CheckoutButton48ms
→ shop.checkout38ms
  transaction29ms
    orders INSERT11ms
    carts UPDATE8ms
And React, if you want it

Components are placed the same way.

Using React? The same rules apply: an async component renders on the server, one with state or a handler in the browser, and the rest wherever they're used. There's no "use client" or "use server", and nothing to install.

  • Server components, sent with React's own Flight format
  • A first paint rendered on the server, then hydrated
  • A server component can render a browser one, and back
shop.tsx
export async function CartList() {        // server: it's async
  const items = await carts.find();
  return <ul>{items.map((i) => <li>{i.value.name}</li>)}</ul>;
}

export function AddToCart({ id }) {     // browser: a handler
  return <button onClick={() => addItem(id)}>Add</button>;
}

export function Shop() {                  // anywhere: where it's used
  return <main><CartList /><AddToCart id="tea" /></main>;
}
$ tcm dev shop.Shop first paint on the server, hydrated in the browser, calls by hash
What goes away

Work you can stop doing.

More on each, and where it stops →

No files

Without files, everything built on them had to be rebuilt.

Git, grep, npm and your agent's edit tool all work on files. tcm has its own, built for definitions, and better at their jobs for it.

Version control that knows a rename from an edit.

History is a history of names, and a diff shows what changed in meaning: renames as renames, restyling apart, and what was re-hashed only because something it uses changed.

git diff3 files, 7 lines
shop/cart.ts | 3 +--
shop/CartView.tsx | 2 +-
shop/cart.test.ts | 2 +-
@@ shop/cart.ts
- export const total = (items: Item[]) =>
- subtotal(items) * (1 + TAX_RATE)
+ export const cartTotal = (items: Item[]) => subtotal(items) * (1 + TAX_RATE);
@@ shop/CartView.tsx
- <b>{formatPrice(total(cart.items))}</b>
+ <b>{formatPrice(cartTotal(cart.items))}</b>
@@ shop/cart.test.ts
- expect(total([{ price: 100, qty: 1 }]))
+ expect(cartTotal([{ price: 100, qty: 1 }]))
tcm diff1 rename
~ renamed shop.total → shop.cartTotal
shop.cartTotal: comments, layout or doc changed
0 definitions changed

The same change: a rename and a restyle. Git sees seven lines in three files; tcm sees what you did.

instead of git log, git revert

History and undo

Every command is an entry. Undoing adds a new one on top, so history is never rewritten, even after a push.

tcm reset 1
instead of git merge

Branches and merges

Merges are three-way by name, so a conflict means both sides changed the same definition, never lines that moved.

tcm merge agent/fix-rounding
instead of git push, CI caches

Sharing

Push and pull send definitions by hash, with their test results. Any codebase can serve others.

tcm push
instead of grep

Search

Find code by name, by text, or by its type, matched up to renaming and argument order.

tcm find ":(xs: T[]) => T"
instead of npm, for your own code

Libraries

Use another codebase's release, mounted read-only under lib and verified by re-hashing as it arrives.

tcm lib add https://ada.dev money@1.4.0
instead of a file tree

Editing

Pull definitions into a scratch file, in any editor, and save them back. Or edit in the studio.

tcm edit shop.cartTotal
instead of an agent's file tools

And tools for your agents.

Coding agents read and edit files too, so tcm mcp gives them their own: find code by type, change a definition and see what it reaches, and run only the tests that matter. Each agent works on its own branch, which you merge from the studio.

agent › tcm_find ":(items: Item[]) => number" shop.cartTotal shop.subtotal agent › tcm_patch shop.TAX_RATE 0.08 → 0.0825 ✓ saved · 3 dependents follow · 0 to fix agent › tcm_test ✓ 1 ran · 2 cached on agent/tax-2027
Forge · Registry

Share your code. Find everyone else's.

The forge is where your team works on a codebase together; the registry is where anyone finds code to use. Both know what only a codebase can: what a change reaches, where code runs, and what's been tested.

The forge

Work together on one codebase.

Every tcm serve comes with a forge: the codebase's code, search, history and releases on the web, and contributions reviewed as definitions.

  • Reviews show what a change reaches and which tests cover it
  • A map of where each namespace's code runs
  • Merges on the server once checks pass
  • Public, or with a token per contributor
tcm serve · at /forge on your server
shop.example/forge/contributions/12
ada / shoppublicSearch shop /
CodeContributions 2HistoryReleasesPeople
Tax rate for 2027 open
B bea wants to merge @bea/tax into main
What it reaches4 re-hashed · 5 unchanged
TAX_RATE
cartTotal
CartViewtestTotal ✓
shop.TAX_RATEchanged#k4n2wq → #p0x8zt
@@ shop.TAX_RATE
- export const TAX_RATE = 0.08
+ export const TAX_RATE = 0.0825
ada commented
Looks right. testTotal still passes, so this is good to go.
Checks
types check
1 test ran, 2 cached
no conflicts
Where its code runs
server · anywhere · browser
Merge
The registry

Find code from anyone, and check it.

Releases are announced on the AT Protocol. A registry finds them, checks each against its author's signed repository, and mirrors the code, re-hashing it as it arrives.

  • Search by name, text or type
  • Each library's API and versions, and what each bump changed
  • Public codebases too, with browsable code
  • Anyone can run one; none is in charge
tcm.codes/registry · or tcm registry for your own
tcm.codes/registry/search?q=money
moneySign in with Bluesky
Libraries3 found
@ada/money 1.4.0✓ signed
Exact currency arithmetic, formatting and rounding rules.
★ 42stars
anywherestableupdated 2 days ago
@kai/ledger 0.9.2✓ signed
Double-entry ledgers as typed tables, with jobs to reconcile.
★ 17stars
mostly serverbeta
@mo/price-tag 2.0.0✓ signed
React components for prices that update as rates change.
★ 9stars
mostly browser
Use it
tcm lib add https://ada.dev money@1.4.0
Latest
A ada released money 1.4.0
minor: 2 added, 0 changed
Versions
1.4.0 minor
1.3.2 patch
1.0.0 major
The studio

Your codebase, in your browser.

Edit code with live values beside it, and see how it really runs: traces by definition, from the browser to the SQL. It runs on your machine, and only there.

127.0.0.1:4747/code/shop/cartTotal
⎇ mainFind a definition /
Needs you 2CodeScratch
Running
TracesJobsDataTests
Work
Branches 3Releases
shop.cartTotalanywhere#w3ha9e
1export const cartTotal = (items: Item[]) =>
2 subtotal(items) * (1 + TAX_RATE)
3
4cartTotal([{ price: 100, qty: 2 }])
⧩ 216.5
GET /carta trace · 41 ms2 min ago
CartView41ms
→ getCart26ms
  select carts15ms
cartTotal0.2ms
agent/fix-rounding · 3 definitions · tests passMerge
agent/cart-coupons · working…Open
What needs youThings to fix, failing tests and merges waiting, first.
Live valuesWatch expressions show their values as you type.
How it runsTraces, jobs, tables and tests, by definition.
Your agentsReview and merge each agent's branch.

tcm studio

Get started

Try it in a minute.

Install tcm and start from a template, saved and ready to run. Then take the tutorial, or browse tcm's own code, which is a codebase too.

# get tcm (Node 22.18 or newer) curl -fsSLO https://tcm.codes/tcm-ts.tgz && npm i -g ./tcm-ts.tgz # a codebase, from a template mkdir shop && cd shop && tcm init react tcm dev app.App # open it # or work on it in your browser tcm studio
AppA web app in plain TypeScript and the DOM: a table, server functions, streaming.tcm init app
ReactServer components and browser components, with no directives, and a table.tcm init react
PackageAn npm package: a documented API with tests, ready to release and pack.tcm init package
PWAAn installable app that works offline, its data on the device, as a static site.tcm init pwa

Each is saved and ready to run; tcm init says what to run next. Plain tcm init makes an empty codebase.