Beyond the primary key, an entity can declare secondary access patterns in its definition. The runtime writes the right index attributes on every record automatically — you never manage a table or a GSI.

Manage indexes from the Indexes page

On the Entities page, click Indexes on an entity to open its Indexes page. It has a card each for Owner, Unique, Composite index and Category: pick the fields, save, and the platform builds the index. The same settings can be set in the entity definition or through MCP.

Unique indexes

UniqueIndexes enforce cross-record uniqueness that a key alone can't. On create, the record and a uniqueness sentinel are written in one transaction; a duplicate value fails the sentinel and the whole write is cancelled.

json
"UniqueIndexes": [{ "indexName": "unique-email", "fields": ["email"] }]

Duplicate -> 409

A conflicting value returns 409 Unique constraint violation on field: <indexName>. On update, freeing a value makes it reusable; only changed indexes are touched.

Adding an index to an entity that already has records

A new or changed unique index (or index-field set) is built for the records you already have, in the background, for every tenant. Results stay correct while it runs — just slower — and a new record that duplicates one not yet indexed may not be rejected until it finishes. Progress shows in the Data Manager. If existing records already share a value a unique index forbids, they're listed by id when it finishes; nothing is deleted or edited for you. Your plan caps how many unique indexes and index-field sets an entity can have (saving past the cap returns 402).

Filling in a lookup column for records you already have

A lookup column is indexed when a record is written, so records that existed before you added it show up in filters on it only once they're next saved. To do it all at once, use Bulk upload from file in the Data Manager: upload a CSV with an entity_id column plus the fields to set (or a JSON array of objects), and each record is re-saved in the background through the normal index rules. You choose what a row does when its id has no record: update existing records only (the default — unknown ids are skipped), add new records only, or add or update. New records follow the same rules as any other (required fields, defaults, unique values), and a row with a blank entity_id gets an id generated — give every row an id if you might upload the same file twice. On existing records a blank cell clears that field. You can include the owner column when adding records (leave it blank for records nobody owns), but an existing record's owner can't be changed this way, and the parent field and encrypted or computed fields can't be set at all. Anything that couldn't be applied is listed with the reason.

Composite index

A composite index finds records by an exact combination of fields, for example order_id and email, without scanning the entity. It works like a multi-column index in SQL, except that the caller must supply every field in the set, with exact values; a request that leaves one out doesn't use it. It does not enforce uniqueness, so a combination can match one record or several. Add one from the Composite index card on the Indexes page; the index is named for its fields (for example by_order_id_and_email).

Owner index

An owner index (exactly one per entity) says who each record belongs to, so GET /list/{entity} can return only the calling user's records. The owner field must be a lookup field that points at the entity owners come from, so an owner id that doesn't exist is rejected. For records owned by users, add a user_id field of type lookup that points at user. A record created without the owner field is marked unassigned.

  • Owned by the signed-in user: user_id (lookup to user). When a user creates a record, it is owned automatically by the user who created it, and a plain list returns each user only their own records.
  • Owned by something else (a team, a folder, a department): add a lookup field that points at that entity, for example ops_id to ops. Nothing is filled in automatically; your app sets ops_id on create.
  • Listing another owner's records: ?by=owner&ops_id=3. This needs admin access (a wildcard permission); other users get 403. Admins add &all=true to see every owner's records.
  • Records nobody owns: ?by=owner&unassigned=1.

Plain list, and which owner it fits

A plain list returns records whose owner field equals the signed-in user's id. That is what you want for user_id. For an owner like ops_id, a plain list matches the signed-in user's id against ops_id, which is normally not an Ops id, so list by owner with ?by=owner&ops_id=<id> instead. The owner field is an ordinary lookup field, so it also counts toward your plan's lookup-column limit.

json
"ownerIndexes": [{
  "ownerFieldName": "user_id",
  "gpkTemplate": "org#{org};user#{user_id}",
  "gskTemplate": "task#{id}"
}]

Parent index

A parent index supports hierarchical lists — GET /list/{entity}?{parentKey}={value} returns only the children of that parent. Records without the parent field simply don't appear in the parent query.

Sorting comes for free

name, created_at, and updated_at are always indexed for sorting at no extra write cost — you don't declare them. Sort with ?sortfield=<field>&sortdir=asc|desc; ?sortby=<field> is a separate param that only routes the query to the matching DynamoDB GSI, it doesn't set sort direction.

Category index

Use a category index to browse records by a category tree, for example Footwear > Running > Trail, and fetch everything in a category or just one branch. The tree is your own data in another entity (a name and a parent). Records point at it with lookup fields, so values are record ids and renaming a category changes nothing on your records.

  • Create the entity that holds your categories (for example category, with name and parent_id).
  • In your entity (for example product), add up to three fields of type lookup, each pointing at that category entity: category_id, subcategory_id, subsubcategory_id.
  • Open the entity's Indexes page, go to Category, and pick those fields in order. It is on as soon as you save.
  • Create records with the category ids. An id that doesn't exist is rejected, and a level can't be set below an empty one.
bash
GET /list/product?by=category&category_id=c8x2
GET /list/product?by=category&category_id=c8x2&subcategory_id=k91d

Who sees what, and records you already have

Admins add &all=true to include other users' records; everyone else sees their own, same as a plain list. Records saved before the index existed appear in category results once they are saved again. Category fields use no lookup-index slot.