@sorb/juice reference

When you finish this page you know every sorb CLI command and its options, every public HTTP route the bridge serves, the library's exports, and the MCP tools an AI agent gets from sorb mcp. You need Node 20 and a sorb.config.json (sorb init writes one). For what sorb dev actually does and why, read The bridge first — this page is the reference.

npm install -D @sorb/juice

@sorb/juice is a dev dependency — it runs the bridge on your machine (or a self-host you run yourself) and never ships to production.

CLI commands

Seven commands, dev is the default (sorb alone == sorb dev).

CommandDescription
sorb dev (default)Start the local token bridge server
sorb serveRun the hosted bridge server (config from env; no sorb.config.json)
sorb initCreate a sorb.config.json in the current directory
sorb commitOpen a GitHub PR with the current token file
sorb handshakeGenerate a shareable invite that auto-configures a designer's Figma plugin
sorb checkRe-run the token build and report evidence of drift, binding-mismatch, or deprecation; exits non-zero on findings (for CI)
sorb mcpRun the Sorb MCP server (stdio) — resolved tokens + component bindings for AI agents

sorb dev

Start the local token bridge server

OptionDescriptionDefault
-p, --port <port>port to listen on (overrides config)
--cloudserve the resolved token map from sorb-cloud HEAD instead of local .sorb/resolved.json
--cloud-url <url>sorb-cloud base URL (or env SORB_CLOUD_URL / CLOUD_API)
--cloud-set <id>cloud token-set id to serve (or env SORB_CLOUD_SET)
--cloud-key <key>API key for the cloud request (or env SORB_CLOUD_KEY)

sorb serve

Run the hosted bridge server (config from env; no sorb.config.json)

No options.

sorb init

Create a sorb.config.json in the current directory

No options.

sorb commit

Open a GitHub PR with the current token file

OptionDescriptionDefault
--owner <owner>GitHub org or user
--repo <repo>GitHub repo name
--pat <pat>GitHub personal access token
--message <message>PR / commit titleUpdate design tokens

sorb handshake

Generate a shareable invite that auto-configures a designer's Figma plugin

OptionDescriptionDefault
--app <url>App URL the preview opens (default: config.appUrl or http://localhost:5173)
--gh <url>GitHub edit URL of the tokens file (default: config.gh or from git remote)
--pk <key>Read-only sorb_pk_ for a hosted bridge (default: blank = keyless-local)
--origin <url>Bridge origin override (default: http://127.0.0.1:&lt;port from config>)
--exp <days>Days until the invite expires (default: 30; pass 0 for no expiry)
--copyCopy the paste code to the clipboard
--link-onlyPrint only the sorb:// link
--code-onlyPrint only the paste code

sorb check

Re-run the token build and report evidence of drift, binding-mismatch, or deprecation; exits non-zero on findings (for CI)

OptionDescriptionDefault
--resolved <path>resolved token snapshot to check.sorb/resolved.json
--baseline <path>previous resolved snapshot to diff against.sorb/baseline.json
--live <path>live-captured cssVar→value map to diff the build against
--computed <path>computed-styles map (property→value) for the hardcoded-value scan
--format <fmt>output format: text | jsontext
--no-buildskip the Style Dictionary rebuild; check the existing snapshot only

sorb mcp

Run the Sorb MCP server (stdio) — resolved tokens + component bindings for AI agents

OptionDescriptionDefault
--dir <path>project dir whose .sorb/ is servedprocess.cwd()
--bridge <url>live bridge base URL for gated write previews (e.g. http://localhost:7777)
--httprun the hosted HTTP/SSE MCP endpoint (auth + tenant scoping; requires DATABASE_URL)
-p, --port <port>port for the hosted --http endpoint7878
--base-dir <path>per-project snapshot root for hosted --http modeprocess.env.SORB_MCP_BASE_DIR || './.sorb-projects'
--gh-owner <owner>GitHub org or user for open_pr
--gh-repo <repo>GitHub repo name for open_pr
--gh-pat <pat>GitHub personal access token for open_pr
--gh-token-path <path>token file path open_pr writes totokens/component.json

HTTP routes

The bridge's public HTTP surface — everything except /enterprise/*. In local mode (no databaseUrl configured — the default for sorb dev) every route below is open, unauthenticated, and un-tenant-scoped, byte for byte what the free bridge has always done. In hosted mode (a self-host or Sorb Cloud with databaseUrl set) every route except GET /health and GET /ready requires a Bearer API key, and two routes additionally require a secret key rather than a read-only publishable one:

RouteHosted-mode key requirement
POST /verifySecret key only — publishable is rejected with 403 {error: 'Publishable keys are read-only', code: 'read_only'}.
POST /tokens/figmaSecret key only — same 403 as above.
Every other route below (including POST/PUT/DELETE /preview*)Any authenticated key, publishable or secret.

That second row is easy to misread from the preview routes' names: PUT and DELETE /preview/:id look like writes that should need a secret key, but the bridge deliberately accepts either scope there — the account-pairing front door mints a read-only publishable key for the Figma plugin, and previews are ephemeral and non-destructive (they never touch the committed token set), so gating them on secret would 403 the plugin's whole preview loop. The committed-token-set writes (POST /tokens/figma) and the plugin's geometry report (POST /verify) are the two routes that actually need secret.

MethodPathWhat it does
GET/orgs/:orgId/preview/:id/subscribeE1 — GET /orgs/:orgId/preview/:id/subscribe (SSE push) The leaf SDK subscribes here to receive live preview updates instead of polling GET /preview/:id.
POST/previewFigma plugin POSTs a proposed token set here.
GET/preview/latestThe project's most-recently-pushed live preview (#4b dependency — lets sorb-cloud snapshot "what's currently being previewed" without knowing an id up front).
GET/preview/:idReact app polls this while a preview is active.
PUT/preview/:idPlugin updates an existing preview in-place (live edit mode).
DELETE/preview/:idPlugin clears a preview when designer exits without committing.
POST/verifyPlugin posts the post-layout geometry of an inserted component so the canvas can be reconciled against the captured artifact.
GET/verify/latestThe most recently reported verification.
GET/verify/figmaFIGMA-vs-DTCG drift check: diffs the latest Figma Variables export (POST /tokens/figma) against the committed resolved map (GET /tokens/resolved), matched by cssVar.
GET/verify/:idA specific verification by id.
POST/verify/activityRecord that the current Figma user just ran a preview.
GET/verify/activityReturns the most recent other user's activity entry (exclude= self).
GET/tokens/latestReturns the latest committed tokens from disk.
GET/tokens/resolvedThe resolved bindable token map produced by Style Dictionary — one entry per token: { id, cssVar, value, tier, type }.
POST/verify/appRUNNING-APP token verification (roadmap §3 / e2e-fix W2): the app reports the values it actually resolved for a set of tokens (@sorb/leaf's verifyResolved reads them off :root), and the bridge diffs them against the committed resolved map.
POST/tokens/figmaThe Figma plugin (sorb-canopy "Export variables" action) POSTs its exported Variables here as a resolved-map artifact: { fileKey: string|null, exportedAt: string|null, tokens: [{id,cssVar,value,tier,type}] } Stored as the single latest Figma-side snapshot (no history) — GET /tokens/figma and GET /verify/figma read it back.
GET/tokens/figmaThe latest Figma Variables export, as posted by the plugin.
GET/artifactsThe captured-component index — list of components/stories with hashes, produced by sorb-seed capture.
GET/artifactLookup by id in the index — never accepts an arbitrary filesystem path.
GET/usagesReverse query over the binding graph: which captured components/roles render a token.
GET/blastLike /usages but expands the alias chain: editing a primitive reports the semantic/component tokens that alias it (same resolved value+type) AND their bound components.
POST/graft/planPlan a theme graft from one component onto another, reconciled BY ROLE (same role key; tie-break by TIER_RANK).
GET/healthInfra probe — open in ALL modes (no auth, no tenant scope).
GET/readyReadiness probe: 200 only when every configured backend is reachable.

GET /orgs/:orgId/preview/:id/subscribe

E1 — GET /orgs/:orgId/preview/:id/subscribe (SSE push) The leaf SDK subscribes here to receive live preview updates instead of polling GET /preview/:id. Ported/promoted from the relay-spike (experiments/sorb-bridge-modes/relay-spike). Registered FIRST — before the hosted header-auth + CORS middleware below — because the browser EventSource API cannot set request headers, so this route authenticates via a ?key= query param and must not be 401'd by the header middleware. Being registered first, its Response short-circuits the middleware chain. AUTH (hosted mode): ?key=<publishable-or-secret-key>. Missing/invalid → 401. Any tenant mismatch → 404 (never 403) so cross-tenant existence is never revealed — same posture as GET /preview/:id. LOCAL mode (no databaseUrl): OPEN, no key required, :orgId is ignored — byte-for-byte the free bridge's no-auth behavior. FRAME CONTRACT (each SSE frame's data: is a JSON object): - on connect: { type: 'snapshot', tokens } - on store change: { type: 'update', tokens } (put + update both → 'update') { type: 'delete', tokens: null } - idle keepalive: { type: 'ping' } every sseHeartbeatMs Frames use the default (message) SSE event so EventSource.onmessage receives them; each carries a monotonically increasing id: from 1.

POST /preview

Figma plugin POSTs a proposed token set here. Returns a short preview ID the React app can use via ?preview=<id> BODY SHAPE (phase-2 dark mode): EITHER - legacy flat — a plain map { "--token": "value", ... } (today's shape), OR - mode-aware — { tokens: {...}, darkTokens?: {...}, darkMode?: {...} }, a wrapper object carrying a tokens key (detection: presence of a tokens key on the body means mode-aware; a flat map never has one, since CSS custom-property names always start with --). The bridge stores + returns the body VERBATIM in either shape — it never inspects, flattens, or strips fields. Store + routes are shape-agnostic by construction (see src/store/memory.js / redis.js: tokens is opaque JSON to both), so this needed no store change. Detection/interpretation of the shape is entirely a leaf/cloud-side concern.

GET /preview/latest

The project's most-recently-pushed live preview (#4b dependency — lets sorb-cloud snapshot "what's currently being previewed" without knowing an id up front). MUST be registered before /preview/:id or Hono treats "latest" as an :id param. Read-only: allowed for BOTH publishable and secret scope in hosted mode (mirrors GET /preview/:id). Response contract (matched to the cloud agent's toResolved()): 200 { id, tokens } — tokens is exactly the shape POST/PUT /preview stored (whatever the plugin pushed), under a tokens field. 404 { error: 'no_preview' } — no current preview for the caller. DARK-MODE ENVELOPE SEAM (phase-2): the stored body may itself be either a legacy flat map ({ "--token": "value" }) or a mode-aware wrapper ({ tokens, darkTokens, darkMode }) — see server.js's POST /preview comment. This route does NOT special-case that: tokens here is always the WHOLE stored body, unmodified — for a mode-aware preview that means the response is { id, tokens: { tokens, darkTokens, darkMode } }, i.e. this route's outer tokens field is "the preview body", not "the light-mode token map". Chosen over introducing a new top-level field (e.g. preview) to keep this route's response shape byte-for-byte unchanged for legacy flat previews (today's only consumers). Downstream (leaf/cloud): apply the SAME mode-aware detection ('tokens' in body) to this route's tokens field that you'd apply to a GET /preview/:id body directly.

GET /preview/:id

React app polls this while a preview is active. Allowed for BOTH read (publishable) and write (secret) scope in hosted mode.

PUT /preview/:id

Plugin updates an existing preview in-place (live edit mode). WRITE op.

DELETE /preview/:id

Plugin clears a preview when designer exits without committing. WRITE op.

POST /verify

Plugin posts the post-layout geometry of an inserted component so the canvas can be reconciled against the captured artifact. Returns a short id. WRITE op. In hosted mode: a best-effort INSERT into verify_events is written after the store write so the beta can measure weekly active verify runs per project (moat-proof retention metric R-0607-16). Like preview bookkeeping, a failed insert does not fail the request.

GET /verify/latest

The most recently reported verification. MUST be registered before /verify/:id or Hono treats "latest" as an :id param.

GET /verify/figma

FIGMA-vs-DTCG drift check: diffs the latest Figma Variables export (POST /tokens/figma) against the committed resolved map (GET /tokens/resolved), matched by cssVar. This route only FLAGS drift — it never resolves it: the DTCG token source is truth, the Figma export is a mirror. Compares format-insensitively (lowercase + collapse whitespace), same as POST /verify/app. ok mirrors /verify/app's contract (mismatches-only); missingInFigma/extraInFigma are reported but don't themselves flip ok, since a partial/in-progress export is a normal transient state. MUST be registered before /verify/:id or Hono treats "figma" as :id.

POST /verify/activity

Record that the current Figma user just ran a preview. Non-critical — no auth gate (same as /health) since it carries no secrets (display name only).

GET /tokens/latest

Returns the latest committed tokens from disk. Plugin fetches this on open to pre-populate the editor.

GET /tokens/resolved

The resolved bindable token map produced by Style Dictionary — one entry per token: { id, cssVar, value, tier, type }. The plugin uses it to create grouped Variables and to bind captured values; capture annotates against it. 404 if it hasn't been built yet.

POST /verify/app

RUNNING-APP token verification (roadmap §3 / e2e-fix W2): the app reports the values it actually resolved for a set of tokens (@sorb/leaf's verifyResolved reads them off :root), and the bridge diffs them against the committed resolved map. "verified" = the LIVE app resolves to the bound token values — not the Figma-side bbox geometry that POST /verify records. Reads the same resolved map as GET /tokens/resolved and writes nothing; in hosted mode the global auth middleware still gates it (only /health + /ready are open). NOTE: getResolvedTokens() is a single global map (no tenant scope) — fine for the local bridge; revisit before any multi-tenant hosted rollout.

POST /tokens/figma

The Figma plugin (sorb-canopy "Export variables" action) POSTs its exported Variables here as a resolved-map artifact: { fileKey: string|null, exportedAt: string|null, tokens: [{id,cssVar,value,tier,type}] } Stored as the single latest Figma-side snapshot (no history) — GET /tokens/figma and GET /verify/figma read it back. WRITE op, same gate as /preview and /verify.

GET /tokens/figma

The latest Figma Variables export, as posted by the plugin. 404 until the plugin has exported at least once.

GET /artifacts

The captured-component index — list of components/stories with hashes, produced by sorb-seed capture. 404 until seed has run.

GET /usages

Reverse query over the binding graph: which captured components/roles render a token. READ-ONLY blast radius (roadmap §7 P1). 400 on missing/both params. An unknown token returns count:0 + components:[] (NOT a 404 — "rendered nowhere" is a valid answer). Response (pinned): { id, cssVar, count, components: [{storyId, role}] } - ?id= → id is the queried token id, cssVar resolved from the map. - ?cssVar= → id is the resolved token id (first match), cssVar echoes the queried var; when a cssVar maps to multiple ids the components UNION and count reflects the union.

GET /blast

Like /usages but expands the alias chain: editing a primitive reports the semantic/component tokens that alias it (same resolved value+type) AND their bound components. READ-ONLY (roadmap §7 P2). Response (pinned): { id, aliasGroup:[tokenId], count, components:[{storyId, role, via}] }

POST /graft/plan

Plan a theme graft from one component onto another, reconciled BY ROLE (same role key; tie-break by TIER_RANK). COMPUTE-ONLY: returns a changeset + conflict list, never writes (apply is canopy P4 via /preview). Body: { from, to, roles? } (from/to = storyId) Response (pinned): { from, to, changeset: [{role, sourceTokenId, targetTokenId}], conflicts: [{role, reason}] } A target role with no compatible token, or a type mismatch (e.g. color onto a dimension role), appears in conflicts — never silently dropped.

GET /ready

Readiness probe: 200 only when every configured backend is reachable. Open in ALL modes (no auth). The in-memory store always pings true; a null db (local mode) is "not configured" and skipped. Returns 503 with { ok:false, checks } when any configured backend is unreachable.

Library exports

For embedding the bridge yourself instead of running the CLI:

ExportKindDescriptionSource
createServervalueBuild the Hono bridge app.src/server.js:94
createStorefunctionCreate the appropriate Store for the given config.src/store/index.js:20
createMemoryStorefunctionCreate the in-memory Store.src/store/memory.js:28
loadConfigfunctionLoad the bridge configuration from env (defaults to process.env), applying the safe local defaults from DEFAULTS.src/config.js:103
createDbfunctionCreate the Postgres durable layer over a connection pool.src/db/index.js:23
watchTokenFilefunctionWatch a single legacy flat tokens file and call onChange with its parsed contents on every change; returns &#123; read, stop &#125;.src/watch.js:17
runStyleDictionaryfunctionRuns a Style Dictionary build after tokens change.src/transform.js:13
openTokenPRfunctionCreates a branch with the updated token file(s) and opens a PR.src/github.js:32

createServer

const createServer = (

Build the Hono bridge app. All preview/verify state lives in the injected store (see storeInterface) — this module keeps NO in-memory Maps and no prune timers (pruning now lives in the store). Id generation (nanoid(8)) stays here; the store never mints ids. ## Two modes — gated entirely on config.databaseUrl LOCAL mode (config.databaseUrl UNSET): behaves EXACTLY as the free local bridge always has — no auth, open CORS ('*' or CORS_ORIGINS), in-memory store, all routes open and un-tenant-scoped. Zero new behavior is registered. HOSTED mode (config.databaseUrl SET): the bridge reads sorb-cloud's shared Postgres for auth + entitlements: - scoped CORS sourced from the project's allowed_origins, - a Bearer API key is required on every route except /health + /ready, - publishable keys are read-only ONLY for POST /verify and POST /tokens/figma (403 read_only, via requireWrite) — every other route, including the /preview writes, accepts any authenticated key, since the account-pairing front door mints a read-only key for the Figma plugin and previews are ephemeral/non-destructive (requirePreviewWrite is a deliberate no-op; see its comment below), - entitlements are enforced (maxActivePreviews → 402, preview TTL, sharing-gated actions → 402), with past_due/canceled orgs degraded to Free.

ParameterTypeDescription
optionsObject
options.storeimport('./types').StoreRequired. A Store instance from src/store/index.js. All preview + verify handlers delegate to it (awaited).
options.configimport('./types').ConfigRequired. Config from src/config.js. Supplies namespace (surfaced in /health), corsOrigins, databaseUrl (the hosted-mode switch) and the TTL/prune windows the store honors.
[options.db]import('./types').DbHandle | nullOptional Postgres handle (or null in local mode). /ready pings it only when it is non-null. In hosted mode it is the source for auth + entitlements + the per-project active-preview count, and MUST be non-null.
[options.getLatestTokens]() => import('./types').TokenSetLatest committed tokens for /tokens/latest. Defaults to () => ({}).
[options.getResolvedTokens]() => (Array<{id:string,cssVar:string,value:string,tier:string,type:string}> | null)Resolved bindable token map (.sorb/resolved.json), or null. Defaults to () => null.
[options.getArtifactIndex]() => (object | null)Captured-component index (.sorb/index.json), or null. Defaults to () => null.
[options.getArtifact](storyId: string) => (object | null)Artifact JSON for a story id (looked up via the index — never a raw path), or null. Defaults to () => null.
[options.onError](err: unknown, context?: Record<string, unknown>) => voidOptional error sink for the DB-error / best-effort catch paths. Threaded in (dependency-injection style, mirroring db/store) so this module stays Sentry-import-free and fake-db testable. Defaults to a no-op; cli.js serve passes captureError from src/sentry.js (itself a no-op when SENTRY_DSN is unset). The test harness omits it, so capture stays a no-op under node --test.

Returns import('hono').Hono: the Hono app (synchronous — /ready does its async backend checks per-request).

createStore

const createStore = async (config)

Create the appropriate Store for the given config.

ParameterTypeDescription
configimport('../types').Config

Returns Promise<import('../types').Store>.

createMemoryStore

const createMemoryStore = (config)

Create the in-memory Store.

ParameterTypeDescription
configimport('../types').Config

Returns import('../types').Store.

loadConfig

const loadConfig = (env = process.env)

Load the bridge configuration from env (defaults to process.env), applying the safe local defaults from DEFAULTS. The result is deeply frozen so downstream units (store, db, server) can treat it as immutable. Local mode (no REDIS_URL / DATABASE_URL) yields: { redisUrl: undefined, databaseUrl: undefined, hosted: false, ... } which keeps the in-memory store + no-DB path active.

ParameterTypeDescription
[env]NodeJS.ProcessEnvEnvironment source. Defaults to process.env.

Returns import('./types').Config.

createDb

async function createDb(config)

Create the Postgres durable layer over a connection pool.

ParameterTypeDescription
configimport('../types.js').ConfigLoaded config (env + sorb.config.json).

Returns Promise<import('../types.js').DbHandle \| null>: A DbHandle when config.databaseUrl is set; null in local mode (no Postgres).

watchTokenFile

const watchTokenFile = (tokenPath, onChange)

Watch a single legacy flat tokens file and call onChange with its parsed contents on every change; returns &#123; read, stop &#125;.

ParameterTypeDescription
tokenPathstring
onChange(tokens: import('./types').TokenSet) => void

Returns Watcher.

runStyleDictionary

const runStyleDictionary = (configPath)

Runs a Style Dictionary build after tokens change. Only runs if a config file exists — non-fatal if it doesn't.

ParameterTypeDescription
configPathstring

Returns boolean.

openTokenPR

const openTokenPR = async (opts)

Creates a branch with the updated token file(s) and opens a PR. Called by the CLI when the designer hits "commit" in the Figma plugin.

ParameterTypeDescription
optsCommitOptions

Returns Promise<string>.

sorb mcp — Model Context Protocol server

sorb mcp runs an MCP server over stdio that exposes a project's resolved tokens and captured component bindings to an MCP-capable agent (Claude Code, Cursor, Copilot). It reads the local .sorb/ map — no sorb dev needed for the read tools.

Read tools: list_tokens, get_token, resolve_value, list_components, get_component_bindings, find_token_usages, find_components_by_token.

Gated write tools — proposal-only, never auto-apply: propose_token_change (a diff + blast-radius, plus a live preview when --bridge is set), rebind_component (proposes a binding change), open_pr (opens a token PR when GitHub credentials are configured). Each returns a proposal, a preview reference, or a PR URL — accepting or merging stays a human step.

{
  "mcpServers": {
    "sorb": { "command": "sorb", "args": ["mcp", "--dir", "/path/to/your/app"] }
  }
}

With no --bridge/--gh-* flags set, the read tools work fully and the write tools return a "config needed" message instead of failing silently. sorb mcp --http runs the hosted HTTP/SSE variant instead (auth + tenant scoping; requires DATABASE_URL) — see the CLI table above for its options.

Types

JSDoc typedefs. Import them in your own JSDoc with import('@sorb/juice').Name.

TokenSet

A flat map of token name → value.

type TokenSet = Object.<string, string | number>

Source: src/types.js:4

PreviewEntry

PropertyTypeDescription
tokensTokenSet
createdAtnumber

Source: src/types.js:9

SorbCliConfig

PropertyTypeDescription
namespacestringMatches the namespace in your SorbProvider config.
tokenSources (optional)string[]DTCG token source files to watch (re-runs Style Dictionary on change). Preferred over tokenPath for the 3-tier taxonomy.
tokenPath (optional)stringLegacy: path to a single flat tokens.json. If present, served at /tokens/latest; otherwise that endpoint derives a flat map from the SD-built .sorb/resolved.json. Also used as a fallback watch source.
styleDictionaryConfig (optional)stringOptional path to style-dictionary config — runs build on startup + token change.
port (optional)numberPort for the local server. Defaults to 7777.
appUrl (optional)stringOptional. The running app page the preview opens (e.g. http://localhost:5173). Read by sorb handshake to assemble an invite; blank/absent is fine. Additive, back-compatible — configs without it still work.
gh (optional)stringOptional. GitHub edit URL of the tokens file (the Open-PR target) baked into a handshake invite. When absent, sorb handshake derives it from git remote.
figmaFileKey (optional)stringOptional. The Figma file key this project's Variables live in. Informational only — surfaced by GET /verify/figma as configuredFileKey, never enforced. The SORB_FIGMA_FILE_KEY env var takes precedence when both are set.

Source: src/types.js:15

Config

12-factor runtime config produced by loadConfig() (src/config.js), merged with sorb.config.json. Shared contract between the config, store, db and server units.

PropertyTypeDescription
portnumberHTTP listen port (default 7777).
namespacestringTenant/app namespace surfaced in /health.
redisUrlstring | undefinedredis: URL. Presence switches the store factory to the Redis impl; undefined → in-memory store.
databaseUrlstring | undefinedpostgres: URL. Presence makes createDb build a Pool; undefined → no DB (local mode).
corsOriginsstring[] | '*'Allowed CORS origins, or '*' (open).
allowedWriteOrigins (optional)string[]Extra origins permitted to make cross-site WRITES to /preview* in LOCAL mode (on top of the built-in localhost/127.0.0.1 + Figma allowlist). Used by the P0.3b CSRF guard.
previewTtlMsnumberTTL for previews + verifications, in ms.
pruneIntervalMsnumberIn-memory prune interval, in ms.
sseHeartbeatMs (optional)numberInterval between ping frames on GET /preview/:id/events (E1). Defaults to 20_000 in server.js when unset — not currently sourced from env, just an injectable override for tests.
figmaFileKeystring | undefinedOptional Figma file key (SORB_FIGMA_FILE_KEY) this project's Variables live in. Informational only — surfaced by GET /verify/figma, never enforced.

Source: src/types.js:42

VerificationEntry

A stored verification entry (post-layout geometry self-reported by the Figma plugin), as returned by the store.

PropertyTypeDescription
storyIdstring
bboxobject
metaobject
createdAtnumber

Source: src/types.js:67

FigmaExportedToken

A single Figma-exported token, matching @sorb/core's ResolvedToken shape (the same one returned by GET /tokens/resolved).

PropertyTypeDescription
idstringDotted token id (e.g. "color.action.primary").
cssVarstringCSS custom-property name (e.g. "--color-action-primary").
valuestring
tier (optional)string"primitive" | "semantic" | "component", when known.
typestring

Source: src/types.js:77

FigmaArtifact

The latest Figma Variables export POSTed by the plugin (sorb-canopy) via POST /tokens/figma. Stored as a single "latest" snapshot per store instance (no history) — same persistence pattern as previews/verifications above.

PropertyTypeDescription
fileKeystring | nullFigma file key the export was taken from, or null.
exportedAtstring | nullISO timestamp the plugin recorded at export time, or null.
tokensFigmaExportedToken[]
receivedAtnumberServer-side receipt timestamp (Date.now()) — juice's own bookkeeping.

Source: src/types.js:88

Store

The async Store surface shared by the in-memory and Redis impls. The server only ever awaits these methods — it never reaches into internal maps. See the frozen storeInterface contract.

PropertyTypeDescription
putPreview(id: string, tokens: TokenSet) => Promise<void>
getPreview(id: string) => Promise<PreviewEntry | null>
hasPreview(id: string) => Promise<boolean>
updatePreview(id: string, tokens: TokenSet) => Promise<boolean>
deletePreview(id: string) => Promise<void>
countPreviews() => Promise<number>
getLatestPreview() => Promise<({id: string} & PreviewEntry) | null>The most-recently put/updated preview (LOCAL mode only — this pointer is process-global, not tenant-scoped; hosted mode's GET /preview/latest derives "latest" from the tenant-scoped previews DB table instead and never calls this). Backs the cloud-snapshot dependency (#4b).
putVerification(id: string, entry: { storyId: string, bbox: object, meta: object }) => Promise<void>
getVerification(id: string) => Promise<VerificationEntry | null>
getLatestVerification() => Promise<VerificationEntry | null>
countVerifications() => Promise<number>
putFigmaArtifact(artifact: FigmaArtifact) => Promise<void>
getFigmaArtifact() => Promise<FigmaArtifact | null>
onUpdate(id: string, listener: (evt: PreviewUpdateEvent) => void) => (() => void)Subscribe to put/update/delete events for one preview id. Returns an unsubscribe function. This is the push primitive the SSE route (E1, GET /preview/:id/events) is built on — it replaces the leaf SDK's poll loop. Both the memory and Redis stores implement it (EventEmitter bus vs. Redis pub/sub over a duplicated connection), so callers never know which backend is behind it.
ping() => Promise<boolean>
close() => Promise<void>

Source: src/types.js:99

PreviewUpdateEvent

An event pushed by Store.onUpdate when a preview is created, updated, or deleted. tokens is null for a delete event.

PropertyTypeDescription
type'put' | 'update' | 'delete'
tokensTokenSet | null

Source: src/types.js:132

BindingGraph

The in-memory binding-graph index built by buildBindingGraph (src/graph/index.js). READ/COMPUTE only — never mutates source. Lazily built + cached in server.js.

PropertyTypeDescription
tokenByIdMap<string, {id:string,cssVar:string,value:string|number,tier:string,type:string}>tokenId → ResolvedToken (the resolved bindable token).
idsByCssVarMap<string, string[]>cssVar → token id(s) that emit it (normally 1:1; union defensively).
usagesByTokenMap<string, Array<{storyId:string, role:string}>>tokenId → the (story, role) bindings that render it (the reverse edge set).
storiesMap<string, {storyId:string, component:string|undefined, name:string|undefined, bindings:Array<{role:string, tokenId:string}>}>storyId → flattened bindings across that story's whole LayerNode tree.

Source: src/types.js:140

DbHandle

The Postgres durable layer returned by createDb (src/db/index.js), or null in local mode when DATABASE_URL is unset.

PropertyTypeDescription
query(text: string, params?: any[]) => Promise<any>
getClient() => Promise<any>
ping() => Promise<boolean>
close() => Promise<void>
runMigrations() => Promise<void>

Source: src/types.js:154

Next

  • The bridge — what sorb dev does and why, in prose.
  • Hosted vs. local — which key type does what, and what stays on your machine.
  • @sorb/leaf — the SDK that talks to these routes from your running app.

Works with Figma. Not affiliated with, or endorsed by, Figma. Figma is a trademark of Figma, Inc.