# Morph Embed — integration guide for AI coding agents You are integrating the `morph-embed` SDK into a web application. Morph Embed adds an AI prompt bar to the app: end-users type what they want ("make this dark", "add a box showing lost deals in the last 15 days") and the page reshapes in place or renders a real-data widget. It is additive — it must never block or break the host app. Docs (human): https://usemorph.xyz/docs Package: https://www.npmjs.com/package/morph-embed Keys: https://usemorph.xyz/dashboard ## Definition of done 1. The SDK loads and `window.Morph.init` is a function. 2. `Morph.init({ key })` is called EXACTLY ONCE with a valid publishable key. 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 + init with just a key → reshape works), verify, then add sources/actions. ## Install - Static/SSR HTML — script tag (adds `window.Morph`); pin a major: - Bundled app — `npm install morph-embed`, then `import { init } from 'morph-embed'` (ESM default export). `morph-embed/iife` is the browser-global build. ## The key (publishable — NOT a secret) - Format `pk_live_…`. Designed to appear in client source. In a bundler, expose it as a PUBLIC env var (NEXT_PUBLIC_MORPH_KEY, VITE_MORPH_KEY, …). Do NOT treat it as a server secret or proxy it. - Get one at https://usemorph.xyz/dashboard and register the app's exact origins (scheme://host[:port]; www and apex are distinct). If you can't get one, leave `key: 'pk_live_REPLACE_ME'` and tell the human to create a key + register the origin. - Unregistered origin → HTTP 403 (fix = register origin, not code). Bad key → 401. ## Config contract (only `key` is required) Morph.init({ key: string, // pk_live_… (publishable) sources?: { [name]: { fetch: (query) => Promise, // return the app's records; runs in-page under app auth schema: { [field]: 'string'|'number'|'date'|'boolean' }, // declare EVERY queryable field description: string, // one line; include enum values } }, actions?: { [name]: { run: (args) => Promise, // your callback; args validated first argsSchema: { [arg]: 'string'|'number' }, description: string, confirm?: boolean, // default true; keep true for destructive actions } }, storage?: { // omit → localStorage (per-browser) get: (key) => value|Promise, set: (key, value) => void|Promise, remove: (key) => void|Promise, }, 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' + your button); `window.Morph.bar` has .show()/.hide()/.toggle(). ## Wire the app's data as sources (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, schema: { status: 'string', total: 'number', createdAt: 'date' }, description: 'All orders (status: paid|refunded|pending)', }, } Rules: only register sources backed by real fetchable data. Never invent a source, field, or number. Every schema field must exist on the returned objects. Any field a user might ask about must be in the schema (missing field = hard error, not a zero). Dates may be ISO strings, epoch ms, or Date objects. ## Wire actions (optional — only for real mutations) 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 (confirm:true) shows a confirm dialog first. Args are schema-validated; unknown/off-type args dropped. ## Storage (optional) Default localStorage (per-browser). To sync a user's features across devices, pass a {get,set,remove} adapter backed by the app's persistence, keyed by the current user. JSON-serializable values; async allowed; must not throw. ## Framework recipes ### Plain HTML ### React (init once, guard double-invoke) import { useEffect } from 'react'; import { init } from 'morph-embed'; function MorphProvider() { useEffect(() => { if (window.__morphInit) return; // idempotent guard (StrictMode/HMR) 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 once, high in the tree, in a client component. ### Next.js (App Router) 1. Make a 'use client' component identical to the React recipe; render it once in app/layout.tsx. 2. .env.local: NEXT_PUBLIC_MORPH_KEY=pk_live_… (public by design). 3. Do NOT import morph-embed in a Server Component — it uses `window`. Keep it in a 'use client' module inside useEffect. ## Rules — DO / DON'T DO: init exactly once; guard re-init on HMR/StrictMode. DON'T: init per render or per route change. DO: put the pk_live_ key in a PUBLIC env var / client source. DON'T: hide it as a server secret or proxy it (it's publishable). DO: register only sources backed by real fetchable data. DON'T: invent sources, fields, or numbers. DO: declare every queryable field in schema. DON'T: omit fields users might ask about. DO: keep confirm:true for destructive actions. DON'T: set confirm:false on irreversible mutations. DO: let Morph load async and fail open. DON'T: block the app's render on Morph or throw on 4xx. DO: load morph-embed only client-side. DON'T: import it in a Server Component / SSR path. DO: tell the human to register the app's origin in the dashboard. DON'T: assume a 403 is a code bug — it's an origin-allowlist gap. ## Verification checklist (browser, at a registered origin) - [ ] typeof window.Morph.init === 'function' - [ ] init ran once (no duplicate launcher/bar in the DOM) - [ ] Alt+M or the ✦ launcher opens the prompt bar - [ ] A reshape prompt ("make this page dark") applies + shows an Undo toast - [ ] If sources registered: a data prompt renders a REAL number, not a placeholder - [ ] Reloading the page reapplies the saved edit - [ ] No console errors; Morph failing to load does not break the page If a 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) The model never returns code — only 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 + field types are sent to the model; it never sees or emits records or numbers). You don't author the DSL; the op grammar is documented for reference at https://usemorph.xyz/docs#dsl. Query language for renderData: filter (equality/IN), dateField + since/until (durations '15d'/'24h'/'30m' — days/hours/minutes, no weeks — or ISO date), groupBy, aggregate {fn: sum|count|avg|min|max, field}, limit. Charts: stat, bar, line. Field types: string|number|date|boolean. Action arg types: string|number. ## Privacy note (vendor-keyed requests) For requests authenticated by a vendor's pk_live_ key, the backend retains product signals for that vendor: the structured shape of the generated change (op type, source name, field names, never field values or records) plus a small sample of truncated prompt texts, aggregated per vendor to power the dashboard's Signals panel. No end-user identifier, IP address, or page content is stored. Disclosing this to your own end users is your responsibility as the vendor. Full statement: https://usemorph.xyz/privacy#embed-signals