| name | miro-reference-architecture |
| description | Implement a production-ready reference architecture for Miro REST API v2
integrations with layered design, caching, and event processing.
Trigger with phrases like "miro architecture", "miro project structure",
"how to organize miro integration", "miro design patterns".
|
| allowed-tools | Read, Grep |
| version | 1.6.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
| tags | ["saas","miro","architecture","design"] |
| compatibility | Designed for Claude Code |
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
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);
..(response);
(response. === ) {
retryAfter = (response..() ?? , );
(retryAfter);
}
(!response.) {
error = response.().( ({}));
(response., error., error.);
}
(response. === ) T;
response.() T;
}
fetchAll<T>(: , limit = ): <T[]> {
: T[] = [];
: | ;
{
params = ({ : (limit) });
(cursor) params.(, cursor);
result = .<<T>>(
);
items.(...result.);
cursor = result.;
} (cursor);
items;
}
}
Board Service
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);
return board;
}
async createBoard(params: CreateBoardParams): Promise<MiroBoard> {
return this.api.fetch<MiroBoard>('/v2/boards', , {
: params.,
: params.,
: params.,
: {
: { : params. ?? },
: { : },
},
});
}
(: , : [], : ): <> {
..(, , {
emails,
role,
});
}
(: ): <[]> {
..();
}
}
Webhook Processor
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> {
const key = `${event.item.type}:${event.type}`;
const handlers = [
...(this.handlers.get(key) ?? []),
...(this.handlers.get(`*:${event.type}`) ?? []),
...(this.handlers.() ?? []),
];
( handler handlers) {
(event);
}
}
}
processor = ();
processor.(, (event) => {
.();
syncService.(event., event..);
});
processor.(, (event) => {
.();
database.(event..);
});
Connector Builder (Fluent API)
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;
}
(): { ... = ; ; }
(): { .. = ; ; }
(): { ... = ; ; }
(): <> {
..(, , .);
}
}
connector = (api, boardId)
.(taskId, )
.(dependencyId, )
.()
.()
.()
.();
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
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.. !== ,
: (process.. ?? ),
},
: {
: (process.. ?? ),
: (process.. ?? ),
},
: {
: process.. ?? ,
: process.. ?? ,
},
};
}
Error Handling
| Layer | Error Type | Handling |
|---|
| Client | 429 Rate Limited | Exponential backoff with Retry-After |
| Client | 401 Token Expired | Auto-refresh via TokenManager |
| Service | Item Not Found | Return null, log, continue |
| Webhook | Invalid Signature | Return 401, do not process |
| Webhook | Duplicate Event | Skip via idempotency check |
| Cache | Redis Down | Fall through to API directly |
Resources
Next Steps
For multi-environment setup, see miro-multi-env-setup.