When you access a folder-based Application, CharmIQ builds it on demand. Every load: source files in, self-contained HTML out. The same build can also be invoked directly through the Application MCP Server. No deploy step, no CI pipeline. Edit a file, reload the Application, or ask an external tool to build the same manifest and files.This page covers how that build works. For the MCP interface itself, see Application MCP Server.What the Build Does
The pipeline takes your source files — TypeScript, SCSS, JSX, whatever — resolves all imports, compiles and bundles everything, and produces a single HTML document that runs in an iframe. From many files to one. No external assets to manage.Source files (in your folder or manifest)
↓
Virtual file system (in-memory path map)
↓
esbuild (TypeScript → JS, SCSS → CSS, resolves imports)
↓
HTML assembly (inject styles, scripts, import maps)
↓
Self-contained HTML document
The Manifest
The manifest is a manifest.json Source document. For folder-based Applications, it lives at the root of your folder. For document-based Applications, it's the first JSON code block in the document.{
"id": "my_app_v1",
"name": "My Application",
"description": "What this does",
"requestedScopes": [
"command.notification.toast.emit"
],
"bundle": {
"format": "esm",
"entry": {
"html": "/index.html",
"script": "/src/main.tsx",
"style": "/src/styles.scss"
},
"dependencies": {
"importMap": {
"react": "https://esm.sh/react@19.3.0",
"react-dom/client": "https://esm.sh/react-dom@19.3.0/client?deps=react@19.3.0"
}
}
}
}
Required fields:| Field | Description |
|---|
id | Stable identifier for this Application |
name | Display name |
bundle.entry.html | Virtual path to the HTML template |
Optional fields:| Field | Description |
|---|
requestedScopes | The scopes your Application calls (see Scopes and Consent below). An Application that declares nothing cannot use any gated bridge surface. |
bundle.entry.script | Virtual path to the JavaScript/TypeScript entry point |
bundle.entry.style | Virtual path to the CSS/SCSS entry point |
bundle.format | 'esm' for ES modules. Omit for IIFE (global variables). |
bundle.files | Explicit virtual path → source query map. Only needed for cross-document references or when not using folder-based discovery. |
bundle.mounts | Folders outside your Application's own, mounted at a virtual path prefix: { "/shared": "../../shared" }, relative to your Application's folder. Imports reach mounted files under the prefix. |
bundle.dependencies | External packages loaded from CDN. |
Scopes and Consent
Every gated bridge call — services, commands, charmiq.fetch, MCP, OAuth, Application-to-Application — resolves to a scope (for example command.notification.toast.emit, fetch.platform). Folder-based and document-based Applications declare these scopes in the manifest's requestedScopes. Inline Applications declare them in the Application container's requested-scopes attribute as a percent-encoded JSON array.- Your own data needs no scope.
appContent and appState are your Application's own: reading and writing them needs no declaration and no permission, and a declaration of them asks for nothing. - Declare everything you call. A call whose declaration excludes its scope is denied outright — no dialog, no fallback. The manifest bounds what your Application may even ask the user for. (
"requestedScopes": [] declares "needs nothing" — everything gated is denied.) - One consent covers the manifest. The first time your Application needs consent, the user sees a single dialog listing every declared scope; one decision covers them all. Undeclared-then-added scopes ask again on next use.
- Inline omission declares nothing. An inline Application without
requested-scopes has an authoritative empty declaration. It loads without prompting, and every gated call is denied until its scope is added to the container. - Old manifest without
requestedScopes? A manifest-backed Application written before declarations existed retains legacy per-capability prompts. Add requestedScopes to bound what it can ask for and to batch consent. - Exact scopes ask once; wildcards ask per use. Prefer exact entries.
- Fetching from the Platform is a scope. A
<script src="charmiq://…"> tag or a charmiq.fetch of a CharmIQ URL runs with the user's credentials and requires fetch.platform. A file your manifest declares in bundle.resources and reads with charmiq.resources.text(name) needs no scope: the declaration covers it, and each read still checks the user's access to the Application and to the file. - MCP scopes follow the server authority, not just the runtime alias. A CharmIQ server such as
'vfs' requires mcp.platform.vfs; Organization-defined servers use mcp.org.{serverId}; explicit connections use mcp.adhoc.{connectionName}. Do not use mcp.vfs, mcp.{alias} or mcp.*. See Developer | MCP in Applications → Declaring MCP Scopes. - Navigation needs no scope. Navigation follows the web's own rules: a link the user clicks opens a real tab, a popup your Application opens (a popup-based OAuth flow, an external picker) leaves the iframe sandbox, and a click may navigate the whole CharmIQ page. Browser popup-blocking rules still apply. Navigating the whole CharmIQ page requires browser-recognized user activation.
- Undo and redo are Commands. Cmd/Ctrl+Z and Shift+Cmd/Ctrl+Z pressed inside your Application reach the Document's undo and redo as
command.editor.undo and command.editor.redo. Declare both so a user can undo without clicking out of your Application, and leave those keys unhandled (no preventDefault). Keys typed into your Application's own text fields stay with the field. Undeclared, the keys are denied silently: nothing in your Application sees the refusal. - Finding your scopes. Run the Application and watch the browser console: a denied call names the missing scope in its error (
denied by the consent gate — scope 'fetch.platform' is not declared in requestedScopes), and the [consent-shadow] rows list every scope the Application exercised with its declared= status.
The Virtual File System
The build system doesn't read from a real filesystem. It builds a virtual one from your manifest and folder structure.For folder-based Applications: The platform walks your folder tree. Each Source document becomes a virtual file. The virtual path comes from the document name — a document named App.tsx in a sub-folder named components becomes /components/App.tsx.For manifest files entries: Virtual paths map directly to source queries. The query format is ?nodeId=<id>&format=plain.Hybrid: Both at once. Folder files are discovered automatically; explicit files entries override or supplement them. Useful when you need to pull a specific file from outside the folder.Folder on disk → Virtual file system
──────────────── ────────────────────
manifest.json (parsed, excluded)
index.html → /index.html
src/main.tsx → /src/main.tsx
src/App.tsx → /src/App.tsx
components/Header.tsx → /components/Header.tsx
Once the virtual file system is built, import resolution works exactly as you'd expect: import { Header } from './components/Header' resolves to /components/Header.tsx.Supported Languages
| Source | Compiled To | Notes |
|---|
TypeScript (.ts) | JavaScript | Full TypeScript support via esbuild |
TSX (.tsx) | JavaScript | React JSX |
JSX (.jsx) | JavaScript | React JSX without TypeScript |
JavaScript (.js) | JavaScript | Passed through |
SCSS (.scss) | CSS | Sass with partials and modules (@use, @forward) |
SASS (.sass) | CSS | Indented Sass syntax |
CSS (.css) | CSS | Passed through |
HTML (.html) | HTML | Used as template, not transformed |
SCSS partials work normally. @import 'variables' resolves to _variables.scss in the virtual file system. A reference resolves from the file it is written in (see Stylesheets).Stylesheets
Style your Application with a style entry (entry.style), with stylesheets your script imports, or both. A style entry works without any script, and a stylesheet both forms name applies once.Share a theme. Mount the shared folder and use the path your editor resolves on disk:"bundle": {
"entry": { "script": "/src/main.ts", "style": "/src/styles.scss" },
"mounts": { "/shared": "../../shared" }
}
// apps/example/src/styles.scss
@use '../../../shared/theme' as theme;
.toolbar { color: theme.$ink; }
Every @use, @forward and @import resolves from the file it is written in, so the theme's own @use 'tokens' finds shared/theme/_tokens.scss. A reference tries _name.scss, name.scss, then .sass, then .css, then a folder's _index; the first file that exists wins. A .css file is read as plain CSS. Configure a shared theme with @use … with (…).Import stylesheets from script (HTML and script output; a style-output build compiles only its style entry).| You write | You get |
|---|
import './toolbar.scss' | The stylesheet styles the page: from the first paint when imported statically, otherwise when this module runs |
import css from './dialog.scss' | The compiled CSS as a minified string, for a shadow root; nothing styled |
import('./dark.scss') | A module whose default is the compiled CSS; nothing styled |
- In an HTML build, stylesheets your script imports statically are written into the page, so it paints styled from the first frame; one reached only through
import() or require() applies when that module runs. - In an HTML build, styles from script come before your style entry, so your own rules override a shared default of equal specificity, even one loaded later.
- In a script-output build, styles from script are added after the styles already on the page that runs your script.
- A stylesheet reached only through
import() or require() of a module that never runs adds no styles. A TypeScript import whose binding you never use adds none either: the compiler removes it. - Importing your own style entry from script adds no second copy; it is already on the page.
What a stylesheet can reach. Only your Application's files and mounts, read with your access. A file:, https: or package reference in a Sass import fails the build; Sass built-ins such as sass:math work. A plain CSS @import url(https://…) stays a browser load.Images and fonts. Write images as data: URIs or use external https: URLs, and give fonts a fallback family so text stays readable if a font does not load. A url() naming one of your Application's files fails the build, naming the stylesheet and the URL.Signing. A signature covers every stylesheet your build reads: the style entry, stylesheets your script imports and the partials they reach. Editing any of them, or importing a stylesheet the signature does not cover, shows viewers the changed-signature warning until you sign again. Changing a shared file changes every Application that uses it on its next build.External Dependencies
External packages are loaded from CDN — they're never bundled into your output. Two formats:ESM + Import Maps (recommended for format: 'esm'):{
"bundle": {
"format": "esm",
"dependencies": {
"importMap": {
"react": "https://esm.sh/react@19.3.0",
"react-dom": "https://esm.sh/react-dom@19.3.0?deps=react@19.3.0",
"react-dom/client": "https://esm.sh/react-dom@19.3.0/client?deps=react@19.3.0",
"lucide-react": "https://esm.sh/lucide-react@1.48.0?deps=react@19.3.0"
}
}
}
}
The build marks these packages as external (not bundled) and injects a <script type="importmap"> into the output HTML. Your code uses normal import statements.CDN + Globals (for IIFE format):{
"bundle": {
"dependencies": {
"cdn": {
"chart.js": {
"url": "https://unpkg.com/chart.js@4.5.1/dist/chart.umd.js",
"global": "Chart"
}
}
}
}
}
The build injects <script> tags for each CDN URL and generates a require() shim that maps package names to window globals.What the Build Produces
The output is a single, self-contained HTML document:CharmIQ adds a Content Security Policy with object-src 'none' and base-uri 'none'. Do not rely on <object>, <embed> or <base href> in Application HTML. Use ordinary elements for embedded content and explicit or document-relative URLs for scripts, styles, links and requests.<html>
<head>
<!-- injected: import map or CDN scripts -->
<!-- injected: compiled CSS -->
</head>
<body>
<!-- your HTML template content -->
<script src="charmiq-bridge.js"></script> <!-- bridge injected -->
<script type="module">/* bundled JS */</script>
</body>
</html>
The bridge is always injected before your script. window.charmiq is available before any of your code runs.Debug Mode
Append ?debug=true to your Application URL to see build diagnostics:- Bundle size and build time
- Dependency breakdown
- Virtual file system listing
- Build warnings with actionable suggestions
During development (unpublished Applications), build info is also available in code:window.charmiq.debug.buildInfo // Full build metadata
window.charmiq.debug.showWarnings() // Log build warnings
window.charmiq.debug.openDebugPage() // Open debug dashboard
Error Handling
When the build fails, the Application renders a styled error page. It includes:- The specific error message, with the file and line it names
- Contextual suggestions: a missing file points to its path,
bundle.mounts or bundle.files; a missing package to its dependency - Suggested manifest snippets for common fixes
- Guidance for people viewing the Application: try again, or contact its author. A failure inside CharmIQ, which the author cannot fix, names CharmIQ support instead
The page never shows a stack trace, a server path or quoted source.The Simplified Folder Manifest
When using folder-based discovery, you don't need a files map. The manifest only needs entry points:{
"id": "my_folder_app",
"name": "My App",
"bundle": {
"format": "esm",
"entry": {
"html": "/index.html",
"script": "/main.tsx"
},
"dependencies": {
"importMap": {
"react": "https://esm.sh/react@19.3.0",
"react-dom/client": "https://esm.sh/react-dom@19.3.0/client?deps=react@19.3.0"
}
}
}
}
The platform discovers everything else from the folder. This is the recommended starting point for any multi-file Application.External Build Calls
The Application MCP Server exposes the same build surface without requiring the source to already live in a CharmIQ folder. build_from_content accepts a manifest and inline virtual files. build_from_uri builds an existing CharmIQ Folder or Application document.Both tools return the built artifact as structured data. They do not persist the artifact, create documents, update folders, place iframe nodes, or publish output.Patterns
- Use ESM + import maps. IIFE + globals is a fallback for packages that don't have ESM builds. For everything else, ESM produces cleaner output and better tree-shaking at the CDN level.
- Name your Source documents with extensions.
App.tsx, styles.scss, utils.ts. The extension is how the build pipeline knows which loader to use. - Pin your CDN versions.
https://esm.sh/react@19.3.0 not https://esm.sh/react. Unpinned CDN URLs can introduce breaking changes silently. - Start without a style entry. Inline
<style> tags in your HTML template work fine for simple Applications. Add a SCSS entry point when you need variables, nesting, or multiple stylesheets.
Next Steps
→ Building Applications — How inline and folder-based Applications differ and when to use each.→ Developer | The Bridge API — Everything available via window.charmiq.→ Application MCP Server — Build from content or URI through MCP.→ Signing & Trust — Sign the built bundle so its publisher can be trusted.