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.
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
*.localhostaddresses on your machine, such as Chrome: the store answers athttp://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#
git clone https://github.com/imgly/starterkit-codesign-agentic-design-manager-ts-web.gitgit clone https://github.com/imgly/starterkit-codesign-agentic-design-manager-ts-web.gitnpx degit imgly/starterkit-codesign-agentic-design-manager-ts-web starterkit-codesign-agentic-design-manager-ts-webnpx degit imgly/starterkit-codesign-agentic-design-manager-ts-web starterkit-codesign-agentic-design-manager-ts-webWhat 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 serverRead src/runner/assistant.ts after your first run; How It Works below
explains it.
Step 2: Install Dependencies#
cd starterkit-codesign-agentic-design-manager-ts-web
npm installcd starterkit-codesign-agentic-design-manager-ts-web
npm installcd starterkit-codesign-agentic-design-manager-ts-web
pnpm installcd starterkit-codesign-agentic-design-manager-ts-web
pnpm installcd starterkit-codesign-agentic-design-manager-ts-web
yarncd starterkit-codesign-agentic-design-manager-ts-web
yarnEvery npm run … command on this page works as pnpm run … or yarn … too.
Step 3: Set Your Keys#
Put both templates into one .env:
cat .env.template src/hosting/cloud/.env.template > .envThen fill in the variables that are empty in the templates; leave
TOKEN_ISSUERS and the MCP server’s variables empty on your machine:
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=meYour 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#
docker compose -f src/hosting/cloud/compose.yaml --env-file .env up -dcurl 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:
npx vite-node src/hosting/cloud/runner/serve.tsnpx vite-node src/hosting/cloud/runner/serve.tspnpm exec vite-node src/hosting/cloud/runner/serve.tspnpm exec vite-node src/hosting/cloud/runner/serve.tsyarn vite-node src/hosting/cloud/runner/serve.tsyarn vite-node src/hosting/cloud/runner/serve.tsIt 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:
npm run devnpm run devpnpm run devpnpm run devyarn devyarn devVite 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 /messagesand reads one stream of JSON lines fromGET /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, runnpm 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:
-
Your sign-in. The kit has none of its own; plug yours in at
startSession()inshared/src/page/session.ts. ItsgetTokenreturns 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’ssubclaim and keeps one chat per person and brand. -
The agent runner, at the address you build into
VITE_RUNNER_URL.npm run build:runnerbundles it intodist/runner/serve.mjs; run that withnode, or add--profile runnerto thedocker composecommand to start it in a Node container beside the executors. It holdsIMGLY_GATEWAY_API_KEYand checks each token againstTOKEN_ISSUERS, the issuers your sign-in writes into a token’sissclaim, comma-separated:TOKEN_ISSUERS=https://auth.example.comIt fetches each issuer’s public keys from
<issuer>/.well-known/jwks.json(orJWKS_URL) and accepts tokens signed with RS256, the RSA signature most hosted sign-ins use. It answers only the page origins inALLOWED_ORIGINS.RUNNER_HOSTandRUNNER_PORTset where it listens. LeaveRUNNER_DEV_USERunset: the agent runner refuses it unless it listens on a loopback address, and refuses it besideTOKEN_ISSUERS. -
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_URLSlists them, and only the agent runner may reach them. -
The store, at its
httpsaddress inVITE_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. -
The page:
npm run build:sitebuilds it intodist/, with theVITE_variables set at build time, for any static host. ItsVITE_CESDK_LICENSEis 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 forEXECUTOR_TOKENorSTORE_TOKEN. Compose names the variable it is missing. Pass--env-file .envfrom the kit’s folder./healthdoes not answer. Rundocker compose -f src/hosting/cloud/compose.yaml logs executor-1.
The Agent Runner Does Not Start#
The runner needs …: set them in .envnames the variables it is missing:EXECUTOR_URLS,VITE_STORE_URL,STORE_TOKEN,IMGLY_GATEWAY_API_KEY, orALLOWED_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: setVITE_CESDK_LICENSEin.envand restart the page’s dev server; Vite reads.envonly at start.- The page names
VITE_RUNNER_URLorVITE_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 thatALLOWED_ORIGINSnames the page’s origin: the agent runner answers no other.the runner answered 401means 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#
- Add a Design Chat — build the same product on your server, step by step
- Host the Executor — run IMG.LY’s image on your servers, one container per conversation
- Bring Your Brand — serve your brand kits as asset sources
- Your Editor — open the agent’s designs in CE.SDK’s editor