Canva Multi-Environment Setup
Overview
Configure Canva Connect API integrations across development, staging, and production. Each environment needs separate OAuth integrations registered in the Canva developer portal with distinct redirect URIs.
Prerequisites
- Separate reviewed OAuth clients, redirect URIs, secret stores, tenant/asset scopes, and owners for each environment.
- Synthetic development/staging assets and a deployment process that cannot silently select production configuration.
Instructions
- Resolve the environment from an explicit allowlist and fail if its OAuth client, redirect URI, or scope configuration is absent or mismatched.
- Keep tokens, caches, queues, and webhook endpoints isolated by environment; never share production credentials with lower environments.
- Verify staging with synthetic assets, then promote immutable configuration through the reviewed release path.
Environment Strategy
| Environment | Canva Integration | Redirect URI | Data |
|---|---|---|---|
| Development | my-app-dev |
http://localhost:3000/auth/canva/callback |
Test account |
| Staging | my-app-staging |
https://staging.myapp.com/auth/canva/callback |
Staging account |
| Production | my-app-prod |
https://myapp.com/auth/canva/callback |
Real users |
Important: Register a separate Canva integration per environment. Each gets its own client ID and secret.
Configuration
// src/config/canva.ts
interface CanvaEnvConfig {
clientId: string;
clientSecret: string;
redirectUri: string;
baseUrl: string; // Always api.canva.com — Canva has no sandbox API
scopes: string[];
debug: boolean;
}
const configs: Record<string, CanvaEnvConfig> = {
development: {
clientId: process.env.CANVA_CLIENT_ID!,
clientSecret: process.env.CANVA_CLIENT_SECRET!,
redirectUri: 'http://localhost:3000/auth/canva/callback',
baseUrl: 'https://api.canva.com/rest/v1', // No sandbox exists
scopes: ['design:content:write', 'design:content:read', 'design:meta:read', 'asset:write', 'asset:read'],
debug: true,
},
staging: {
clientId: process.env.CANVA_CLIENT_ID!,
clientSecret: process.env.CANVA_CLIENT_SECRET!,
redirectUri: process.env.CANVA_REDIRECT_URI!,
baseUrl: 'https://api.canva.com/rest/v1',
scopes: ['design:content:write', 'design:content:read', 'design:meta:read', 'asset:write', 'asset:read'],
debug: false,
},
production: {
clientId: process.env.CANVA_CLIENT_ID!,
clientSecret: process.env.CANVA_CLIENT_SECRET!,
redirectUri: process.env.CANVA_REDIRECT_URI!,
baseUrl: 'https://api.canva.com/rest/v1',
scopes: ['design:content:write', 'design:content:read', 'design:meta:read'],
debug: false,
},
};
export function getCanvaConfig(): CanvaEnvConfig {
const env = process.env.NODE_ENV || 'development';
return configs[env] || configs.development;
}
Secret Management
Local Development
# .env.local (git-ignored)
CANVA_CLIENT_ID=OCA_dev_xxxxxxxx
CANVA_CLIENT_SECRET=dev_xxxxxxxx
GitHub Actions / CI
# Per-environment secrets
gh secret set CANVA_CLIENT_ID --env staging --body "OCA_staging_xxx"
gh secret set CANVA_CLIENT_SECRET --env staging --body "staging_xxx"
gh secret set CANVA_CLIENT_ID --env production --body "OCA_prod_xxx"
gh secret set CANVA_CLIENT_SECRET --env production --body "prod_xxx"
Production — Cloud Secret Managers
# GCP Secret Manager
gcloud secrets create canva-client-id-prod --data-file=-
gcloud secrets create canva-client-secret-prod --data-file=-
# AWS Secrets Manager
aws secretsmanager create-secret \
--name canva/production/client-id \
--secret-string "OCA_prod_xxx"
# HashiCorp Vault
vault kv put secret/canva/production \
client_id="OCA_prod_xxx" \
client_secret="prod_xxx"
Environment Isolation Guards
// Prevent accidental cross-environment operations
function assertEnvironment(expected: string): void {
const actual = process.env.NODE_ENV || 'development';
if (actual !== expected) {
throw new Error(`Expected ${expected} environment, got ${actual}`);
}
}
// Guard destructive operations
async function deleteAllUserDesigns(userId: string, token: string) {
assertEnvironment('development'); // Block in staging/production
// ...
}
Token Storage per Environment
// Development: file-based for convenience
// Staging/Production: encrypted database
function getTokenStore(): TokenStore {
const env = process.env.NODE_ENV || 'development';
if (env === 'development') {
return new FileTokenStore('.canva-tokens.json'); // git-ignored
}
return new DatabaseTokenStore({
connectionString: process.env.DATABASE_URL!,
encryptionKey: process.env.TOKEN_ENCRYPTION_KEY!,
});
}
Canva-Specific Considerations
- No sandbox API — Canva has no separate sandbox environment. All environments hit
api.canva.com/rest/v1. Use separate Canva accounts for dev/staging. - Separate integrations — Each environment should be a distinct integration in the Canva developer portal to avoid redirect URI conflicts.
- Scope differences — Use broader scopes in dev for testing, minimal scopes in production.
- Token isolation — Never share tokens across environments. Refresh tokens are single-use.
Output
Environment setup yields an approved environment label, configuration/version receipt, OAuth/redirect validation, and scope-isolation result. It excludes client secrets, tokens, tenant identifiers, and design data.
Examples
For a staging release, load the dedicated staging client and redirect URI from protected configuration, verify a synthetic design callback, and record the redacted result. If the environment resolves to production or shares a credential, fail the deployment rather than falling back.
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| Wrong redirect URI | Environment mismatch | Use per-environment integration |
| Missing secret | Not deployed to env | Add via secret manager |
| Token cross-contamination | Shared token store | Isolate by environment prefix |
| Production guard triggered | Wrong NODE_ENV | Set correct environment variable |
Resources
Next Steps
For observability setup, see canva-observability.