agentsclimarketplace

Canva reference architecture

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/canva-pack/skills/canva-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 canva-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 Canva Connect API reference architecture with best-practice project layout.

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

7.4 KB, as published. Nobody here has run it

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

IssueCauseSolution
Circular dependenciesWrong layeringServices import client, not vice versa
Token not foundUser hasn't connected CanvaRedirect to OAuth flow
Cache staleDesign updated in CanvaInvalidate on webhook events
Service timeoutExport taking too longIncrease timeout, add job queue

Resources

Next Steps

For multi-environment setup, see canva-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.