Builtin Entities
Every generated app needs more than the business entities you define in types.json — authentication, settings storage, background job tracking, logging, and request idempotency are infrastructure every app needs regardless of domain. Radish provides these as builtin entities: schema definitions shipped with the CLI and merged into your app's data layer at generation time.
Builtin entities are separate from blueprint-defined business entities in a few important ways:
- They come from schema templates in
src/templates/*.schema.json, not from yourtypes.json. - They're excluded from generic CRUD generation — the admin panel gives them dedicated pages (Users, Roles, Settings, Jobs) instead of the standard list/create/detail/edit scaffold.
- They're tracked in a single canonical list,
src/lib/system-entities.mjs, which every consumer (admin panel generator, entity overview counts, business entity listings) imports from.
// src/lib/system-entities.mjs
export const SYSTEM_ENTITIES = new Set([
'User', 'Role', 'Permission', 'Setting', 'Profile', 'ApiKey', 'PasswordResetToken',
'LogMessage', 'ScheduledJob', 'JobExecution', 'JobSnapshot', 'IdempotencyKey',
'SearchSyncTask', 'SearchQuery', 'SearchClick', 'SearchAssociation', 'SearchSynonym',
'ContentBlock', 'MediaAsset', 'Attribute', 'FormTemplate', 'Rule',
'Grant',
'DatasetDefinition', 'DatasetField', 'DatasetRow'
]);
Builtin entities fall into three activation tiers:
- Always-on — merged into every generated app, no configuration needed.
- Auto-detected — merged automatically when your blueprint declares a triggering feature (e.g.
search.enabled: trueon an entity). - Feature-gated — merged only when you pass an explicit
--featuresflag tocreate datalayer.
Always-On Entities
These entities are always included when you run create datalayer or create app — no flags needed. They're loaded from six schema templates.
Auth
Schema template: auth.schema.json
| Entity | Purpose |
|---|---|
| User | Core authentication account — email, passwordHash, roles, isActive |
| Profile | User profile — personal info, address, timezone, locale |
| Role | Role definition with permission keys. System roles (USER, ADMIN, API, ANONYMOUS) seeded from roles.json |
| ApiKey | Personal API keys — stores token hash only, supports scopes and expiration |
Schema template: password-reset.schema.json
| Entity | Purpose |
|---|---|
| PasswordResetToken | Single-use tokens for self-service password reset. Plaintext never stored — only a SHA-256 hash. TTL auto-cleans expired records. |
Key points: User has a unique email index, passwordHash is internal (never exposed via API), roles is a string array, ApiKey supports scopes/expiration/revocation. PasswordResetToken has a unique index on tokenHash and a 1-day TTL on expiresAt.
Link to: Auth
Settings
Schema template: settings.schema.json
| Entity | Purpose |
|---|---|
| Setting | Key-value config with scope cascade (user → tenant → system). Supports field-level encryption for secrets. |
Key points: unique sparse compound index on (key, scope, tenantId, userId), plus secondary indexes on (category, scope) and (tenantId, scope). Category groups related settings. dataType drives type coercion on read. fieldSchema declares per-field types for object-valued settings — fields marked 'encryptedKey' are encrypted at rest.
Link to: Settings
Jobs
Schema template: jobs.schema.json
| Entity | Purpose |
|---|---|
| ScheduledJob | Job definitions — name, handler, cron/interval schedules, timezone, timeout, enable/disable |
| JobExecution | Individual execution records — status, duration, output, error, triggered by scheduler or manual |
| JobSnapshot | Point-in-time metric snapshots recorded by jobs — dimensions, metrics, bucket grain |
Key points: ScheduledJob tracks lastRunAt/nextRunAt/lastStatus/failureCount. JobExecution links to ScheduledJob and optionally to User (for manual triggers). JobSnapshot supports aggregation via dimensionHash deduplication.
Link to: Scheduled Jobs, Job Snapshots
Logging
Schema template: logging.schema.json
| Entity | Purpose |
|---|---|
| LogMessage | Structured log entries — level (debug/info/warn/error/fatal), message, source, metadata, stackTrace |
Key points: stackTrace is internal (not exposed via API). Indexed on timestamp, (level, timestamp), (source, timestamp), and userId.
Idempotency
Schema template: idempotency.schema.json
| Entity | Purpose |
|---|---|
| IdempotencyKey | Prevents duplicate API request processing. Stores client-provided key, request hash (SHA-256), cached response. |
Key points: unique index on key. Status progression: processing → completed | error. expiresAt drives TTL cleanup. requestHash ensures the same key with a different body is rejected rather than silently replayed.
Auto-Detected Entities
These load automatically when entity blueprints declare certain features. No --features flag needed — the generator inspects the merged entity list from types.json and adds the matching builtin templates.
Search
Loaded when: any entity in the blueprint has search.enabled: true
Schema templates: search-feedback.schema.json, search-synonyms.schema.json
| Entity | Purpose |
|---|---|
| SearchQuery | Logged search queries for analytics and relevance feedback (TTL: 90 days) |
| SearchClick | Click-through events linking queries to results |
| SearchAssociation | Learned associations between queries and entities for relevance tuning |
| SearchSynonym | Synonym definitions synced to the search engine — multi-way and one-way |
Additional (background sync): when search.sync: 'background' on any searchable entity:
Schema template: search-sync.schema.json
| Entity | Purpose |
|---|---|
| SearchSyncTask | Queue entries for background search index synchronization — entity, operation, retry tracking |
Link to: Search
Feature-Gated Entities
These require explicit opt-in via --features cms, --features grants, or --features datasets on create datalayer (or create app).
CMS (features.cms)
Schema template: cms.schema.json
| Entity | Purpose |
|---|---|
| ContentBlock | Editable HTML/Markdown/text content regions identified by slot key |
| MediaAsset | Uploaded images, videos, documents with MIME type and category |
| Attribute | Reusable custom field definitions (text, select, boolean, number, etc.) — apply to any entity type |
| FormTemplate | Reusable form layouts — ordered lists of attributes for entity forms |
| Rule | Generic rule engine — condition trees + actions for pricing, workflows, access, notifications |
Grants (features.grants)
Schema template: grants.schema.json
| Entity | Purpose |
|---|---|
| Grant | Per-record ACL — grants specific permissions to users, consumers, agents, or roles on individual records |
Key points: unique compound index on (entity, entityId, grantedTo), plus secondary indexes on (grantedTo, entity, isActive), (entity, entityId, isActive), grantedBy, and a sparse index on expiresAt. Supports direct and inherited scope, plus expiration and revocation. Used by the four-layer access control system (RBAC → Ownership → Scoped → Grants).
Datasets (features.datasets)
Schema template: datasets.schema.json
| Entity | Purpose |
|---|---|
| DatasetDefinition | Semantic layer definition — source, sync config, grain columns, indexed dimensions |
| DatasetField | Per-field metadata — role (dimension/measure/excluded), aggregation function, provenance |
| DatasetRow | Generic row store — shared collection discriminated by datasetId, wildcard-indexed dimensions |
Link to: Datasets
Entity Counts by Category
| Category | Entities | Activation |
|---|---|---|
| Auth | 5 | Always |
| Settings | 1 | Always |
| Jobs | 3 | Always |
| Logging | 1 | Always |
| Idempotency | 1 | Always |
| Search | 4–5 | Auto-detected |
| CMS | 5 | --features cms |
| Grants | 1 | --features grants |
| Datasets | 3 | --features datasets |
| Total | 24–25 |
Extending Builtin Entities
The most commonly extended builtin entity is User. Add custom fields from your blueprint's types.json using extends:
{
"User": {
"extends": "User",
"fields": {
"companyName": { "type": "string", "label": "Company" }
}
}
}
Extension merges fields rather than replacing the entity: inherited fields (email, passwordHash, roles, isActive, etc.) come from the builtin definition, and any fields you declare alongside extends are added on top. The merge happens in collectParentFields() (see contracts.mjs), and is applied consistently across contracts, models, services, routes, and metadata generation.
Extension keys are namespaced internally (builtin:User vs blueprint:User) to detect circular extension chains — you'll see a Circular extension detected: User error if a blueprint entity ends up extending itself indirectly.
Related Documentation
- Auth
- Settings
- Scheduled Jobs
- Job Snapshots
- Search
- Datasets
- Telemetry — always-on observability infrastructure (correlation IDs, structured logging, health endpoint) that doesn't use entities
- Access Control — how Grants and RBAC combine in the four-layer access control system