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 (hasownerId)system- System-level entity, no ownertenant- Exists in the schema but is currently reserved; the generator treats it identically touser
{
"ownership": "system"
}
extends
Inherit fields from another entity or base type.
{
"extends": "EntityBase"
}
Built-in base types:
EntityBase- Standard entity with id, ownerId, timestampsSystemEntityBase- System entity with id, timestamps (no owner)ContentBase- Extends EntityBase with title, slug, tags, status, etc.
Important: ContentBase is the default. Entities without an
extendsproperty and withoutownership: "system"automatically extendContentBase. This means they inherittitle,slug,summary,tags,status,publishedAt, andfeaturedImagein addition to the standardid,ownerId,createdAt, andupdatedAtfields. If your entity does not need these content fields, set"extends": "EntityBase"explicitly.
versioning
Enable version tracking:
full- Complete snapshots with revert capabilitysimple- 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.
search
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"
}
}
| Property | Default | Description |
|---|---|---|
enabled | false | Enable search indexing |
engine | "typesense" | Search engine (typesense, elastic, opensearch, meilisearch, mongoAtlas) |
indexName | entity plural | Search 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.
| Property | Type | Description |
|---|---|---|
field | string | Date field to check for expiry (must exist on the entity) |
days | number | Number 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 fieldssparse-- Only index documents that contain the indexed fieldsname-- 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
indexanduniqueare no-ops for the Node/Mongo stack. Setting"index": trueon a field does nothing at the database level -- it is only used as a UI heuristic score in the admin panel. Field-level"unique": trueis only wired for the Laravel stack. For actual database indexes in Node/Mongo, always use the entity-levelindexesarray 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.matchUserFieldfor multi-org access - Search: Typesense integration with searchable/filterable/sortable fields
- Field access:
costPricerestricted to ADMIN and MANAGER roles - url[] type: Array of URLs for images
- Enum with default: Status field with draft/published/archived