Back to Blog

Frontend as a Platform: When Your Team Becomes an Internal Infrastructure Team

At some point, your frontend team stops building features and starts building the tools that let other teams build features. You become infrastructure. Your users aren't customers—they're other engineers. Your product isn't the app—it's the design system, the CLI, the component library, the build tooling.

This transition is jarring. The skills that made you a great product engineer—shipping fast, iterating on user feedback, moving on to the next feature—become liabilities. Platform engineering rewards different things: stability, documentation, backwards compatibility, and saying "no" more than you say "yes."

This guide covers building and operating a frontend platform team.


The Platform Team Mandate

┌─────────────────────────────────────────────────────────────────────┐
│                    PRODUCT TEAM vs PLATFORM TEAM                    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   PRODUCT TEAM                     PLATFORM TEAM                    │
│   ════════════                     ═════════════                    │
│                                                                     │
│   Users: Customers                 Users: Engineers                 │
│   Success: Feature adoption        Success: Developer productivity  │
│   Metric: User engagement          Metric: Time to ship             │
│   Pace: Fast iteration             Pace: Deliberate, stable         │
│   Failure: Bad UX (recoverable)    Failure: Breaking change (costly)│
│                                                                     │
│   VALUES                                                            │
│   ──────                                                            │
│   • Ship fast                      • Ship right                     │
│   • Experiment                     • Standardize                    │
│   • Optimize for now               • Optimize for scale             │
│   • Break things, learn            • Don't break things             │
│   • Direct user feedback           • Engineer feedback (filtered)   │
│                                                                     │
│   OUTPUTS                                                           │
│   ───────                                                           │
│   • Features                       • Design system                  │
│   • Pages                          • Component library              │
│   • User flows                     • CLI tooling                    │
│   • Experiments                    • Build infrastructure           │
│   • Analytics                      • Templates & scaffolding        │
│                                                                     │
│   SUPPORT MODEL                                                     │
│   ─────────────                                                     │
│   • Owns the feature               • Enables feature teams          │
│   • Fixes own bugs                 • Responds to bug reports        │
│   • No SLA needed                  • SLA with consumers             │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

The Platform Stack

A mature frontend platform provides multiple layers:

┌─────────────────────────────────────────────────────────────────────┐
│                    FRONTEND PLATFORM STACK                          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   ┌─────────────────────────────────────────────────────────────┐  │
│   │  Layer 5: GOVERNANCE                                        │  │
│   │  • Contribution guidelines                                  │  │
│   │  • RFC process                                              │  │
│   │  • Architecture decision records                            │  │
│   │  • Deprecation policies                                     │  │
│   └─────────────────────────────────────────────────────────────┘  │
│                           ▲                                         │
│   ┌─────────────────────────────────────────────────────────────┐  │
│   │  Layer 4: DEVELOPER EXPERIENCE                              │  │
│   │  • CLI tools (scaffolding, generators)                      │  │
│   │  • IDE extensions                                           │  │
│   │  • Documentation site                                       │  │
│   │  • Storybook / Component playground                         │  │
│   └─────────────────────────────────────────────────────────────┘  │
│                           ▲                                         │
│   ┌─────────────────────────────────────────────────────────────┐  │
│   │  Layer 3: DESIGN SYSTEM                                     │  │
│   │  • Component library                                        │  │
│   │  • Design tokens                                            │  │
│   │  • Icons & assets                                           │  │
│   │  • Layout primitives                                        │  │
│   └─────────────────────────────────────────────────────────────┘  │
│                           ▲                                         │
│   ┌─────────────────────────────────────────────────────────────┐  │
│   │  Layer 2: SHARED LIBRARIES                                  │  │
│   │  • Data fetching (API clients)                              │  │
│   │  • State management utilities                               │  │
│   │  • Auth / session handling                                  │  │
│   │  • Analytics / logging                                      │  │
│   └─────────────────────────────────────────────────────────────┘  │
│                           ▲                                         │
│   ┌─────────────────────────────────────────────────────────────┐  │
│   │  Layer 1: BUILD INFRASTRUCTURE                              │  │
│   │  • Bundler configuration                                    │  │
│   │  • CI/CD pipelines                                          │  │
│   │  • Testing infrastructure                                   │  │
│   │  • Linting / formatting                                     │  │
│   └─────────────────────────────────────────────────────────────┘  │
│                                                                     │
│   Feature teams build on top of this stack                          │
│   Platform team maintains all layers                                │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Design Systems at Scale

Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                    DESIGN SYSTEM ARCHITECTURE                       │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   packages/                                                         │
│   ├── tokens/                  ← Design tokens (foundation)         │
│   │   ├── colors.json                                               │
│   │   ├── spacing.json                                              │
│   │   ├── typography.json                                           │
│   │   └── build/               Generated CSS vars, TS constants     │
│   │                                                                 │
│   ├── icons/                   ← Icon library                       │
│   │   ├── svg/                                                      │
│   │   ├── react/               Generated React components           │
│   │   └── sprite.svg           Combined sprite sheet                │
│   │                                                                 │
│   ├── primitives/              ← Unstyled, accessible primitives    │
│   │   ├── Button/                                                   │
│   │   ├── Dialog/                                                   │
│   │   ├── Select/                                                   │
│   │   └── ...                  Built on Radix/HeadlessUI            │
│   │                                                                 │
│   ├── components/              ← Styled, opinionated components     │
│   │   ├── Button/                                                   │
│   │   │   ├── Button.tsx                                            │
│   │   │   ├── Button.styles.ts                                      │
│   │   │   ├── Button.test.tsx                                       │
│   │   │   ├── Button.stories.tsx                                    │
│   │   │   └── index.ts                                              │
│   │   └── ...                                                       │
│   │                                                                 │
│   ├── patterns/                ← Composed patterns                  │
│   │   ├── DataTable/           Multi-component compositions         │
│   │   ├── FormField/                                                │
│   │   └── PageLayout/                                               │
│   │                                                                 │
│   └── theme/                   ← Theme provider & utilities         │
│       ├── ThemeProvider.tsx                                         │
│       ├── useTheme.ts                                               │
│       └── themes/                                                   │
│           ├── light.ts                                              │
│           └── dark.ts                                               │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Component API Design Principles

// packages/components/Button/Button.tsx

/**
 * PRINCIPLE 1: Props should be explicit, not magical
 */

// ❌ BAD: Magic props that do multiple things
interface BadButtonProps {
  type?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'link';
  // What does 'link' mean? Different styling AND behavior?
}

// ✅ GOOD: Separate concerns
interface ButtonProps {
  /** Visual style variant */
  variant?: 'solid' | 'outline' | 'ghost';

  /** Color scheme */
  colorScheme?: 'primary' | 'neutral' | 'danger';

  /** Render as a different element (for links styled as buttons) */
  as?: 'button' | 'a';
}

/**
 * PRINCIPLE 2: Composition over configuration
 */

// ❌ BAD: Props for every possible configuration
interface BadButtonProps {
  leftIcon?: ReactNode;
  rightIcon?: ReactNode;
  isLoading?: boolean;
  loadingText?: string;
  loadingSpinnerPosition?: 'left' | 'right';
}

// ✅ GOOD: Composable children
interface ButtonProps {
  children: ReactNode;
  isLoading?: boolean;
}

// Usage:
<Button>
  <Icon name="download" />
  Download
</Button>

<Button isLoading>
  <Spinner />
  Downloading...
</Button>

/**
 * PRINCIPLE 3: Sensible defaults, full override capability
 */

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'solid' | 'outline' | 'ghost';
  colorScheme?: 'primary' | 'neutral' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  isLoading?: boolean;
  // All native button props available via spread
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  (
    {
      variant = 'solid',
      colorScheme = 'primary',
      size = 'md',
      isLoading = false,
      disabled,
      className,
      children,
      ...props // Native props pass through
    },
    ref
  ) => {
    return (
      <button
        ref={ref}
        className={cn(
          buttonStyles({ variant, colorScheme, size }),
          className // User can add classes
        )}
        disabled={disabled || isLoading}
        {...props}
      >
        {isLoading ? <Spinner size={size} /> : children}
      </button>
    );
  }
);

/**
 * PRINCIPLE 4: Accessible by default
 */

// Component handles ARIA automatically
export const Dialog = ({ open, onClose, title, children }: DialogProps) => {
  return (
    <RadixDialog.Root open={open} onOpenChange={onClose}>
      <RadixDialog.Portal>
        <RadixDialog.Overlay className={overlayStyles} />
        <RadixDialog.Content
          className={contentStyles}
          aria-describedby={undefined} // Only if no description
        >
          <RadixDialog.Title className={titleStyles}>
            {title}
          </RadixDialog.Title>
          {children}
          <RadixDialog.Close asChild>
            <IconButton
              aria-label="Close dialog"
              icon={<CloseIcon />}
              className={closeButtonStyles}
            />
          </RadixDialog.Close>
        </RadixDialog.Content>
      </RadixDialog.Portal>
    </RadixDialog.Root>
  );
};

Design Tokens Pipeline

// packages/tokens/build.ts

import StyleDictionary from 'style-dictionary';

// Source tokens (design tool export or manual)
const tokens = {
  color: {
    primary: {
      50: { value: '#eff6ff' },
      100: { value: '#dbeafe' },
      500: { value: '#3b82f6' },
      600: { value: '#2563eb' },
      900: { value: '#1e3a8a' },
    },
    neutral: {
      0: { value: '#ffffff' },
      50: { value: '#f9fafb' },
      900: { value: '#111827' },
      1000: { value: '#000000' },
    },
  },
  spacing: {
    0: { value: '0' },
    1: { value: '0.25rem' },
    2: { value: '0.5rem' },
    4: { value: '1rem' },
    8: { value: '2rem' },
  },
  typography: {
    fontFamily: {
      sans: { value: 'Inter, system-ui, sans-serif' },
      mono: { value: 'JetBrains Mono, monospace' },
    },
    fontSize: {
      sm: { value: '0.875rem' },
      base: { value: '1rem' },
      lg: { value: '1.125rem' },
      xl: { value: '1.25rem' },
    },
  },
  radius: {
    none: { value: '0' },
    sm: { value: '0.25rem' },
    md: { value: '0.375rem' },
    lg: { value: '0.5rem' },
    full: { value: '9999px' },
  },
};

// Build to multiple formats
StyleDictionary.extend({
  source: ['tokens/**/*.json'],
  platforms: {
    // CSS Custom Properties
    css: {
      transformGroup: 'css',
      buildPath: 'build/css/',
      files: [
        {
          destination: 'tokens.css',
          format: 'css/variables',
        },
      ],
    },

    // TypeScript constants
    ts: {
      transformGroup: 'js',
      buildPath: 'build/ts/',
      files: [
        {
          destination: 'tokens.ts',
          format: 'javascript/es6',
        },
      ],
    },

    // Tailwind config
    tailwind: {
      transformGroup: 'js',
      buildPath: 'build/tailwind/',
      files: [
        {
          destination: 'tailwind.config.js',
          format: 'tailwind/config',
        },
      ],
    },

    // Figma (for design tool sync)
    figma: {
      transformGroup: 'js',
      buildPath: 'build/figma/',
      files: [
        {
          destination: 'figma-tokens.json',
          format: 'figma/tokens',
        },
      ],
    },
  },
}).buildAllPlatforms();

// Output: build/css/tokens.css
/*
:root {
  --color-primary-50: #eff6ff;
  --color-primary-500: #3b82f6;
  --spacing-1: 0.25rem;
  --spacing-4: 1rem;
  --font-family-sans: Inter, system-ui, sans-serif;
  --radius-md: 0.375rem;
}
*/

// Output: build/ts/tokens.ts
/*
export const colorPrimary50 = '#eff6ff';
export const colorPrimary500 = '#3b82f6';
export const spacing1 = '0.25rem';
export const spacing4 = '1rem';
*/

CLI Tooling and Scaffolding

CLI Architecture

// packages/cli/src/index.ts

import { Command } from 'commander';
import { scaffold } from './commands/scaffold';
import { generate } from './commands/generate';
import { lint } from './commands/lint';
import { upgrade } from './commands/upgrade';

const program = new Command();

program
  .name('platform')
  .description('Frontend Platform CLI')
  .version('1.0.0');

program
  .command('scaffold <type>')
  .description('Scaffold a new project or feature')
  .option('-n, --name <name>', 'Name of the project/feature')
  .option('-t, --template <template>', 'Template to use')
  .option('--dry-run', 'Show what would be created')
  .action(scaffold);

program
  .command('generate <type>')
  .alias('g')
  .description('Generate component, hook, or service')
  .option('-n, --name <name>', 'Name')
  .option('-p, --path <path>', 'Path to generate in')
  .action(generate);

program
  .command('lint')
  .description('Run platform linting rules')
  .option('--fix', 'Automatically fix issues')
  .action(lint);

program
  .command('upgrade')
  .description('Upgrade platform packages')
  .option('--check', 'Check for updates without applying')
  .action(upgrade);

program.parse();

Scaffolding Templates

// packages/cli/src/commands/scaffold.ts

import { mkdir, writeFile } from 'fs/promises';
import { join } from 'path';
import Handlebars from 'handlebars';
import ora from 'ora';
import prompts from 'prompts';

interface ScaffoldOptions {
  name?: string;
  template?: string;
  dryRun?: boolean;
}

const templates = {
  'feature': {
    description: 'Full feature module with components, hooks, and API',
    files: [
      { path: '{{name}}/index.ts', template: 'feature/index.ts.hbs' },
      { path: '{{name}}/components/index.ts', template: 'feature/components-index.ts.hbs' },
      { path: '{{name}}/components/{{pascalName}}View.tsx', template: 'feature/view.tsx.hbs' },
      { path: '{{name}}/hooks/index.ts', template: 'feature/hooks-index.ts.hbs' },
      { path: '{{name}}/hooks/use{{pascalName}}.ts', template: 'feature/hook.ts.hbs' },
      { path: '{{name}}/api/index.ts', template: 'feature/api.ts.hbs' },
      { path: '{{name}}/types.ts', template: 'feature/types.ts.hbs' },
    ],
  },
  'component': {
    description: 'Single component with tests and stories',
    files: [
      { path: '{{pascalName}}/index.ts', template: 'component/index.ts.hbs' },
      { path: '{{pascalName}}/{{pascalName}}.tsx', template: 'component/component.tsx.hbs' },
      { path: '{{pascalName}}/{{pascalName}}.test.tsx', template: 'component/test.tsx.hbs' },
      { path: '{{pascalName}}/{{pascalName}}.stories.tsx', template: 'component/stories.tsx.hbs' },
    ],
  },
  'app': {
    description: 'New Next.js application',
    files: [
      // Full app structure
    ],
  },
};

export async function scaffold(type: string, options: ScaffoldOptions) {
  const template = templates[type as keyof typeof templates];

  if (!template) {
    console.error(`Unknown template type: ${type}`);
    console.log('Available types:', Object.keys(templates).join(', '));
    process.exit(1);
  }

  // Interactive prompts if options not provided
  const answers = await prompts([
    {
      type: options.name ? null : 'text',
      name: 'name',
      message: `What is the ${type} name?`,
      validate: (value) => /^[a-z][a-z0-9-]*$/.test(value) || 'Use lowercase with dashes',
    },
  ]);

  const name = options.name || answers.name;
  const pascalName = toPascalCase(name);

  const context = {
    name,
    pascalName,
    camelName: toCamelCase(name),
    kebabName: name,
    year: new Date().getFullYear(),
  };

  const spinner = ora('Generating files...').start();

  for (const file of template.files) {
    const filePath = Handlebars.compile(file.path)(context);
    const templateContent = await loadTemplate(file.template);
    const content = Handlebars.compile(templateContent)(context);

    if (options.dryRun) {
      console.log(`Would create: ${filePath}`);
      continue;
    }

    const fullPath = join(process.cwd(), filePath);
    await mkdir(join(fullPath, '..'), { recursive: true });
    await writeFile(fullPath, content);
    spinner.text = `Created ${filePath}`;
  }

  spinner.succeed(`Scaffolded ${type}: ${name}`);

  // Post-scaffold instructions
  console.log('\nNext steps:');
  console.log(`  cd ${name}`);
  console.log('  npm install');
  console.log('  npm run dev');
}

// Template example: feature/view.tsx.hbs
/*
import { FC } from 'react';
import { use{{pascalName}} } from '../hooks';
import type { {{pascalName}}Props } from '../types';

export const {{pascalName}}View: FC<{{pascalName}}Props> = ({ id }) => {
  const { data, isLoading, error } = use{{pascalName}}(id);

  if (isLoading) {
    return <Skeleton />;
  }

  if (error) {
    return <ErrorMessage error={error} />;
  }

  return (
    <div>
      {/* {{pascalName}} content */}
    </div>
  );
};
*/

Code Generation

// packages/cli/src/commands/generate.ts

import { Project, SyntaxKind } from 'ts-morph';

interface GenerateOptions {
  name: string;
  path?: string;
}

export async function generate(type: string, options: GenerateOptions) {
  switch (type) {
    case 'component':
      await generateComponent(options);
      break;
    case 'hook':
      await generateHook(options);
      break;
    case 'api':
      await generateApiClient(options);
      break;
  }
}

async function generateComponent(options: GenerateOptions) {
  const project = new Project();

  const componentName = toPascalCase(options.name);
  const path = options.path || 'src/components';

  // Generate component file
  const componentFile = project.createSourceFile(
    `${path}/${componentName}/${componentName}.tsx`,
    `
import { forwardRef } from 'react';
import { cn } from '@company/utils';
import type { ${componentName}Props } from './types';
import styles from './${componentName}.module.css';

export const ${componentName} = forwardRef<HTMLDivElement, ${componentName}Props>(
  ({ className, children, ...props }, ref) => {
    return (
      <div
        ref={ref}
        className={cn(styles.root, className)}
        {...props}
      >
        {children}
      </div>
    );
  }
);

${componentName}.displayName = '${componentName}';
    `.trim()
  );

  // Generate types file
  const typesFile = project.createSourceFile(
    `${path}/${componentName}/types.ts`,
    `
import type { HTMLAttributes, ReactNode } from 'react';

export interface ${componentName}Props extends HTMLAttributes<HTMLDivElement> {
  children?: ReactNode;
}
    `.trim()
  );

  // Generate index file
  const indexFile = project.createSourceFile(
    `${path}/${componentName}/index.ts`,
    `
export { ${componentName} } from './${componentName}';
export type { ${componentName}Props } from './types';
    `.trim()
  );

  // Generate test file
  const testFile = project.createSourceFile(
    `${path}/${componentName}/${componentName}.test.tsx`,
    `
import { render, screen } from '@testing-library/react';
import { ${componentName} } from './${componentName}';

describe('${componentName}', () => {
  it('renders children', () => {
    render(<${componentName}>Content</${componentName}>);
    expect(screen.getByText('Content')).toBeInTheDocument();
  });

  it('forwards ref', () => {
    const ref = { current: null };
    render(<${componentName} ref={ref}>Content</${componentName}>);
    expect(ref.current).toBeInstanceOf(HTMLDivElement);
  });

  it('applies custom className', () => {
    render(<${componentName} className="custom">Content</${componentName}>);
    expect(screen.getByText('Content')).toHaveClass('custom');
  });
});
    `.trim()
  );

  await project.save();

  console.log(`Generated component: ${componentName}`);
  console.log(`  ${path}/${componentName}/${componentName}.tsx`);
  console.log(`  ${path}/${componentName}/types.ts`);
  console.log(`  ${path}/${componentName}/index.ts`);
  console.log(`  ${path}/${componentName}/${componentName}.test.tsx`);
}

Documentation as Product

Documentation Site Architecture

// Documentation structure
docs/
├── app/
│   ├── page.tsx                    // Landing page
│   ├── getting-started/
│   │   ├── page.mdx                // Getting started guide
│   │   └── installation/
│   │       └── page.mdx
│   ├── components/
│   │   ├── page.tsx                // Component index
│   │   └── [component]/
│   │       └── page.tsx            // Individual component docs
│   ├── patterns/
│   │   └── page.mdx
│   ├── tokens/
│   │   └── page.tsx                // Token browser
│   └── api/
│       └── page.tsx                // API reference
├── components/
│   ├── ComponentDoc.tsx            // Component documentation layout
│   ├── PropsTable.tsx              // Auto-generated props table
│   ├── CodeBlock.tsx               // Syntax-highlighted code
│   └── LiveEditor.tsx              // Interactive playground
└── lib/
    ├── getComponentDocs.ts         // Extract docs from source
    └── getPropsFromSource.ts       // Parse TypeScript for props

// packages/docs/lib/getPropsFromSource.ts
import { Project, SyntaxKind, TypeAliasDeclaration } from 'ts-morph';

interface PropDefinition {
  name: string;
  type: string;
  required: boolean;
  defaultValue?: string;
  description?: string;
}

export async function getPropsFromSource(
  componentPath: string,
  propsTypeName: string
): Promise<PropDefinition[]> {
  const project = new Project();
  const sourceFile = project.addSourceFileAtPath(componentPath);

  // Find the props interface/type
  const propsType = sourceFile.getTypeAlias(propsTypeName)
    || sourceFile.getInterface(propsTypeName);

  if (!propsType) {
    return [];
  }

  const props: PropDefinition[] = [];

  // Extract properties
  const type = propsType.getType();
  const properties = type.getProperties();

  for (const prop of properties) {
    const declarations = prop.getDeclarations();
    const declaration = declarations[0];

    if (!declaration) continue;

    const jsDoc = declaration.getJsDocs?.()[0];
    const description = jsDoc?.getDescription?.().trim();

    props.push({
      name: prop.getName(),
      type: prop.getTypeAtLocation(declaration).getText(),
      required: !prop.isOptional(),
      description,
    });
  }

  return props;
}

Auto-Generated Component Documentation

// packages/docs/components/ComponentDoc.tsx

import { getPropsFromSource } from '@/lib/getPropsFromSource';
import { PropsTable } from './PropsTable';
import { LiveEditor } from './LiveEditor';
import * as Components from '@company/design-system';

interface ComponentDocProps {
  name: string;
  description: string;
  examples: Array<{
    title: string;
    code: string;
  }>;
}

export async function ComponentDoc({ name, description, examples }: ComponentDocProps) {
  // Auto-extract props from source
  const props = await getPropsFromSource(
    `../packages/components/src/${name}/${name}.tsx`,
    `${name}Props`
  );

  const Component = Components[name as keyof typeof Components];

  return (
    <div className="space-y-12">
      {/* Header */}
      <header>
        <h1 className="text-4xl font-bold">{name}</h1>
        <p className="text-xl text-gray-600 mt-2">{description}</p>
      </header>

      {/* Import */}
      <section>
        <h2>Import</h2>
        <CodeBlock language="tsx">
          {`import { ${name} } from '@company/design-system';`}
        </CodeBlock>
      </section>

      {/* Live Examples */}
      <section>
        <h2>Examples</h2>
        <div className="space-y-8">
          {examples.map((example, i) => (
            <div key={i}>
              <h3>{example.title}</h3>
              <LiveEditor
                code={example.code}
                scope={{ ...Components }}
              />
            </div>
          ))}
        </div>
      </section>

      {/* Props Table (auto-generated) */}
      <section>
        <h2>Props</h2>
        <PropsTable props={props} />
      </section>

      {/* Accessibility */}
      <section>
        <h2>Accessibility</h2>
        <AccessibilityChecklist component={name} />
      </section>
    </div>
  );
}

// packages/docs/components/PropsTable.tsx

export function PropsTable({ props }: { props: PropDefinition[] }) {
  return (
    <table className="w-full">
      <thead>
        <tr>
          <th className="text-left">Prop</th>
          <th className="text-left">Type</th>
          <th className="text-left">Default</th>
          <th className="text-left">Description</th>
        </tr>
      </thead>
      <tbody>
        {props.map((prop) => (
          <tr key={prop.name}>
            <td>
              <code>{prop.name}</code>
              {prop.required && <span className="text-red-500">*</span>}
            </td>
            <td>
              <code className="text-sm text-purple-600">{prop.type}</code>
            </td>
            <td>
              {prop.defaultValue ? (
                <code>{prop.defaultValue}</code>
              ) : (
                <span className="text-gray-400">-</span>
              )}
            </td>
            <td>{prop.description}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

Governance Models

RFC Process

<!-- .github/RFC_TEMPLATE.md -->

# RFC: [Title]

## Summary

One paragraph explanation of the proposal.

## Motivation

Why are we doing this? What problem does it solve?

## Detailed Design

### API Changes

```tsx
// Before
<Button type="primary">Click me</Button>

// After
<Button variant="solid" colorScheme="primary">Click me</Button>

Migration Path

How do existing consumers migrate?

Breaking Changes

List all breaking changes.

Alternatives Considered

What other approaches were considered?

Adoption Strategy

How will this be rolled out?

Unresolved Questions

What questions remain?


Checklist

  • API design reviewed
  • Accessibility reviewed
  • Performance impact assessed
  • Migration guide written
  • Documentation updated

### Contribution Guidelines

```markdown
<!-- CONTRIBUTING.md -->

# Contributing to the Design System

## Before You Contribute

1. **Check existing issues** - Someone may have already proposed this
2. **Open a discussion** - For new components or major changes
3. **RFC required** - For breaking changes or new APIs

## Contribution Types

### Bug Fixes

1. Open issue with reproduction
2. Fork and create branch: `fix/issue-number-description`
3. Write test that fails
4. Fix the bug
5. Verify test passes
6. Open PR

### New Components

1. Open RFC issue
2. Wait for approval
3. Fork and create branch: `feat/component-name`
4. Implement component following structure:

ComponentName/ ├── ComponentName.tsx ├── ComponentName.test.tsx ├── ComponentName.stories.tsx ├── types.ts └── index.ts

5. Add documentation
6. Open PR

### Component Requirements

- [ ] TypeScript strict mode
- [ ] Exported types
- [ ] Unit tests (>80% coverage)
- [ ] Storybook stories
- [ ] Accessibility tested
- [ ] Documentation
- [ ] Follows API conventions

## Review Process

1. **Automated checks** - CI must pass
2. **Design review** - Design team approval for visual changes
3. **Code review** - Platform team member approval
4. **Accessibility review** - For new interactive components

## Versioning

We follow semver:
- **Patch** (1.0.x): Bug fixes, no API changes
- **Minor** (1.x.0): New features, backwards compatible
- **Major** (x.0.0): Breaking changes

Breaking changes require:
- RFC approval
- Migration guide
- Deprecation notice in previous minor version
- Codemod if possible

Deprecation Policy

// packages/components/Button/Button.tsx

import { useEffect } from 'react';

interface ButtonProps {
  variant?: 'solid' | 'outline' | 'ghost';
  colorScheme?: 'primary' | 'neutral' | 'danger';

  /**
   * @deprecated Use `variant="ghost"` instead.
   * Will be removed in v3.0.0.
   */
  isGhost?: boolean;

  /**
   * @deprecated Use `colorScheme="danger"` instead.
   * Will be removed in v3.0.0.
   */
  isDanger?: boolean;
}

export function Button({
  variant,
  colorScheme,
  isGhost,
  isDanger,
  ...props
}: ButtonProps) {
  // Deprecation warnings (development only)
  useEffect(() => {
    if (process.env.NODE_ENV === 'development') {
      if (isGhost !== undefined) {
        console.warn(
          '[DesignSystem] Button: `isGhost` is deprecated. ' +
          'Use `variant="ghost"` instead. ' +
          'This prop will be removed in v3.0.0.'
        );
      }
      if (isDanger !== undefined) {
        console.warn(
          '[DesignSystem] Button: `isDanger` is deprecated. ' +
          'Use `colorScheme="danger"` instead. ' +
          'This prop will be removed in v3.0.0.'
        );
      }
    }
  }, [isGhost, isDanger]);

  // Support deprecated props during transition
  const resolvedVariant = isGhost ? 'ghost' : variant;
  const resolvedColorScheme = isDanger ? 'danger' : colorScheme;

  return (
    <button
      className={buttonStyles({
        variant: resolvedVariant,
        colorScheme: resolvedColorScheme,
      })}
      {...props}
    />
  );
}

// ESLint rule to flag deprecated props
// packages/eslint-plugin-company/rules/no-deprecated-props.ts
module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description: 'Disallow deprecated design system props',
    },
    fixable: 'code',
  },
  create(context) {
    const deprecatedProps = {
      Button: {
        isGhost: { replacement: 'variant="ghost"', removeIn: '3.0.0' },
        isDanger: { replacement: 'colorScheme="danger"', removeIn: '3.0.0' },
      },
    };

    return {
      JSXAttribute(node) {
        // Check if using deprecated prop
        // Provide autofix to new API
      },
    };
  },
};

Codemods for Migration

// packages/codemods/transforms/button-v3.ts

import { Transform } from 'jscodeshift';

const transform: Transform = (file, api) => {
  const j = api.jscodeshift;
  const root = j(file.source);

  // Find all Button components
  root
    .findJSXElements('Button')
    .forEach((path) => {
      const attributes = path.node.openingElement.attributes;

      // Transform isGhost to variant="ghost"
      const isGhostAttr = attributes?.find(
        (attr) =>
          attr.type === 'JSXAttribute' &&
          attr.name.name === 'isGhost'
      );

      if (isGhostAttr) {
        // Remove isGhost
        const index = attributes!.indexOf(isGhostAttr);
        attributes!.splice(index, 1);

        // Add variant="ghost"
        attributes!.push(
          j.jsxAttribute(
            j.jsxIdentifier('variant'),
            j.stringLiteral('ghost')
          )
        );
      }

      // Transform isDanger to colorScheme="danger"
      const isDangerAttr = attributes?.find(
        (attr) =>
          attr.type === 'JSXAttribute' &&
          attr.name.name === 'isDanger'
      );

      if (isDangerAttr) {
        const index = attributes!.indexOf(isDangerAttr);
        attributes!.splice(index, 1);

        attributes!.push(
          j.jsxAttribute(
            j.jsxIdentifier('colorScheme'),
            j.stringLiteral('danger')
          )
        );
      }
    });

  return root.toSource();
};

export default transform;

// Usage:
// npx jscodeshift -t ./transforms/button-v3.ts src/**/*.tsx

Metrics and Success

Platform Health Metrics

// packages/analytics/src/platform-metrics.ts

interface PlatformMetrics {
  // Adoption
  componentUsage: Record<string, number>;      // How often each component is used
  packageVersions: Record<string, number>;     // Version distribution
  adoptionRate: number;                        // % of teams using platform

  // Quality
  bugReportsPerMonth: number;
  averageTimeToFix: number;
  breakingChangesPerYear: number;

  // Developer Experience
  timeToFirstComponent: number;                // Onboarding metric
  documentationSatisfaction: number;           // Survey score
  slackQuestionsPerWeek: number;               // Support burden

  // Performance
  bundleSizeImpact: Record<string, number>;    // Size per component
  averagePageLoadImpact: number;
}

// Automated usage tracking
export function trackComponentUsage() {
  // Parse all consumer codebases for import analysis
  // Could also use runtime telemetry in development
}

// packages/analytics/src/dashboards/platform-dashboard.tsx

export function PlatformDashboard() {
  const metrics = usePlatformMetrics();

  return (
    <div className="grid grid-cols-3 gap-4">
      {/* Adoption */}
      <Card>
        <h3>Adoption</h3>
        <Metric value={metrics.adoptionRate} label="Teams Using Platform" />
        <Trend data={metrics.adoptionTrend} />
      </Card>

      {/* Quality */}
      <Card>
        <h3>Quality</h3>
        <Metric value={metrics.bugReportsPerMonth} label="Bugs/Month" />
        <Metric value={metrics.averageTimeToFix} label="Avg Fix Time" />
      </Card>

      {/* DX */}
      <Card>
        <h3>Developer Experience</h3>
        <Metric value={metrics.timeToFirstComponent} label="Time to First Component" />
        <Metric value={metrics.documentationSatisfaction} label="Doc Satisfaction" />
      </Card>

      {/* Component Usage */}
      <Card className="col-span-3">
        <h3>Component Usage</h3>
        <ComponentUsageChart data={metrics.componentUsage} />
      </Card>
    </div>
  );
}

Support Model

┌─────────────────────────────────────────────────────────────────────┐
│                    SUPPORT TIERS                                    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   TIER 1: SELF-SERVICE                                              │
│   ════════════════════                                              │
│   • Documentation site                                              │
│   • Storybook examples                                              │
│   • FAQ                                                             │
│   • Searchable Slack history                                        │
│                                                                     │
│   TIER 2: COMMUNITY                                                 │
│   ══════════════════                                                │
│   • #design-system Slack channel                                    │
│   • GitHub discussions                                              │
│   • Office hours (weekly 30min)                                     │
│   • Response SLA: 1 business day                                    │
│                                                                     │
│   TIER 3: DIRECT SUPPORT                                            │
│   ═══════════════════════                                           │
│   • Bug reports (GitHub issues)                                     │
│   • Feature requests (RFC process)                                  │
│   • Pairing sessions (scheduled)                                    │
│   • Response SLA: same business day for bugs                        │
│                                                                     │
│   TIER 4: EMBEDDED                                                  │
│   ═════════════════                                                 │
│   • Platform engineer embeds with product team                      │
│   • For major migrations or new product launches                    │
│   • Time-boxed (1-2 weeks)                                          │
│   • Requires director approval                                      │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Anti-Patterns to Avoid

1. The Ivory Tower

❌ Platform team builds what they think is cool
   → Product teams don't adopt it
   → Platform team blames "resistance to change"
   → Platform becomes irrelevant

✅ Platform team talks to users constantly
   → Regular feedback sessions
   → Roadmap driven by consumer needs
   → Quick wins build trust

2. The Bottleneck

❌ Everything goes through platform team
   → PRs wait weeks for review
   → Product teams blocked on platform
   → Teams fork and build their own
   → Platform loses control

✅ Clear contribution path
   → Review SLA (< 2 business days)
   → Community reviewers from product teams
   → Self-service for common patterns

3. The Kitchen Sink

❌ Platform tries to solve every problem
   → Massive API surface
   → Maintenance burden explodes
   → Breaking changes constantly
   → Nobody knows what's stable

✅ Small, focused scope
   → Do one thing well
   → Say "no" to scope creep
   → Document what's NOT included
   → Let consumers solve their own problems

4. The Perfectionist

❌ Won't release until perfect
   → Takes 6 months for v1
   → By then, teams built alternatives
   → Adoption is near zero
   → Platform was dead on arrival

✅ Ship early, iterate fast
   → MVP in weeks
   → Unstable API warnings
   → Frequent releases
   → Perfect is the enemy of shipped

Production Checklist

Design System

  • Token pipeline (design → code)
  • Component library (accessible, tested)
  • Storybook for development
  • Figma integration (sync or kit)

Developer Experience

  • CLI for scaffolding
  • Code generators
  • IDE extensions (snippets, autocomplete)
  • Documentation site

Infrastructure

  • Versioning strategy (semver)
  • Release automation
  • Changelog generation
  • Breaking change detection

Governance

  • Contribution guidelines
  • RFC process for changes
  • Deprecation policy
  • Codemod support

Support

  • Slack channel
  • Office hours
  • Response SLA
  • Metrics dashboard

Summary

Becoming a platform team is a fundamental shift in how you work. Your product is developer productivity. Your users are engineers. Your success is measured not by features shipped, but by how much faster other teams ship because of you.

Key principles:

  1. Treat DX as UX - Engineers are users; their experience matters
  2. Stable APIs over clever features - Breaking changes destroy trust
  3. Documentation is product - Undocumented features don't exist
  4. Say no more than yes - Scope discipline prevents platform sprawl
  5. Measure adoption - If teams aren't using it, it's not working

The best platform teams are invisible. When everything just works—when engineers reach for your tools without thinking—that's success.

What did you think?

© 2026 Vidhya Sagar Thakur. All rights reserved.