Custom Apps: Vue engine 3
Start here before writing an app. New apps default to engineVersion 3. Rendering and browser compilation are owned by @cococo/surfaces/customApp, shared by the developer console and CoCoCo UI. Tenant Vue/JS is NEVER compiled or executed on the platform server.
Authoring format
One App.vue Single File Component, supplied as source to create_custom_app.
Use normal Vue 3: <script setup> (JS or lang="ts"), <template>, and
optional <style scoped>. A normal <script>export default ...</script>
or a template-only component also works. Import Vue helpers explicitly.
Top-level script-setup bindings are visible in the template; NO setupReturn.
TypeScript syntax is erased; successful compilation is NOT a type-check.
Supported runtime imports (exact names):
vue: the bundled Vue runtime (ref, computed, watch, onMounted, etc.)@cococo/app: graphql, graphqlMutation, rpc, context
V3 is a single-file environment, not an npm project. Relative/URL/package imports, dynamic imports, external src blocks, JSX, CSS modules, Sass/Less and custom SFC blocks are not supported. Keep reusable local components in the same file. No external dependencies are downloaded at runtime. Source limit: 512 KiB UTF-8. Browser compiler timeout: 10 seconds.
Host SDK
import { graphql, graphqlMutation, rpc, context } from '@cococo/app'
const data = await graphql('query ...', { /* variables */ })
// data is ALREADY the GraphQL data object; do not use data.data.
const changed = await graphqlMutation('mutation ...', { /* variables */ })
// Inspect mutation payload `errors` as well as catching transport errors.
const result = await rpc('handler_name', arg1, arg2)
// RPC arguments are positional; result is the Lua return value.
// context: { mode, appInfo: { id, name, handle, kind }, hasServerApi }RPC requires a saved app/version with a Lua serverApi. The host retains all credentials. An unsaved frontend preview supports GraphQL but cannot run a new Lua handler. A preview can execute real data operations as the viewer; it is not a transaction sandbox or mock database.
Styling your app
Add a <style scoped> section to your app to control its appearance with CSS.
Scoped styles apply to your app's elements. Use the theme colours below to
make your app fit the surrounding interface and adapt to light and dark mode.
Start with a card
This example creates a card with delivery details that the reader can expand. Copy it into an empty app, or adapt its template and styles for your own content.
<template>
<section class="delivery-card" aria-labelledby="delivery-title">
<h2 id="delivery-title">Delivery information</h2>
<p class="description">Check delivery details before placing your order.</p>
<details>
<summary>Show delivery details</summary>
<p>Standard delivery takes two working days.</p>
</details>
</section>
</template>
<style scoped>
.delivery-card {
box-sizing: border-box;
width: 100%;
padding: 1.25rem;
color: var(--host-card-foreground);
background: var(--host-card);
border: 1px solid var(--host-border);
border-radius: 0.75rem;
}
h2 { margin: 0 0 0.5rem; }
p { margin: 0.5rem 0; }
.description { color: var(--host-muted-foreground); }
summary { cursor: pointer; }
summary:focus-visible {
outline: 2px solid var(--host-ring);
outline-offset: 4px;
}
</style>Choose colours from the theme
Use a background colour together with its matching foreground colour so text
remains readable. For example, a primary button uses --host-primary for its
background and --host-primary-foreground for its text.
| What you are styling | Background | Text |
|---|---|---|
| The app's main area | --host-background |
--host-foreground |
| A card or panel | --host-card |
--host-card-foreground |
| A primary action | --host-primary |
--host-primary-foreground |
| A secondary action | --host-secondary |
--host-secondary-foreground |
| Supporting content | --host-muted |
--host-muted-foreground |
| Highlighted content | --host-accent |
--host-accent-foreground |
Use --host-border for panel borders, --host-input for input borders,
--host-ring for keyboard focus outlines, and --host-destructive to draw
attention to an error or destructive action. Include a clear message or label
as well as colour.
Keep the layout usable
- Let cards and fields fit the available width. Avoid fixed page widths that force people to scroll sideways on a smaller screen.
- Give each field a visible label and each action a descriptive name, such as “Save changes”. Use ordinary HTML buttons, forms, and tables for these controls.
- Keep keyboard focus visible. Try moving through your app with the Tab key.
- Explain loading, empty, and error states: “Loading orders…”, “No orders yet”, or an error message with a useful next step.
- Preview your app at a narrow width and in both light and dark mode before publishing it.
Write these styles as ordinary CSS in your app. For ready-made components, enable Include DaisyUI styles in the code editor. This optional setting includes DaisyUI 5 CSS inside your app, so buttons, cards, inputs and other components use the same colours as your CoCoCo host. It defaults to off and is saved with each version, including previews and rollbacks.
With DaisyUI enabled, you can write:
<template>
<section class="card">
<div class="card-body">
<h2 class="card-title">Delivery details</h2>
<label for="reference">Reference</label>
<input id="reference" class="input input-bordered" placeholder="Order reference" />
<button type="button" class="btn btn-primary">Save</button>
</div>
</section>
</template>DaisyUI supplies component styles; use ordinary CSS in <style scoped> for your
own layout and spacing. The option does not add Tailwind utility classes or a
Tailwind compiler. No external stylesheet or import is needed. Host background,
text, primary, secondary, accent, border and error colours are mapped automatically
and follow theme changes. DaisyUI's success and warning colours keep their defaults.
For GraphQL, set config.useDaisyUI: true when creating a custom app version;
set it to false to turn the styles off. Omitting it inherits the published app's
setting. The MCP create/update tools accept the same useDaisyUI boolean.
Optional Lua serverApi
V3 uses the same bounded Lua exports runtime as v2. Example:
exports = {}
function exports.echo(value)
return value
endService-reaching Lua APIs return (ok, result); check both values. Discover
the published NORMAL Lua API contract. Do not assume os/io or generic Lua
modules are available. This engine is not the experimental Lua-view architecture.
GraphQL storage and migration
Existing wire fields are retained for compatibility. For engine 3: config = { engineVersion: 3, template: "", script: "", serverApi: "" } createCustomAppVersion snapshots the engine with the source. Omit config.engineVersion to retain the parent's current engine. Publishing or rolling back a version switches both source and engine together.
Boundaries
The iframe has allow-scripts only; it has no same-origin access to host DOM or credentials. V3 blocks direct fetch/network connections and external resources with CSP, and runs prebuilt local compiler tooling in a worker. Data access uses the authorised host SDK. This is not a claim of complete browser egress confinement or a per-app capability allowlist: existing user authorisation still applies. Compilation accepts code, not trusted code.
Minimal runnable v3 app
Pass the following complete component as source to create_custom_app.
It needs no data permissions or server API. It demonstrates Vue reactivity,
explicit imports, scoped CSS and host theme tokens.
<script setup lang="ts">
import { ref } from 'vue'
const count = ref<number>(0)
</script>
<template>
<main class="page">
<h1>My app</h1>
<p>A Vue app running securely inside CoCoCo.</p>
<button @click="count++">Count: {{ count }}</button>
</main>
</template>
<style scoped>
.page { padding: 24px; }
h1 { font-size: 24px; }
button { padding: 8px 16px; border: 0; border-radius: 8px;
background: var(--host-primary); color: var(--host-primary-foreground);
cursor: pointer; }
</style>Add data after discovering and validating the real schema. Use
import { graphql, graphqlMutation, rpc } from '@cococo/app'.
For an optional minimal serverApi, use:
exports = {}
function exports.echo(value) return value endThen await rpc('echo', 'hello') resolves to 'hello'.
Legacy engines 1 and 2 only
The following generated contract applies to split template/script apps. Engine 3 uses the imports documented above.
The API a custom app may call
Injected by the host into the app's frame. Nothing else is available: the frame has no ambient network or platform access.
| Call | Available | Hosts | What it does |
|---|---|---|---|
window.graphql(query, variables = {}) |
always | all | Runs a GraphQL query as the signed-in user and resolves to data. Throws when the request fails or the response carries errors. |
window.graphqlMutation(mutation, variables = {}) |
always | all | Runs a GraphQL mutation as the signed-in user and resolves to data. Throws when the request fails or the response carries errors. |
window.rpc(method, ...args) |
needs a serverApi |
all | Calls a handler in the app's Lua serverApi. Throws when the app declares no serverApi, or the handler errors. |
window.APP_CONTEXT |
always | all | { mode, hasServerApi } plus appInfo (id, name, handle, kind) in customApp mode or instance (id, name, integrationId, version) in integration mode. |
window.INTEGRATION_CONTEXT |
integrations only | web — not yet on native | Alias of window.APP_CONTEXT, for integration-specific code. |
window.integration.containers.get(schemaName, name) |
integrations only | web — not yet on native | Reads a schema-validated data container. Throws when the container does not exist or the read fails. |
window.integration.containers.set(schemaName, name, data) |
integrations only | web — not yet on native | Creates or replaces a schema-validated data container. Throws when the data does not satisfy the named schema. |
window.integration.containers.delete(schemaName, name) |
integrations only | web — not yet on native | Deletes a data container. Throws when the delete fails. |
Vue bindings in scope
createApp, ref, reactive, computed, watch, onMounted, onUnmounted.
Authoring rules
templateis raw HTML with Vue bindings — do not wrap it in<template>…</template>.- Styling uses the bundled Tailwind and DaisyUI classes; there is no separate stylesheet.
scriptruns inside the frame, not on the server. Usewindow.rpcfor anything privileged.- The frame has no ambient network access: reach the platform through the API below, not
fetch.
Published platform manual · revision 928876eaa4e2