NEWSupero Studio — generate, control, and deploy production apps.Start free →HIRINGSee the roles →

Extending Supero: your own endpoints, your own logic, your own UI

A platform is only useful if you can leave it. Four escape hatches, and the order to reach for them in.

ExtensibilityBuild estimate ~75 minutes9 min readAugust 25, 2026

Supero has four extension points — the services catalog, `extends`, the plugin registry, your own `ui/app.js` — and none of them opts you out of tenancy or RBAC. This is the decision rule for which one a given problem calls for.

The brief

The question every evaluator asks second

Teams hand-roll a cart because they did not know cart existed. They register a plugin for Stripe that the catalog already has. They fork the whole UI to change one card layout. Each is the same mistake made at a different altitude, and it is the mistake this article exists to prevent: Supero has four extension points, and the useful part is not the list, it is knowing which one a given problem calls for.

The first question anyone asks is "can it build my app". The second — the one that actually decides the purchase — is "what happens when it cannot". Every platform has a wall; what matters is whether the wall has a door, and whether going through it costs you the platform's guarantees.

4
extension points, in a deliberate order
1
decision rule that picks between them
0
extension points that opt you out of tenancy or RBAC
The decision rule

Work down the list, and stop at the first yes

If what you need is…Reach forYou keep
A third-party service someone else also uses (Stripe, Twilio, SendGrid, Okta, S3…)The managed services catalogCredentials in a store, retries, one call shape, no SDK
A lifecycle with states — a cart, an order, a booking, an approval, a signatureextends on a transactional base schemaA tested state machine, idempotency classes, an audit trail
A vendor not in the catalog, or one of your own internal HTTP servicesThe plugin registryStored auth header, retries, a circuit breaker, invocation stats
A screen the generated UI does not produceui_schemas.py, then ui/app.jsThe same data client, services and access rules underneath
The main escape hatch

The plugin registry — your endpoint, with guardrails

A plugin is a record of one HTTP endpoint plus the retry and breaker behaviour you would otherwise write yourself: method, timeout, an optional auth header held on the platform side, retry counts and circuit-breaker thresholds. Plugins live at the domain level — you register one against a project you have access to, and every project in that domain can then resolve and invoke it by name.

Registering and calling your own endpoint
Your appor an SDK scriptPlugin registrydomain-scopedRetry + breakerplatform sideYour endpointyour infraONCE1POST /plugins/register {project_uuid, plugin_name, endpoint_url}2201 {plugin_uuid, enabled, health_status}auth_header_value is stored securely and redacted on every readEVERY CALL AFTER THAT3POST /plugins/invoke/{object_type} {plugin_name, ...payload}4attach stored auth header + custom headers5POST {object, metadata} · timeout applies6response — or a failure7retry with exponential backoff, up to max_retriesconsecutive failures past the threshold open the circuit for its timeout8result · stats updated (count, latency, health)
Your endpoint receives the payload wrapped — {"object": { …the fields you sent… }, "metadata": { request_id, timestamp }} — plus an X-Request-ID header and your stored auth header. Read your fields off object; this is the first thing every integration gets wrong. And note what the wrapper buys you: the credential never travels through your application code, so it is the difference between "we call an internal API" and "every service that calls it needs the key".
bash
curl -X POST https://api.supero.dev/api/v1/plugins/register \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "project_uuid": "8f14e45f-2d3a-4b1c-9e6a-1234567890ab",
    "plugin_name":  "internal-sms-gateway",
    "endpoint_url": "https://sms.example.com/api/send",
    "http_method":  "POST",
    "auth_type":    "header",
    "auth_header_name":  "X-Api-Key",
    "auth_header_value": "REDACTED"
  }'

# then, from anywhere: object_type is just a label for the logs
curl -X POST https://api.supero.dev/api/v1/plugins/invoke/sms_notification \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "plugin_name": "internal-sms-gateway",
        "to": "+15551234567", "body": "Your order has shipped" }'
FieldDefaultWhat it buys you
http_methodPOSTOne of POST, PUT, PATCH
timeout_seconds30300 is the documented ceiling — set it to what your endpoint really needs; nothing caps it for you
auth_type / auth_header_name / auth_header_valuenoneThe credential lives here, not in your code, and is redacted on read
custom_headers—Static headers sent with every call
max_retries / retry_backoff_seconds3 / 2Exponential backoff (backoff × 2^attempt), so a blip does not become an incident. A 4xx is never retried — only failures worth retrying are
circuit_breaker_threshold / _timeout_seconds5 / 60A failing dependency stops being hammered while the breaker is warm. It is per-process and armed at registration, so treat it as a fast local guard — max_retries and timeout_seconds are the settings that always apply
enabledtrue on createNot a register-body field — flip it with PUT /api/v1/plugins/{uuid}/status. Disabled invocations are rejected with 503: a kill switch you can hit without a deploy

A GET on the plugin also returns what you would otherwise have to instrument yourself: health_status, total_invocations, success_count, failure_count, last_invocation_at and average_latency_ms. GET /api/v1/plugins/{uuid}/health gives the same picture plus the circuit-breaker configuration.

The UI hatch

The UI — declarative, then hand-built, never both

Supero renders an app's UI one of two ways, and a single app uses exactly one. The declarative path is a UI_SCHEMAS list in ui_schemas.py — pure data extending the platform's UI grammar, covering per-entity admin screens, dashboard widgets, landing pages and the app shell. The SDK renders all of it; ui/app.js becomes a thin shim.

The hand-built path drops ui_schemas.py and writes ui/app.js yourself — either calling AppShell.render({tabs, appName, theme, …}) with your own tab definitions, or mounting your own React tree for full control.

What is yours and what is regenerated on every run
YOU AUTHORThe bundleschemas.pyconfig.pysetup.pyui_schemas.py OR ui/app.jsREGENERATED EVERY RUNThe UI runtimeui/index.htmlui/config.jsui/server.pyui/*.js runtimeWRITTEN ONCE IF MISSINGContainer filesDockerfiledocker-compose.yml
Editing anything in the middle row is a wasted edit — it is rewritten the next time the CLI runs. Your customisation belongs in the top row.

Whichever path you take, the primitives underneath are the same: the same data client, the same services.* calls, the same access rules. Hand-building the UI does not opt you out of tenancy or RBAC — those are enforced server-side, so a hand-rolled screen is exactly as governed as a generated one.

The schema hatch

Your own fields, including on the platform's own objects

extends is the extension point people notice last and use most. It works in two directions.

  • Down from a service base — extends: "cart:base_cart" inherits a whole lifecycle and lets you add your own fields on top. This is how you get a state machine you did not write; the transactional catalog lists the base schema for every service.
  • Onto a system object — extends: "general:project" adds custom fields to a platform entity without touching the base. Your own metadata on a project, tenant or user, carried through the same APIs as everything else.
  • Between your own schemas — put shared fields on a base of your own and inherit from it. Resolution is server-side, so clients never walk the chain; they see the merged shape.
The logic hatch

Your own logic — and where it should run

There are three honest places to put business logic, and the choice is mostly about who has to be able to change it.

A workflow

Declarative steps — call a service, write a record, branch, run in parallel, reshape. Every run and every step is a queryable record. Best when an operator may need to change the behaviour without a deploy.

Your own backend

A service you host, registered as a plugin and invoked from your app code or a script. Best when the logic is genuinely yours — pricing engines, proprietary models, legacy systems.

Your app's own code

In ui/app.js or a script against the SDK, calling the same CRUD and service APIs. Best for anything user-facing and interactive.

For AI-shaped logic inside your own features — a "summarise this" button, a draft-reply field, a classification step — services.ai is the catalog entry to import. Keep it distinct from the in-app agent: the agent is a finished surface for end users, services.ai is a raw call for your code, and wiring one through the other only adds a failure mode.

Step by step

Adding your first plugin, end to end

Check the catalog before you build anything

bash
curl https://api.supero.dev/api/v1/services/catalog \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# and, for one entry's operations and config fields
curl https://api.supero.dev/api/v1/services/catalog/email \
  -H "Authorization: Bearer $ACCESS_TOKEN"

The catalog is the authoritative list of what is already available and what is already imported for the project. Most "we need a plugin for X" turns out to be a service_id someone did not know about.

⏱ ~10 minutes

Register the endpoint, with the breaker settings you actually want

The defaults are sensible but generic. Think about two of them specifically: timeout_seconds should reflect what your endpoint really does — a 30-second default in front of a 2-second API turns a hang into a stall — and circuit_breaker_threshold should be low enough that a dead dependency stops being hammered before your on-call notices.

⏱ ~20 minutes

Invoke it from one place, not from every caller

Put the invoke behind a single helper in your app rather than scattering it. The registry is then the one place the credential lives and the one place the invocation stats accumulate — which is what you will want the first time someone asks why the legacy API did not get called.

If the trigger is a data change, a workflow can still drive it indirectly: bind the event to a workflow whose step calls your code, and let that code invoke the plugin.

⏱ ~30 minutes

Watch the numbers the registry is already collecting

An extension point you cannot see the health of becomes the thing everyone blames and nobody checks — and the registry is already collecting health_status, success_count/failure_count and average_latency_ms for you. The kill switch is worth knowing too: PUT /api/v1/plugins/{uuid}/status with {"enabled": false} takes a failing dependency out of the path without a deploy.

⏱ ~15 minutes

Know the edges

What extending does not get you

  • There is no plugin step type in workflows. A service_call step resolves against the managed services catalog, so a data-triggered call to your own endpoint needs a workflow step that calls code of yours, which then invokes the plugin. This is the limitation you will actually hit.
  • Plugins are outbound HTTP calls with POST, PUT or PATCH. They are not a way to host arbitrary code inside the platform.
  • A plugin is registered once per domain and is resolvable by name from any project in it — convenient, but it means the name is a domain-wide namespace, not a per-project one.
  • The runtime UI files are regenerated on every run. If you find yourself editing them, you are on the wrong path — move the change into ui/app.js or ui_schemas.py.
  • Extending a base schema inherits its state machine, which means you also inherit its rules. If your lifecycle genuinely differs, model your own entity rather than fighting a base that nearly fits.
The takeaway

A door in the wall, not a cliff

The usual failure of an application platform is binary: inside the box everything is fast, and one step outside you are writing a second system with none of the guarantees. What makes extension work here is that the escape hatches keep the foundation — a plugin call still has its credential managed for you, a hand-written screen is still tenant-isolated and role-enforced, a custom field on a system object still travels the same APIs.

Which means the honest advice is not "use the platform for everything". It is: work down the list, stop at the first yes, and know that going one level lower costs you convention rather than governance.

Delivery estimates in this guide are for the scope described, built by one experienced developer with any vendor sandbox credentials already in hand. Treat them as a realistic starting point, not a commitment — and check the documentation for the authoritative behaviour of any API named here.

Build this on Supero

Describe it and get schemas, APIs, SDKs and a governed multi-tenant backend — or point your own AI editor at Supero over MCP and build it there.