S
supero.docs
Documentation/Building Your App/Managing Data

Managing Data

Create, read, update, and delete records through the CRUD API, the Python SDK, or the JavaScript client available in your app's frontend code.

Overview

Every schema you define is automatically exposed as a REST resource under one predictable pattern. This page covers creating, reading, updating, and deleting records, filtering and sorting result sets, and following references between records.
All data endpoints live under a single base pattern: POST | GET | PUT | DELETE https://api.supero.dev/api/v1/crud/{domain}/{type}[/{uuid}] {domain} is your Supero domain name and {type} is a schema's type name (snake_case, optionally namespace-qualified as ns:type). Authenticate with a user JWT (Authorization: Bearer <token>) from login, or a service API key (X-API-Key: ak_...) — never send an API key as a Bearer token.
bash
curl -X POST https://api.supero.dev/api/v1/crud/acme/product \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "wireless-headphones",
    "display_name": "Wireless Headphones",
    "price": 79.99,
    "category": "electronics",
    "in_stock": true
  }'

ℹ️ Scope

Reads and writes are scoped to the caller's domain/project/tenant, derived from the JWT or API key. Non-admin roles can optionally narrow that scope with project/tenant query params — they can never widen it.

Creating Records

Every record needs a name — it's used to build the record's fq_name (its position in the domain / project / tenant hierarchy) alongside the system-generated uuid. On success the API returns 201 with the record nested under its type name, plus uuid and fq_name mirrored at the top level.

REST API

bash
curl -X POST https://api.supero.dev/api/v1/crud/acme/product \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "wireless-headphones",
    "display_name": "Wireless Headphones",
    "price": 79.99,
    "category": "electronics",
    "in_stock": true
  }'
json
{
  "success": true,
  "data": {
    "product": {
      "uuid": "3f9c1e2a-6b7d-4e11-9c2f-2a6f1d0b9a10",
      "name": "wireless-headphones",
      "fq_name": ["acme", "wireless-headphones"],
      "display_name": "Wireless Headphones",
      "price": 79.99,
      "category": "electronics",
      "in_stock": true,
      "created_at": "2026-07-18T10:00:00Z",
      "updated_at": "2026-07-18T10:00:00Z"
    }
  },
  "uuid": "3f9c1e2a-6b7d-4e11-9c2f-2a6f1d0b9a10",
  "fq_name": ["acme", "wireless-headphones"]
}

Python SDK

python
import supero

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

product = org.crud.create(
    "product",
    name="wireless-headphones",
    display_name="Wireless Headphones",
    price=79.99,
    category="electronics",
    in_stock=True,
)

print(product["uuid"])                      # mirrored at the top level
print(product["data"]["product"]["price"])  # nested under the type name

JavaScript client

Inside your app's frontend code, a client instance is already wired up for the schemas your app defines.
javascript
const product = await client.createObject("product", {
  name: "wireless-headphones",
  display_name: "Wireless Headphones",
  price: 79.99,
  category: "electronics",
  in_stock: true,
});
console.log(product.uuid);

ℹ️ Validation

Incoming data is validated against the schema before it's persisted. A missing required field, an out-of-range value, or a value outside an enum's values list returns a 422 with error_code VALIDATION_FAILED. A duplicate name under the same parent returns 409 with error_code ALREADY_EXISTS.

Reading Records

Get a single record

Fetch one record by UUID. The response nests the record under its resolved type name — useful when you queried a bare type that resolved to a namespaced one.
bash
curl https://api.supero.dev/api/v1/crud/acme/product/3f9c1e2a-6b7d-4e11-9c2f-2a6f1d0b9a10 \
  -H "Authorization: Bearer $ACCESS_TOKEN"

List records

GET the type endpoint without a UUID to list. This supports pagination and sorting but not field filtering — the response is a flat array under results (not nested under the type name, unlike the single-record GET above).
bash
curl -G https://api.supero.dev/api/v1/crud/acme/product \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  --data-urlencode "limit=20" \
  --data-urlencode "offset=0" \
  --data-urlencode "sort_by=price" \
  --data-urlencode "sort_order=asc"
Query ParamPurpose
limitMax records to return
offsetRecords to skip
cursorOpaque cursor from the previous page — use instead of offset for deep iteration
sort_byField to sort by
sort_orderasc or desc
include_totalCompute a total count alongside the page (adds overhead)
parent_id / parent_uuidRestrict to children of a specific parent record
These are the GET list parameters. POST /query and POST /list-bulk take a different set — Querying & Pagination is the single reference for which parameter applies to which read endpoint, and it is the one to trust if any other page here disagrees.
json
{
  "success": true,
  "obj_type": "product",
  "results": [
    { "uuid": "3f9c1e2a-...", "name": "wireless-headphones", "price": 79.99 },
    { "uuid": "7a1b2c3d-...", "name": "usb-cable", "price": 12.5 }
  ],
  "result_count": 2
}

Python SDK / JavaScript client

python
product = org.crud.get("product", uuid)          # {"success": true, "data": {"product": {...}}}
products = org.crud.list("product", limit=20)    # flat list of records
javascript
const product = await client.getObjects("product");                       // all records
const page = await client.getObjects("product", { limit: 20, sort_by: "price", sort_order: "asc" });

⚠️ A plain list only shows one project

The list call above returns records from the single project your token was issued for — including when you sign in as a domain_admin. Records of the same type that live in your other projects are left out, and nothing tells you so: you get 200 and a shorter list, not an error. If a list comes back empty or shorter than you expect, this is the first thing to check. To read a type across your whole domain, use POST /api/v1/crud/{domain}/list-bulk and pass an fq_name_prefix containing just your domain name.
bash
curl -X POST https://api.supero.dev/api/v1/crud/acme/list-bulk \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "product",
    "fq_name_prefix": ["acme"],
    "detail": true,
    "limit": 200
  }'
json
{
  "success": true,
  "results": [
    { "uuid": "3f9c1e2a-...", "name": "wireless-headphones", "fq_name": ["acme", "storefront", "northwind", "wireless-headphones"] },
    { "uuid": "7a1b2c3d-...", "name": "usb-cable", "fq_name": ["acme", "warehouse", "northwind", "usb-cable"] }
  ],
  "result_count": 2
}
An fq_name_prefix with a single segment constrains only the domain, so records from every project and tenant beneath it come back. Only an admin role can widen a read this way — for other roles the prefix can narrow scope but never broaden it beyond what the token already grants. See Querying & Pagination for the full list-bulk body.

Querying and Filtering

For anything beyond straight pagination, filter through the dedicated query endpoint rather than hand-building GET query strings — the plain list endpoint above does not apply field filters.

POST /query — equality filters

bash
curl -X POST https://api.supero.dev/api/v1/crud/acme/query \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "product",
    "filters": {"category": "electronics", "in_stock": true},
    "sort": [{"field": "price", "order": "desc"}],
    "limit": 10
  }'
filters is an equality match — every key/value pair must match exactly. The response is {success, results, result_count}, with results as a flat array (same shape as the list endpoint).

Python SDK query builder

python
products = org.crud.query("product") \
    .filter(category="electronics", in_stock=True) \
    .order_by("-price") \
    .limit(10) \
    .all()
.filter() sends its keyword arguments as the equality filters dict shown above; a leading - on the order_by field means descending.

Structured filters (JavaScript client)

The JavaScript client's queryObjects() sends a structured filter with an explicit operator per condition, instead of equality-only matching.
javascript
const results = await client.queryObjects("product", {
  op: "and",
  conditions: [
    { field: "category", op: "eq", value: "electronics" },
    { field: "price", op: "lte", value: 100 },
  ],
}, { sort_by: "price", sort_order: "asc", limit: 10 });

ℹ️ sort_by here is a client convenience, not the wire format

queryObjects takes sort_by / sort_order as options and converts them into the sort array POST /query actually reads, so both spellings are correct in their own place. If you hand-build the request body yourself, send sort — sort_by in a /query body is ignored and you get default ordering with no error. See Querying & Pagination.
OperatorMatches
eq / neequal to / not equal to
gt / gtegreater than / greater than or equal to
lt / lteless than / less than or equal to
in / not_invalue is / is not in a list
containssubstring match
starts_withprefix match
existsfield is present on the record
betweenvalue falls within a range
and / or / allcombine nested conditions

Updating and Deleting Records

Updating a record

PUT the record's UUID with the fields you want to change; fields you omit are left as-is. There is a single update verb — no separate partial-update method.
bash
curl -X PUT https://api.supero.dev/api/v1/crud/acme/product/3f9c1e2a-6b7d-4e11-9c2f-2a6f1d0b9a10 \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"price": 89.99, "in_stock": false}'
python
org.crud.update("product", uuid, price=89.99, in_stock=False)
javascript
await client.updateObject("product", uuid, { price: 89.99, in_stock: false }, product);
The JavaScript client's fourth argument is the record you already have loaded in memory — pass it through so the client can resolve which tenant the write belongs to.

Deleting a record

bash
curl -X DELETE https://api.supero.dev/api/v1/crud/acme/product/3f9c1e2a-6b7d-4e11-9c2f-2a6f1d0b9a10 \
  -H "Authorization: Bearer $ACCESS_TOKEN"
python
org.crud.delete("product", uuid)
javascript
await client.deleteObject("product", uuid, product);
A successful delete returns 200 with {"success": true, "message": "..."}. Deleting a UUID that does not exist, or that you do not have access to, returns 404.

⚠️ Heads up

Deletes are permanent — the CRUD API has no soft-delete or undo. Confirm before wiring a delete call to a UI button.

Working with References

A schema declares references as a named, typed list of links to other object types (each with a cardinality of one or many, and an optional reverse name). Fetch them through dedicated /refs endpoints rather than an ?expand= query parameter.

Reading references

bash
curl https://api.supero.dev/api/v1/crud/acme/project/{uuid}/refs \
  -H "Authorization: Bearer $ACCESS_TOKEN"
json
{
  "success": true,
  "uuid": "project-uuid",
  "obj_type": "project",
  "fq_name": ["acme", "project-name"],
  "refs": {
    "user_account": [
      { "uuid": "u1", "fq_name": ["acme", "project-name", "tenant", "alice"], "attr": { "role": "admin" } }
    ]
  },
  "total_refs": 1
}
EndpointReturns
GET /{type}/{uuid}/refsAll forward references, grouped by referenced type
GET /{type}/{uuid}/refs/{ref_type}One reference type only, as a flat list
GET /{type}/{uuid}/back-refsObjects that reference this record
Add ?include_back_refs=true to the /refs or /refs/{ref_type} call to include the reverse edges in the same response.

Adding or removing a reference

bash
curl -X PUT https://api.supero.dev/api/v1/crud/acme/ref-update \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "order",
    "uuid": "order-uuid",
    "ref-type": "customer",
    "ref-uuid": "customer-uuid",
    "operation": "ADD"
  }'
operation is one of ADD, UPDATE, or DELETE. Adding a reference requires create-or-update permission on the source type and read on the target; changing or removing an existing edge requires update on both.

Browsing Data in the Dashboard

The dashboard at app.supero.dev includes a Data Explorer for each project — a schema-generated table view for browsing and editing records without writing code. It honors the same role-based access policy as the API: a viewer or tenant_user only sees and can modify what their role permits.

💡 Quick data entry

The Data Explorer is the fastest way to seed test data while you're iterating on a schema, before your app's own UI is wired up.

Next steps

  • •Schema Reference — define the fields, references, and namespaces that shape every record.
  • •Access Control — how roles and access policies decide who can read or write which records.
  • •Dynamic UI — render data-driven views and forms on top of the JavaScript client.
  • •API Integrations — bring external data in and keep it in sync with the managed services catalog.