Digital asset management

Digital asset management system using Canva app components.

Running this example

To run this example locally:

  1. 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.

  2. 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.

  3. Clone the starter kit:

    git clone https://github.com/canva-sdks/canva-apps-sdk-starter-kit.git
    cd canva-apps-sdk-starter-kit
    SHELL
  4. Install dependencies:

    npm install
    SHELL
  5. Run the example:

    npm run start digital_asset_management
    SHELL
  6. 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 production
const 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}>
<SearchableListView
config={config}
findResources={findResources}
// TODO remove `saveExportedDesign` and `config.export` if your app does not support exporting the Canva design into an external platform
saveExportedDesign={(
exportedDesignUrl: string,
containerId: string | undefined,
designTitle: string | undefined,
) => {
// TODO update the function to save the design to your platform
return new Promise((resolve) => {
setTimeout(() => {
// eslint-disable-next-line no-console
console.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 findResourcesRequest
const {
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 i18n
type: "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 file
const 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 platform
export: {
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 container
containerTypes: ["folder"],
// TODO remove file types that are not supported by your platform
acceptedFileTypes: [
"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 management
Demonstrates 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 variable
1. 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 servers
1. Navigate into the starter kit:
```bash
cd canva-apps-sdk-starter-kit
```
1. Run the following command:
```bash
npm 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 ngrok
If 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:
```bash
export 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 app
These steps demonstrate how to start the local development server with ngrok.
From the `canva-apps-sdk-starter-kit` directory
1. 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.
```bash
npm 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 to
date 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 ensures
the 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 apps
setAuthState("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`, and
for 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-party
asset 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?