Skip to main content

DO2 — Token Pipeline

Scope: How design tokens flow from Figma → Style Dictionary → JSON → Tailwind preset → theme-config-service defaults and tenant overrides. Single source of truth for token definitions.


1. Overview

Figma (Foundations file)
↓ Figma Tokens Plugin (export JSON)
packages/design-tokens/src/tokens.json
↓ Style Dictionary (build step)

├── packages/design-tokens/dist/css/tokens.css ← CSS custom properties
├── packages/design-tokens/dist/js/tokens.js ← JS object (Tailwind preset)
├── packages/design-tokens/dist/json/tokens.json ← Raw JSON (for theme-config-service)
└── packages/design-tokens/dist/ios/tokens.swift ← iOS Swift constants (R2)

├── apps/web-*/tailwind.config.ts ← extends @ghasi/tailwind-preset
├── apps/desktop-*/tailwind.config.ts
├── apps/mobile-*/theme/tokens.ts ← React Native StyleSheet tokens
└── services/theme-config-service/ ← platform defaults seed

2. Token schema and naming conventions

2.1 Naming structure

{category}.{role}.{variant}.{state} (dot-separated in JSON; --{category}-{role}-{variant}-{state} in CSS)

Category taxonomy:

CategoryExamples
colorcolor.surface.default, color.primary.default, color.error.text
typographytypography.size.base, typography.weight.semibold, typography.leading.normal
spacingspacing.1 (4px), spacing.4 (16px), spacing.16 (64px)
radiusradius.none, radius.sm, radius.md, radius.lg, radius.full
shadowshadow.sm, shadow.md, shadow.lg, shadow.xl
motionmotion.duration.fast (150ms), motion.duration.base (300ms), motion.easing.standard
zz.modal (50), z.toast (60), z.drawer (45)

2.2 Semantic vs primitive tokens

Primitive tokens (in color/primitives): raw values — color.blue.500: #3b82f6
Semantic tokens (in color/semantic): reference primitives — color.primary.default: {color.blue.500}

Only semantic tokens should be used in component code. Primitive tokens are intermediate values only — they never appear in component CSS or component props.

2.3 Tenant override schema

Tenant tokens extend the platform semantic token set. A tenant may override a curated subset:

{
"tenant_overrides": {
"color.primary.default": "#c8102e",
"color.primary.hover": "#a50d26",
"color.surface.default": "#ffffff",
"typography.fontFamily.base": "'Estedad', sans-serif",
"radius.md": "12px"
}
}

Allowed override keys are defined in packages/design-tokens/src/tenant-overridable.json. Keys not in that list are rejected by theme-config-service validation.


3. Style Dictionary build

Config file: packages/design-tokens/style-dictionary.config.js

module.exports = {
source: ['src/tokens.json'],
platforms: {
css: {
transformGroup: 'css',
prefix: 'mel',
buildPath: 'dist/css/',
files: [{ destination: 'tokens.css', format: 'css/variables' }],
},
js: {
transformGroup: 'js',
buildPath: 'dist/js/',
files: [{ destination: 'tokens.js', format: 'javascript/es6' }],
},
json: {
buildPath: 'dist/json/',
files: [{ destination: 'tokens.json', format: 'json/flat' }],
},
},
};

Build command: pnpm --filter @ghasi/design-tokens build
Watch mode: pnpm --filter @ghasi/design-tokens build:watch (used during design iteration)
CI validation: pnpm --filter @ghasi/design-tokens validate — ensures all semantic token references resolve; fails if a primitive is referenced directly in a component.


4. Tailwind preset

packages/design-tokens/src/tailwind-preset.ts exports a Tailwind config preset that:

export const melmastoonPreset = {
theme: {
colors: {
primary: {
DEFAULT: 'var(--mel-color-primary-default)',
hover: 'var(--mel-color-primary-hover)',
// ...
},
surface: {
DEFAULT: 'var(--mel-color-surface-default)',
alt: 'var(--mel-color-surface-alt)',
},
// ... all semantic color tokens
},
spacing: {
'0': '0',
'1': 'var(--mel-spacing-1)', // 4px
'2': 'var(--mel-spacing-2)', // 8px
'4': 'var(--mel-spacing-4)', // 16px
// ... up to spacing-64 (256px)
},
borderRadius: {
none: 'var(--mel-radius-none)',
sm: 'var(--mel-radius-sm)',
DEFAULT: 'var(--mel-radius-md)',
lg: 'var(--mel-radius-lg)',
full: 'var(--mel-radius-full)',
},
// ... typography, shadows, transitionDuration, transitionTimingFunction
},
};

Apps consume via tailwind.config.ts:

import { melmastoonPreset } from '@ghasi/design-tokens/tailwind-preset';
export default { presets: [melmastoonPreset] };

Tenant theming at runtime: Tailwind classes reference CSS custom properties. Tenant tokens overwrite the custom properties in <style id="theme-tokens"> at SSR time. Tailwind classes remain unchanged — the CSS variable values change. This is why all Tailwind color/spacing tokens MUST use var(--mel-*) references, never hardcoded hex.


5. React Native token consumption

React Native does not use CSS. Mobile apps consume tokens from packages/design-tokens/dist/js/tokens.js:

// packages/mobile-tokens/src/index.ts
import tokens from '@ghasi/design-tokens/dist/js/tokens';

export const colors = {
primary: tokens['color.primary.default'],
surface: tokens['color.surface.default'],
// ...
};

export const spacing = {
1: tokens['spacing.1'], // 4
4: tokens['spacing.4'], // 16
// ...
};

Tenant theming on mobile (React Native): ThemeProvider from @ghasi/mobile-theme receives a tenantTokens prop (fetched from BFF on login) and merges with base tokens. Components use useTheme() hook to read token values.


6. theme-config-service integration

theme-config-service stores the platform default token set (seeded from dist/json/tokens.json) and per-tenant overrides. The service exposes:

  • GET /api/themes/defaults → base platform tokens
  • GET /api/themes/{tenant_id} → merged tenant tokens (base + overrides)
  • PUT /api/themes/{tenant_id}/overrides → update tenant overrides (validated against tenant-overridable.json)

SSR token injection flow:

  1. Next.js middleware reads tenant_id from hostname
  2. BFF calls theme-config-service GET /api/themes/{tenant_id}
  3. BFF serializes token map as CSS custom properties string
  4. Injected in <style id="theme-tokens"> before <body> in SSR response

Cache: Tenant token responses are cached in Memorystore Redis with a 5-minute TTL. Publish event from theme-config-service triggers cache invalidation via Pub/Sub theme.published event.


7. Token governance

7.1 Adding a new token

  1. Designer adds token to Figma Foundations file in the correct section
  2. Figma Tokens Plugin exports updated tokens.json
  3. PR opens to packages/design-tokens/src/tokens.json
  4. CI validates: all existing references still resolve; no orphan tokens
  5. If new token is tenant-overridable, it must also be added to tenant-overridable.json
  6. PR reviewed by FE Platform lead + Design lead

7.2 Deprecating a token

  1. Mark token as deprecated in tokens.json: "deprecated": true, "replacedBy": "color.surface.alt"
  2. CI lint rule (no-deprecated-token) warns on all usages
  3. After one release cycle: remove token; all usages must have migrated

7.3 Stability guarantees

Platform tokens (not tenant-overridable) are stable within a major version. Breaking changes require a major version bump and a migration script.
Tenant-overridable tokens are stable within a minor version. New overridable tokens are additive (non-breaking).


References