@tosiiko/cup
v0.3.4
Published
CUP universal UI runtime for protocol-driven browser rendering
Readme
CUP
A protocol-driven UI runtime for backend-first browser apps.
Your backend returns JSON views that match schema/uiview.v1.json. The browser runtime validates, renders, and remounts them on each interaction. Auth, permissions, sessions, routing, and mutations stay on the server. The browser is a thin view layer.
┌───────────────┐ JSON view ┌──────────────────────┐
│ │ ───────────────▶ │ │
│ Backend │ │ Browser runtime │
│ (owns state) │ action payload │ (validate + mount) │
│ │ ◀─────────────── │ │
└───────────────┘ └──────────────────────┘Table of Contents
- When To Use CUP
- Install
- Initialize A Project
- The Protocol In One View
- Quick Start (Browser)
- Backend Example (Python)
- Actions
- Templates
- Regions & Patches
- Streaming
- Router
- Dispatcher & Middleware
- Deployment Profiles
- AI Eval Engine
- Capability Negotiation
- Offline Drafts
- Inspection & Tracing
- Security Defaults
- Recommended App Structure
- Public API
- Adapters
- Starters & Demos
- Docs
- Development
- Status & License
When To Use CUP
Best fit:
- dashboards, admin panels, CRMs, portals, internal tools
- authenticated workflows where authorization, sessions, and mutations belong on the server
- multi-language backends that want the same browser runtime
- apps where view structure is data, not code — so it can be validated, generated, tested, and audited
Less ideal:
- animation-heavy consumer SPAs
- offline-first client apps with large local state
- apps that want React/Vue/Svelte component trees to own most business logic
Install
npm install @tosiiko/cupInitialize A Project
Scaffold a runnable CUP app into the current directory or a new folder.
npx @tosiiko/cup init --adapter py-cup
npx @tosiiko/cup init my-login --adapter ts-cupRunnable init adapters: py-cup, go-cup, node-cup, ts-cup.
py-cup defaults to the standard structured Python layout:
my-cup-app/
app/
templates/
static/
cup/
server.py
README.mdFor the minimal Python login demo:
npx @tosiiko/cup init --adapter py-cup --template loginTo refresh a generated app to the latest packaged browser runtime:
npx @tosiiko/cup upgradeThat updates the vendored cup/index.js snapshot.
Generate a new page scaffold inside an existing project:
npx @tosiiko/cup generate page billing
npx @tosiiko/cup generate page account-health --route /account/healthcup generate page auto-detects py-cup, go-cup, node-cup, and ts-cup projects from the current folder, or you can force a target with --adapter.
The generated files follow the detected project layout, write the view/template files, and print the exact route wiring snippet for the chosen adapter and route.
The Protocol In One View
Every CUP view is one JSON document. Minimum required fields: template, meta.version.
{
"template": "<section><h1>{{ title }}</h1><button data-action=\"refresh\">Refresh</button></section>",
"state": {
"title": "Accounts"
},
"actions": {
"refresh": {
"type": "fetch",
"url": "/api/accounts",
"method": "GET"
}
},
"meta": {
"version": "1",
"title": "Accounts",
"route": "/accounts"
}
}| Field | Purpose |
|--------------|----------------------------------------------------------------------|
| template | Micro-template string (see Templates) |
| state | JSON data bound into the template |
| actions | Keyed map of fetch / emit / navigate descriptors |
| regions | Named sub-views with their own template, state, and actions |
| pagination | Pagination hints (page, pageSize, totalItems, hasNext, …) |
| forms | Form field metadata (labels, validation, options) |
| meta | version, title, route, lang, generator, provenance, extensions |
Adapters may attach meta.provenance (where the view came from — human, AI, adapter, hybrid) and meta.extensions (additive protocol features the view relies on).
Quick Start (Browser)
import {
STARTER_VIEW_POLICY,
mountRemoteView,
validateProtocolView,
validateViewPolicy,
} from '@tosiiko/cup';
async function loadView(url: string, root: HTMLElement) {
const response = await fetch(url, {
credentials: 'same-origin',
headers: { Accept: 'application/json' },
});
const payload = await response.json();
const view = validateProtocolView(payload);
validateViewPolicy(view, STARTER_VIEW_POLICY);
mountRemoteView(view, root);
}
loadView('/api/view', document.getElementById('app')!);The thin browser shell:
- load the current route
- validate the returned view
- mount it
- submit forms or action payloads back to the server
- remount the next server-approved view
Backend Example (Python)
from cup import UIView, FetchAction, EmitAction
def accounts_page(session):
accounts = load_accounts(session.user_id)
return (
UIView("""
<section>
<h1>{{ title }}</h1>
<button data-action="refresh">Refresh</button>
<ul>
{% for a in accounts %}
<li>
{{ a.name }} — ${{ a.balance }}
<button data-action="open" data-id="{{ a.id }}">Open</button>
</li>
{% endfor %}
</ul>
</section>
""")
.state(title="Accounts", accounts=accounts)
.action("refresh", FetchAction("/api/accounts", "GET"))
.action("open", EmitAction("sheet:open", detail={"id": "{{ id }}"}))
.title("Accounts")
.route("/accounts")
)See adapters/python for the full API. A Go adapter with the same shape lives at adapters/go.
Actions
Actions are how the browser talks back to the server. Each action is a keyed descriptor on the view's actions map and is triggered by a data-action attribute in the template.
fetch — HTTP request:
{ "type": "fetch", "url": "/api/accounts/{{ id }}", "method": "DELETE" }Supports GET, POST, PUT, PATCH, DELETE. Form data and explicit payloads are both supported.
navigate — browser navigation:
{ "type": "navigate", "url": "/accounts" }emit — client-side custom event for app-specific behavior:
{ "type": "emit", "event": "sheet:open", "detail": { "id": "{{ id }}" } }Trigger from the template:
<button data-action="refresh">Refresh</button>
<button data-action="delete" data-id="{{ item.id }}">Delete</button>
<form data-action="save">…</form>Each action may also carry target (which view/region to refresh), semantics (read/mutate), priority, and a confirm prompt.
Templates
CUP templates are small and predictable — not a general-purpose template language.
{{ value }}— escapes HTML by default{{ value|safe }}— renders trusted HTML (only for sanitized content)- chained filters are supported:
{{ name|trim|upper }},{{ total|currency:"USD" }},{{ count|pluralize:"item,items" }} {% if %},{% elif %},{% else %},{% endif %}— withnot/!and comparisons==,!=,>,<,>=,<={% for item in items %}…{% endfor %}— exposesloop.index,loop.index1,loop.first,loop.last- unsupported tags like
{% include %}fail with parser errors
Built-in filters cover common string, number, date, JSON, join, replace, currency, percent, truncate, default, and pluralization cases.
Custom filters can be added globally with registerFilter(name, fn), and listFilters() returns the currently available filter names for debugging or inspection tooling.
Good practice:
- keep templates focused on rendering
- keep permission logic out of templates
- prefer fixed class names over dynamic class generation
- treat
|safeas exceptional
Regions & Patches
Regions are named sub-views with their own template, state, and actions. They allow granular updates without replacing the whole view.
{
"template": "<section>…<div id=\"sidebar\"></div></section>",
"regions": {
"sidebar": {
"template": "<ul>{% for n in notifications %}<li>{{ n.text }}</li>{% endfor %}</ul>",
"state": { "notifications": [] }
}
}
}A ProtocolPatch targets a region by name and specifies a mode — replace, append, or prepend — so the backend can push incremental updates (e.g. new list items) without re-rendering the page.
import { applyProtocolPatch, isProtocolPatch } from '@tosiiko/cup';
if (isProtocolPatch(frame)) {
applyProtocolPatch(root, frame);
}Streaming
Two streaming modes are built in.
NDJSON fetch stream with automatic reconnect:
import { streamView } from '@tosiiko/cup';
streamView('/api/feed', document.getElementById('app')!, {
onFrame: (frame) => console.log('frame', frame.type),
onError: (err) => console.error(err),
});Server-Sent Events:
import { streamSSE } from '@tosiiko/cup';
streamSSE('/api/live', document.getElementById('app')!);Frame types: view (full replace), patch (region update), heartbeat, done, error. Both helpers validate frames before mounting and expose onFrame, onError, and onClose callbacks plus a StreamHandle to close the stream.
Router
Client-side navigation without full page reloads:
import { createRouter } from '@tosiiko/cup';
const router = createRouter({
routes: [
{ path: '/accounts', handler: () => loadView('/api/accounts') },
{ path: '/accounts/:id', handler: ({ id }) => loadView(`/api/accounts/${id}`) },
],
});
router.start();Supports named path params, query strings, CSS view transitions, and programmatic navigation.
Dispatcher & Middleware
Centralize action handling with a composable pipeline:
import {
createDispatcher,
loggerMiddleware,
loadingMiddleware,
errorMiddleware,
delayMiddleware,
} from '@tosiiko/cup';
const container = document.getElementById('app')!;
const dispatcher = createDispatcher(container, {
template: '<button data-action="delete">Delete</button>',
state: {},
});
dispatcher.use(loggerMiddleware());
dispatcher.use(loadingMiddleware(dispatcher));
dispatcher.use(errorMiddleware(dispatcher));
dispatcher.use(delayMiddleware(150));
dispatcher.register('delete', async (ctx) => {
await fetch('/api/delete', { method: 'DELETE', body: JSON.stringify(ctx.payload) });
});
dispatcher.mount();
dispatcher.dispatch('delete', { id: '123' });Built-in middleware: loggerMiddleware, loadingMiddleware, errorMiddleware, delayMiddleware. Add your own for auth headers, analytics, or optimistic updates.
loadingMiddleware(dispatcher) now writes DOM-visible loading state in addition to the dispatcher signals:
- the container gets
data-cup-loading="<action names>"while actions are active - the container and triggering element get a
.cup-loadingclass for CSS hooks
Mounted views also annotate the DOM with CUP debugging metadata such as data-cup-mount, data-cup-source, data-cup-view-route, data-cup-view-title, data-cup-view-version, and per-action data-cup-action-* attributes.
If you need to preserve state across a remount, mark nodes with data-cup-preserve, data-cup-preserve-value, data-cup-preserve-scroll, or data-cup-focus-key.
Deployment Profiles
Profiles bundle a security policy, eval thresholds, and required capabilities into a named trust level.
import {
STARTER_PROFILE,
REGULATED_PROFILE,
ENTERPRISE_PROFILE,
applyProfile,
} from '@tosiiko/cup';
const result = applyProfile(view, STARTER_PROFILE);
if (!result.ok) throw new Error(result.reason);| Profile | Intended Use |
|----------------------|----------------------------------------------------|
| STARTER_PROFILE | Safe defaults — relative URLs only, no unsafe HTML |
| REGULATED_PROFILE | Healthcare, finance, legal — stricter checks |
| ENTERPRISE_PROFILE | Large internal tools — balanced |
AI Eval Engine
Score AI-generated or adapter-generated views before mounting or serving them.
import { evalView, evalBatch, repairAndEval } from '@tosiiko/cup';
const result = evalView(view);
// result.score — 0–1 weighted aggregate
// result.validity — schema compliance
// result.security — policy compliance
// result.accessibility — a11y checks
// result.completeness — required fields, state/template syncDefault dimension weights:
| Dimension | Weight | |---------------|--------| | Validity | 35% | | Security | 35% | | Accessibility | 15% | | Completeness | 15% |
repairAndEval(candidate) auto-repairs a malformed view then re-evaluates it. evalBatch(views) evaluates many views and surfaces common failure patterns.
Capability Negotiation
Clients and servers negotiate protocol version and extension support before mounting.
import {
createCapabilityHeaders,
negotiateCapabilities,
parseCapabilityHeaders,
DEFAULT_RUNTIME_CAPABILITIES,
} from '@tosiiko/cup';
const headers = createCapabilityHeaders(DEFAULT_RUNTIME_CAPABILITIES);
const response = await fetch('/api/view', { headers });
const support = parseCapabilityHeaders(response.headers);
const result = negotiateCapabilities(DEFAULT_RUNTIME_CAPABILITIES, support);
if (!result.ok) {
// server required an extension the client does not support
}Views that declare a required extension in meta.extensions are rejected before mount if the client cannot honor it.
Offline Drafts
Server-authoritative patterns with offline tolerance:
import { createDraftStore, createRetryQueue } from '@tosiiko/cup';
const drafts = createDraftStore({ key: 'account-edits' });
drafts.save('account-42', { name: 'Pending change' });
const queue = createRetryQueue();
queue.enqueue({ url: '/api/accounts/42', method: 'PUT', body: { … } });
queue.flush();Drafts persist in browser storage. The retry queue handles transient failures and replays when the client comes back online.
Inspection & Tracing
Debug a mounted view or the whole runtime:
import {
inspectView,
createInspector,
createTraceObserver,
inspectTraces,
} from '@tosiiko/cup';
// Snapshot a single view
const snapshot = inspectView(view);
// Long-lived inspector
const inspector = createInspector(root);
console.log(inspector.snapshot());
// Trace renders, actions, and validations
const observer = createTraceObserver();
observer.on('action', (trace) => console.log(trace));Traces cover renders, actions, validations, and stream frames. They're the backbone for production observability and AI debugging.
Security Defaults
Runtime:
{{ value }}escapes HTML by defaultmountRemoteView()validates protocol views by default- the TypeScript adapter's remote helpers require an explicit
fetchImplfrom the host app - core bundle has no transport markers — no baked-in
fetch,http://, orhttps:// - targets modern evergreen browsers
Starter-grade backend defaults:
- signed cookie sessions
- CSRF protection on every state-changing POST
- no-store headers on HTML and JSON
- server-owned authorization
- policy validation before JSON leaves the server
- relative action URLs by default
Policy example:
import { STARTER_VIEW_POLICY, validateViewPolicy } from '@tosiiko/cup';
validateViewPolicy(view, STARTER_VIEW_POLICY);See docs/security.md.
Recommended App Structure
my-cup-app/
app/
server.py
routes.py
actions.py
sessions.py
security.py
data.py
views/
auth.py
overview.py
accounts.py
pipeline.py
templates/
login.html
shell.html
pages/
overview.html
accounts.html
pipeline.html
static/
app.js
app.css
README.mdWhy:
server.pystays thinroutes.pydecides which view to returnactions.pyowns mutationssecurity.pyandsessions.pyisolate security-sensitive codeviews/assembles template statetemplates/keeps markup editable without bloating backend files
Public API
Everything exported from @tosiiko/cup:
Validation
validateProtocolView, validateProtocolPatch, ValidationError
Policy
validateViewPolicy, STARTER_VIEW_POLICY, PolicyError
Mounting
mount, createMountUpdater, mountRemoteView
Templates
parseTemplate, render, registerFilter, listFilters, TemplateError
Actions & Router
createDispatcher, loggerMiddleware, loadingMiddleware, errorMiddleware, delayMiddleware, createRouter
Streaming
streamView, streamSSE
Patches & Composition
applyProtocolPatch, isProtocolPatch, renderProtocolRegions, applyProtocolForms, applyProtocolPagination, collectProtocolActionBindings, serializeProtocolForm, findProtocolRegionName, getProtocolActionState, resolveProtocolActionTarget
Eval & Repair
evalView, evalBatch, repairAndEval, repairProtocolViewCandidate, repairProtocolPatchCandidate
Profiles
STARTER_PROFILE, REGULATED_PROFILE, ENTERPRISE_PROFILE, PROFILES, applyProfile, getProfile
Capability Negotiation
createCapabilityHeaders, parseCapabilityHeaders, negotiateCapabilities, DEFAULT_RUNTIME_CAPABILITIES, CUP_CAPABILITY_NEGOTIATION_EXTENSION, CUP_PROVENANCE_EXTENSION
Offline
createDraftStore, createRetryQueue
Inspection & Tracing
createInspector, inspectView, createTraceObserver, inspectTraces
Binding & CSS
bind, unbind, cssState, animate, waitTransition, waitAnimation, waitForVisualCompletion, theme, createSignal
Schema & Styles
./schema/uiview.v1.json./styles/reference.css
Adapters
- Python adapter — production
- Go adapter — production
- TypeScript adapter — alpha, owns transport-aware remote helpers
- Node adapter — alpha
- Rust, Java — in-repo source adapters
- Namespace registry and status
Remote loading helpers (fetchView, fetchViewStream) live on the TypeScript adapter path so @tosiiko/cup core stays transport-free.
Every official adapter must emit v1-compatible views, preserve meta.version, meta.lang, and meta.generator, and pass the shared contract tests in tests/runtime/contract.test.ts.
Starters & Demos
Starters (begin real projects):
- python-minimal — smallest runnable CUP app
- python-portal — request/review/history workflow
- python-crm — richer authenticated shell
- node-dashboard — Node.js backend
Demos (study patterns):
- login — simple login flow
- dashboard2 — structured CRM
- dashboard — financial dashboard prototype
Fast bootstrap instead of cloning:
npx @tosiiko/cup init --adapter py-cupDocs
- Architecture
- Routing and streaming
- Security
- Testing
- Compatibility and deprecation policy
- Reference UI vocabulary
- Generators
- AI guidance
- AI prompts, evals, and fixtures
- Adapter namespaces and status
- Migration notes
Development
npm install
npm run build
npm run check # build + test + pack:check
npm run test # TS + Python + Go adapter tests
npm run demo:smoke
npm run starter:smokeRun demos and starters locally:
python3 demo/login/server.py
python3 demo/dashboard/server.py
python3 demo/dashboard2/server.py
python3 starters/python-minimal/server.py
python3 starters/python-portal/server.py
python3 starters/python-crm/server.py
node starters/node-dashboard/server.mjsStatus & License
- protocol version:
1 - package version:
0.3.4 - browser target: modern evergreen browsers
- focus: stable backend-first runtime, adapters, starters, and release tooling
Licensed under Apache License 2.0.
- npm releases through
0.2.3were published under MIT - releases from this repository follow Apache-2.0 unless explicitly noted otherwise
