---
title: "Launch the Simple Editor on your website | Customer&#x27;s Canvas Hub"
canonical: "https://customerscanvas.com/docs/hub/dev-center/frontend-integration/workflow-elements/simple-editor/quick-start/"
---

# Launch the Simple Editor on your website

> This tutorial walks you through building a small Vanilla TypeScript app that opens the Simple Editor for a configured Customer's Canvas product and submits the customization result for rendering. A small Express backend keeps the OAuth2 client credentials, the access token, and the storefront user token on the server, so the browser receives only the data needed to initialize the editor. The result is a sandbox project for experiments with Customer's Canvas Hub APIs and SDKs.

## Prerequisites

- A Customer's Canvas Hub tenant [prepared for frontend integration](https://customerscanvas.com/docs/hub/dev-center/frontend-integration/prepare-for-frontend-integration.md)
- The collected `tenantId`, `storefrontId`, OAuth2 client credentials, and a test `productId`
- A workflow of the Workflow Elements type with the `simple-editor` component assigned to the product (see [Workflow Elements Overview](https://customerscanvas.com/docs/hub/admin-guide/workflows/workflow-elements.md))
- Node.js 18 or later

## Set up the project

1. Create a project folder.

   ```bash
   mkdir simple-editor-quick-start
   cd simple-editor-quick-start
   ```

2. Create a Vite app.

   ```bash
   npm create vite@latest . -- --template vanilla-ts
   ```

3. Install the backend dependencies.

   ```bash
   npm install @aurigma/axios-storefront-api-client axios dotenv express
   npm install -D @types/express @types/node concurrently tsx
   ```

4. Create `vite.config.ts` in the project root.

   ```typescript
   import { defineConfig } from "vite";

   export default defineConfig({
     server: {
       host: "127.0.0.1",
       proxy: {
         "/api": "http://127.0.0.1:3001",
       },
     },
   });
   ```

   Vite runs on `127.0.0.1` and forwards browser requests from `/api/*` to the local backend.

5. Update the `dev` script in `package.json`.

   ```json
   {
     "scripts": {
       "dev": "concurrently vite \"tsx watch server/server.ts\""
     }
   }
   ```

   The `dev` script starts Vite and the Express backend together. Leave the other Vite scripts unchanged.

   If you run `npm run dev` now, it fails because the backend files don't exist yet.

## Configure Customer's Canvas connection

1. Create an `.env` file in the project root.

2. Add your Customer's Canvas values.

   ```bash
   CCHUB_BASE_URL=https://customerscanvashub.com
   CCHUB_API_GATEWAY_URL=https://api.customerscanvashub.com
   CCHUB_TENANT_ID=<tenant_id>
   CCHUB_STOREFRONT_ID=<storefront_id>
   CCHUB_CLIENT_ID=<client_id>
   CCHUB_CLIENT_SECRET=<client_secret>
   ```

   Use the EU or AU URLs when your tenant runs in another region. Keep `CCHUB_CLIENT_SECRET` only on the backend. Do not expose it in `VITE_*` variables or browser code.

## Load the editor on the page

Open `index.html`, add the Simple Editor script and styles to the `<head>`, and place the `<au-simple-editor>` tag where the editor must appear.

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Simple Editor quick start</title>
    <!-- The environment segment must match your tenant: us, eu, or au. -->
    <link
      href="https://staticjs-aurigma.azureedge.net/libs/us/workflow-elements/simple-editor/styles.css"
      rel="stylesheet"
    />
    <script
      defer
      src="https://staticjs-aurigma.azureedge.net/libs/us/workflow-elements/simple-editor/index.js"
    ></script>
    <link rel="stylesheet" href="/src/style.css" />
  </head>
  <body>
    <main id="status">Loading the Simple Editor...</main>
    <au-simple-editor></au-simple-editor>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
```

The component tag is known in advance, so the page loads the editor statically. For the dynamic loading approach, refer to [Workflow Elements](https://customerscanvas.com/docs/hub/dev-center/frontend-integration/workflow-elements/workflow-element-basics.md).

## Add the backend

1. Create a `server` folder in the project root.

2. Create `server/cchub-auth.ts`.

3. Add the Customer's Canvas connection and OAuth2 helper.

   ```typescript
   import axios from "axios";
   import dotenv from "dotenv";

   dotenv.config();

   type AuthResponse = {
     access_token: string;
     expires_in: number;
     token_type: "Bearer";
     scope: string;
   };

   export const cchubEnv = {
     baseUrl: requireEnv("CCHUB_BASE_URL"),
     apiGatewayUrl: requireEnv("CCHUB_API_GATEWAY_URL"),
     tenantId: Number(requireEnv("CCHUB_TENANT_ID")),
     storefrontId: Number(requireEnv("CCHUB_STOREFRONT_ID")),
     clientId: requireEnv("CCHUB_CLIENT_ID"),
     clientSecret: requireEnv("CCHUB_CLIENT_SECRET"),
   };

   let cachedAccessToken: { value: string; expiresAt: number } | null = null;

   export async function getAccessToken(): Promise<string> {
     if (cachedAccessToken !== null && cachedAccessToken.expiresAt > Date.now()) {
       return cachedAccessToken.value;
     }

     const body = new URLSearchParams({
       client_id: cchubEnv.clientId,
       client_secret: cchubEnv.clientSecret,
       grant_type: "client_credentials",
     });

     const { data } = await axios.post<AuthResponse>(
       `${cchubEnv.baseUrl}/connect/token`,
       body,
       {
         headers: {
           "Content-Type": "application/x-www-form-urlencoded",
         },
       }
     );

     cachedAccessToken = {
       value: data.access_token,
       expiresAt: Date.now() + (data.expires_in - 60) * 1000,
     };

     return data.access_token;
   }

   function requireEnv(name: string): string {
     const value = process.env[name];

     if (value === undefined || value === "") {
       throw new Error(`Missing ${name} environment variable.`);
     }

     return value;
   }
   ```

   This module keeps OAuth2 client credentials on the backend, requests an access token through the Client Credentials flow, and caches the token until shortly before expiration.

4. Create `server/cchub-api.ts`.

5. Add Storefront API operations.

   ```typescript
   import {
     ApiClientConfiguration,
     ProjectsApiClient,
     ProductsApiClient,
     ProjectItemProductType,
     StorefrontUsersApiClient,
   } from "@aurigma/axios-storefront-api-client";
   import type {
     CreateSingleItemProjectDto,
     CreateStorefrontUserDto,
   } from "@aurigma/axios-storefront-api-client";
   import { cchubEnv, getAccessToken } from "./cchub-auth";

   export type LineItem = {
     key: string;
     quantity: number;
     productId?: number;
     productVariantId?: number;
     productVersionId?: number;
     productLinkId?: number;
     sku?: string;
     properties: {
       _stateId: string[];
       _userId: string;
     };
   };

   export type WorkflowElementConfig = {
     component?: string;
     tagName?: string;
     configVersion?: number;
     resources?: Record<string, any>;
     settings?: Record<string, any>;
     localization?: Record<string, any>;
   };

   export async function getStorefrontUserToken(
     userId: string
   ): Promise<string> {
     const accessToken = await getAccessToken();
     const usersApi = new StorefrontUsersApiClient(createApiConfig(accessToken));

     try {
       const user: CreateStorefrontUserDto = {
         storefrontUserId: userId,
         isAnonymous: true,
       };

       await usersApi.create(cchubEnv.storefrontId, undefined, user);
     } catch (error) {
       if (getErrorStatus(error) !== 409) {
         throw new Error(`Cannot create storefront user. User ID = ${userId}.`, {
           cause: error,
         });
       }
     }

     try {
       return await usersApi.getToken(userId, cchubEnv.storefrontId);
     } catch (error) {
       throw new Error(`Cannot get storefront user token. User ID = ${userId}.`, {
         cause: error,
       });
     }
   }

   export async function getSimpleEditorWorkflowConfig(
     productId: number
   ): Promise<WorkflowElementConfig> {
     const accessToken = await getAccessToken();
     const productsApi = new ProductsApiClient(createApiConfig(accessToken));

     let content: any = "";

     try {
       const workflow = await productsApi.getPersonalizationWorkflow(
         productId,
         undefined,
         undefined,
         undefined,
         cchubEnv.tenantId
       );

       content = workflow.content;
     } catch (error) {
       throw new Error(`Cannot get product workflow. Product ID = ${productId}.`, {
         cause: error,
       });
     }

     if (content == null || content.length == 0) {
       throw new Error("The product personalization workflow is empty.");
     }

     return findSimpleEditorConfig(JSON.parse(content));
   }

   export async function createProject(lineItem: LineItem) {
     const accessToken = await getAccessToken();
     const projectsApi = new ProjectsApiClient(createApiConfig(accessToken));

     const body: CreateSingleItemProjectDto = {
       ownerId: lineItem.properties._userId,
       item: {
         productSpecifier: {
           type: ProjectItemProductType.Product,
           id: lineItem.productId,
           versionId: lineItem.productVersionId,
           variantId: lineItem.productVariantId,
         },
         designIds: lineItem.properties._stateId,
         sku: lineItem.sku,
         quantity: lineItem.quantity,
       },
     };

     try {
       return await projectsApi.createWithSingleItem(
         cchubEnv.storefrontId,
         undefined,
         body
       );
     } catch (error) {
       throw new Error("Cannot create a project from the Simple Editor result.", {
         cause: error,
       });
     }
   }

   function findSimpleEditorConfig(content: any): WorkflowElementConfig {
     const configs = Array.isArray(content) ? content : [content];
     const config = configs.find(x => x.component === "simple-editor");

     if (!config) {
       throw new Error("The product workflow does not contain the Simple Editor.");
     }

     return config;
   }

   export function getErrorStatus(error: any): number | undefined {
     return error?.response?.status
       ?? error?.status
       ?? (error instanceof Error ? getErrorStatus(error.cause) : undefined);
   }

   function createApiConfig(accessToken: string): ApiClientConfiguration {
     const config = new ApiClientConfiguration();
     config.apiUrl = cchubEnv.apiGatewayUrl;
     config.setAuthorizationToken(accessToken);

     return config;
   }
   ```

   This module registers a storefront user if needed, reads the personalization workflow assigned to the product through the [`GET /api/storefront/v1/products/{id}/personalization-workflow`](https://customerscanvas.com/docs/hub/dev-center/api/storefront.md#tag/products/GetPersonalizationWorkflow) endpoint, extracts the Simple Editor configuration, and creates a project from the line item returned by the Simple Editor.

6. Create `server/server.ts`.

7. Add the Express routes.

   ```typescript
   import express from "express";
   import {
     createProject,
     getErrorStatus,
     getSimpleEditorWorkflowConfig,
     getStorefrontUserToken,
     type LineItem,
   } from "./cchub-api";
   import { cchubEnv } from "./cchub-auth";

   const app = express();
   app.use(express.json());

   app.get("/", (_request, response) => {
     response.send("Simple Editor quick start backend is running.");
   });

   app.get("/api/editor-session", async (request, response) => {
     const userId = String(request.query.userId ?? "quick-start-user");
     const productId = Number(request.query.productId);

     if (!Number.isInteger(productId) || productId <= 0) {
       response
         .status(400)
         .json({ message: "The productId query parameter is required." });
       return;
     }

     try {
       const userToken = await getStorefrontUserToken(userId);
       const workflowConfig = await getSimpleEditorWorkflowConfig(productId);

       response.json({
         config: {
           ...workflowConfig,
           configVersion: 2,
           input: {
             productId,
           },
           integration: {
             tenantId: cchubEnv.tenantId,
             storefrontId: cchubEnv.storefrontId,
             cchubUrl: cchubEnv.baseUrl,
             cchubApiGatewayUrl: cchubEnv.apiGatewayUrl,
             user: {
               id: userId,
               token: userToken,
             },
           },
           resources: workflowConfig.resources ?? {},
           settings: workflowConfig.settings ?? {},
           localization: workflowConfig.localization ?? {
             language: "en",
           },
         },
       });
     } catch (error) {
       sendError(response, error);
     }
   });

   app.post("/api/projects", async (request, response) => {
     try {
       const lineItem = request.body as LineItem;
       const project = await createProject(lineItem);

       response.json(project);
     } catch (error) {
       sendError(response, error);
     }
   });

   app.listen(3001, () => {
     console.log("Backend is running at http://localhost:3001");
   });

   function sendError(response: express.Response, error: any): void {
     const status = getErrorStatus(error) ?? 500;
     const cause =
       error instanceof Error && error.cause != null
         ? ` ${String(error.cause)}`
         : "";
     const message =
       error instanceof Error ? `${error.message}${cause}` : "Unexpected error";

     response.status(status).json({ message });
   }
   ```

   It implements HTTP endpoints which work with Customer's Canvas Hub API to prepare the editor configuration and send the user's customization to the server for rendering.

8. Run the dev script.

   ```bash
   npm run dev
   ```

   The terminal shows a Vite local URL and the backend message:

   ```bash
   Backend is running at http://localhost:3001
   ```

   Stop the command after both processes start.

## Add the page code

1. Replace the content of `src/main.ts`. Replace `2502` with your product ID.

   ```typescript
   const productId = 2502; // Replace with your product ID.

   await customElements.whenDefined("au-simple-editor");

   const editor = document.getElementsByTagName("au-simple-editor").item(0) as HTMLElement & {
     init(config: any): Promise<void>;
   };

   const session = await getEditorSession("quick-start-user", productId);

   editor.addEventListener("addtocart", async (event: any) => {
     const lineItem = event.detail;
     const project = await createProject(lineItem);
     console.log("Created project", project);
   });

   await editor.init(session.config);
   document.getElementById("status")?.remove();

   type EditorSession = {
     config: any;
   };

   async function getEditorSession(
     userId: string,
     productId: number
   ): Promise<EditorSession> {
     const params = new URLSearchParams({
       userId,
       productId: String(productId),
     });

     const response = await fetch(
       `/api/editor-session?${params.toString()}`
     );

     if (!response.ok) {
       throw new Error(await response.text());
     }

     return response.json();
   }

   async function createProject(lineItem: any) {
     const response = await fetch("/api/projects", {
       method: "POST",
       headers: {
         "Content-Type": "application/json",
       },
       body: JSON.stringify(lineItem),
     });

     if (!response.ok) {
       throw new Error(await response.text());
     }

     return response.json();
   }
   ```

   The page fetches the editor session from the backend and passes its `config` to `init()`. The OAuth2 client secret, the access token, and the storefront user token stay on the backend. In a real storefront, set `productId` from the current product page, route, or catalog state.

2. Replace the content of `src/style.css`.

   ```css
   html,
   body {
     height: 100%;
     margin: 0;
   }

   au-simple-editor {
     display: block;
     height: 100%;
   }

   #status {
     max-width: 720px;
     padding: 32px;
     font-family: system-ui, sans-serif;
   }
   ```

   The Simple Editor fills the browser viewport.

## Run the app

1. Start the frontend and backend.

   ```bash
   npm run dev
   ```

2. Open the local URL printed by Vite.

   The Simple Editor loads the product from the `productId` value in `src/main.ts`.

3. Finish the customization flow and select **Add to cart**.

   The browser console prints the created project object.

## Validate the result

Make sure that:

- The design preview loads on the left without errors in the browser console.
- The input fields and product options appear on the right and update the preview when you change them.
- After selecting **Add to cart**, the console prints an object with the `properties._stateId` array containing the saved design ID.

Check the project status and download the rendered result by visiting **Projects** in Customer's Canvas BackOffice.

## Next steps

- [Configure the editor appearance and behavior](https://customerscanvas.com/docs/hub/dev-center/frontend-integration/workflow-elements/simple-editor/settings.md)
- [Embed custom content with slots](https://customerscanvas.com/docs/hub/dev-center/frontend-integration/workflow-elements/simple-editor/slots.md)
- [Process the output data](https://customerscanvas.com/docs/hub/dev-center/frontend-integration/workflow-elements/simple-editor/output-data.md)
- [Look up the Web-component API](https://customerscanvas.com/docs/hub/dev-center/frontend-integration/workflow-elements/simple-editor/web-component-api.md)
