Skillshub canva-reference-architecture
install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/jeremylongshore/claude-code-plugins-plus-skills/canva-reference-architecture" ~/.claude/skills/comeonoliver-skillshub-canva-reference-architecture && rm -rf "$T"
manifest:
skills/jeremylongshore/claude-code-plugins-plus-skills/canva-reference-architecture/SKILL.mdsource content
Canva Reference Architecture
Overview
Production-ready architecture for Canva Connect API integrations. All interactions use the REST API at
api.canva.com/rest/v1/* with OAuth 2.0 PKCE authentication.
Project Structure
my-canva-integration/ ├── src/ │ ├── canva/ │ │ ├── client.ts # REST client wrapper with auto-refresh │ │ ├── auth.ts # OAuth 2.0 PKCE flow │ │ ├── types.ts # API request/response TypeScript types │ │ └── errors.ts # CanvaAPIError class │ ├── services/ │ │ ├── design.service.ts # Design creation, export, listing │ │ ├── asset.service.ts # Asset upload and management │ │ ├── template.service.ts # Brand template autofill (Enterprise) │ │ └── folder.service.ts # Folder management │ ├── routes/ │ │ ├── auth.ts # OAuth callback endpoints │ │ ├── designs.ts # Design CRUD routes │ │ ├── exports.ts # Export trigger/download routes │ │ └── webhooks.ts # Webhook receiver │ ├── middleware/ │ │ ├── auth.ts # Verify user has valid Canva token │ │ └── rate-limit.ts # Client-side rate limit guard │ ├── store/ │ │ └── tokens.ts # Encrypted token storage (DB) │ └── index.ts ├── tests/ │ ├── mocks/ │ │ └── canva-server.ts # MSW mock server │ ├── unit/ │ │ └── design.service.test.ts │ └── integration/ │ └── canva-api.test.ts ├── .env.example └── package.json
Layer Architecture
┌─────────────────────────────────────────┐ │ Routes Layer │ │ (Express/Next.js — HTTP in/out) │ ├─────────────────────────────────────────┤ │ Service Layer │ │ (Business logic, caching, validation) │ ├─────────────────────────────────────────┤ │ Canva Client Layer │ │ (REST calls, token refresh, retry) │ ├─────────────────────────────────────────┤ │ Infrastructure Layer │ │ (Token store, cache, queue) │ └─────────────────────────────────────────┘
Service Layer Pattern
// src/services/design.service.ts import { CanvaClient } from '../canva/client'; import { LRUCache } from 'lru-cache'; export class DesignService { private cache = new LRUCache<string, any>({ max: 200, ttl: 300_000 }); constructor(private canva: CanvaClient) {} async create(opts: { type: 'preset' | 'custom'; name?: string; width?: number; height?: number; title: string; assetId?: string; }) { const designType = opts.type === 'preset' ? { type: 'preset' as const, name: opts.name! } : { type: 'custom' as const, width: opts.width!, height: opts.height! }; return this.canva.request('/designs', { method: 'POST', body: JSON.stringify({ design_type: designType, title: opts.title, ...(opts.assetId && { asset_id: opts.assetId }), }), }); } async get(id: string) { const cached = this.cache.get(id); if (cached) return cached; const result = await this.canva.request(`/designs/${id}`); this.cache.set(id, result); return result; } async export(designId: string, format: object): Promise<string[]> { // Start export job const { job } = await this.canva.request('/exports', { method: 'POST', body: JSON.stringify({ design_id: designId, format }), }); // Poll for completion return this.pollExport(job.id); } private async pollExport(exportId: string, timeoutMs = 60000): Promise<string[]> { const start = Date.now(); while (Date.now() - start < timeoutMs) { const { job } = await this.canva.request(`/exports/${exportId}`); if (job.status === 'success') return job.urls; if (job.status === 'failed') throw new Error(`Export failed: ${job.error?.message}`); await new Promise(r => setTimeout(r, 2000)); } throw new Error('Export timeout'); } }
Data Flow
User clicks "Create Design" │ ▼ ┌─────────────┐ │ Route │ POST /api/designs │ Handler │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ Design │ Validates input, checks auth │ Service │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ Canva │ POST api.canva.com/rest/v1/designs │ Client │ (auto-refreshes token if expired) └──────┬──────┘ │ ▼ ┌─────────────┐ │ Canva │ Returns design.id, edit_url, view_url │ API │ └─────────────┘ │ ▼ Redirect user to edit_url → Canva Editor
Auth Middleware
// src/middleware/auth.ts export function requireCanvaAuth(tokenStore: TokenStore) { return async (req: Request, res: Response, next: NextFunction) => { const userId = req.user?.id; if (!userId) return res.status(401).json({ error: 'Not authenticated' }); const tokens = await tokenStore.get(userId); if (!tokens) return res.status(403).json({ error: 'Canva not connected' }); // Attach client to request for downstream use req.canva = new CanvaClient({ clientId: process.env.CANVA_CLIENT_ID!, clientSecret: process.env.CANVA_CLIENT_SECRET!, tokens, onTokenRefresh: (newTokens) => tokenStore.save(userId, newTokens), }); next(); }; }
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| Circular dependencies | Wrong layering | Services import client, not vice versa |
| Token not found | User hasn't connected Canva | Redirect to OAuth flow |
| Cache stale | Design updated in Canva | Invalidate on webhook events |
| Service timeout | Export taking too long | Increase timeout, add job queue |
Resources
Next Steps
For multi-environment setup, see
canva-multi-env-setup.