Examples
App elements
Assets and media
Fundamentals
Intents
Design interaction
Drag and drop
Design elements
Localization
Content replacement
Digital asset management
Digital asset management system using Canva app components.
Running this example
To run this example locally:
-
If you haven't already, create a new app in the Developer Portal(opens in a new tab or window). For more information, refer to our Quickstart guide.
-
In your app's configuration on the Developer Portal(opens in a new tab or window), ensure the "Development URL" is set to
http://localhost:8080
. -
Clone the starter kit:
git clone https://github.com/canva-sdks/canva-apps-sdk-starter-kit.gitcd canva-apps-sdk-starter-kitSHELL -
Install dependencies:
npm installSHELL -
Run the example:
npm run start digital_asset_managementSHELL -
Click the Preview URL link shown in the terminal to open the example in the Canva editor.
Example app source code
import type {FindResourcesRequest,FindResourcesResponse,} from "@canva/app-components";import { auth } from "@canva/user";export async function findResources(request: FindResourcesRequest<"folder">,): Promise<FindResourcesResponse> {const userToken = await auth.getCanvaUserToken();// TODO: Update the API path to match your backend// If using the backend example, the URL should be updated to `${BACKEND_HOST}/api/resources/find` to ensure requests are authenticated in productionconst url = new URL(`${BACKEND_HOST}/resources/find`);try {const response = await fetch(url, {method: "POST",headers: {Authorization: `Bearer ${userToken}`,"Content-Type": "application/json",},body: JSON.stringify(request),});const body = await response.json();if (body.resources) {return {type: "SUCCESS",resources: body.resources,continuation: body.continuation,};}return {type: "ERROR",errorCode: body.errorCode || "INTERNAL_ERROR",};} catch {return {type: "ERROR",errorCode: "INTERNAL_ERROR",};}}
TYPESCRIPT
// For usage information, see the README.md file.import { SearchableListView } from "@canva/app-components";import { Box } from "@canva/app-ui-kit";import "@canva/app-ui-kit/styles.css";import { useConfig } from "./config";import { findResources } from "./adapter";import * as styles from "./index.css";export function App() {const config = useConfig();return (<Box className={styles.rootWrapper}><SearchableListViewconfig={config}findResources={findResources}// TODO remove `saveExportedDesign` and `config.export` if your app does not support exporting the Canva design into an external platformsaveExportedDesign={(exportedDesignUrl: string,containerId: string | undefined,designTitle: string | undefined,) => {// TODO update the function to save the design to your platformreturn new Promise((resolve) => {setTimeout(() => {// eslint-disable-next-line no-consoleconsole.info(`Saving file "${designTitle}" from ${exportedDesignUrl} to ${config.serviceName} container id: ${containerId}`,);resolve({ success: true });}, 1000);});}}/></Box>);}
TYPESCRIPT
import * as express from "express";import * as crypto from "crypto";import type { Container, Resource } from "@canva/app-components";/*** Generates a unique hash for a url.* Handy for uniquely identifying an image and creating an image id*/export async function generateHash(message: string) {const msgUint8 = new TextEncoder().encode(message);const hashBuffer = await crypto.subtle.digest("SHA-256", msgUint8);const hashArray = Array.from(new Uint8Array(hashBuffer));const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");return hashHex;}const imageUrls = ["https://images.pexels.com/photos/1495580/pexels-photo-1495580.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2","https://images.pexels.com/photos/3943197/pexels-photo-3943197.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2","https://images.pexels.com/photos/7195267/pexels-photo-7195267.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2","https://images.pexels.com/photos/2904142/pexels-photo-2904142.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2","https://images.pexels.com/photos/5403478/pexels-photo-5403478.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",];export const createDamRouter = () => {const router = express.Router();/*** This endpoint returns the data for your app.*/router.post("/resources/find", async (req, res) => {// You should modify these lines to return data from your// digital asset manager (DAM) based on the findResourcesRequestconst {types,continuation,locale,// other available fields from the `FindResourcesRequest`// containerTypes,// limit,// filters,// query,// sort,// tab,// containerId,// parentContainerType,} = req.body;let resources: Resource[] = [];if (types.includes("IMAGE")) {resources = await Promise.all(Array.from({ length: 40 }, async (_, i) => ({id: await generateHash(i + ""),mimeType: "image/jpeg",name: `My new thing in ${locale}`, // Use the `locale` value from the request if your backend supports i18ntype: "IMAGE",thumbnail: {url: imageUrls[i % imageUrls.length],},url: imageUrls[i % imageUrls.length],})),);}if (types.includes("CONTAINER")) {const containers = await Promise.all(Array.from({ length: 10 },async (_, i) =>({id: await generateHash(i + ""),containerType: "folder",name: `My folder ${i}`,type: "CONTAINER",}) satisfies Container,),);resources = resources.concat(containers);}res.send({resources,continuation: +(continuation || 0) + 1,});});return router;};
TYPESCRIPT
import * as express from "express";import * as cors from "cors";import { createDamRouter } from "./routers/dam";import { createBaseServer } from "../../../../utils/backend/base_backend/create";async function main() {// TODO: Set the CANVA_APP_ID environment variable in the project's .env fileconst APP_ID = process.env.CANVA_APP_ID;if (!APP_ID) {throw new Error(`The CANVA_APP_ID environment variable is undefined. Set the variable in the project's .env file.`,);}const router = express.Router();/*** TODO: Configure your CORS Policy** Cross-Origin Resource Sharing* ([CORS](https://developer.mozilla.org/en-US/docs/Glossary/CORS)) is an* [HTTP](https://developer.mozilla.org/en-US/docs/Glossary/HTTP)-header based* mechanism that allows a server to indicate any* [origins](https://developer.mozilla.org/en-US/docs/Glossary/Origin)* (domain, scheme, or port) other than its own from which a browser should* permit loading resources.** A basic CORS configuration would include the origin of your app in the* following example:* const corsOptions = {* origin: 'https://app-abcdefg.canva-apps.com',* optionsSuccessStatus: 200* }** The origin of your app is https://app-${APP_ID}.canva-apps.com, and note* that the APP_ID should to be converted to lowercase.** https://www.npmjs.com/package/cors#configuring-cors** You may need to include multiple permissible origins, or dynamic origins* based on the environment in which the server is running. Further* information can be found* [here](https://www.npmjs.com/package/cors#configuring-cors-w-dynamic-origin).*/router.use(cors());/*** Add routes for digital asset management.*/const damRouter = createDamRouter();router.use(damRouter);const server = createBaseServer(router);server.start(process.env.CANVA_BACKEND_PORT);}main();
TYPESCRIPT
import type { Config } from "@canva/app-components";import { useIntl } from "react-intl";type ContainerTypes = "folder";export const useConfig = (): Config<ContainerTypes> => {const intl = useIntl();return {serviceName: intl.formatMessage({defaultMessage: "Example App",description:"Name of the service where the app will pull digital assets from",}),search: {enabled: true,filterFormConfig: {containerTypes: ["folder"],filters: [{filterType: "CHECKBOX",label: intl.formatMessage({defaultMessage: "File Type",description: "Label of filters for file type",}),key: "fileType",options: [{ value: "mp4", label: "MP4" },{ value: "png", label: "PNG" },{ value: "jpeg", label: "JPEG" },],allowCustomValue: true,},{filterType: "RADIO",label: intl.formatMessage({defaultMessage: "Size",description: "Label of filters for asset size",}),key: "size",options: [{label: intl.formatMessage({defaultMessage: "Large (800+ px)",description: "One of the filter options for asset size",}),value: "large",},{label: intl.formatMessage({defaultMessage: "Medium (200-799px)",description: "One of the filter options for asset size",}),value: "medium",},],allowCustomValue: true,customValueInputType: "SIZE_RANGE",},{filterType: "RADIO",label: intl.formatMessage({defaultMessage: "Update Date",description: "Label of the filters for asset's update date",}),key: "updateDate",options: [{value: ">now-30m",label: intl.formatMessage({defaultMessage: "Last 30 Minutes",description:"One of the filter options for asset update date",}),},{value: ">now-7d",label: intl.formatMessage({defaultMessage: "Last 7 days",description:"One of the filter options for asset update date",}),},],allowCustomValue: true,customValueInputType: "DATE_RANGE",},{filterType: "RADIO",label: intl.formatMessage({defaultMessage: "Design Status",description: "Label of the filters for asset's design status",}),key: "designStatus",options: [{value: "approved",label: intl.formatMessage({defaultMessage: "Approved",description:"One of the filter options for asset design status",}),},{value: "rejected",label: intl.formatMessage({defaultMessage: "Rejected",description:"One of the filter options for asset design status",}),},{value: "draft",label: intl.formatMessage({defaultMessage: "Draft",description:"One of the filter options for asset design status",}),},],allowCustomValue: true,customValueInputType: "PLAIN_TEXT",},],},},containerTypes: [{value: "folder",label: intl.formatMessage({defaultMessage: "Folders",description: "Name of the asset container type",}),listingSurfaces: [{ surface: "HOMEPAGE" },{surface: "CONTAINER",parentContainerTypes: ["folder"],},{ surface: "SEARCH" },],searchInsideContainer: {enabled: true,placeholder: intl.formatMessage({defaultMessage: "Search for media inside this folder",description: "Placeholder of a search input box",}),},},],sortOptions: [{value: "created_at DESC",label: intl.formatMessage({defaultMessage: "Creation date (newest)",description: "One of the sort options",}),},{value: "created_at ASC",label: intl.formatMessage({defaultMessage: "Creation date (oldest)",description: "One of the sort options",}),},{value: "updated_at DESC",label: intl.formatMessage({defaultMessage: "Updated (newest)",description: "One of the sort options",}),},{value: "updated_at ASC",label: intl.formatMessage({defaultMessage: "Updated (oldest)",description: "One of the sort options",}),},{value: "name ASC",label: intl.formatMessage({defaultMessage: "Name (A-Z)",description: "One of the sort options",}),},{value: "name DESC",label: intl.formatMessage({defaultMessage: "Name (Z-A)",description: "One of the sort options",}),},],layouts: ["MASONRY", "LIST"],resourceTypes: ["IMAGE", "VIDEO", "EMBED"],moreInfoMessage: intl.formatMessage({defaultMessage:"At the moment, we only support images and videos. Corrupted and unsupported files will not appear.",description: "Helper text to explain why some assets are not visible",}),// TODO remove `export` if your app does not support exporting the Canva design into an external platformexport: {enabled: true,// TODO provide a container type that user can choose to save into, or remove this field if user doesn't need to choose a containercontainerTypes: ["folder"],// TODO remove file types that are not supported by your platformacceptedFileTypes: ["png","pdf_standard","jpg","gif","svg","video","pptx",],},};};
TYPESCRIPT
.rootWrapper {height: 100%;overflow-y: hidden;}.centerInPage {display: flex;flex-direction: column;justify-content: center;}
CSS
import { AppUiProvider } from "@canva/app-ui-kit";import { createRoot } from "react-dom/client";import { App } from "./app";import "@canva/app-ui-kit/styles.css";import { AppI18nProvider } from "@canva/app-i18n-kit";const root = createRoot(document.getElementById("root") as Element);function render() {root.render(<AppI18nProvider><AppUiProvider><App /></AppUiProvider></AppI18nProvider>,);}render();if (module.hot) {module.hot.accept("./app", render);}
TYPESCRIPT
# Digital asset managementDemonstrates how to integrate with external digital asset management systems using SearchableListView. Shows patterns for browsing, searching, and importing assets from third-party platforms while supporting design export.For API reference docs and instructions on running this example, see: https://www.canva.dev/docs/apps/examples/digital-asset-management/.Related examples: See asset_upload for direct asset upload patterns, or content_replacement examples for asset substitution workflows.NOTE: This example differs from what is expected for public apps to pass a Canva review:- Console.log statements are used for debugging purposes but should be replaced with proper error handling and logging in production apps- ESLint rule `no-console` is disabled for example purposes only. Production apps should not disable linting rules without proper justification- Mock data and placeholder functions are used for demonstration purposes only. Production apps should implement proper authentication, real API integration, and compliance with external platform terms of service- Error handling is simplified for demonstration. Production apps must implement comprehensive error handling with clear user feedback and graceful failure modes- Internationalization is not implemented. Production apps must support multiple languages using the `@canva/app-i18n-kit` package to pass Canva review requirements
MARKDOWN
# Setup## Step 1: Set the `APP_ID` environment variable1. Get the ID of your app.1. Log in to the [Developer Portal](https://www.canva.com/developers/).2. Navigate to the [Your apps](https://www.canva.com/developers/apps) page.3. Copy the ID from the **App ID** column in the apps table.2. Open the starter kit's [.env file](../../.env).3. Set the `APP_ID` environment variable to the ID of the app.## Step 2: Run the development servers1. Navigate into the starter kit:```bashcd canva-apps-sdk-starter-kit```1. Run the following command:```bashnpm start digital_asset_management```This will launch one development server for the frontend and backend.1. Navigate to your app at `https://www.canva.com/developers/apps`, and click **Preview** to preview the app.1. If your app requires authentication with a third party service, continue to Step 3.Otherwise, you can make requests to your service via [./backend/server.ts](./backend/server.ts) inside `"/resources/find"`.## Step 3: (optional) Configure ngrokIf your app requires authentication with a third party service,your server needs to be exposed via a publicly available URL, so that Canva can send requests to it. This step explains how to do this with [ngrok](https://ngrok.com/).**Note:** ngrok is a useful tool, but it has inherent security risks, such as someone figuring out the URL of your server and accessing proprietary information. Be mindful of the risks, and if you're working as part of an organization, talk to your IT department.You must replace ngrok urls with hosted API endpoints for production apps.To use ngrok, you'll need to do the following:1. Sign up for a ngrok account at <https://ngrok.com/>.2. Locate your ngrok [authtoken](https://dashboard.ngrok.com/get-started/your-authtoken).3. Set an environment variable for your authtoken, using the command line. Replace `<YOUR_AUTH_TOKEN>` with your actual ngrok authtoken:For macOS and Linux:```bashexport NGROK_AUTHTOKEN=<YOUR_AUTH_TOKEN>```For Windows PowerShell:```shell$Env:NGROK_AUTHTOKEN = "<YOUR_AUTH_TOKEN>"```This environment variable is available for the current terminal session, so the command must be re-run for each new session. Alternatively, you can add the variable to your terminal's default parameters.## Step 4: Run the development server with ngrok and add authentication to the appThese steps demonstrate how to start the local development server with ngrok.From the `canva-apps-sdk-starter-kit` directory1. Stop any running scripts, and run the following command to launch the backend and frontend development servers. The `--ngrok` parameter exposes the backend server via a publicly accessible URL.```bashnpm start digital_asset_management --ngrok```2. After ngrok is running, copy your ngrok URL(e.g. <https://0000-0000.ngrok-free.app>) to the clipboard.1. Go to your app in the [Developer Portal](https://www.canva.com/developers/apps).2. Navigate to the "Add authentication" section of your app.3. Check "This app requires authentication"4. In the "Redirect URL" text box, enter your ngrok URL followed by `/redirect-url` e.g.<https://0000-0000.ngrok-free.app/redirect-url>5. In the "Authentication base URL" text box, enter your ngrok URL followed by `/` e.g.<https://0000-0000.ngrok-free.app/>Note: Your ngrok URL changes each time you restart ngrok. Keep these fields up todate to ensure your example authentication step will run.3. Make sure the app is authenticating users by making the following changes:1. Replace`router.post("/resources/find", async (req, res) => {`with`router.post("/api/resources/find", async (req, res) => {`in [./backend/server.ts](./backend/server.ts). Adding `/api/` to the route ensuresthe JWT middleware authenticates requests.2. Replace``const url = new URL(`${BACKEND_HOST}/resources/find`);``with``const url = new URL(`${BACKEND_HOST}/api/resources/find`);``in [./adapter.ts](./adapter.ts)3. Comment out these lines in [./app.tsx](./app.tsx)```typescript// Comment this next line out for production appssetAuthState("authenticated");```4. Navigate to your app at `https://www.canva.com/developers/apps`, and click **Preview** to preview the app.1. A new screen will appear asking if you want to authenticate.Press **Connect** to start the authentication flow.2. A ngrok screen may appear. If it does, select **Visit Site**3. An authentication popup will appear. For the username, enter `username`, andfor the password enter `password`.4. If successful, you will be redirected back to your app.5. You can now modify the `/redirect-url` function in `server.ts` to authenticate with your third-partyasset manager, and `/api/resources/find` to pull assets from your third-party asset manager.See `https://www.canva.dev/docs/apps/authenticating-users/` for more details.
MARKDOWN
API Reference
Need Help?
- Join our Community Forum(opens in a new tab or window)
- Report issues with this example on GitHub(opens in a new tab or window)