agentsclimarketplace

Miro reference architecture

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/miro-pack/skills/miro-reference-architecture

425 plugins, 2,810 skills, 200 agents for Claude Code. Open-source marketplace at tonsofskills.com with the ccpi CLI package manager.

Install
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill miro-reference-architecture

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its author says it does

Copied from the file, not written here

'Implement a production-ready reference architecture for Miro REST API v2

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

12.3 KB, as published. Nobody here has run it

Miro Reference Architecture

Overview

Production-ready architecture for Miro REST API v2 integrations. Layered design with a board service, item factory, webhook event processor, and caching layer.

Architecture Diagram

┌──────────────────────────────────────────────────────────┐
│                    API / UI Layer                         │
│   Express routes, Next.js API routes, CLI commands       │
├──────────────────────────────────────────────────────────┤
│                   Service Layer                          │
│   BoardService, ItemService, SyncService                 │
│   (business logic, orchestration, validation)            │
├──────────────────────────────────────────────────────────┤
│                  Miro Client Layer                        │
│   MiroApiClient (REST v2), TokenManager (OAuth 2.0)      │
│   ItemFactory (typed creation), ConnectorBuilder          │
├──────────────────────────────────────────────────────────┤
│                Infrastructure Layer                       │
│   Cache (LRU/Redis), Queue (PQueue), Monitor (metrics)   │
│   WebhookProcessor (signature + idempotency)             │
└──────────────────────────────────────────────────────────┘
                         │
                         ▼
            https://api.miro.com/v2/

Project Structure

src/
├── miro/
│   ├── client.ts              # MiroApiClient — wraps fetch with auth, retries, monitoring
│   ├── token-manager.ts       # OAuth 2.0 token lifecycle (refresh, storage)
│   ├── item-factory.ts        # Typed item creation (sticky notes, shapes, cards, etc.)
│   ├── connector-builder.ts   # Fluent API for creating connectors
│   ├── types.ts               # TypeScript types for all Miro v2 responses
│   └── errors.ts              # MiroApiError, MiroAuthError, MiroRateLimitError
├── services/
│   ├── board-service.ts       # Board CRUD + member management
│   ├── item-service.ts        # Item CRUD + tag operations
│   ├── sync-service.ts        # Two-way sync between Miro and your database
│   └── search-service.ts      # Find items by content, type, or tag
├── webhooks/
│   ├── handler.ts             # Express/serverless webhook endpoint
│   ├── processor.ts           # Event routing and processing
│   └── idempotency.ts         # Duplicate event prevention
├── cache/
│   ├── board-cache.ts         # Board metadata cache
│   └── item-cache.ts          # Item data cache with webhook invalidation
├── config/
│   ├── miro.ts                # Environment-based Miro configuration
│   └── index.ts               # Config loader
└── monitoring/
    ├── metrics.ts             # Prometheus counters/histograms for Miro API
    └── health.ts              # Health check endpoint

Core Components

MiroApiClient

// src/miro/client.ts
export class MiroApiClient {
  constructor(
    private tokenManager: TokenManager,
    private cache: ItemCache,
    private monitor: MiroMetrics,
  ) {}

  async fetch<T>(path: string, method = 'GET', body?: unknown): Promise<T> {
    const token = await this.tokenManager.getValidToken();
    const start = performance.now();

    const response = await fetch(`https://api.miro.com${path}`, {
      method,
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      ...(body ? { body: JSON.stringify(body) } : {}),
    });

    const duration = performance.now() - start;
    this.monitor.recordRequest(method, path, response.status, duration);
    this.monitor.updateRateLimit(response);

    if (response.status === 429) {
      const retryAfter = parseInt(response.headers.get('Retry-After') ?? '5', 10);
      throw new MiroRateLimitError(retryAfter);
    }

    if (!response.ok) {
      const error = await response.json().catch(() => ({}));
      throw new MiroApiError(response.status, error.message, error.code);
    }

    if (response.status === 204) return null as T;
    return response.json() as T;
  }

  // Paginated fetch — returns all pages
  async fetchAll<T>(path: string, limit = 50): Promise<T[]> {
    const items: T[] = [];
    let cursor: string | undefined;

    do {
      const params = new URLSearchParams({ limit: String(limit) });
      if (cursor) params.set('cursor', cursor);

      const result = await this.fetch<PaginatedResponse<T>>(
        `${path}?${params}`
      );
      items.push(...result.data);
      cursor = result.cursor;
    } while (cursor);

    return items;
  }
}

Board Service

// src/services/board-service.ts
export class BoardService {
  constructor(
    private api: MiroApiClient,
    private cache: BoardCache,
  ) {}

  async getBoard(boardId: string): Promise<MiroBoard> {
    const cached = await this.cache.get(boardId);
    if (cached) return cached;

    const board = await this.api.fetch<MiroBoard>(`/v2/boards/${boardId}`);
    await this.cache.set(boardId, board, 120);  // 2 min TTL
    return board;
  }

  async createBoard(params: CreateBoardParams): Promise<MiroBoard> {
    return this.api.fetch<MiroBoard>('/v2/boards', 'POST', {
      name: params.name,
      description: params.description,
      teamId: params.teamId,
      policy: {
        sharingPolicy: { access: params.access ?? 'private' },
        permissionsPolicy: { sharingAccess: 'team_members_and_collaborators' },
      },
    });
  }

  async shareBoard(boardId: string, emails: string[], role: BoardRole): Promise<void> {
    await this.api.fetch(`/v2/boards/${boardId}/members`, 'POST', {
      emails,
      role,  // 'viewer' | 'commenter' | 'editor' | 'coowner'
    });
  }

  async getMembers(boardId: string): Promise<BoardMember[]> {
    return this.api.fetchAll(`/v2/boards/${boardId}/members`);
  }
}

Webhook Processor

// src/webhooks/processor.ts
export class WebhookProcessor {
  private handlers = new Map<string, EventHandler[]>();

  on(eventType: string, handler: EventHandler): void {
    const existing = this.handlers.get(eventType) ?? [];
    existing.push(handler);
    this.handlers.set(eventType, existing);
  }

  async process(event: MiroBoardEvent): Promise<void> {
    // Type-based routing
    const key = `${event.item.type}:${event.type}`;  // e.g., 'sticky_note:create'
    const handlers = [
      ...(this.handlers.get(key) ?? []),
      ...(this.handlers.get(`*:${event.type}`) ?? []),  // Wildcard item type
      ...(this.handlers.get('*:*') ?? []),               // Catch-all
    ];

    for (const handler of handlers) {
      await handler(event);
    }
  }
}

// Usage
const processor = new WebhookProcessor();

processor.on('sticky_note:create', async (event) => {
  console.log(`New sticky note on board ${event.boardId}: ${event.item.id}`);
  await syncService.syncItem(event.boardId, event.item.id);
});

processor.on('*:delete', async (event) => {
  console.log(`Item deleted from board ${event.boardId}: ${event.item.id}`);
  await database.deleteItem(event.item.id);
});

Connector Builder (Fluent API)

// src/miro/connector-builder.ts
export class ConnectorBuilder {
  private config: any = { style: {} };

  constructor(private api: MiroApiClient, private boardId: string) {}

  from(itemId: string, snapTo?: SnapPosition): this {
    this.config.startItem = { id: itemId, ...(snapTo ? { snapTo } : {}) };
    return this;
  }

  to(itemId: string, snapTo?: SnapPosition): this {
    this.config.endItem = { id: itemId, ...(snapTo ? { snapTo } : {}) };
    return this;
  }

  caption(text: string, position = 0.5): this {
    this.config.captions = [{ content: text, position }];
    return this;
  }

  dashed(): this { this.config.style.strokeStyle = 'dashed'; return this; }
  curved(): this { this.config.shape = 'curved'; return this; }
  arrow(): this { this.config.style.endStrokeCap = 'stealth'; return this; }

  async build(): Promise<MiroConnector> {
    return this.api.fetch(`/v2/boards/${this.boardId}/connectors`, 'POST', this.config);
  }
}

// Usage
const connector = await new ConnectorBuilder(api, boardId)
  .from(taskId, 'right')
  .to(dependencyId, 'left')
  .caption('depends on')
  .dashed()
  .arrow()
  .build();

Data Flow

User Action (or cron job)
     │
     ▼
┌─────────────┐
│   Service   │  ←── Business logic
│   Layer     │
└──────┬──────┘
       │
  ┌────┴────┐
  │         │
  ▼         ▼
┌──────┐ ┌──────┐
│Cache │ │ Miro │  ←── api.miro.com/v2
│Layer │ │Client│
└──────┘ └──────┘

Miro Board Change
     │
     ▼
┌─────────────┐
│  Webhook    │  ←── Signature verification
│  Handler    │
└──────┬──────┘
       │
       ▼
┌─────────────┐
│ Processor   │  ←── Idempotency + routing
└──────┬──────┘
       │
  ┌────┴────┐
  │         │
  ▼         ▼
┌──────┐ ┌──────┐
│Cache │ │  DB  │  ←── Sync + invalidation
│Inval │ │Sync  │
└──────┘ └──────┘

Configuration

// src/config/miro.ts
export interface MiroConfig {
  clientId: string;
  clientSecret: string;
  accessToken?: string;
  environment: 'development' | 'staging' | 'production';
  cache: { enabled: boolean; ttlSeconds: number };
  rateLimit: { maxConcurrency: number; requestsPerSecond: number };
  webhook: { secret: string; callbackUrl: string };
}

export function loadMiroConfig(): MiroConfig {
  return {
    clientId: requireEnv('MIRO_CLIENT_ID'),
    clientSecret: requireEnv('MIRO_CLIENT_SECRET'),
    accessToken: process.env.MIRO_ACCESS_TOKEN,
    environment: (process.env.NODE_ENV ?? 'development') as MiroConfig['environment'],
    cache: {
      enabled: process.env.MIRO_CACHE_ENABLED !== 'false',
      ttlSeconds: parseInt(process.env.MIRO_CACHE_TTL ?? '120'),
    },
    rateLimit: {
      maxConcurrency: parseInt(process.env.MIRO_MAX_CONCURRENCY ?? '5'),
      requestsPerSecond: parseInt(process.env.MIRO_RPS ?? '10'),
    },
    webhook: {
      secret: process.env.MIRO_WEBHOOK_SECRET ?? '',
      callbackUrl: process.env.MIRO_WEBHOOK_URL ?? '',
    },
  };
}

Error Handling

LayerError TypeHandling
Client429 Rate LimitedExponential backoff with Retry-After
Client401 Token ExpiredAuto-refresh via TokenManager
ServiceItem Not FoundReturn null, log, continue
WebhookInvalid SignatureReturn 401, do not process
WebhookDuplicate EventSkip via idempotency check
CacheRedis DownFall through to API directly

Resources

Next Steps

For multi-environment setup, see miro-multi-env-setup.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.