packages/twenty-docs/developers/extend/apps/data/overview.mdx
A Twenty app's data layer is the data your app adds to a workspace — the new record types it declares, the columns it adds to existing objects, and how those records connect to each other.
┌──────────────────────────────────────────────────┐
│ Object — a record type, e.g. PostCard │
│ ├─ Field (name, type, label) │
│ ├─ Field │
│ └─ Relation (link to another object) │
└──────────────────────────────────────────────────┘
│
├── lives in your app, OR
│
▼
┌──────────────────────────────────────────────────┐
│ Standard / other apps' objects │
│ └─ Field added by your app via defineField │
└──────────────────────────────────────────────────┘
| Entity | Purpose | Defined with |
|---|---|---|
| Object | A new custom record type (e.g. PostCard, Invoice) with its own fields | defineObject() |
| Field | A column on an object. Standalone fields can extend objects you didn't create (e.g. add loyaltyTier to Company) | defineField() |
| Relation | A bidirectional link between two objects — both sides declared as fields | defineField() with FieldType.RELATION |
| Index | A database index to speed up a recurring query on one of your objects | defineIndex() |
The SDK detects these via AST analysis at build time, so file organization is up to you — the convention is src/objects/, src/fields/, and src/indexes/. Stable universalIdentifier UUIDs tie everything together across deploys.
Apps can ship indexes alongside their objects to keep recurring queries fast. The most common case is a status or foreign-key column that you read frequently.
import { defineIndex } from 'twenty-sdk/define';
import {
POST_CARD_UNIVERSAL_IDENTIFIER,
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
} from '../objects/post-card.object';
export default defineIndex({
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
},
],
});
defineIndex accepts isUnique: true for both single- and multi-column uniqueness. This is the recommended primitive — defineField({ isUnique: true }) is deprecated and will be removed in a future release.
defineIndex({
universalIdentifier: '…',
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
isUnique: true,
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
});
WHERE clauses stay under admin control — apps can't declare them.Order the fields array the way Postgres should use it — leftmost column first, like a phone book. Indexes are not free: every write to the table updates them. Add one only when you have a query that needs it.