Search Docs
Loading...
Skip to content

Agentic Design Manager

Give your users a design platform with an assistant: they ask in a chat, and the agent makes or changes designs in their brand. Run the kit on your machine, deploy it, and replace its parts to build your own.

15 mins
estimated time
Download
GitHub

What It Does#

The kit is a design platform for two fictional brands, Nordwind Coffee, a roastery, and Tidewell Botanics, an online skincare shop.

  • A brand kit per brand. Each brand, a tenant of the platform, has its own fonts, colors, logos, images, and files. The agent finds them through the brand’s asset sources and designs with them instead of lookalikes. Asset sources are the catalogs of things a design can use in CE.SDK, IMG.LY’s design engine and editor.
  • A grid of the brand’s designs, with a thumbnail of each.
  • CE.SDK’s editor for any design, with the same brand sources as the agent. Saves from the editor and the agent never overwrite each other’s newer work, and the editor refuses to open a design that a newer CE.SDK saved ( Your Editor).
  • An assistant. You ask in a chat; the agent creates designs, changes them, generates images, and exports files, which arrive in the chat as links. You can also upload a PDF, Photoshop, InDesign, or PowerPoint file, and it becomes a design.
  • The same tools for your customers’ own AI assistants. The kit also serves the agent’s tools as a Model Context Protocol (MCP) server ( Serve the Design Tools as an MCP Server).

Start from this kit if your product has brands or tenants, such as an agency platform or a marketing tool, and wants a design chat next to its editor. Four things run on your machine:

  • The page, which a person opens in the browser: the grid, the chat, and the editor.
  • The agent runner, your Node server that runs the agent loop: it calls the model, hands it CoDesign’s design tools, and runs each tool the model asks for. A turn keeps running when the person closes the page.
  • The executors, Docker containers from IMG.LY’s image imgly/codesign-executor. Each holds CE.SDK’s engine and runs the code the model writes to change a design, apart from your keys ( Security).
  • The store, a plain HTTP file server that keeps the designs, the brands’ files, the thumbnails, the exports, and the chats.

Pre-requisites#

  • Docker with Docker Compose, which starts several containers from one file, for the executors and the store – Download
  • Node.js v22+ with npm, pnpm, or yarn – Download
  • A desktop browser that opens *.localhost addresses on your machine, such as Chrome: the store answers at http://store.localhost:8790. The kit’s page is tested in desktop Chrome.
  • A CE.SDK license for the editor in the page, where a person changes a design by hand. The agent’s engine is the executor, which takes the CoDesign key below. A CE.SDK trial key works on localhost.
  • A CoDesign license key, for the model calls and the exports. Contact sales to get a CoDesign license key.

Get Started#

Step 1: Clone the Repository#

Terminal
git clone https://github.com/imgly/starterkit-codesign-agentic-design-manager-ts-web.git
git clone https://github.com/imgly/starterkit-codesign-agentic-design-manager-ts-web.git

What is inside, a Vite project (Vite is a web dev server and build tool):

starterkit-codesign-agentic-design-manager-ts-web/
├── src/
│ ├── runner/ # The agent loop and its prepareCoDesign call
│ ├── brand/ # The two brands: fonts, colors, logos, seed designs
│ ├── page/ # The page: design grid, chat, editor, usage
│ ├── store/ # The designs, the brands' files, and the chats, over the store
│ ├── hosting/cloud/
│ │ ├── runner/ # The agent runner's server, and the page's client for it
│ │ ├── mcp/ # The same tools as an MCP server
│ │ ├── compose.yaml # Two executors, and nginx with the store
│ │ ├── tenant.plugin.ts # The CE.SDK plugin with a brand's sources, over the store
│ │ └── .env.template # The variables of the agent runner and Compose
│ └── plugins.ts # The CE.SDK plugins a brand's sessions and its editor apply
├── shared/ # Code the CoDesign starter kits share: page session, agent loop, editor modal
├── scripts/ # generate-plugin.mjs: rebuilds the plugin module the executors receive
├── seeds/ # Images the seed designs use
├── .env.template # The page's variables
├── LICENSE.md # IMG.LY's license, the same as the CoDesign SDK's
├── index.html
├── package.json
└── vite.config.ts # The page's dev server

Read src/runner/assistant.ts after your first run; How It Works below explains it.

Step 2: Install Dependencies#

Terminal
cd starterkit-codesign-agentic-design-manager-ts-web
npm install
cd starterkit-codesign-agentic-design-manager-ts-web
npm install

Every npm run … command on this page works as pnpm run … or yarn … too.

Step 3: Set Your Keys#

Put both templates into one .env:

Terminal window
cat .env.template src/hosting/cloud/.env.template > .env

Then fill in the variables that are empty in the templates; leave TOKEN_ISSUERS and the MCP server’s variables empty on your machine:

.env
VITE_CESDK_LICENSE=<your CE.SDK license or trial key>
IMGLY_GATEWAY_API_KEY=<your CoDesign license key>
IMGLY_CESDK_LICENSE=<your CoDesign license key>
EXECUTOR_TOKEN=<a secret of your own; openssl rand -hex 32 makes one>
STORE_TOKEN=<a secret of your own, letters and digits; openssl rand -hex 32 makes one>
RUNNER_DEV_USER=me

Your CoDesign license key goes in twice, as IMGLY_GATEWAY_API_KEY and IMGLY_CESDK_LICENSE, and neither reaches the page. VITE_CESDK_LICENSE is a different key, the CE.SDK license of the editor. Vite sends every variable whose name starts with VITE_ to the page, where anyone can read it, so keep only that license and addresses there.

Variable Read by What it is
VITE_CESDK_LICENSE the page The CE.SDK license of the editor.
VITE_RUNNER_URL the page The agent runner’s address. Default http://localhost:8792.
VITE_STORE_URL the page, the agent runner The store’s address. Default http://store.localhost:8790/store.
VITE_MODEL the page Optional: the model the chat starts with.
IMGLY_GATEWAY_API_KEY the agent runner Your CoDesign license key. The agent runner sends it with every model call and generated image to IMG.LY’s AI gateway, the service that reaches the AI models.
EXECUTOR_URLS the agent runner The executors, as the agent runner reaches them through nginx, the web server in the Compose file. Default the two on http://localhost:8790.
RUNNER_DEV_USER the agent runner On your machine only: any id. Every request to the agent runner is that user, in place of a sign-in.
ALLOWED_ORIGINS the agent runner The page origins that may call it, comma-separated. Default http://localhost:5190.
STORE_TOKEN the agent runner, Compose The store’s write token. The executors get it too, in the brand plugin’s options, where the model’s code can read it: fine on your machine, but a store online takes a token scoped to one brand ( Security).
IMGLY_CESDK_LICENSE Compose Your CoDesign license key again, under the executor image’s name for it. The executors start with it and charge every export to it. It is not the CE.SDK license above.
EXECUTOR_TOKEN Compose The token nginx adds to every request to the executors, so only the agent runner reaches them.

src/hosting/cloud/.env.template also describes the variables of a deployed agent runner (Deploy It) and of the MCP server.

Step 4: Start the Executors and the Store#

Terminal window
docker compose -f src/hosting/cloud/compose.yaml --env-file .env up -d
curl http://localhost:8790/executor-1/health
# {"result":{"sessions":0,...,"accepting":true,...}}

The first start downloads the images. nginx answers on port 8790: the executors under /executor-1/ and /executor-2/, and the store under /store/, at the name store.localhost that the browser and the executors both resolve. curl asks the first executor, through nginx, whether it is ready, and prints a line like the comment above. If it does not answer, run docker compose -f src/hosting/cloud/compose.yaml logs executor-1: the executor stops at start when the license key is missing or cannot be activated, or when it cannot reach api.img.ly.

Step 5: Start the Agent Runner#

In a terminal of its own, from the kit’s folder:

Terminal
npx vite-node src/hosting/cloud/runner/serve.ts
npx vite-node src/hosting/cloud/runner/serve.ts

It prints Runner: http://127.0.0.1:8792 and keeps running. curl http://127.0.0.1:8792/health answers {}.

Step 6: Start the Page#

In a second terminal, from the kit’s folder:

Terminal
npm run dev
npm run dev

Vite prints the page’s address, http://localhost:5190/. Open it in desktop Chrome. You see a grid of the brand’s designs and the chat. Ask for a design, such as “A square post for our autumn roast”, and open the PNG link the agent answers with. A typical design job takes around 3 minutes, depending on the model and how hard the brief is.

To stop, press Ctrl+C in both terminals and run docker compose -f src/hosting/cloud/compose.yaml down. The store keeps its files in a Docker volume; down -v deletes them.

What It Costs#

Model calls and generated images are charged to the IMG.LY account behind IMGLY_GATEWAY_API_KEY, and each exported file, such as a PNG or a PDF, to the key the executors run with; saving, editing, previews, and thumbnails are free ( What It Costs).

How It Works#

The Agent Runner#

The core of the kit is one prepareCoDesign call per chat, in src/runner/assistant.ts, the same call your server would make. The code below is a reference: it type-checks against the SDK, and the functions marked “yours to write” are your product’s.

const hosting = await forTenant(init.tenant);
const codesign = prepareCoDesign({
// One id per chat: every design of it opens on one executor.
conversation: init.conversation,
executors: hosting.executors, // where the engine runs
designs: hosting.designs, // where designs are kept
// Google Fonts, the brand's fonts, colors, images, and files, and CE.SDK's demo photos.
plugins: [
...hosting.plugins, // the brand's sources first: the font lookup takes the first match
googleFonts(),
cesdkDemo({ include: ['ly.img.image'] })
],
// Image generation, through your gateway route: the key behind it pays, as it does for the model calls.
gateway: { baseURL: init.gatewayBaseURL, bearer: init.bearer },
// The agent's exports, handed to the person; the note goes back to the model.
uploadExport: async ({ title, format, mimeType, bytes }) => {
deliver({ name: `${title}.${format}`, mimeType, bytes });
return { note: 'The file is in the chat as a download.' };
}
});
return createDesignAgent({
tools: codesign.tools,
instructions: `Everything you make is in ${init.tenant}'s brand.\n\n${codesign.instructions}`
});
The whole file, with the functions that are yours to write
import {
cesdkDemo,
googleFonts,
prepareCoDesign,
type CoDesignTool,
type DesignStore,
type EnginePluginModule,
type Executors
} from '@imgly/codesign-sdk';
// Yours to write: where a tenant's executor runs, its designs store (read and write over your table) and its brand plugins.
async function forTenant(tenant: string): Promise<{
executors: Executors;
designs: DesignStore;
plugins: EnginePluginModule[];
}> {
throw new Error('yours to write');
}
// Yours to write: your agent loop, on IMG.LY's AI gateway.
function createDesignAgent(opts: {
tools: readonly CoDesignTool[];
instructions: string;
}): { send(message: string): Promise<string> } {
throw new Error('yours to write');
}
// Yours to write: hands a file to the person, for example as a download in the chat.
function deliver(file: { name: string; mimeType: string; bytes: Uint8Array }) {
throw new Error('yours to write');
}
export async function openAssistant(init: {
tenant: string;
conversation: string;
// Your route that forwards to IMG.LY's AI gateway with your CoDesign license key, such as 'https://app.example.com/gateway'.
gatewayBaseURL: string;
// The signed-in person's token for that route.
bearer: () => Promise<string | undefined>;
}) {
const hosting = await forTenant(init.tenant);
const codesign = prepareCoDesign({
// One id per chat: every design of it opens on one executor.
conversation: init.conversation,
executors: hosting.executors, // where the engine runs
designs: hosting.designs, // where designs are kept
// Google Fonts, the brand's fonts, colors, images, and files, and CE.SDK's demo photos.
plugins: [
...hosting.plugins, // the brand's sources first: the font lookup takes the first match
googleFonts(),
cesdkDemo({ include: ['ly.img.image'] })
],
// Image generation, through your gateway route: the key behind it pays, as it does for the model calls.
gateway: { baseURL: init.gatewayBaseURL, bearer: init.bearer },
// The agent's exports, handed to the person; the note goes back to the model.
uploadExport: async ({ title, format, mimeType, bytes }) => {
deliver({ name: `${title}.${format}`, mimeType, bytes });
return { note: 'The file is in the chat as a download.' };
}
});
return createDesignAgent({
tools: codesign.tools,
instructions: `Everything you make is in ${init.tenant}'s brand.\n\n${codesign.instructions}`
});
}
  • One chat per person and brand. The agent runner keeps the chat while no page is open, and a turn keeps running when the person closes the page. The page sends messages with POST /messages and reads one stream of JSON lines from GET /stream; a page that reconnects gets what it missed, and a new page gets the whole chat.
  • Chats are kept in memory, and saved to the store after every turn (tenants/<brand>/chats/<user>.json). A turn running when the agent runner restarts is lost, and the page then shows the chat as it was saved.
  • The brand’s sources reach the executors as a CE.SDK engine plugin, the same file the editor applies ( Bring Your Brand). After you change src/hosting/cloud/tenant.plugin.ts, run npm run generate:plugin: it rebuilds the module the executors and the editor receive. Without it, both keep the previous sources, with no error.

The Executors#

A chat is one conversation, your id for one chat or job, and each executor serves one conversation at a time, so that no other conversation gets an engine that ran this one’s code. Every design the chat opens takes turns on that executor’s one engine, one call at a time ( Run Enough Executors). The Compose file starts two executors, so two chats run at a time; a third waits up to 60 seconds, then fails with an error that says so. An executor exits about 3½ minutes after its conversation’s last call (3 idle minutes, then 30 seconds), and Compose starts it again empty. For more, add services to compose.yaml and their URLs to EXECUTOR_URLS.

The Store#

Each image, file, thumbnail, and export the page shows is a URL in the store, under tenants/<brand>/: designs in designs/, uploads in files/, and thumbnails/, previews/, exports/, and chats/. A design names its images by URL, so the browser, the agent runner, and the executors must all open the same address. On your machine that is http://store.localhost:8790/store/…, which Chrome opens on your machine and the executors reach through Compose under the same name.

The store answers three requests, which nginx serves in the kit:

Request Sent by Authorization
GET /store/<path> the page, the editor, the agent runner, the executors none
PUT /store/<path> the agent runner, the executors Authorization: Bearer <STORE_TOKEN>
DELETE /store/<path> the agent runner Authorization: Bearer <STORE_TOKEN>

A PUT creates the folders on its path. A GET of a missing file answers 404, and a write without the token 401.

Deploy It#

Deployed, each piece runs on your infrastructure:

  1. Your sign-in. The kit has none of its own; plug yours in at startSession() in shared/src/page/session.ts. Its getToken returns your sign-in’s short-lived JSON Web Token (JWT), a fresh one on each call, and the page sends it to the agent runner with every request. The agent runner takes the person from the token’s sub claim and keeps one chat per person and brand.

  2. The agent runner, at the address you build into VITE_RUNNER_URL. npm run build:runner bundles it into dist/runner/serve.mjs; run that with node, or add --profile runner to the docker compose command to start it in a Node container beside the executors. It holds IMGLY_GATEWAY_API_KEY and checks each token against TOKEN_ISSUERS, the issuers your sign-in writes into a token’s iss claim, comma-separated:

    TOKEN_ISSUERS=https://auth.example.com

    It fetches each issuer’s public keys from <issuer>/.well-known/jwks.json (or JWKS_URL) and accepts tokens signed with RS256, the RSA signature most hosted sign-ins use. It answers only the page origins in ALLOWED_ORIGINS. RUNNER_HOST and RUNNER_PORT set where it listens. Leave RUNNER_DEV_USER unset: the agent runner refuses it unless it listens on a loopback address, and refuses it beside TOKEN_ISSUERS.

  3. The executors, IMG.LY’s image, one container per conversation, with their network closed to all but the hosts they need ( Host the Executor). EXECUTOR_URLS lists them, and only the agent runner may reach them.

  4. The store, at its https address in VITE_STORE_URL, answering the three requests above. Reads are open to the page and the editor, with cross-origin resource sharing (CORS) headers for your page’s origin, so the browser lets the page read them.

  5. The page: npm run build:site builds it into dist/, with the VITE_ variables set at build time, for any static host. Its VITE_CESDK_LICENSE is a CE.SDK license for your page’s domain.

What to Replace#

Piece In the kit In your product
The agent runner the kit’s own, on IMG.LY’s AI gateway, in a Node server your server, in any agent framework, on the same gateway
The tools prepareCoDesign, as above the same call, on your server
The executor IMG.LY’s image, two containers in Compose IMG.LY’s image in your containers, one per conversation
Designs and files the store: nginx and a Docker volume in Compose your database and bucket
Brand kits data in the kit, served by each tenant’s asset sources your brand kits, behind the same sources
Sign-in none of its own: startSession() and RUNNER_DEV_USER your sign-in, at startSession() and in TOKEN_ISSUERS

To build the same product on your server, step by step, follow Add a Design Chat.


Troubleshooting#

The Executors Do Not Start#

  • required variable IMGLY_CESDK_LICENSE is missing a value: set IMGLY_CESDK_LICENSE in .env, or the same for EXECUTOR_TOKEN or STORE_TOKEN. Compose names the variable it is missing. Pass --env-file .env from the kit’s folder.
  • /health does not answer. Run docker compose -f src/hosting/cloud/compose.yaml logs executor-1.

The Agent Runner Does Not Start#

  • The runner needs …: set them in .env names the variables it is missing: EXECUTOR_URLS, VITE_STORE_URL, STORE_TOKEN, IMGLY_GATEWAY_API_KEY, or ALLOWED_ORIGINS.
  • The runner needs TOKEN_ISSUERS or RUNNER_DEV_USER to know who calls it. Set one of them, not both.

The Page Shows an Error#

  • No CE.SDK license: set VITE_CESDK_LICENSE in .env and restart the page’s dev server; Vite reads .env only at start.
  • The page names VITE_RUNNER_URL or VITE_STORE_URL: set both and restart the page’s dev server.
  • The runner stopped: the runner at … does not answer: the page could not reach the agent runner three times in a row. Start it, and check that ALLOWED_ORIGINS names the page’s origin: the agent runner answers no other. the runner answered 401 means it did not accept the page’s token.

A Chat Waits, Then Fails#

  • Both executors serve another conversation. The error says so. One is free again about 3½ minutes after its conversation’s last call; or add executors (see The Executors).

Exports Are Refused#

  • The executors charge exports to IMGLY_CESDK_LICENSE. With a key that does not include CoDesign, every export is refused with a message that says why. For what each message means, see Exports Are Refused in Troubleshooting.

Next Steps#