| name | miro-sdk-integration |
| description | Use when working with Miro SDK v2 operations, board manipulation, creating shapes, text, frames, or syncing data between Miro and database. Helps implement features for master board, project rows, and client reports. |
| allowed-tools | Read, Grep, Glob, Edit, Bash, mcp__plugin_context7_context7__resolve-library-id, mcp__plugin_context7_context7__query-docs |
Miro SDK v2 Integration Skill
Overview
This skill guides implementation of Miro SDK v2 features for the project management system. It ensures proper use of existing services, adherence to board architecture patterns, and safe database synchronization.
Critical Services (Always Read First)
Location: /src/features/boards/services/
Core Services
miroSdkService.ts (106KB - Primary Workhorse)
- 50+ methods for board operations
- Shape creation, text manipulation, frame organization
- Safe operations that return null on error instead of throwing
- Methods:
createFrame(),createStickyNote(),createText(),createShape(), etc.
masterBoardService.ts
- Manages "Master Board" - consolidated view of all clients with projects
- Creates vertical frames with mini-kanbans
- Updates when projects change status
- Coordinates with
projectRowService
projectRowService.ts
- Manages individual project timeline rows
- Status columns for deliverables (pending, wip, review, approved)
- Real-time updates from database changes
- Tracks Miro item IDs in
miroItemRegistry
projectSyncOrchestrator.ts
- Orchestrates bidirectional sync: Database ↔ Miro
- Source of truth is always Supabase
- Handles create, update, delete operations
- Prevents infinite sync loops
brandWorkspaceService.ts
- Creates project workspace frames
- Sections: Brief, Process, Deliverables
- Project-specific collaboration area
miroReportService.ts & miroClientReportService.ts
- Generate visual reports on Miro boards
- Charts, metrics, status summaries
- Client-facing report generation
miroItemRegistry.ts
- Tracks Miro item IDs ↔ Database entity relationships
- Critical for identifying what items represent
- Prevents duplicate creation
Safe SDK Access Pattern
ALWAYS use miroAdapter from @shared/lib/miroAdapter:
import { miroAdapter } from '@shared/lib/miroAdapter';
// ✅ Check availability before operations
if (miroAdapter.isAvailable()) {
await miroAdapter.showInfo('Message to user');
}
// ✅ Safe operations return null instead of throwing
const boardInfo = await miroAdapter.getBoardInfo();
if (!boardInfo) {
logger.warn('Not in Miro context');
return;
}
// ✅ Get board ID safely
const boardId = await miroAdapter.getBoardId();
// ❌ NEVER access window.miro directly
// window.miro.board.get() // DON'T DO THIS
Board Architecture
Master Board Layout
┌─────────────────────────────────────────┐
│ MASTER BOARD │
├─────────────────────────────────────────┤
│ Client A Client B │
│ ┌─────────┐ ┌─────────┐ │
│ │ Project │ │ Project │ │
│ │ Mini │ │ Mini │ │
│ │ Kanban │ │ Kanban │ │
│ └─────────┘ └─────────┘ │
└─────────────────────────────────────────┘
Managed by: masterBoardService.ts
Project Row Layout
┌───────────────────────────────────────────────┐
│ PROJECT: "Brand Identity for Client X" │
├──────────┬──────────┬──────────┬─────────────┤
│ Pending │ WIP │ Review │ Approved │
├──────────┼──────────┼──────────┼─────────────┤
│ [Logo] │ [Colors] │ [Fonts] │ [Guidelines]│
└──────────┴──────────┴──────────┴─────────────┘
Managed by: projectRowService.ts
Constants & Configuration
Location: /src/features/boards/services/constants/
// Layout constants
import { LAYOUT } from '@shared/config/boardConfig';
LAYOUT.frameStartX // Starting X position for frames
LAYOUT.frameStartY // Starting Y position for frames
LAYOUT.frameWidth // Standard frame width
LAYOUT.frameHeight // Standard frame height
LAYOUT.stickyNoteSize // Standard sticky note dimensions
// Colors (Use explicit hex - CSS vars don't work in Miro iframe!)
import { COLORS } from '@shared/config/boardConfig';
COLORS.primary // #050038
COLORS.accent // #2563EB
COLORS.success // #10B981
COLORS.warning // #F59E0B
COLORS.error // #EF4444
// Briefing templates
import { BRIEFING_TEMPLATE } from '@shared/config/boardConfig';
Database Synchronization Pattern
Critical Rule: Database is Source of Truth
// ✅ CORRECT: Update database first, then sync to Miro
async function updateProjectStatus(projectId: string, status: ProjectStatus) {
// 1. Update Supabase
const { data, error } = await supabase
.from('projects')
.update({ status })
.eq('id', projectId)
.select()
.single();
if (error) throw error;
// 2. Sync to Miro board
await projectSyncOrchestrator.syncProjectToBoard(data);
}
// ❌ WRONG: Don't update Miro first
async function updateProjectStatusWrong(projectId: string, status: ProjectStatus) {
// DON'T DO THIS
await miroSdkService.updateFrameTitle(frameId, newTitle);
await supabase.from('projects').update({ status }); // Out of sync!
}
Sync Orchestrator Usage
import { projectSyncOrchestrator } from '@features/boards/services/projectSyncOrchestrator';
// Sync single project
await projectSyncOrchestrator.syncProjectToBoard(project);
// Sync all projects on board
await projectSyncOrchestrator.syncAllProjectsToBoard(boardId);
// Handle deletions
await projectSyncOrchestrator.removeProjectFromBoard(projectId);
Common Implementation Patterns
Pattern 1: Creating Board Objects
import { miroSdkService } from '@features/boards/services/miroSdkService';
import { LAYOUT, COLORS } from '@shared/config/boardConfig';
// Create frame
const frameId = await miroSdkService.createFrame({
title: 'Project Workspace',
x: LAYOUT.frameStartX,
y: LAYOUT.frameStartY,
width: LAYOUT.frameWidth,
height: LAYOUT.frameHeight,
style: {
fillColor: COLORS.primary, // Use hex, not CSS variable!
}
});
// Create sticky note
const stickyId = await miroSdkService.createStickyNote({
content: 'Logo design',
x: 100,
y: 200,
style: {
fillColor: COLORS.warning,
}
});
// Create text
const textId = await miroSdkService.createText({
content: 'Project Brief',
x: 300,
y: 100,
style: {
fontSize: 24,
color: COLORS.primary,
}
});
Pattern 2: Updating Board Objects
// Update frame title
await miroSdkService.updateFrame(frameId, {
title: 'Updated Title',
});
// Move sticky note
await miroSdkService.updateStickyNote(stickyId, {
x: 150,
y: 250,
});
// Update text content
await miroSdkService.updateText(textId, {
content: 'Updated Brief',
});
Pattern 3: Querying Board Objects
// Get all frames on board
const frames = await miroSdkService.getAllFrames();
// Get specific item by ID
const item = await miroSdkService.getItemById(itemId);
// Get items by type
const stickies = await miroSdkService.getItemsByType('sticky_note');
Pattern 4: Using Miro Item Registry
import { miroItemRegistry } from '@features/boards/services/miroItemRegistry';
// Track new Miro item
miroItemRegistry.register({
miroItemId: frameId,
entityType: 'project',
entityId: projectId,
});
// Lookup database entity from Miro item
const entity = miroItemRegistry.getEntity(miroItemId);
if (entity?.entityType === 'project') {
const projectId = entity.entityId;
}
// Remove tracking
miroItemRegistry.unregister(miroItemId);
Handling Board Events
Webhook Processing
Miro board changes trigger webhooks processed by Edge Function:
File: supabase/functions/miro-webhook/index.ts
// Webhook receives events: item:created, item:updated, item:deleted
// Process events and sync back to database if needed
// Example event types:
{
type: 'item:created',
data: {
id: 'miro-item-id',
type: 'sticky_note',
// ... other properties
}
}
Important: Prevent infinite loops by checking if change originated from sync operation.
CSS Styling for Miro Context
Critical: CSS Variables Don't Work in Miro Iframe!
// ❌ WRONG: CSS variables won't resolve
style: {
fillColor: 'var(--color-primary)', // Won't work!
}
// ✅ CORRECT: Use explicit hex colors
style: {
fillColor: '#050038', // Works!
}
// ✅ BETTER: Use constants
import { COLORS } from '@shared/config/boardConfig';
style: {
fillColor: COLORS.primary, // Resolves to '#050038'
}
Entry Points & HTML Files
The app has 4 HTML entry points (configured in vite.config.ts):
- index.html - Main panel entry (sidebar panel)
- app.html - Full app interface (modal or standalone)
- board-modal.html - Modal dialogs opened from board UI
- admin-modal.html - Admin-specific modal dialogs
When adding Miro features, ensure proper entry point is configured.
Rate Limiting & Performance
Miro API Limits
- Max 25 API calls per second
- Use batching for bulk operations
- Implement retry logic with exponential backoff
// ✅ Batch operations
const items = await Promise.all(
itemIds.map(id => miroSdkService.getItemById(id))
);
// ❌ Sequential operations in loop (slow + may hit rate limit)
for (const id of itemIds) {
const item = await miroSdkService.getItemById(id); // DON'T
}
Testing in Miro Context
Availability Checks
import { miroAdapter } from '@shared/lib/miroAdapter';
// Gate Miro-specific code
if (!miroAdapter.isAvailable()) {
logger.warn('Not in Miro context, skipping board operation');
return null;
}
// Proceed with Miro operations
const board = await miroAdapter.board.get();
Testing Entry Points
Test all 4 HTML entry points:
- Panel view (index.html)
- Full app (app.html)
- Board modal (board-modal.html)
- Admin modal (admin-modal.html)
Browser Console
Check for SDK errors in browser console:
- Miro SDK initialization errors
- API rate limit warnings
- Item not found errors
Common Pitfalls
❌ Don't Trust Miro Item Timestamps
Miro doesn't guarantee accurate timestamps. Use database timestamps as source of truth.
❌ Don't Assume Items Exist
Always validate before manipulating:
const item = await miroSdkService.getItemById(itemId);
if (!item) {
logger.error('Item not found', { itemId });
return;
}
// Now safe to update
await miroSdkService.updateItem(item.id, { ... });
❌ Don't Create Duplicate Items
Check miroItemRegistry before creating:
const existing = miroItemRegistry.getByEntity('project', projectId);
if (existing) {
// Update existing item
await miroSdkService.updateFrame(existing.miroItemId, { ... });
} else {
// Create new item
const frameId = await miroSdkService.createFrame({ ... });
miroItemRegistry.register({
miroItemId: frameId,
entityType: 'project',
entityId: projectId,
});
}
Role-Based Access
RLS policies enforce access control:
- Admin: Full access to all boards
- Designer: Access to assigned projects only
- Client: Access to own projects only
Always verify permissions before Miro operations that expose sensitive data.
Before Making Changes Checklist
- Read relevant service in
/src/features/boards/services/ - Check if similar functionality already exists
- Understand board layout from constants
- Verify role-based access requirements
- Plan database-first sync strategy
- Use explicit hex colors, not CSS variables
- Check
miroItemRegistryfor item tracking - Test with
miroAdapter.isAvailable()guards - Test all 4 HTML entry points
- Verify browser console for errors
Useful Context7 Queries
When implementing Miro features, query Context7 for:
"Miro SDK v2 create frame with custom style"
"Miro SDK v2 sticky note positioning best practices"
"Miro SDK v2 webhook event handling"
"Miro SDK v2 batch operations rate limits"
"Miro SDK v2 board selection and viewport"
Quick Reference
// Services
import { miroSdkService } from '@features/boards/services/miroSdkService';
import { masterBoardService } from '@features/boards/services/masterBoardService';
import { projectRowService } from '@features/boards/services/projectRowService';
import { projectSyncOrchestrator } from '@features/boards/services/projectSyncOrchestrator';
import { miroItemRegistry } from '@features/boards/services/miroItemRegistry';
// Safe SDK access
import { miroAdapter } from '@shared/lib/miroAdapter';
// Constants
import { LAYOUT, COLORS, BRIEFING_TEMPLATE } from '@shared/config/boardConfig';
// Logger
import { createLogger } from '@shared/lib/logger';
const logger = createLogger('MiroFeature');
Remember: Database is source of truth. Miro is a visual representation. Always sync from database to Miro, never the reverse.