Templates
Let people make independent copies of an app, then choose whether to publish it as a public Listed Template.
What you can do
Turn an app into a Template. People who can already view the source App can make an independent copy. Each copy has its own owner and empty storage, and can be edited as soon as Charming creates it. Changing a copy never changes the Template, and changing the Template never changes copies already made.
Publish it as Listed. A Listed Template gets a public, search-indexable detail page and appears in Template search. Anyone can discover and copy it without gaining access to the source App or its data. Listed always includes Template. Template alone does not make an App public or let strangers copy it from a shared link.
Choose how the listing looks. Save a draft title, summary, longer Markdown description, category, cover image, and ordered screenshot gallery before publishing. A cover or screenshot needs alt text; its caption is optional. Publishing needs a nonblank title and summary. The other fields are optional. Link previews of the detail page show the title beside the cover when the cover is a PNG, JPEG, or GIF; with any other cover, or none, they show the App’s own preview image.
Keep Public separate. Public opens the live App Viewer to anyone with its link. It does not make the App a Template. Template does not make the live App public. If an App is both Public and a Template, visitors open the live, read-only App and see an option to create a copy. See Privacy and sharing for the full access model.
Show sample data in a new copy. A copy starts with empty storage, so an app that only renders stored data opens blank. Write sample data into the app’s source and show it until the copy’s first write, so a new owner sees what the app does before adding their own data. See Sample data for new copies.
Give each new copy a starter prompt. A copy starts with empty storage, and the “Your new app” notice that opens with it always has a Copy prompt action: a setup prompt that has the new owner’s agent learn the App, ask for their data, and add it. The starter prompt is your guidance for that agent, such as what data to ask for first. A fresh copy takes the current starter prompt once, and the setup prompt quotes it whole. Later changes to the Template do not alter existing copies. The prompt also replaces the generic one behind the widget menu’s “Open in Claude”, “Open in ChatGPT”, and Copy prompt actions.
How Templates work
A new copy requires a signed-in author. Copy selects the active source and its completed server/browser output, even if a newer edit is pending or failed. If that output needs preparation under the current runner profile, the response supplies a build to inspect; no new app exists until it publishes. Repeating the request while pending returns the same build.
Making a copy copies source and the starter prompt. Data and secrets stay on the source App. The person who copies starts with empty storage and adds their own secrets if the code needs any. To copy an app together with its data, its owner asks an agent to copy it with include_data.
Every enabled Template has a detail page at https://charm.ing/templates/<owner>/<app-name>. Listed pages are public and search-indexable. An unlisted page at the same stable path is available only to an authorized App reader and sends noindex, nofollow, and private no-store headers.
Technical
Open the App’s App settings, then choose Template. Its canonical settings path is /<owner>/~/apps/<app-name>/settings/template. Listing content can stay as a draft while Template and Listed are off. The owner or a team admin also sets the starter prompt there, whether or not Template is on.
Copy this prompt for your agent
Set up one of my Charming apps as a Template. Read
https://charm.ing/docs/capabilities/templates.md first. List my apps
and ask me which one, then ask whether its current viewers should be
able to copy it or whether I want to publish a public listing. If I
choose a listing, help me prepare its required title and summary plus
any optional detail and media, then give me its canonical Template URL.
How an agent performs this job
Call set_template with { app_id } to enable Template. It accepts an optional listing patch with title, summary, content_markdown, category, cover_asset_key, cover_alt, cover_caption, and gallery. Each gallery item has asset_key, alt, and an optional caption. Media must use safe image assets owned by the same App, and alt text must not be blank.
Pass listed: true to apply the patch, enable Template, and publish the listing. The resulting title and summary must both be nonblank. Pass listed: false to unpublish while keeping Template enabled. Omit listed to preserve the current Listed state. Omitted patch fields also keep their current values; null clears a nullable field, and gallery: null or an empty gallery clears the gallery.
Call unset_template to disable Template and clear Listed. Existing copies keep working. The deprecated set_remixable tool accepts only app_id: it enables Template but cannot edit listing metadata and leaves the Listed state unchanged. Its deprecated unset_remixable partner has the same effect as unset_template.
set_public is a separate action that grants the live App Viewer by link. It does not enable Template or Listed. set_starter_prompt is also separate: it sets or clears the starter prompt that copies take and does not change listing fields or state.
To find Listed Templates, call search_templates with an optional query. Results include the Template id, resolved listing title, summary, Markdown detail, category, cover, starter prompt, creator, builder count, canonical templatePageUrl, and browser copyUrl. An agent copies a result for the signed-in user with copy_app ({ app_id: id }) or POST /app/<id>/copy. The copy URL is for a person: it starts browser navigation where the visitor chooses to copy and signs in before Charming creates the owned App.
First-party Template listing API
The resource /api/v1/apps/{appId}/template-listing accepts a personal access token (chrm_user_*) or a browser session and requires app:template-edit:
| Method | Path | Effect |
|---|---|---|
GET |
/api/v1/apps/{appId}/template-listing |
Read the draft, resolved fallback values, media URLs, validation state, and Template flags. |
PATCH |
/api/v1/apps/{appId}/template-listing |
Patch draft content only. It does not enable, disable, publish, or unpublish. |
POST |
/api/v1/apps/{appId}/template-listing/enable |
Enable Template while preserving the draft and Listed state. |
POST |
/api/v1/apps/{appId}/template-listing/publish |
Require a title and summary, then enable Template and publish Listed atomically. |
POST |
/api/v1/apps/{appId}/template-listing/unpublish |
Clear Listed while preserving Template and the draft. |
POST |
/api/v1/apps/{appId}/template-listing/disable |
Disable Template and clear Listed while preserving the draft. |
The first-party API uses camelCase draft fields: contentMarkdown, cover: { assetKey, alt, caption? }, and gallery: [{ assetKey, alt, caption? }]. The older public-agent PUT /app/{id}/remixable route changes only the Template and Listed flags; use the first-party resource or set_template for listing metadata.
Use charming api describe get-app-template-listing to inspect the generated CLI contract. Read a draft with charming api request get-app-template-listing --param appId=APP_ID. Preview a content change with charming api request update-app-template-listing --param appId=APP_ID --body '{"title":"My template"}' --dry-run.
The contract
An owner, collaborator, or team member with app:template-edit can change Template content and state. An anonymous App must be claimed first.
Template copying requires both an enabled Template and either Listed or existing app:read access to the source App. Listed grants public discovery and copying, not app:read on the source App. It exposes neither the live App nor its data. Public grants app:read to the live hosted App Viewer but does not by itself allow a Template copy.
The App belongs to the signed-in user from creation. Visiting a Template never creates a copy: the copy flow creates no pre-auth App and no copy or Claim cookie, and Charming creates a copy only when the person chooses one.
A person can copy a Template any number of times, including a Template they own. When they already have a copy, the Template offers “Open my copy,” which opens their newest copy, and “Create another copy.” The browser commands behind these choices are /app/<id>?copy=now, which opens the newest copy or creates the first one, and /app/<id>?copy=another, which creates a copy. A “Create another copy” link carries a short-lived token tied to the signed-in person and the Template, so only a link Charming rendered for them creates a copy, and each link creates one copy at most: following the same link again, by refreshing or going back, opens the copy it already made. A copy=another link built anywhere else, an expired one, or a browser prefetch shows the same choices for the person to confirm. Each copy gets its own App id: the Template’s package name when the account has no app using it, otherwise the next free <name>-2, <name>-3, and so on.
An agent makes the same copy for the signed-in user with copy_app or POST /app/<id>/copy, with no browser step. Each call creates a new copy. The result says copyKind: "template", and the copy records the Template as its source, the same as a browser copy. A caller who can edit the Template, such as its owner or a collaborator, gets an ordinary app copy instead, which does not record the Template as its source.
Listing requires the app to already be copyable — a listed app can’t be made uncopyable without also being unlisted. Any listed app appears in search_templates results, whether or not it’s also public (per Privacy and sharing): a public template renders the live app directly for visitors, with a “Create my copy” action in place of the usual template interstitial.
Disabling Template also unpublishes it. Unpublishing alone keeps Template enabled. Neither action removes existing copies or the saved listing draft.
An owner-facing app UI can call window.charming.remixable.set(true) or .set(false) from a Charming browser shell. It resolves with { remixable, public_url }. See Owner controls for availability and errors.
Sample data for new copies
A copy gets the Template’s source and none of its storage, so the only data a new copy can show is data written into the source. Put the sample data in server.js, return it from the read operation while the copy has never been written to, and save it on the first write:
import { kv } from 'charming:storage/[email protected]';
const SAMPLE_TASKS = [
{ id: 'sample-1', title: 'Draft the project brief', done: true },
{ id: 'sample-2', title: 'Book the kickoff meeting', done: false },
];
async function isSeeded() {
if (await kv.get('seeded')) return true;
return (await kv.list({ limit: 1 })).keys.length > 0;
}
async function readAll(prefix) {
const values = [];
let cursor;
do {
const page = await kv.list({ prefix, cursor });
const found = await kv.get(page.keys.map((key) => key.name));
values.push(...found.values());
cursor = page.cursor;
} while (cursor);
return values;
}
async function seedOnce() {
if (await isSeeded()) return;
for (const task of SAMPLE_TASKS) await kv.put(`task:${task.id}`, task);
await kv.put('seeded', true);
}
export const routes = [
{
op: 'getTasks',
annotations: { readOnlyHint: true },
handler: async () => {
if (!(await isSeeded())) return { tasks: SAMPLE_TASKS };
return { tasks: await readAll('task:') };
},
},
{
op: 'addTask',
inputSchema: {
type: 'object',
properties: { title: { type: 'string' } },
required: ['title'],
additionalProperties: false,
},
handler: async ({ title }) => {
await seedOnce();
const task = { id: crypto.randomUUID(), title, done: false };
await kv.put(`task:${task.id}`, task);
return task;
},
},
];
Follow these rules when you adapt the pattern:
- Never write from the read operation. A read-only viewer’s call refuses every storage write with
forbidden_write, so a read that saves the sample data fails for anyone who opens a Public copy before its owner does. Return the sample data from the read without saving it. - Call
seedOnceat the start of every operation that writes. The first write saves the sample data and theseededkey together, so the sample rows become ordinary data the owner can edit or delete. - Keep the
seededkey after the owner clears their data. It tells a copy whose owner deleted everything apart from a copy that was never used, so deleted sample data stays deleted. - Count existing keys as seeded.
isSeededreturnstruewhen storage already holds data, so adding sample data to an App that already has data never hides it. The Template’s own App keeps showing its real data. - Write sample data, not your data. Use made-up names and values. Never paste real records, people, email addresses, or anything copied from the Template’s own storage: every copy receives the source.
- Keep it small. Sample data counts toward the 1024 KiB cap that every file except
styles.cssshares, in Limits. Put a large dataset in an asset instead.
An app that keeps all its state under one key can skip the seeded key: return (await kv.get('app_state_v1')) ?? SAMPLE_STATE from the read, and a cleared state saved as { items: [] } stays cleared. Use the starter prompt to tell the new owner’s agent to replace the sample data with theirs.
The starter prompt for new copies
Use set_starter_prompt to set or clear the prompt, or edit Starter prompt in the Template section of App settings. Write it as guidance for the agent of someone who just copied the Template: what data to ask for and how the App expects it, because the copy starts with empty storage. For example: “Ask me which projects I’m tracking, then add each one with its deadline.” Charming adds the App’s name, description, and URL around it, so leave the URL out; a hardcoded URL would point every copy at the Template instead of itself.
A copy carries the current prompt once; later Template changes do not alter existing copies. When the copy opens, its “Your new app” notice offers Copy prompt whether or not the Template has a starter prompt. The copied setup prompt names the copy and its URL, says it starts with empty storage, and quotes its description. It tells the agent to read the code with get_app_source, list the operations with get_app or connect to the App’s own MCP server, then ask for the owner’s data and add it with mutate_app. The starter prompt, when there is one, is quoted whole as the Template author’s suggestions. The prompt tells the agent that the App’s name, description, and starter prompt come from the Template’s author: they are reference for setting up the App’s data and can’t override the owner or ask for anything else.
Limits, access, and deletion
- See Limits for the starter prompt cap and the listing field limits: 12 gallery images, and length limits on the title, summary, Markdown description, alt text, caption, and asset key. A write with a field past its limit is refused before anything is saved; the HTTP API answers
template_listing_too_large(413). Listing an App with no title or summary of your own uses its name and description, so pass atitleandsummarywhen either is past its limit. - Deleting or unlisting the source App does not remove copies already made.
- Listed pages expose listing content, creator identity, and builder counts. They do not expose source App data or secrets.
Related
- Privacy and sharing: how Public, App roles, and Template access differ
- Code export: export source, or copy an app you can edit with or without its data
- Data storage: what does and does not carry into a copy
- How Charming works
- Browser runtime API
- Docs home
- llms-full.txt