Back to Tldraw

Product stable ids

internal/docs/product-stable-ids.md

5.3.27.5 KB
Original Source

Product stable ids

Every published package in packages/ declares which commercial component it belongs to via a tldraw_product field in its package.json. These stable ids are what legal and sales reference in order forms, so they must be unambiguous and durable: renaming, moving, or refactoring code never changes a stable id.

The shape of the taxonomy

The taxonomy has exactly two levels, matching the order form's "Product in Scope" section:

  • Products are the top-level line items — the checkboxes a customer ticks. Today: SDK and the Collaboration module.
  • Features are parts of a product that can be licensed on their own, listed indented beneath their parent. Commenting is a feature of the Collaboration module.
[ ] SDK
      [ ] Sync, Driver, Mermaid, Create tldraw CLI   (included, not separately licensed)
[ ] Collaboration module
      [ ] Commenting

A feature that isn't premium is included in its parent rather than separately licensable — it appears in the manifest so legal can see what a product contains, but it isn't its own checkbox.

Individual npm packages are not part of this taxonomy. A package declares the component it implements and inherits that component's entitlement; several packages can (and do) share one stable id. @tldraw/commenting, @tldraw/mentions, and @tldraw/sync-collaboration all implement tldraw:commenting, and none of them are order form line items.

Why package.json

  • npm publishes package.json verbatim and published versions are immutable, so every shipped version carries a permanent record of its stable id and premium status. "What did @tldraw/[email protected] claim to be" is answerable forever via the npm registry (for example https://registry.npmjs.org/@tldraw/commenting/latest).
  • The metadata travels with the package through renames and moves.
  • The release process already rewrites every package's package.json, so no separate manifest file can drift out of date.

The tldraw_product field

json
"tldraw_product": {
	"stableId": "tldraw:commenting",
	"name": "Commenting",
	"type": "feature",
	"parent": "tldraw:collaboration",
	"premium": true,
	"licenseFlag": "FEAT_COMMENTING"
}
  • stableId — immutable commercial identifier, tldraw:<kebab-case>. This is what contracts reference.
  • name — the customer-facing label, matching the wording on the order form. Unlike the stable id this may be reworded.
  • typeproduct (top-level line item) or feature (part of a parent).
  • parent — for features, the component they belong to.
  • premium — whether the component is separately licensed rather than part of the standard SDK offering.
  • licenseFlag — for premium components, the license key FLAGS bit in packages/editor/src/lib/license/LicenseManager.ts that entitles a customer to the component. This is the bridge between order forms and runtime enforcement: a license key whose flags include this bit grants the component.

Rules

  • A stable id never changes when code is renamed, moved, split, or refactored. If a package is renamed, the tldraw_product field moves with it unchanged.
  • A new stable id is minted only for a genuinely new commercial component — something sales would list on an order form. Major version bumps do not mint new ids; the order form grants "all versions made available during the Order Form Term", so version entitlement is handled by the term and the license key's expiry, never by the id.
  • Moving a package to a different component is the expensive operation, because customers have signed order forms citing the old one. When a package is likely to become separately licensable later, give it its own component up front.
  • All packages sharing a stable id must have identical metadata.
  • Every published package in packages/ must have the field. yarn check-packages (run in CI) enforces this, validates licenseFlag values against LicenseManager, checks that every parent resolves, and checks that packages pointing at LICENSE.md actually ship one.

Reference components by stable id, for example: "SDK (tldraw:sdk-core) and the Collaboration module (tldraw:collaboration), including Commenting (tldraw:commenting)".

  • yarn product-manifest prints the manifest as a JSON tree: products, their features, and the packages implementing each.
  • For any published version, the npm registry copy of each package's package.json is the immutable record.

Current components

Run yarn product-manifest for the authoritative list. As of the introduction of this convention:

Stable idNameParentPremiumPackages
tldraw:sdk-coreSDKnotldraw, @tldraw/tldraw, @tldraw/editor, @tldraw/assets, @tldraw/state, @tldraw/state-react, @tldraw/store, @tldraw/tlschema, @tldraw/utils, @tldraw/validate
tldraw:syncSynctldraw:sdk-coreno@tldraw/sync, @tldraw/sync-core
tldraw:driverDrivertldraw:sdk-coreno@tldraw/driver
tldraw:mermaidMermaidtldraw:sdk-coreno@tldraw/mermaid
tldraw:create-tldrawCreate tldraw CLItldraw:sdk-corenocreate-tldraw
tldraw:collaborationCollaboration moduleyes (FEAT_COLLABORATION)@tldraw/collaboration
tldraw:commentingCommentingtldraw:collaborationyes (FEAT_COMMENTING)@tldraw/commenting, @tldraw/mentions, @tldraw/sync-collaboration

Entitlement flows down the tree:

  • FEAT_COLLABORATION — the umbrella flag; grants the whole Collaboration module.
  • FEAT_COMMENTING — grants commenting.

@tldraw/mentions implements commenting today because @tldraw/commenting depends on it, so every customer entitled to commenting is entitled to mentions. If mentions later becomes usable on its own — in shape rich text, say — it will need its own component and license flag at that point.