Skip to main content

Entity Definitions

Entities are the core building blocks of your Radish blueprint. Each entity represents a data type in your application.

Basic Entity Structure​

{
"entities": {
"EntityName": {
"label": "Display Name",
"description": "Description of the entity",
"plural": "entities",
"fields": {},
"filters": [],
"indexes": []
}
}
}

Entity Properties​

label​

Human-readable name for the entity (singular form).

{
"label": "Task"
}

description​

Brief description of what this entity represents.

{
"description": "A todo task that can be assigned to users"
}

plural​

Plural form used for collections and endpoints.

{
"plural": "tasks"
}

ownership​

Determines entity ownership model:

  • user (default) - Entity belongs to a specific user (has ownerId)
  • system - System-level entity, no owner
  • tenant - Exists in the schema but is currently reserved; the generator treats it identically to user
{
"ownership": "system"
}

extends​

Inherit fields from another entity or base type.

{
"extends": "EntityBase"
}

Built-in base types:

  • EntityBase - Standard entity with id, ownerId, timestamps
  • SystemEntityBase - System entity with id, timestamps (no owner)
  • ContentBase - Extends EntityBase with title, slug, tags, status, etc.

Important: ContentBase is the default. Entities without an extends property and without ownership: "system" automatically extend ContentBase. This means they inherit title, slug, summary, tags, status, publishedAt, and featuredImage in addition to the standard id, ownerId, createdAt, and updatedAt fields. If your entity does not need these content fields, set "extends": "EntityBase" explicitly.

versioning​

Enable version tracking:

  • full - Complete snapshots with revert capability
  • simple - Lightweight audit logs
{
"versioning": "simple"
}

scope​

Enable relationship-based access control (Layer 3). Two modes:

Through-entity — access via ownership of a related entity:

{
"scope": {
"field": "appId",
"through": "App",
"ownerField": "ownerId"
}
}

Direct-match (tenancy) — access via a shared field on the user:

{
"scope": {
"field": "orgId",
"matchUserField": "orgIds"
}
}

Supports array match: if user.orgIds is ["org1", "org2"], records where orgId is either value are accessible.

See Access Control for full documentation.

Enable search index integration for this entity:

{
"search": {
"enabled": true,
"engine": "typesense",
"indexName": "products",
"fields": {
"searchable": ["name", "description"],
"filterable": ["brand", "price"],
"sortable": ["price", "createdAt"],
"facetable": ["brand"]
},
"sync": "inline"
}
}
PropertyDefaultDescription
enabledfalseEnable search indexing
engine"typesense"Search engine (typesense, elastic, opensearch, meilisearch, mongoAtlas)
indexNameentity pluralSearch index name
fields.searchable[]Full-text searchable fields
fields.filterable[]Fields for filtering
fields.sortable[]Fields for sorting
fields.facetable[]Fields for faceted search
sync"inline"Sync mode: inline (immediate) or background
indexFilter—MongoDB-style query controlling index membership. Match → upsert; no match → DELETE from engine. See Search — Index Filter.

ttl​

Automatically expire documents after a set number of days. Generates a MongoDB TTL index on the specified date field.

{
"ttl": {
"field": "createdAt",
"days": 90
}
}

This creates an expireAfterSeconds index — MongoDB automatically deletes documents 90 days after the value of createdAt. Useful for ephemeral data like search analytics, audit logs, or session records.

PropertyTypeDescription
fieldstringDate field to check for expiry (must exist on the entity)
daysnumberNumber of days after field value before the document is deleted

Example — search feedback with auto-expiry:

{
"SearchQuery": {
"label": "Search Query",
"ownership": "system",
"ttl": { "field": "createdAt", "days": 90 },
"fields": {
"query": { "type": "string", "required": true },
"entity": { "type": "string", "required": true },
"resultCount": { "type": "int", "default": 0 }
}
}
}

See Search for how TTL is used with search feedback entities.

collation​

MongoDB collation settings for this entity. Controls case sensitivity, accent sensitivity, and numeric ordering for queries and sorts.

{
"collation": {
"locale": "en",
"strength": 2,
"numericOrdering": true
}
}

If omitted, every entity gets the default collation: { "locale": "en", "strength": 2, "numericOrdering": true }. This makes all queries and sorts case-insensitive with natural number ordering (e.g., "item2" sorts before "item10"). Override per-entity if you need case-sensitive matching or a different locale.

indexes​

Entity-level indexes are defined as an array of index specifications:

{
"indexes": [
{ "fields": ["authorId", "status"] },
{ "fields": ["slug"], "unique": true },
{ "fields": ["expiresAt"], "expireAfterSeconds": 86400 },
{ "fields": ["email"], "sparse": true, "name": "email_sparse_idx" }
]
}

Supported index options:

  • unique -- Enforce uniqueness across the indexed fields
  • sparse -- Only index documents that contain the indexed fields
  • name -- Custom index name (otherwise MongoDB auto-generates one)
  • expireAfterSeconds -- TTL index; MongoDB automatically deletes documents this many seconds after the indexed date field's value

Warning: Field-level index and unique are no-ops for the Node/Mongo stack. Setting "index": true on a field does nothing at the database level -- it is only used as a UI heuristic score in the admin panel. Field-level "unique": true is only wired for the Laravel stack. For actual database indexes in Node/Mongo, always use the entity-level indexes array shown above.

defaults block​

The defaults block (owned, timestamps, adminRole) appears in template schemas as metadata. It is not consumed by the code generator. Ownership behavior is controlled by each entity's own ownership property, which defaults to "user".

Example Entity​

{
"entities": {
"Product": {
"label": "Product",
"description": "Product in the catalog",
"plural": "products",
"ownership": "system",
"versioning": "simple",
"scope": {
"field": "orgId",
"matchUserField": "orgIds"
},
"search": {
"enabled": true,
"engine": "typesense",
"indexFilter": { "status": { "$ne": "archived" } },
"fields": {
"searchable": ["name", "description", "brand"],
"filterable": ["brand", "price", "status"],
"sortable": ["price", "name", "createdAt"]
}
},
"fields": {
"name": {
"type": "string",
"required": true,
"label": "Product Name",
"search": true
},
"description": {
"type": "string",
"optional": true
},
"brand": {
"type": "string",
"label": "Brand"
},
"price": {
"type": "float",
"required": true
},
"costPrice": {
"type": "float",
"access": { "read": ["ADMIN", "MANAGER"] }
},
"status": {
"type": "enum",
"values": ["draft", "published", "archived"],
"default": "draft"
},
"orgId": {
"type": "objectId",
"ref": "Org",
"required": true
},
"images": {
"type": "url[]",
"default": []
}
},
"filters": ["status", "brand", "orgId"],
"indexes": [
{ "fields": ["orgId", "status"] },
{ "fields": ["brand"] }
]
}
}
}

This example shows:

  • Tenant scoping: scope.matchUserField for multi-org access
  • Search: Typesense integration with searchable/filterable/sortable fields
  • Field access: costPrice restricted to ADMIN and MANAGER roles
  • url[] type: Array of URLs for images
  • Enum with default: Status field with draft/published/archived

Next Steps​