Platform

Shared contracts

The @totlob/contracts-* packages are the single definition of every endpoint: its path, method, request schema and response schemas. The API implements them and the apps consume them, with no hand-written request types anywhere.


Packages

PackagePinned in api-v2Used byCovers
@totlob/contracts-merchant0.7.18api-v2, dashboard (0.7.18)Auth, shop, payment and product search for the dashboard
@totlob/contracts-pos0.1.14api-v2, POS (0.1.14 on origin/master)Everything under /pos/v1/*
@totlob/contracts-storefront0.1.8api-v2Merchant-run websites (/storefront/v1/*)
@totlob/contracts-internal0.1.1api-v2Internal tooling (/internal/*)
@totlob/contracts-accounting0.0.4api-v2, dashboardAccounting and reports services
@totlob/contracts-customer0.1.1api-v2 (installed, not mounted)Reserved for a customer audience

All packages come from GitHub Packages under the @totlob scope. Each repo's .npmrc reads ${NPM_TOKEN}, so npm install fails without it.

How they're consumed

In the API

Routes are lifted by reference into per-feature sub-routers:

const discountContract = c.router({
  getDiscounts: shopDashboardServiceContract.getDiscounts,
  createDiscount: shopDashboardServiceContract.createDiscount,
  // …
})

Because the API never redeclares a path or schema, the transport layer (src/api/<audience>/v1/*.contract.ts + *.router.ts) can't drift from what clients expect. accounting and reports lift whole service contracts from contracts-accounting.

In the apps

Both apps build @ts-rest/core clients from the contract objects with a custom fetcher. The fetcher handles the bearer token, a single shared refresh on 401, and safe JSON parsing. An unwrap() helper turns any non-200 response into an ApiError(message, status, code), which TanStack Query treats as an error.

The dashboard builds five clients: auth, shop, payment and product search from contracts-merchant, plus reports from contracts-accounting. The POS builds one client, from posServiceContract.

Release workflow

  1. Change the contract in its own repo and push to that repo's master. CI bumps the patch version and publishes it.
  2. Wait about a minute for the package to appear.
  3. Pin the exact version in api-v2 first, implement the endpoint, then pin it in the app(s).
  4. Before publishing, test against a locally overlaid dist (see api-v2/.claude/LOCAL-DEV.md).

Pin the minimum, not the latest

Later contract releases sometimes make fields required, which breaks branches that don't implement them yet. Pin the lowest version that has what your branch needs. npm 11 also adds spurious peer markers to the lockfile when you bump a pin; hand-edit the four lockfile fields instead.

Money on the wire

ContractMoney typeNotes
merchantnumberFormatted for display. Billing invoices are in cents.
posstringFixed-point decimal. The till parses it into bigint at scale 6.

Declared but not served

Some contract endpoints exist with no API implementation: merchant analytics, shop rates, billing address, label purchasing. The dashboard deliberately never calls them (merchant-dashboard-v2/src/api/hooks.ts:118-131) and computes the Overview figures in the browser instead.

Previous
Architecture overview