The typed contract every CCEN app uses.
One SDK. One surface across web, the agent runtime, and embedded shells. Read live data, mutate with audit trail, open host modals, register shortcuts, theme tokens, locale. iframes do the isolation. The SDK does the typing.
# Install
npm install @ccen/app-bridge
# Or with the CLI scaffold
npx ccen create my-returns-app --template returnsany.One config. Two boundaries.
Every CCEN app is defined in one TypeScript module. The SDK draws the line at the iframe boundary. You write business logic. We move the bytes.
// apps/returns-pro/src/index.ts
import { defineApp } from "@ccen/app-bridge";
import { z } from "zod";
export default defineApp({
id: "returns-pro",
name: "Returns Pro",
scopes: ["orders:read", "returns:read", "returns:write"],
routes: {
"/": HomeView,
"/rmas/:id": RmaDetail,
},
shortcuts: {
"cmd+r": ({ navigate }) => navigate("/rmas/new"),
},
tools: {
"issue-refund": {
input: z.object({
orderId: z.string(),
amount: z.number().positive(),
reason: z.enum(["damaged", "wrong_item", "changed_mind"]),
}),
requires: "approval",
handler: async ({ orderId, amount, reason }, { ccen }) => {
return ccen.refunds.create({ orderId, amount, reason });
},
},
},
});Declared up front. The host enforces them at the API boundary. Operators see exactly what your app can read and write at install time.
Each route is a React component. The host renders your iframe at the right URL. Deep links work. Browser back works.
Scoped to your app. They fire when focus is inside your iframe. They never collide with host shortcuts.
The agent runtime calls these. Every tool is typed (Zod). Approval policy lives next to the handler.
Six namespaces. Most apps need three.
Every method is typed. Every payload is validated at the host boundary. The SDK is small (the bundle is a single iife script per app).
L0 entity access
Read and write every L0 entity through typed methods. Filters compile to indexed queries. All mutations record actor, source app, and scope to the audit log.
const orders = await ccen.orders.list({
status: "processing",
channel: ["shopify", "amazon"],
limit: 200,
});
await ccen.orders.tag(order.id, ["fraud-watch"]);Host modals
Open a modal that renders in the host frame so it escapes your iframe. Pass JSX or markdown. The host serializes and validates the payload before render.
await ccen.modal.open({
title: "Confirm refund",
body: <RefundReview order={order} />,
cta: { label: "Refund $84.20", variant: "primary" },
cancel: { label: "Cancel" },
size: "md",
});Keyboard shortcuts
Register shortcuts scoped to your app. They fire only when focus is inside your iframe. The host owns Cmd+K and a small reserved set.
ccen.shortcut.register("cmd+s", saveDraft);
ccen.shortcut.register("g r", () => navigate("/rmas"));
// Unregister on unmount
return () => ccen.shortcut.unregister("cmd+s");Theme tokens
Receive the full token set on mount. Subscribe to changes for light, dark, or merchant-custom themes. The SDK injects CSS variables into your iframe automatically.
const tokens = await ccen.theme.tokens();
ccen.theme.onChange((next) => applyTokens(next));
// Tokens include radius, color, spacing, type
console.log(tokens.color.primary);
console.log(tokens.radius.md);Notifications
Send transactional and operational notifications through the host. Routing, priority, and channel selection are handled by Knock under the hood.
await ccen.notify({
to: order.assignee,
template: "fraud-flag",
data: { orderId: order.id },
priority: "high",
});Agent tools
Register tools your app exposes to the agent runtime. Each tool is typed with Zod and declares its approval policy. Tools execute under the agent identity in the audit log.
ccen.tools.register({
name: "rebalance-inventory",
input: z.object({
sku: z.string(),
fromLocation: z.string(),
toLocation: z.string(),
quantity: z.number().int().positive(),
}),
description: "Move inventory between warehouses",
requires: "approval",
handler: async (input) => ccen.transfers.create(input),
});Operators see exactly what you ask for.
Every scope your app declares shows up in the install dialog. There is no hidden surface. Tokens are short-lived and scoped to the install, not the developer account. RLS at the API boundary enforces them.
Tokens are scoped to (app, merchant, install). Rotated automatically. Revoked instantly when the operator uninstalls.
Postgres row-level security backs every scope. We do not trust the SDK alone. The API boundary re-checks every call.
Apps run from <app-id>.apps.ccen.co. Cookies, storage, and CSP are scoped per origin. No shared JS context with the host.
Local-first. Real data on tap.
The CLI runs a real CCEN host on localhost. Your app loads inside it. Read scopes return mocked data by default; write scopes go to a sandbox merchant. Connect a dev install to read real data when you need to.
The dev host hot-reloads on file change. Theme tokens, locale, and timezone are configurable from a debug drawer.
$ npx ccen create returns-pro --template returnsScaffold an app from a template. Three apps come with the CLI.
$ npx ccen devRun a sandboxed CCEN host on localhost. Hot-reload your iframe.
$ npx ccen testRun typed contract tests against a mocked SDK. CI-friendly.
$ npx ccen publish --version 0.4.0Submit to the marketplace, or publish privately to your org.
Stable surfaces. Honest deprecations.
The SDK follows semver. Major versions are a year apart. Stable surfaces are guaranteed for two majors. Preview surfaces are flagged inline in the types and the docs.
Things developers ask.
- Why iframes instead of a JS plugin model?
- Real isolation. Apps cannot read each other's state, cannot break the host, cannot leak secrets across origins. Browser-enforced security with Stripe and Shopify track records. We rejected Shadow DOM (style-only encapsulation), Web Workers (no DOM), and WASM UI sandboxes (still renders through host JS).
- Can my app run outside CCEN?
- Yes. The SDK has a `local` mode for development and the host shell is open source. You can host the iframe contract in your own admin if it makes sense. Most developers don't, because the marketplace is where the operators are.
- Are there rate limits?
- Yes. Per-install rate limits on read and write scopes. Tool calls have separate limits with budget enforced per app. Limits scale with the operator tier. Documented per surface in the reference.
- How do I handle errors?
- Every method returns typed Result objects with discriminated error codes. The SDK never throws on operational errors. It does throw on programmer errors (wrong type, missing scope) so they surface in dev.
- Does the SDK work with Server Components?
- Yes. SDK calls work in both client and server components. Reads inside server components use the operator's session token. Writes are always client-initiated and audit-trailed.
- Is there a TypeScript-only requirement?
- No. The SDK ships type definitions but vanilla JS works. We strongly recommend TypeScript because the value of typed scopes and inputs is real.