Entities
An entity is a distinct, identifiable object or concept that can have data stored about it. Entities are the primary building blocks of your data model.
Example
If you're modeling a bookstore, you might define entities such as:
book— represents an item in your inventoryauthor— represents a person who wrote a bookcustomer— represents someone who buys books
Defining an entity
Each entity is defined using createEntityConfig with a Zod schema:
import { createEntityConfig } from 'monorise/base';
import { z } from 'zod/v4';
const baseSchema = z
.object({
name: z.string().min(1),
email: z.string().email(),
role: z.enum(['admin', 'member']),
})
.partial();
const createSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
const config = createEntityConfig({
name: 'user',
displayName: 'User',
baseSchema,
createSchema,
searchableFields: ['name', 'email'],
uniqueFields: ['email'],
});
export default config;Configuration fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique kebab-case identifier (e.g., 'user', 'learning-activity') |
displayName | string | Yes | Human-readable name |
baseSchema | z.ZodObject | Yes | All possible fields (.partial() for updates) |
createSchema | z.ZodObject | No | Minimum required fields for creation |
searchableFields | string[] | No | Fields indexed for text search |
uniqueFields | string[] | No | Fields that must be unique per entity type |
mutual | object | No | Mutual relationship configuration (see Mutuals) |
tags | array | No | Tag access patterns (see Tags) |
adjustmentConditions | object | No | Named conditions for adjustEntity — enforces preconditions on numeric adjustments |
updateConditions | object | No | Named conditions for editEntity — enforces preconditions on updates |
allowLegacyWhere | boolean | No | Opt in to accepting raw $where on editEntity (deprecated, disabled by default — see Conditional updates) |
effect | function | No | Applies a final Zod refinement to the merged entity schema |
ttl | object | No | DynamoDB TTL config — see TTL below |
Conditional writes
Conditions are named, server-owned preconditions. The client sends only a condition name; Monorise resolves the condition to a DynamoDB condition expression and enforces it atomically.
const config = createEntityConfig({
name: 'wallet',
displayName: 'Wallet',
baseSchema: z.object({
balance: z.number(),
status: z.enum(['pending', 'active', 'archived']),
}).partial(),
adjustmentConditions: {
withdraw: (_data, adjustments) => ({
balance: { $gte: Math.abs(adjustments.balance ?? 0) },
}),
},
updateConditions: {
activate: { status: { $eq: 'pending' } },
archive: (_data) => ({ status: { $ne: 'archived' } }),
},
});adjustmentConditions receives (data, adjustments) and requires every adjustEntity call to name a condition when the config is present:
await adjustEntity(Entity.WALLET, walletId, { balance: -500 }, {
condition: 'withdraw',
});updateConditions receives (data) and is optional for editEntity calls:
await editEntity(Entity.WALLET, walletId, {
status: 'active',
$condition: 'activate',
});If a named condition is unknown, or the entity has no matching condition config, the request is rejected. A condition that does not hold returns a conflict. Supported operators are $eq, $ne, $gt, $lt, $gte, $lte, $exists, and $beginsWith.
Legacy conditions
adjustmentConstraints and raw $where are deprecated compatibility mechanisms. $where is disabled by default; prefer named adjustmentConditions and updateConditions for all new code.
Advanced validation
Use effect when validation depends on the final merged entity schema rather than one individual field:
const config = createEntityConfig({
name: 'invite',
displayName: 'Invite',
baseSchema,
effect: (schema) => schema.refine(
(data) => !data.expiresAt || !data.createdAt || data.expiresAt > data.createdAt,
'expiresAt must be after createdAt',
),
});Unique fields
Fields listed in uniqueFields are enforced at the database level. If you try to create an entity with a duplicate unique field value, the API returns an error.
const config = createEntityConfig({
name: 'user',
displayName: 'User',
baseSchema,
uniqueFields: ['email'],
});You can also query entities by unique field:
GET /core/entity/user/unique/email/alice@example.comOr using the React hook:
const { entity } = useEntityByUniqueField(Entity.USER, 'email', 'alice@example.com');Searchable fields
Fields listed in searchableFields are indexed for text search. You can search via the query parameter:
GET /core/entity/user?query=aliceOr using the React hook:
const { entities, searchField } = useEntities(Entity.USER);
// Bind to an input
<input {...searchField} placeholder="Search users..." />TTL (time-to-live)
Use ttl.processor to have DynamoDB automatically delete an entity once it expires. The processor returns the expiry as epoch seconds or a Date — return undefined for no expiry.
const config = createEntityConfig({
name: 'session',
displayName: 'Session',
baseSchema,
ttl: {
// fixed 30-day TTL from creation/each update
processor: () => Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
},
});TTL can also be data-driven, computed from the entity's own fields:
const config = createEntityConfig({
name: 'invite',
displayName: 'Invite',
baseSchema,
ttl: {
processor: (entity) => {
return entity.data.expiresOn
? new Date(entity.data.expiresOn)
: undefined;
},
},
});processor is called again on every update/upsert, using the entity's data as it will be after the update is applied — so a sliding-expiration TTL (e.g. extending a session on activity) works out of the box. If processor returns undefined on an update, any existing expiresAt is left untouched rather than cleared.
Internally, this always writes to an expiresAt attribute — the same attribute name the DynamoDB table's TTL is configured on (see SST SDK), so there's nothing else to wire up.
Transactional writes
Multiple entity operations can be executed atomically using the transaction API. All operations succeed or all fail — no partial writes.
import { coreService, transactional } from 'monorise/react';
await coreService.transaction([
transactional.createEntity('order', { ... }),
transactional.adjustEntity('wallet', '...', { balance: -100, $condition: 'withdraw' }),
]);Supported operations: createEntity, updateEntity, adjustEntity, deleteEntity. Conditions from adjustmentConditions and updateConditions are supported within transactions. Events are published only after the transaction commits.
Data layout
In DynamoDB, entities use these access patterns:
| Pattern | Key structure |
|---|---|
| Entity metadata | PK = <entityType>#<entityId>, SK = #METADATA# |
| Entity list | PK = LIST#<entityType>, SK = <entityType>#<entityId> |
| Unique fields | PK = UNIQUE#<field>#<value>, SK = <entityType> |
