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.
| Kind | Needs parent_type? | Has uuid / CRUD? | Purpose |
|---|---|---|---|
| object | required | yes | A first-class entity with its own records — Customer, Order, Task. |
| type | no | no | An embedded/composite value structure used inside an object or another type — Address, Money. |
| enum | no | no | A 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:
| Key | Description |
|---|---|
| type | A primitive type, a custom type/enum schema name, or namespace:type for a cross-namespace reference. |
| mandatory | Marks the field as required. |
| default-value | Default value applied when the field is not supplied. |
| list | true if this field holds a list of values of the given type (also accepted as IsList). |
| values | Inline allowed-values list — used to declare an enum of values directly on the field. |
| pattern / min-length / max-length | String constraints (pattern is a regular expression). |
| min-value / max-value | Numeric range constraints. |
| index | Name of a simple or compound index to build on this field. |
| description | Documentation 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
| Type | Maps to |
|---|---|
| string | Text |
| integer | Whole numbers |
| float | Decimal numbers |
| boolean | true / false |
| date | ISO 8601 date |
| datetime | ISO 8601 date + time |
| json | Key-value / nested data — a native dict in Python, an object in JS |
| uuid | Identifier string |
| any | Unvalidated 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.
| Field | Description |
|---|---|
| uuid | Unique identifier, generated on creation. |
| fq_name | Fully-qualified name path — the record’s position in the parent_type hierarchy, from the domain down to this object. |
| parent_type | The schema name of this object’s parent in the hierarchy. |
| parent_uuid | The uuid of the specific parent record. |
| created_at | ISO 8601 timestamp set on creation. |
| updated_at | ISO 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_type | Isolation | Typical use |
|---|---|---|
| domain | Shared across every project and tenant in the domain. | Rare — domain-wide shared configuration. |
| project | Shared across all tenants within a project. | Rare — templates or config meant to be shared. |
| tenant | Full 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.
| Field | Required | Description |
|---|---|---|
| name | yes | Name of the reference, e.g. "owner", "customer". |
| type | no (defaults to name) | Target object type. Supports namespace:type for a cross-namespace reference. |
| cardinality | no (default "many") | "one" or "many". |
| attr | no | Name 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_name | no | Overrides 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()| Operation | Endpoint |
|---|---|
| Create | POST /api/v1/crud/{domain}/{type} |
| Read | GET /api/v1/crud/{domain}/{type}/{uuid} |
| List | GET /api/v1/crud/{domain}/{type} |
| Update | PUT /api/v1/crud/{domain}/{type}/{uuid} |
| Delete | DELETE /api/v1/crud/{domain}/{type}/{uuid} |
| Rich query | POST /api/v1/crud/{domain}/query |
| Bulk read | POST /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.
On this page