S
supero.docs
Developers/SDK/Build an App with Your Own AI Agent

Build an App with Your Own AI Agent

Use your own coding agent (Claude Code, Cursor, or similar) to author a full Supero app locally, then validate, publish, and deploy it over the Supero MCP server.

The deal: the backend is locked, the UI is entirely yours

This page is for teams who want to drive the whole build with their own AI coding agent instead of the built-in AI Assistant — pointing Claude Code, Cursor, or a similar tool at a written specification and letting it author real files on your machine. The Supero MCP server is a bridge, not a generator: your agent designs the schema and writes every line of the front end; MCP only validates what you authored against the live platform, publishes it, and deploys it. No code is generated on Supero's side and none of your source is executed during validation.
Every build sits on one side of a fixed line. Call the left column exactly as documented — never reimplement it, never fight it. The right column is yours with no template and no fixed navigation.
LOCKED — call it exactlyYOURS — full creative freedom
The HTTP API (CRUD, transactional services, auth, public reads)Every pixel: layout, navigation, animation, components
The client.* JS runtime (auth + CRUD + refs + transactional)Routing model, page structure, state management
The services.* integrations (email, Stripe, AI, Slack, …)Visual design, theme, typography, motion
The Python SDK (the supero package: app setup, CLI)Data-fetching strategy, caching, optimistic updates
The record contract (uuid, name, display_name, created_at, …)Dashboards, charts, kanban boards, calendars, wizards
Transactional state machines (order, booking, payment, …)How and when you surface each state transition
The packaging contract — the file shapes belowui/app.js and any modules/assets you add under ui/
The RBAC engine and server-side policy enforcementHow you reflect permissions in the UI

ℹ️ Two failure modes this boundary prevents

Reimplementing locked things (your own auth, your own fetch wrapper, a hardcoded API base URL) diverges from platform behavior in ways that break later. Fighting locked things (wrong argument count to a call, sending auth headers on a public read, inventing a method that does not exist) tends to fail silently — a blank screen or a spinner, not a console error.

Author the eight-file bundle locally

A Supero app is a directory, not a loose set of files. Point your agent at the build specification (fetched over MCP as build_get_skills) and have it produce all eight files below at these exact paths, under a top-level folder named for your app (for example ~/acme-store/). Do not stop after the "interesting" files — __main__.py, requirements.txt, and run.sh are required, and the front end lives at ui/app.js, not app.js at the root.
text
acme-store/
├── schemas.py        data model — fields, references, namespace
├── config.py         AppConfig dataclass — app identity, tenants, users, services
├── setup.py          seed data, RBAC policies, workflow definitions
├── __main__.py        3-line entry point
├── requirements.txt  one line: supero
├── .env.example       credentials template — never ship a real .env
└── ui/
    └── app.js    ★    your entire front end — this exact path

# generated on first local run — never hand-write these, edits are lost:
#   ui/index.html  ui/config.js  ui/server.py  Dockerfile  docker-compose.yml
FilePurpose
schemas.pyDefines ALL_SCHEMAS and PUBLIC_SCHEMAS — each schema a dict with fields[], references[], parent_type, and a literal namespace string
config.pyA dataclass named AppConfig — app_name/app_emoji, domain/project read from the environment, tenants, seeded users, the services list, public_schemas
setup.pySeeds records, defines RBAC PolicyDef/PolicyRule entries, and (optionally) workflow definitions and event bindings
__main__.pyA three-line entry point that calls setup.main()
requirements.txtA single unpinned line: supero — the CLI manages the version floor
run.shCreates a virtualenv, installs supero, and runs python3 -m supero.cli
.env.exampleA template for SUPERO_URL, SUPERO_DOMAIN, SUPERO_PROJECT, SUPERO_API_KEY, etc. — ship this, never a real .env
ui/app.jsYour entire front end — the only file the CLI never generates or overwrites

💡 Read the namespace off the project, not out of thin air

Every schema's namespace must be the exact schema_namespace value the platform assigned to your project (returned when you look up the project — see the build flow below) — written as a plain string literal, never a variable or an environment read. A mismatched namespace is one of the checks build_validate runs before you publish.

The locked API you call: client.*, services.*, and the record contract

Two globals are guaranteed to exist before ui/app.js runs: client (auth, CRUD, references, transactional services) and services (fire-and-forget integration wrappers). Never re-declare, reinitialize, or reimplement either — the runtime scripts that provide them load before your code does.
javascript
var cfg = window.__SUPERO_CONFIG || {};

// Auth — 5 positional args; tenant is ALWAYS '' so the server resolves the
// signed-in user's own tenant (multi-tenant users 401 if you pass a fixed tenant name)
await client.login(cfg.domain, email, password, cfg.project, '');

// CRUD — bare or namespaced schema name; both resolve
var leads = await client.getObjects('lead');
var created = await client.createObjectWithRefs('lead',
  { display_name: 'Jane Doe', contact_name: 'Jane Doe', email: '[email protected]', lead_state: 'new' },
  [{ ref_name: 'Company', ref_uuid: companyUuid }]);
if (created.refErrors.length) {
  showToast('Created, but a link failed: ' + created.refErrors.map(function (e) { return e.ref_name; }).join(', '), 'warning');
}

// Filter/sort/search — the real server-side query API for lists and dashboards
var hot = await client.queryObjects('lead',
  { op: 'and', conditions: [{ field: 'lead_state', op: 'eq', value: 'qualified' }] },
  { sort_by: 'value', sort_order: 'desc', limit: 50 });
Record fieldNotes
uuidPrimary key — key on this, never id
name / display_namename is a slug; createObject auto-derives it from display_name when omitted
created_at / updated_atTimestamps, set by the server
parent_type / parent_uuidWhere the record sits in the hierarchy — set these to create a child record
fq_nameFully-qualified path, used internally for multi-tenant routing
services.* wraps the integrations your config.py services list imports — email, sms, stripe, ai, slack, calendar, and more. Each call is fire-and-forget: one promise, no retry, no state. Always handle rejection and design the UI to degrade if a service is not configured yet.
javascript
services.email.send({ to: lead.email, subject: 'Welcome', bodyHtml: '<p>Thanks for reaching out!</p>' })
  .then(function () { showToast('Email sent', 'success'); })
  .catch(function (e) { showToast('Email failed: ' + e.message, 'error'); });

⚠️ Two different AI paths

A chat that answers questions over your app's own data should use the built-in agentic assistant (the aiChatStream global, or the ready-made AIChatPanel component) — it is on by default and needs no service import. services.ai is a separate integration for when your app logic itself needs to call an AI feature (summarize, classify, generate); it must be imported and configured like any other integration, so do not wire your in-app assistant to it.

Two ways to build the UI — pick exactly one

Supero supports exactly two front-end patterns for ui/app.js. Choose deliberately, per app, and never mix them — mounting your own React tree alongside a call to AppShell.render() gives you two React roots fighting over the DOM.
PathShapeWhen to use it
A — AppShell.render() (fast path)app.js is roughly 50 lines of configuration: a tabs array plus one AppShell.render({...}) callPlain CRUD apps where you want speed over a bespoke look — the library renders a complete standard app: login, sidebar, CRUD tables, modals, dashboard
B — full custom UIapp.js mounts your own React tree and renders whatever you want, calling the same client.*/services.* primitives underneathAdvanced navigation, bespoke layouts, complex interactions, a designed brand experience — this is what your own AI agent is best suited to build
A Path-B app.js starts from a mount pattern that satisfies three rules at once: route on the URL hash (so Back/Forward and deep links work), mount into a sibling container rather than the library's own root, and dismiss the platform's first-paint splash screen — the library only removes that splash inside AppShell.render(), which a custom app never calls.
javascript
(function () {
  var h = React.createElement;               // React is a global — never import or re-declare it

  function App() {
    var [route, setRoute] = React.useState(window.location.hash || '#/');
    var [authed, setAuthed] = React.useState(client.isAuthenticated());
    React.useEffect(function () {
      function onHash() { setRoute(window.location.hash || '#/'); }
      window.addEventListener('hashchange', onHash);
      return function () { window.removeEventListener('hashchange', onHash); };
    }, []);
    if (!authed) return h(LoginScreen, { onDone: function () { setAuthed(true); } });
    return h(Shell, { route: route });          // your app
  }

  function mountApp() {
    var pl = document.getElementById('supero-preloader');
    if (pl && pl.parentNode) pl.parentNode.removeChild(pl);   // dismiss the boot splash — required in Path B
    var el = document.getElementById('myapp-root');
    if (!el) { el = document.createElement('div'); el.id = 'myapp-root'; document.body.appendChild(el); }
    ReactDOM.createRoot(el).render(h(App, null));              // a SIBLING container, not the library's own root
  }

  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', mountApp);
  else mountApp();
})();
That is an abbreviated version to show the shape — the full pattern (including the async auth-rehydration guard, needed because the client restores its session from local storage asynchronously) is in the build specification your agent fetches at the start of the session.

Connect your agent to Supero over MCP

Point your MCP client (Claude Code, Cursor, or any MCP-capable agent) at Supero's MCP endpoint and authenticate with an API key sent as X-API-Key — never as a Bearer token.
http
POST https://api.supero.dev/mcp/v1/messages
X-API-Key: ak_...    (an opaque, scoped secret)
Key scopeWhat it can build
Domain-admin keyCan create projects (plan permitting) and build into any project in the domain
Project-admin keyPinned to a single project — can build into that one project only
Tenant-scoped keyCannot build apps — the build tools require a domain- or project-admin key

ℹ️ A domain-wide key does not pick a project for you

A project-scoped key is already pinned; a domain-wide credential still has to select or create a project before authoring begins. Use the project tools below to do that — never scan the local filesystem or read local project state for this, since the connection carries all the context you need.

The build flow: select a project, validate, publish, deploy

Once connected, the build tools take you from a fresh idea to a running app without leaving the MCP session — the same flow walked through end to end in Build an App over MCP. Local deploy is always available; a managed cloud preview is available where enabled for your platform.
ToolWhat it does
build_whoamiResolves your key's role, domain, and plan — call this first to decide the flow
build_get_skillsFetches the build specification your agent authors against, plus the SDK version floor to target
build_list_projects / build_get_projectFind or inspect a project — build_get_project returns the schema_namespace you must set as the literal namespace on every schema
build_create_projectDomain-admin only — creates a fresh project and returns its API key once
build_validateValidates your authored bundle against the live platform before you publish — structural checks only, your code is never executed
build_publishPackages your files, uploads them, and records a new version — returns a version id and a download URL; runs build_validate first unless you force it
build_deploytarget: local hands you run instructions using the project's own key; a managed cloud-preview target is also available where enabled
build_smoke_testChecks a deployed app actually loads, dismisses the boot splash, and reads a row — run it after every deploy
text
build_whoami
build_get_skills                                → author schemas.py / config.py / setup.py / ui/app.js locally
build_create_project {name:"acme-store"}            (or build_list_projects → build_get_project)
build_validate {project_uuid, files:{...}}          → fix any errors it reports
build_publish  {project_uuid, files:{...}}          → version_uuid, download_url
build_deploy   {project_uuid, target:"local"}       → run instructions with the project's own key
Send your files as a map of {relative_path: content} — a leading folder name like acme-store/ is stripped automatically. Author at least the eight required files; do not ship .env, ui/index.html, ui/config.js, ui/server.py, or a Dockerfile — the CLI generates those locally, and they are dropped if you include them in a publish.

Validation, secret hygiene, and versioning

build_validate is structural and AST-only: it checks the file manifest, syntax, import-safety, that config.py exports AppConfig, schema validity, and that every schema's namespace matches the project's schema_namespace. Nothing you send is executed on the platform.

🚨 Never inline a secret

Publishing drops any secret-pathed file automatically (everything under .ssh/.aws/.gnupg, private-key files, credential files) and rejects content that contains an inline API key. Keep real credentials out of every file you send — .env.example is a template, not a place for a live key.
  • •Iterate by editing locally, then re-running build_validate → build_publish → build_deploy — each publish records a new version, and re-publishing bumps it.
  • •A validation error you can't explain from your own edit is almost always a namespace mismatch or a missing required file — re-check schemas.py against the project's schema_namespace and the eight-file manifest above.
  • •Keep requirements.txt unpinned (just supero) so the CLI can manage the version floor for you as the SDK evolves.

Next steps