Skip to content

Using a Backend

Overview

How to integrate an app with a backend.

At their most basic, apps are web pages embedded in the Canva editor. As a result, they have access to standard web technologies, such as the Fetch API. This allows them to integrate with backend services and APIs.

Why use a backend?

You can make great apps without a backend, but there are a number of reasons to use one, such as:

  • Fetching content from a third-party server, such as images or videos.
  • Running tasks that are difficult to run in the browser, such as generative AI tasks.
  • Tracking a user's activities to better understand how they're using your app.

Note: An app requires a backend for certain features, such as authentication.

Setting up a backend

In the starter kit, we've provided an example backend, built with Express.js. Some of the example apps rely on this backend, but it's only intended to illustrate certain concepts — not as a production-ready solution.

Fortunately, Canva doesn't require backends to be built in any particular way. You can use any combination of language or framework, and if you already have a backend up and running, you can adapt it to work with your app.

Sending requests

To send a request to an app's backend, use the Fetch API as you would on any other web page:

ts
const response = await fetch("http://localhost:3000/hello-world");
console.log(response.status);

The backend can respond with whatever data the frontend needs. The requests don't pass through Canva's servers, so there's no particular format that Canva requires.

Warning: If the app's backend is not configured to receive requests from the app's frontend, the requests will fail. This is a security feature built into web browsers. To learn more, see Cross-Origin Resource Sharing.

Sending JSON

If you're sending JSON to the app's backend, include the Content-Type header and stringify the body:

ts
const response = await fetch("http://localhost:3000/hello-world", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ message: "Hello world" }),
});

console.log(response.status);

This is standard usage of the Fetch API, but it's easy to forget.

Developing locally

When developing an app, the backend does not always need to be available via the public internet. You can run the backend on your local machine and send requests directly to a localhost URL.

There are, however, some exceptions:

  • When implementing authentication, the backend must expose certain endpoints via the public internet.
  • When uploading assets, the assets themselves must be available via the public internet.

To make endpoints or resources available to the public internet, either:

  • Deploy the endpoints to a testing or staging environment.
  • Use an SSH tunneling tool, such as ngrok or localtunnel.

Warning: There's an inherent risk in using SSH tunnels. Be mindful about what you're exposing to the public internet and shut down tunnels when they're not in use.

Security requirements

For the sake of security, apps must verify HTTP requests. This ensures that the backend only accepts requests from known and trusted sources. This is also how apps can access user data, such as the ID of the user.

To learn more, see HTTP request verification.

Using Design IDs

How to store and retrieve data against a user's design.

Sometimes, apps need to associate data with a user's design. For example, an app could present the user with settings that persist on a per-design basis. To allow for this, apps can use the Apps SDK to access the ID of the current design.

Warning: If your app uses design IDs, we strongly encourage you to follow our Security guidelines.

Step 1: Get a design and user token

For security reasons, Canva uses JSON Web Tokens (JWTs) to encode certain information. To access this information, apps must decode and verify the JWTs.

In the Apps SDK, there are two types of tokens:

  • Design tokens
  • User tokens

Design tokens encode information about the current design, such as the ID of the design, while user tokens encode information about the current user, such as the ID of the user and their team.

Your app needs to use both tokens to securely store data against a user's design.

To get a user token:

  1. Import the auth constant from the @canva/user package:

    tsx
    import { auth } from "@canva/user";
  2. Call the getCanvaUserToken method:

    tsx
    const userToken = await auth.getCanvaUserToken();

To get a design token:

  1. Import the getDesignToken method from the @canva/design package:

    tsx
    import { getDesignToken } from "@canva/design";
  2. Call the getDesignToken method:

    tsx
    const designToken = await getDesignToken();

Step 2: Send the tokens to the app's backend

For security reasons, apps must decode and verify tokens through their backend — never through the frontend.

In the same request, the app should also send whatever data it wants to store against the user's design, such as any settings the user has configured through the app's frontend. The shape of this data is highly dependent on the behavior of the app and isn't specific to the Apps SDK, so it's not demonstrated here.

The following code snippet demonstrates how an app can send tokens to a backend:

tsx
const response = await fetch(
  `http://localhost:3001/my/api/endpoint?designToken=${designToken}`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${userToken}`,
    },
  }
);

In this case, the design token is sent as a query parameter, but this isn't a strict requirement. You could send the token in some other way, such as in the body of a POST request.

The user token must always be sent as an Authorization header. Learn more about HTTP request verification.

Step 3: Verify the tokens

After the backend receives the tokens, it must decode and verify them before it can access the encoded data. For a step-by-step walkthrough of how to do this, see JSON Web Tokens.

<Note> For Node.js backends: We recommend using the @canva/app-middleware package, which provides built-in JWT verification, JWKS caching, and error handling. This package simplifies the verification process and follows security best practices. </Note>

Installation

shell
npm install @canva/app-middleware

Express.js middleware

For Express.js applications, use the built-in middleware:

typescript
import express from "express";
import { design, tokenExtractors } from "@canva/app-middleware/express";

const app = express();

// Design token middleware requires explicit token extraction
app.post(
  "/my/api/design",
  design.verifyToken({
    appId: process.env.CANVA_APP_ID,
    tokenExtractor: tokenExtractors.fromQuery("designToken"),
  }),
  (req, res) => {
    // Access verified design information
    const { designId, appId } = req.canva.design;

    // Your application logic here

    res.sendStatus(200);
  }
);

app.listen(process.env.PORT || 3000);

NOTE: Design tokens require an explicit tokenExtractor parameter. The available extractors are tokenExtractors.fromQuery(), tokenExtractors.fromCookie(), and tokenExtractors.fromBearerAuth().

Framework-agnostic function

For other Node.js environments (Next.js, AWS Lambda, Cloudflare Workers, and so on):

Manually verifying JWTs

If you're using a non-Node.js backend, you can manually verify JWTs. The following steps show how to implement JWT verification from scratch in TypeScript, which you can then adapt to your preferred language or framework.

Step 4: Verify the tokens

Use the public key to verify both the user token and design token:

ts
const verifiedUserToken = jwt.verify("USER_JWT_GOES_HERE", publicKey, {
  audience: "YOUR_APP_ID",
});

const verifiedDesignToken = jwt.verify("DESIGN_JWT_GOES_HERE", publicKey, {
  audience: "YOUR_APP_ID",
});

The exact syntax will depend on the library you're using.

If the tokens are valid, the verified tokens will be dictionaries. The user token will contain the following properties:

  • aud - The ID of the app.
  • brandId - The ID of the user's team.
  • userId - The ID of the user.

The design token will contain the following properties:

  • aud - The ID of the app.
  • designId - The ID of the current design.

If these properties aren't available, it means the tokens or public key are invalid:

tsx
if (!verifiedUserToken.aud || !verifiedUserToken.brandId || !verifiedUserToken.userId) {
  throw new Error("The user token is not valid");
}

if (!verifiedDesignToken.aud || !verifiedDesignToken.designId) {
  throw new Error("The design token is not valid");
}

Step 4: Store data against the design

After the app's backend verifies the tokens, it will have access to:

  • The ID of the design
  • The ID of the user
  • The ID of the user's team

The backend can then use the combination of these properties to store data that's linked to the design. The key word here is combination, as it's important to note that:

  • A design may have multiple users collaborating on it.
  • A user may belong to multiple teams.

Therefore, any data shouldn't only be linked with the ID of the design, as this would allow data to be leaked between users or between teams. The data should be linked with the ID of the design, the user, and the user's team.

This means a database table containing data linked to a design would likely have the following columns:

  • design_id
  • team_id

But the exact implementation details may be different.

Step 5: Retrieve data for the design

To retrieve data linked with a design, repeat the previous steps but for an endpoint that performs a read operation instead of a write operation. Be sure the data is scoped to the ID of the design, the user, and the user's team.

Security guidelines

To ensure that data is always linked with the correct design, user, and team, follow these guidelines:

  • Decode and verify tokens through the backend. Your app should never attempt to decode and verify tokens through its frontend. To learn more, see JSON Web Tokens.
  • Get fresh tokens from Canva before sending the tokens to the app's backend. Don't attempt to cache or reuse tokens across multiple HTTP requests.
  • Send tokens to an app's backend with any data relevant to the request, such as data required to perform a read or write operation related to a particular design. Don't send tokens and relevant data in separate requests.

The following code sample demonstrates what these guidelines look like in practice:

tsx
import { auth } from "@canva/user";
import { getDesignToken } from "@canva/design";

// Get fresh tokens before every request
const designToken = await getDesignToken();
const userToken = await auth.getCanvaUserToken();

// Send tokens to the app's backend
const response = await fetch(
  `http://localhost:3001/my/api/endpoint?designToken=${designToken}`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${userToken}`,
      "Content-Type": "application/json",
    },
    // Include relevant data in the same request as the tokens
    body: JSON.stringify({
      name: "David",
      age: 33,
      location: "Australia",
    }),
  }
);

Querying

Reading and updating content in a user's design.

Apps can query the content in a user's design, similar to how the HTML DOM can be queried. They can then read and update the queried content, which unlocks a range of powerful use-cases.

Content is not the same as an element in a design. However, they are related because elements contain content. An app can operate on content through content querying, but needs design editing to position elements and set element dimensions. Even if design editing can reach content in some instances, content querying operates consistently on the content contained within elements. See Design Editing API for more information, and the Design Editing API guidelines.

Content types

There are different types of content that may appear in a design, including:

  • Images
  • Text, including plaintext and richtext
  • Videos

For the time being, richtext is the only content type that's compatible with querying.

In the future, other content types will be supported.

Targets

When an app queries a design, it must specify a target. This determines what part of the design to query. For the time being, the only supported target is the current page. In the future, other targets will be supported.

Reading content

Apps can register a callback for any combination of the supported content types and targets:

tsx
import { editContent } from "@canva/design";

// Read and edit richtext content on current page
await editContent(
  {
    contentType: "richtext",
    target: "current_page",
  },
  async (session) => {
    console.log(session.contents);
  }
);

This callback receives a session object that contains both a sync method for managing changes and a contents property containing an array of content items that can be read or modified.

A single design may contain multiple pieces of content. You'll typically want to loop through the individual content items:

tsx
import { editContent } from "@canva/design";

// Read and edit richtext content on current page
await editContent(
  {
    contentType: "richtext",
    target: "current_page",
  },
  async (session) => {
    // Loop through content items
    for (const content of session.contents) {
      console.log(content);
    }
  }
);

Each content item exposes methods and properties for reading and updating the content. The available methods and properties depend on the type of content.

In the case of richtext content, each item is a richtext range:

tsx
import { editContent } from "@canva/design";

// Read and edit richtext content on current page
await editContent(
  {
    contentType: "richtext",
    target: "current_page",
  },
  async (session) => {
    // Loop through content items
    for (const content of session.contents) {
      // Each content item is a richtext range
      const regions = content.readTextRegions();
      console.log(regions);
    }
  }
);

To learn more, see Richtext ranges.

Updating content

Each content item exposes methods and properties that can be used to update the queried content:

tsx
import { editContent } from "@canva/design";

await editContent(
  {
    contentType: "richtext",
    target: "current_page",
  },
  async (session) => {
    // Loop through each content item
    for (const content of session.contents) {
      // Get the richtext content as plaintext
      const plaintext = content.readPlaintext();

      // Format the richtext
      content.formatParagraph(
        { index: 0, length: plaintext.length },
        { fontWeight: "bold" }
      );
    }

    // Sync the content so that it's reflected in the design
    await session.sync();
  }
);

But before a change is reflected in the user's design, the session must be synced:

tsx
import { editContent } from "@canva/design";

await editContent(
  {
    contentType: "richtext",
    target: "current_page",
  },
  async (session) => {
    // Loop through each content item
    for (const content of session.contents) {
      // Get the richtext content as plaintext
      const plaintext = content.readPlaintext();

      // Format the richtext
      content.formatParagraph(
        { index: 0, length: plaintext.length },
        { fontWeight: "bold" }
      );
    }

    // Sync the content so that it's reflected in the design
    await session.sync();
  }
);

Syncing a session:

  • Commits the changes to the user's design.
  • Updates the content items so they contain the modified content.

Be aware that:

  • Any unsynced changes will be discarded.
  • A synced session will not contain new content items that have been created since the editContent method was called. To access new content items, an app must make another call to editContent.
  • When editing content, there's a one-minute timeout to avoid the snapshot of the content from becoming too stale. This timeout is reset when the session is synced, so we recommend syncing the session as often as possible.

Handling conflicts

If queried content is changed after the editContent method is called but before the session is synced, it can lead to conflicts between the state of the content and the true state of the design.

Canva will attempt to resolve conflicts, but we can't make any guarantees about how conflicts will be resolved. To minimize the risk of unexpected behavior, we recommend syncing the session as often as possible. This will reduce the size and complexity of conflicts, leading to more predictable behavior.

There's no limit to how many times a session can be synced.

Handling deleted content

If a queried content item is deleted after the editContent method is called, the deleted content will continue to exist within the content array, even after the session is synced.

To identify deleted content, each content item has a deleted property that is either true or false. You can use this property to avoid operating on content that no longer exists in the user's design:

tsx
import { editContent } from "@canva/design";

// Read and edit richtext content on current page
await editContent(
  {
    contentType: "richtext",
    target: "current_page",
  },
  async (session) => {
    // Loop through the individual content items
    for (const content of session.contents) {
      // Ignore deleted content
      if (content.deleted) {
        continue;
      }

      // Get the richtext content as plaintext
      const plaintext = content.readPlaintext();

      // Format the richtext
      content.formatParagraph(
        { index: 0, length: plaintext.length },
        { fontWeight: "bold" }
      );
    }

    // Sync the content so that it's reflected in the design
    await session.sync();
  }
);

Gotchas

  • You can't use querying to read or update the position of elements on a page. You can only use querying to read and update the content of a design, not its structure. Use the Design Editing features to reposition design elements.
  • In content arrays, the order of the content items is not guaranteed. This means an app shouldn't rely on the order of the items being consistent or predictable.
  • You can't call the editContent method from within the callback of another editContent method — that is, the calls can't be nested. If you try to do this, an error will be thrown.

Example

tsx
import { Button, Rows } from "@canva/app-ui-kit";
import { editContent } from "@canva/design";
import * as styles from "styles/components.css";

export function App() {
  async function handleClick() {
    // Read and edit richtext content on current page
    await editContent(
      {
        contentType: "richtext",
        target: "current_page",
      },
      async (session) => {
        // Loop through each content item
        for (const content of session.contents) {
          // Get the richtext content as plaintext
          const plaintext = content.readPlaintext();

          // Format the richtext
          content.formatParagraph(
            { index: 0, length: plaintext.length },
            { fontWeight: "bold" }
          );
        }

        // Sync the content so that it's reflected in the design
        await session.sync();
      }
    );
  }

  return (
    <div className={styles.scrollContainer}>
      <Rows spacing="2u">
        <Button variant="primary" onClick={handleClick}>
          Update richtext content
        </Button>
      </Rows>
    </div>
  );
}

Canva Developer Documentation SOP Site