Your goal
Add Morph Embed to the target web app so its end-users can reshape the UI and generate real-data
widgets by prompt. A correct integration is: (1) the SDK loads,
(2) Morph.init is called exactly once with a valid publishable key, and
(3) the app's real data/actions are registered as sources/actions
so widgets show live values, not placeholders.
Do the minimal thing first (script tag +
init with just a key → reshape works), confirm it, then add
sources/actions. Don't block or wrap the host app's render on Morph; it is
strictly additive and must fail open.
Minimal integration
<script src="https://unpkg.com/morph-embed@0.3/dist/morph-embed.js"></script>
<script>
Morph.init({ key: 'pk_live_…' }); // reshape works immediately; add sources next
</script>
Install
Pick the form that matches the target project.
- Script tag (static site / server-rendered HTML) adds
window.Morph. Pin a major:morph-embed@0.3. - npm (bundled app):
npm install morph-embed, thenimport { init } from 'morph-embed'(ESM). The package default export path is the ESM build;morph-embed/iifeis the browser-global build.
The publishable key
- The key is
pk_live_…and is publishable: it is designed to appear in client source. It is NOT a secret. In a bundler, expose it as a public env var (NEXT_PUBLIC_MORPH_KEY,VITE_MORPH_KEY, etc.), not a server secret. - Get one at usemorph.xyz/dashboard. If you cannot
obtain one, leave a clearly-marked
'pk_live_REPLACE_ME'placeholder and tell the human to create a key and register the app's exact origin(s) (scheme://host[:port];wwwand apex are distinct). - A request from an unregistered origin returns 403; a bad key returns 401. If reshape fails with 403, the fix is registering the origin, not changing code.
Config schema
Morph.init(config) is the full contract. Only key is required.
type MorphConfig = {
key: string; // pk_live_… (required, publishable)
sources?: Record<string, {
fetch: (query) => Promise<object[]>; // return the app's records; runs in-page under app auth
schema: Record<string, 'string'|'number'|'date'|'boolean'>;
description: string; // one line; enum values help
}>;
actions?: Record<string, {
run: (args) => Promise<unknown>; // your callback; args validated first
argsSchema: Record<string, 'string'|'number'>;
description: string;
confirm?: boolean; // default true; keep true for destructive actions
}>;
storage?: { // omit → localStorage (per-browser)
get: (key: string) => unknown | Promise<unknown>;
set: (key: string, value: unknown) => void | Promise<void>;
remove: (key: string) => void | Promise<void>;
};
trigger?: 'alt+m' | 'ctrl+shift+k' | 'cmd+k' | 'none'; // default 'alt+m'
launcher?: false | { position?: 'bottom-right'|'bottom-left'; accent?: string };
backend?: string; // override API endpoint (rarely)
};
After init, window.Morph.open() opens the bar (pair with trigger:'none' and
your own button); window.Morph.bar has .show()/.hide()/.toggle().
Wire the app's data as sources
This is the high-value step. For each dataset the app already has, register a source whose
fetch calls the app's existing API/state and returns an array of plain
objects, and whose schema declares every field a widget might reference.
sources: {
orders: {
fetch: async () => (await api.get('/orders')).data, // reuse the app's real client
schema: { status: 'string', total: 'number', createdAt: 'date' },
description: 'All orders (status: paid|refunded|pending)',
},
}
Only register sources backed by real, fetchable data.
Never invent a source or a field. Every field named in schema must actually exist on the
returned objects, and any field a user might ask about must be in the schema. A missing field is a
hard error at query time, not a silent zero. Dates may be ISO strings, epoch ms, or
Date.
Wire actions (optional)
Register an action only if the app should let users trigger a real mutation from a generated button.
actions: {
createTask: {
run: async (args) => api.post('/tasks', args),
argsSchema: { note: 'string' }, // string|number only
description: 'Create a follow-up task',
confirm: true,
},
}
The generated button is inert until a real user clicks it, and (with confirm:true) shows a
confirm dialog first. Args are schema-validated; unknown/off-type args are dropped.
Storage (optional)
Default is localStorage (per-browser). To sync a user's generated features across devices,
pass a {get,set,remove} adapter backed by the app's own persistence, keyed by the current
user. Values are JSON-serializable; methods may be async; failures must not throw.
Recipe · plain HTML
<script src="https://unpkg.com/morph-embed@0.3/dist/morph-embed.js"></script>
<script>
Morph.init({
key: 'pk_live_…',
sources: { orders: { fetch: () => window.__ORDERS__, schema: { total: 'number', createdAt: 'date' }, description: 'Orders' } },
});
</script>
Recipe · React
Init once, after mount. Guard against double-init (React 18 StrictMode / HMR double-invoke).
import { useEffect } from 'react';
import { init } from 'morph-embed';
function MorphProvider() {
useEffect(() => {
if (window.__morphInit) return; // idempotent guard
window.__morphInit = true;
init({
key: process.env.NEXT_PUBLIC_MORPH_KEY,
sources: {
deals: {
fetch: async () => api.deals(),
schema: { stage: 'string', amount: 'number', closedAt: 'date' },
description: 'CRM deals (stage: won|lost|open)',
},
},
});
}, []);
return null;
}
Render <MorphProvider/> once, high in the tree, in a client component.
Recipe · Next.js (App Router)
- Create a client component (
'use client') identical to the React recipe above and render it once inapp/layout.tsx. - Put the key in
.env.localasNEXT_PUBLIC_MORPH_KEY=pk_live_…(public by design; do not use a server-only var). - Do NOT import
morph-embedin a Server Component; it toucheswindow. Keep it in a'use client'module insideuseEffect.
Rules: do / don't
| Do | Don't |
|---|---|
Call init exactly once; guard against re-init on HMR/StrictMode | Call init per render or per route change |
Put the pk_live_ key in a PUBLIC env var / client source | Hide it as a server secret or proxy it (it's publishable by design) |
| Register only sources backed by real fetchable data | Invent sources, fields, or numbers |
Declare every queryable field in schema | Omit fields users might ask about (hard error at query time) |
Keep confirm:true for anything destructive | Set confirm:false on mutations that can't be undone |
| Let Morph load async and fail open | Block the app's render on Morph, or throw if it 4xxs |
Load morph-embed only client-side | Import it in a Server Component / SSR path (uses window) |
| Tell the human to register the app's origin in the dashboard | Assume a 403 is a code bug (it's an origin-allowlist gap) |
Verification checklist
After integrating, confirm each of these (in a browser at a registered origin):
- The SDK loaded:
typeof window.Morph.init === 'function'. initran once (no duplicate launcher/bar in the DOM).- Pressing Alt+M or clicking the launcher opens the prompt bar.
- A reshape prompt ("make this page dark") applies and shows an Undo toast.
- If sources are registered: a data prompt ("total <metric> this month") renders a real number, not a placeholder.
- Reloading the page reapplies the saved edit.
- No console errors from the host app; Morph failing to load does not break the page.
If a data widget shows a placeholder instead of a number, the source isn't registered or a query field isn't in its schema. Fix the source/schema; do not hardcode a value.
How it works (context you may need)
The model never returns code. It answers only in a fixed declarative JSON DSL that the SDK validates and sanitizes in the browser before touching the DOM; your data never leaves the page (only source names and field types are sent to the model). You don't write the DSL, but if you're debugging what a prompt produced, the op grammar the model emits is documented for reference in the developer docs. Everything else an integrator needs is on this page and in the developer docs.
Package: morph-embed · keys: dashboard · raw guide for your agent: /llms.txt.