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
- The collected
tenantId,storefrontId, OAuth2 client credentials, and a testproductId - A workflow of the Workflow Elements type with the
simple-editorcomponent assigned to the product (see Workflow Elements Overview) - Node.js 18 or later
Set up the project
-
Create a project folder.
mkdir simple-editor-quick-startcd simple-editor-quick-start -
Create a Vite app.
npm create vite@latest . -- --template vanilla-ts -
Install the backend dependencies.
npm install @aurigma/axios-storefront-api-client axios dotenv expressnpm install -D @types/express @types/node concurrently tsx -
Create
vite.config.tsin the project root.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.1and forwards browser requests from/api/*to the local backend. -
Update the
devscript inpackage.json.{"scripts": {"dev": "concurrently vite \"tsx watch server/server.ts\""}}The
devscript starts Vite and the Express backend together. Leave the other Vite scripts unchanged.If you run
npm run devnow, it fails because the backend files don't exist yet.
Configure Customer's Canvas connection
-
Create an
.envfile in the project root. -
Add your Customer's Canvas values.
CCHUB_BASE_URL=https://customerscanvashub.comCCHUB_API_GATEWAY_URL=https://api.customerscanvashub.comCCHUB_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_SECRETonly on the backend. Do not expose it inVITE_*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.
<!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.
Add the backend
-
Create a
serverfolder in the project root. -
Create
server/cchub-auth.ts. -
Add the Customer's Canvas connection and OAuth2 helper.
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.
-
Create
server/cchub-api.ts. -
Add Storefront API operations.
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-workflowendpoint, extracts the Simple Editor configuration, and creates a project from the line item returned by the Simple Editor. -
Create
server/server.ts. -
Add the Express routes.
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.
-
Run the dev script.
npm run devThe terminal shows a Vite local URL and the backend message:
Backend is running at http://localhost:3001Stop the command after both processes start.
Add the page code
-
Replace the content of
src/main.ts. Replace2502with your product ID.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
configtoinit(). The OAuth2 client secret, the access token, and the storefront user token stay on the backend. In a real storefront, setproductIdfrom the current product page, route, or catalog state. -
Replace the content of
src/style.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
-
Start the frontend and backend.
npm run dev -
Open the local URL printed by Vite.
The Simple Editor loads the product from the
productIdvalue insrc/main.ts. -
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._stateIdarray containing the saved design ID.
Check the project status and download the rendered result by visiting Projects in Customer's Canvas BackOffice.