S
supero.docs
Documentation/Building Your App/Schema Reference

Schema Reference

How data models are defined in Supero: the attributes[]/references[] schema format, system fields, the domain-project-tenant hierarchy, namespaces, and schema inheritance via extends.

Overview

Every entity in a Supero app is defined by a JSON schema. Schemas drive the generated CRUD API, the Python and JavaScript SDKs, and the dynamic admin UI. This page documents the actual schema format used by the platform: how fields, types, references, and hierarchy are declared, and how schemas relate to one another through namespaces and inheritance.
This is a reference for developers hand-editing or reviewing schemas produced by App Studio, or writing schemas directly. For the conceptual overview first, read Core Concepts. A minimal object schema looks like this:
json
{
  "name": "Task",
  "parent_type": "tenant",
  "description": "A unit of work owned by a tenant.",
  "attributes": [
    { "name": "status", "type": "string", "values": ["pending", "in_progress", "done"], "default-value": "pending" },
    { "name": "priority", "type": "integer", "default-value": 5 },
    { "name": "due_date", "type": "date" }
  ]
}

ℹ️ name is the only field you must supply

The platform automatically adds identity and audit fields (uuid, fq_name, created_at, updated_at, and more — see System Fields below) to every object schema. You only declare the fields that are specific to your entity.

The Three Schema Kinds

A schema is one of three kinds. The kind determines whether it gets lifecycle management (a uuid, CRUD endpoints, a place in the hierarchy) or is just a value shape embedded inside another schema.
KindNeeds parent_type?Has uuid / CRUD?Purpose
objectrequiredyesA first-class entity with its own records — Customer, Order, Task.
typenonoAn embedded/composite value structure used inside an object or another type — Address, Money.
enumnonoA fixed set of allowed values.

Enum example

json
{
  "name": "OrderStatus",
  "type": "string",
  "values": ["pending", "processing", "shipped", "delivered", "cancelled"]
}

Type example

json
{
  "name": "Address",
  "attributes": [
    { "name": "street", "type": "string", "mandatory": true },
    { "name": "city", "type": "string", "mandatory": true },
    { "name": "zip", "type": "string", "pattern": "^[0-9]{5}(-[0-9]{4})?$" },
    { "name": "country", "type": "string", "default-value": "US" }
  ]
}
A type has no parent_type and no lifecycle fields — it exists only when embedded inside an object field, e.g. a field named shipping_address with "type": "Address".

Fields & Supported Types

Every schema (object, type, or enum) declares its fields in a single attributes array of {name, type, ...} objects — not a JSON-Schema properties map. Each field supports the following keys:
KeyDescription
typeA primitive type, a custom type/enum schema name, or namespace:type for a cross-namespace reference.
mandatoryMarks the field as required.
default-valueDefault value applied when the field is not supplied.
listtrue if this field holds a list of values of the given type (also accepted as IsList).
valuesInline allowed-values list — used to declare an enum of values directly on the field.
pattern / min-length / max-lengthString constraints (pattern is a regular expression).
min-value / max-valueNumeric range constraints.
indexName of a simple or compound index to build on this field.
descriptionDocumentation shown in the generated SDK and admin UI.

ℹ️ attributes is canonical; fields is accepted

The samples on this page use the canonical spelling: attributes for the field list at the top of a schema body, and mandatory on an individual field. That is also what App Studio writes into a generated app, so a sample here lines up with the schema file you get. 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 GET never returns the key you wrote. 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 — one schema, two spellings in the same response. Write attributes and mandatory in anything you maintain, and compare against a real read rather than against what you sent.

Supported primitive types

TypeMaps to
stringText
integerWhole numbers
floatDecimal numbers
booleantrue / false
dateISO 8601 date
datetimeISO 8601 date + time
jsonKey-value / nested data — a native dict in Python, an object in JS
uuidIdentifier string
anyUnvalidated value — use sparingly

💡 Type aliases are normalized automatically

Common synonyms are accepted and silently canonicalized: str/text→string, int→integer, number/double→float, bool→boolean, timestamp→datetime, map/dict/object→json. You can write whichever your tooling produces; the stored schema always uses the canonical name.
Beyond primitives, a field type can name a custom type or enum schema you defined ("type": "Address"), or one of the built-in system types: Image, File, Currency, PhoneNumber, Email, Address, GeoLocation, Signature, DateRange, URL, ContactInfo.

Full object example

json
{
  "name": "Customer",
  "parent_type": "tenant",
  "description": "A customer, isolated per tenant.",
  "attributes": [
    { "name": "email", "type": "string", "mandatory": true, "pattern": "^[^@]+@[^@]+\\.[^@]+$" },
    { "name": "status", "type": "string", "values": ["prospect", "active", "inactive"], "default-value": "prospect" },
    { "name": "lifetime_value", "type": "float", "min-value": 0 },
    { "name": "tags", "type": "string", "list": true },
    { "name": "shipping_address", "type": "Address" }
  ]
}

System Fields

Object schemas automatically receive a set of platform-managed fields — do not declare these yourself. Records key on uuid, never id.
FieldDescription
uuidUnique identifier, generated on creation.
fq_nameFully-qualified name path — the record’s position in the parent_type hierarchy, from the domain down to this object.
parent_typeThe schema name of this object’s parent in the hierarchy.
parent_uuidThe uuid of the specific parent record.
created_atISO 8601 timestamp set on creation.
updated_atISO 8601 timestamp set on every update.
name is the one field you must always supply yourself when creating a record — it is used to derive fq_name and must be unique among siblings under the same parent.

Parent-Child Hierarchy & Tenant Isolation

Every object schema declares a parent_type, which places it in a tree rooted at the platform’s built-in hierarchy: Domain, then Project, then Tenant — see Projects & Tenants and Building Multi-Tenant Apps. The parent_type you choose is what determines the isolation boundary for records of that schema.
parent_typeIsolationTypical use
domainShared across every project and tenant in the domain.Rare — domain-wide shared configuration.
projectShared across all tenants within a project.Rare — templates or config meant to be shared.
tenantFull per-tenant isolation.Default for first-level business objects: Customer, Order, Product, Invoice, Task.
<a custom object type>Inherits the isolation of its parent chain.Nested/child objects, e.g. Contact with parent_type "customer".

💡 Default rule

First-level business objects should use parent_type: "tenant". This gives each tenant its own isolated set of records without any extra configuration. Reserve parent_type: "project" or "domain" for genuinely shared resources.
Nested objects reference the specific parent object type rather than a hierarchy level — e.g. a LineItem under Order uses parent_type: "order", and inherits Order’s tenant isolation through the parent chain.

References Between Objects

Where parent_type expresses containment, references express a link between two independent objects. A reference stores the target’s uuid and fq_name — not an embedded copy of the object.
FieldRequiredDescription
nameyesName of the reference, e.g. "owner", "customer".
typeno (defaults to name)Target object type. Supports namespace:type for a cross-namespace reference.
cardinalityno (default "many")"one" or "many".
attrnoName of a type or enum schema carrying extra metadata about the link (link data). Must be a type or enum, never an object.
back_ref_namenoOverrides the auto-generated name of the reverse-lookup property on the referenced type.
json
{
  "name": "Order",
  "parent_type": "tenant",
  "attributes": [
    { "name": "status", "type": "OrderStatus", "default-value": "pending" }
  ],
  "references": [
    { "name": "customer", "type": "Customer", "cardinality": "one" }
  ]
}

Namespaces & Extending Schemas

Every schema belongs to a namespace — an orthogonal grouping label, independent of the domain/project/tenant hierarchy. A schema’s true identity is namespace:name, so two schemas can share a bare name in different namespaces and remain completely separate, each with its own fields and its own data.
  • •Namespace is optional on upload. A schema that omits one inherits the namespace of the project you upload it into, falling back to tenant if that project has none set.
  • •Reserved namespaces you cannot use for your own schemas: general (built-in Domain/Project/Tenant/UserAccount schemas), system, supero.
  • •Cross-namespace type references and extends both use the namespace:type format, e.g. references: [{ "type": "healthcare:patient" }].
  • •A bare (unqualified) type name that exists in more than one namespace is rejected rather than guessed — qualify it to disambiguate.

Extending a schema

Rather than modifying a base schema in place, a schema can extend another with extends: "<namespace>:<base-name>". The derived schema inherits the base’s fields and references and can add its own — it cannot remove or redefine an inherited field. The platform’s own extendable base schemas are catalogued in Transactional Services.
json
{
  "name": "EnterpriseProject",
  "extends": "general:project",
  "description": "Adds enterprise metadata to the built-in project schema.",
  "attributes": [
    { "name": "department", "type": "string", "mandatory": true },
    { "name": "cost_center", "type": "string" },
    { "name": "budget", "type": "float", "min-value": 0 },
    { "name": "compliance_level", "type": "string", "values": ["standard", "hipaa", "sox", "pci"] }
  ]
}

ℹ️ Extending built-in schemas

Project, Tenant, UserAccount, and UserSession can each be extended with custom fields via extends: "general:<name>" (e.g. general:project, general:tenant). Domain, ApiKey, and SchemaRegistry are platform-managed and cannot be extended.

Working with Schema Data

Once a schema exists, its records are read and written like any other resource, through the standard CRUD API — see Managing Data for the day-to-day workflow and Querying & Pagination for filtered reads. Use the schema’s name in the path — qualified with its namespace (namespace:name) whenever the bare name is ambiguous or you defined the schema in a non-default namespace.
bash
curl -X POST https://api.supero.dev/api/v1/crud/acme-corp/Customer \
  -H "X-API-Key: ak_..." \
  -H "Content-Type: application/json" \
  -d '{"name": "acme-industries", "email": "[email protected]", "status": "active"}'
From the Python SDK (pip install supero):
python
import supero

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

# Create
customer = org.crud.create("Customer", name="acme-industries", email="[email protected]", status="active")

# Read / update
record = org.crud.get("Customer", customer["uuid"])
org.crud.update("Customer", customer["uuid"], status="inactive")

# Query builder
active = (org.crud.query("Customer")
            .filter(status="active")
            .order_by("-created_at")
            .all())

# A schema defined in a non-default namespace
patients = org.crud.ns("healthcare").patient.list()
OperationEndpoint
CreatePOST /api/v1/crud/{domain}/{type}
ReadGET /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 readPOST /api/v1/crud/{domain}/list-bulk

Next steps

Related pages:
  • •Core Concepts — the domain/project/tenant hierarchy and platform terminology.
  • •Managing Data — creating, reading, updating, and deleting records through the API, SDKs, and admin console.
  • •Dynamic UI — how forms, tables, and detail views are generated from your schema.
  • •Schema API — uploading, listing, and describing schemas programmatically.
  • •Transactional Services — the base schemas you can extend instead of writing from scratch.