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 nameJavaScript 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 Param | Purpose |
|---|---|
| limit | Max records to return |
| offset | Records to skip |
| cursor | Opaque cursor from the previous page — use instead of offset for deep iteration |
| sort_by | Field to sort by |
| sort_order | asc or desc |
| include_total | Compute a total count alongside the page (adds overhead) |
| parent_id / parent_uuid | Restrict 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 recordsjavascript
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.
| Operator | Matches |
|---|---|
| eq / ne | equal to / not equal to |
| gt / gte | greater than / greater than or equal to |
| lt / lte | less than / less than or equal to |
| in / not_in | value is / is not in a list |
| contains | substring match |
| starts_with | prefix match |
| exists | field is present on the record |
| between | value falls within a range |
| and / or / all | combine 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
}| Endpoint | Returns |
|---|---|
| GET /{type}/{uuid}/refs | All forward references, grouped by referenced type |
| GET /{type}/{uuid}/refs/{ref_type} | One reference type only, as a flat list |
| GET /{type}/{uuid}/back-refs | Objects 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.
On this page