DO2 — Token Pipeline
Scope: How design tokens flow from Figma → Style Dictionary → JSON → Tailwind preset →
theme-config-servicedefaults 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:
| Category | Examples |
|---|---|
color | color.surface.default, color.primary.default, color.error.text |
typography | typography.size.base, typography.weight.semibold, typography.leading.normal |
spacing | spacing.1 (4px), spacing.4 (16px), spacing.16 (64px) |
radius | radius.none, radius.sm, radius.md, radius.lg, radius.full |
shadow | shadow.sm, shadow.md, shadow.lg, shadow.xl |
motion | motion.duration.fast (150ms), motion.duration.base (300ms), motion.easing.standard |
z | z.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 tokensGET /api/themes/{tenant_id}→ merged tenant tokens (base + overrides)PUT /api/themes/{tenant_id}/overrides→ update tenant overrides (validated againsttenant-overridable.json)
SSR token injection flow:
- Next.js middleware reads
tenant_idfrom hostname - BFF calls
theme-config-serviceGET /api/themes/{tenant_id} - BFF serializes token map as CSS custom properties string
- 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
- Designer adds token to Figma
Foundationsfile in the correct section - Figma Tokens Plugin exports updated
tokens.json - PR opens to
packages/design-tokens/src/tokens.json - CI validates: all existing references still resolve; no orphan tokens
- If new token is tenant-overridable, it must also be added to
tenant-overridable.json - PR reviewed by FE Platform lead + Design lead
7.2 Deprecating a token
- Mark token as deprecated in
tokens.json:"deprecated": true, "replacedBy": "color.surface.alt" - CI lint rule (
no-deprecated-token) warns on all usages - 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).