S
supero.docs
Documentation/Getting Started/Core Concepts

Core Concepts

The vocabulary and data model every Supero app is built on: the Domain/Project/Tenant hierarchy, namespaces, schemas, records, and how roles map to the generated UI.

Overview

This page defines the building blocks you will see throughout the Supero docs and SDKs: the account hierarchy, the schema model that drives every generated API and admin screen, how records are keyed and scoped, and how a user's role determines which generated UI surface they land on. Read this before the Quick Start if you want the "why" behind the commands; read it after if you just ran your first request and want the mental model.

The Two Halves: Builder and Runtime

Before any of the vocabulary below, there is one distinction worth fixing in your head, because almost every confusing question about Supero resolves once you know which half you are asking about.
The Builder produces a generated bundle; the Runtime is the platform and client environment that bundle runs on
The Builder produces an application. The Runtime is what that application runs on. They meet at one artifact.
The Builder is where you make choices: how you build (App Studio, your AI editor over MCP, or Schema Studio — see Ways to Build), what the application is made of (which object types, which platform services, which integrations), and where its data comes from (records the platform stores, or tables a connector registers from a system you already run). What comes out is a generated bundle — your schemas, your UI schemas, your configuration, and the application script.
The Runtime is what that bundle then runs on, and it spans two environments rather than one. The Supero side provides the control plane (authentication, roles, tenancy), the data plane (CRUD, query and aggregate over your records), and the service hooks that reach payments, email, storage and workflows. Your side is the client application your users actually load, and — if you brought one — your own database.

ℹ️ Why the distinction is worth keeping

"How do I add a field?" is a Builder question, answered by changing a schema and regenerating. "Why did this request come back with no rows?" is a Runtime question, answered by looking at the caller's role, tenant and connector binding. Reaching for the wrong half is the single most common way to spend an hour on a five-minute problem.

The path a request takes at runtime

It is worth knowing the hops, because knowing which one failed is most of debugging.
Browser to UI server to api.supero.dev to storage
Four hops. The UI server in the middle is the one people forget.
The piece that surprises people is the UI server. A generated application is not a static bundle talking straight to the API: it is served by a small server that proxies /api/* and /ai/* through to api.supero.dev, serves the static files, and enriches the login response with the caller's access policy so the front end knows what to render. A request can therefore fail at that proxy without ever reaching the platform — which is why an error message that names no Supero service is usually a UI-server or configuration problem, not a platform one.
For the full service-by-service breakdown, see Architecture; for what a generated bundle contains file by file, see Anatomy of a Supero App.

The Vocabulary

Everything below is expressed the same way whether you call the REST API directly, use the Python SDK, or look at data through the dashboard — they are three views onto the same objects.

Domain, Project, Tenant

Every object in Supero lives under a fixed ownership hierarchy:
text
Domain
  └─ Project
       └─ Tenant
            └─ UserAccount / ApiKey
LevelWhat it is
DomainThe top-level account. Domain names are globally unique and identify your organization.
ProjectAn isolated backend environment inside a domain — its own schemas, data, and generated API surface. Most day-to-day work happens inside one project.
TenantA data-isolation boundary inside a project. Every project has a default tenant; SaaS apps create additional tenants so customers cannot see each other's data.
UserAccount / ApiKeyThe credentials that authenticate as a specific tenant. A user account is a person; an API key is a machine credential — both resolve to a domain/project/tenant scope.
Object creation and lookups walk this hierarchy: a tenant belongs to a project (parent_type "project"), a user_account or api_key belongs to a tenant (parent_type "tenant"), and your own business objects typically hang off the tenant too.

Namespace

Namespace is a separate, orthogonal concept from the Domain/Project/Tenant hierarchy — it is a label used to group and qualify schemas, not another level of ownership. Every schema is uploaded into a namespace (a project has a default schema namespace, commonly "tenant", used automatically when a schema omits one).
  • •A schema type is addressed unqualified within its own namespace (e.g. "customer"), or qualified with its namespace when referenced from elsewhere (e.g. "billing:customer").
  • •Namespaces let two projects, or two schema packages within a project, define types with the same short name without colliding.
  • •The "extends" field on a schema (see below) uses the same "namespace:type" qualified form to point at its base schema.

Authentication and Context

You authenticate with a POST to /api/v1/auth/login. The domain, project, and tenant are fields in the JSON request body — they are not URL query parameters. Project and tenant are optional; omitting them resolves to that user's default project and default tenant. Authentication is the full reference for tokens, API keys, and scoping.
http
POST /api/v1/auth/login
Content-Type: application/json

{
  "domain": "your-domain",
  "email": "[email protected]",
  "password": "your-password",
  "project": "default-project",
  "tenant": "your-tenant"
}
A successful login returns an auth block, the user record, a context block, and a resolved access policy:
json
{
  "auth": {
    "access_token": "...",
    "refresh_token": "...",
    "token_type": "Bearer",
    "expires_in": 28800,
    "session_id": "..."
  },
  "user": { "email": "...", "role": "tenant_admin", "ui_view": "admin", ... },
  "context": { "domain_name": "...", "project_name": "...", "tenant_name": "..." },
  "policy": { ... }
}
Access tokens are valid for 8 hours; refresh tokens for 7 days. Refresh with POST /api/v1/auth/refresh and revoke a session with POST /api/v1/auth/logout.
For subsequent requests, send the access token as Authorization: Bearer <token>, or send an API key as the X-API-Key header. Scope (which domain/project/tenant a request operates on) comes from the token or key itself, and can be narrowed further with project and tenant query parameters on read endpoints — it is never set via a separate required header.

🚨 Bearer vs API key

Never send an API key value in the Authorization: Bearer header. API keys are opaque secrets (they start with ak_) and must be sent as X-API-Key. Bearer is reserved for JWT access tokens issued by login.

Roles and Generated UI Surface

Every user and API key has a role. Roles determine both what a request is allowed to do (via access policies) and which generated UI surface a logged-in user sees. The Roles & Permissions Reference lists every role and what it can do; Access Control (RBAC) covers how enforcement works.
RoleTypical scope
domain_admin / domain_userManage or view everything inside a domain, across projects.
project_admin / project_userManage or view a single project, across its tenants.
developerProject-scoped, build-oriented access.
tenant_admin / tenant_userManage or use data within a single tenant — the common roles for an app's own customers and staff.
viewerRead-only access within a tenant.
On login, the platform derives a ui_view field on the user object: "admin" for roles that manage a domain, project, or tenant (plus developer), and "customer" for everyone else. Generated apps read client.userInfo.ui_view (or the equivalent SDK field) at boot to decide whether to render the admin dashboard/data-management shell or the public storefront-style landing experience — so your app does not need to hand-roll role-string checks. Admin vs Customer Views covers the split in detail.

Schemas

A schema defines one object type: its fields, its parent in the hierarchy, and its relationships. Schemas are what the platform uses to generate CRUD endpoints, validation, and admin screens. This section is the short version; the Schema Reference is the complete one.
A schema is uploaded as JSON with a name, a namespace, and a schema_content body:
json
{
  "name": "Order",
  "namespace": "tenant",
  "schema_type": "object",
  "schema_content": {
    "name": "Order",
    "parent_type": "tenant",
    "attributes": [
      { "name": "order_number", "type": "string", "mandatory": true },
      { "name": "total_amount", "type": "float", "mandatory": true },
      { "name": "status", "type": "string", "mandatory": false,
        "values": ["pending", "confirmed", "shipped", "delivered", "cancelled"],
        "default-value": "pending" }
    ],
    "references": [
      { "name": "customer", "cardinality": "one" }
    ]
  }
}

Fields

Each entry in attributes[] describes one field: name, type, mandatory (true/false), and for enum-like fields a values[] list plus an optional default-value. There is no separate JSON-Schema-style properties{} object — attributes[] is the whole field list.

ℹ️ attributes is canonical; fields is accepted

The sample above uses the canonical spelling: attributes for the field list, and mandatory on an individual field. The older spellings still work — fields is accepted as a synonym for attributes, and required as a synonym for mandatory — but the two pairs do not behave the same once you read back. A body sent with fields is stored under attributes, so a schema you upload and then read never returns the key you wrote; expect the response to differ from your request there. required is accepted but never rewritten, so a schema authored with required reads back with required while anything it inherits from a platform base carries mandatory. Write attributes and mandatory in anything you maintain, and see the Schema Reference.
TypeNotes
stringText. Also used for enums — pair with values[] to constrain to a fixed set.
textLong-form text.
integerWhole numbers.
floatDecimal numbers. There is no separate "number" or "decimal" type — use float.
booleanTrue or false.
datetimeISO 8601 timestamp.

parent_type and extends

parent_type places a schema in the Domain/Project/Tenant hierarchy (commonly "tenant" for your own business objects). extends builds one schema on top of another using the qualified "namespace:base_name" form — e.g. "catalog:base_product" — so the derived schema inherits the base schema's fields and references and can add or override its own. The base schemas the platform ships are catalogued in Transactional Services.

references — relationships

Relationships between object types are declared with references[], not with an inline field of type "reference". Each entry names the related type and a cardinality ("one" or "many"); the platform manages the underlying link and exposes it through the refs endpoints (get/update references) rather than as a plain data field.

Records and System Fields

Every record the platform stores — whether it is your own business object or a platform entity like a tenant — carries the same system fields:
FieldMeaning
uuidThe record's primary key. Records are always keyed on uuid, never on "id".
created_at / updated_atTimestamps maintained by the platform.
fq_nameThe fully-qualified name path from the domain down to this record.
parent_typeThe hierarchy or schema type this record hangs off (e.g. "tenant", "project").
parent_uuidThe uuid of that parent record.
A create response returns the record's system fields plus whatever fields you sent; look up a specific record with its uuid, not a name or slug.

CRUD and Queries

Every schema you upload gets a matching REST surface under /api/v1/crud/{domain}/{type} — see CRUD Endpoints for the full contract and Querying & Pagination for filters, sorting, and cursors:
OperationRequest
CreatePOST /api/v1/crud/{domain}/{type}
Get oneGET /api/v1/crud/{domain}/{type}/{uuid}
ListGET /api/v1/crud/{domain}/{type}
UpdatePUT /api/v1/crud/{domain}/{type}/{uuid}
DeleteDELETE /api/v1/crud/{domain}/{type}/{uuid}
Rich queryPOST /api/v1/crud/{domain}/query
Bulk listPOST /api/v1/crud/{domain}/list-bulk
The Python SDK mirrors this one-to-one: org.crud.create(type, name=..., **fields), org.crud.get(type, uuid), org.crud.list(type), org.crud.update(type, uuid, **fields), org.crud.delete(type, uuid). For filtered, ordered reads use the query builder: org.crud.query(type).filter(status="open").order_by("-created_at").limit(10).all().
python
import supero

org = supero.login(
    domain_name="your-domain",
    email="[email protected]",
    password="your-password",
)

order = org.crud.create("order", name="o-1001", order_number="1001", total_amount=42.50)

open_orders = (
    org.crud.query("order")
    .filter(status="pending")
    .order_by("-created_at")
    .limit(10)
    .all()
)

💡 Machine access

For server-to-server calls without an interactive login, use supero.connect(domain_name, api_key="ak_...") to get the same org object without a password.

Next Steps