Architecture
How config becomes a running API
Monorise uses a small build step to turn entity configs into runnable handlers.
monorise.config.ts + entity configs → monorise dev/build (CLI) → .monorise/config.ts + handle.ts → SST stackmonorise.config.tspoints to your entity config directory and optional custom routes (Hono). When analytics is enabled, the CLI also generates its analytics schema manifest from entity and named mutual configs.- The CLI writes
.monorise/handle.tswhich exports Lambda handlers used by SST (API + processors + replication).
SST infrastructure
The MonoriseCore SST construct provisions the full runtime infrastructure on AWS:

The architecture consists of:
- API Gateway — routes HTTP requests to the Hono Lambda handler
- DynamoDB single table — stores all entities, mutuals, tags, and unique fields
- EventBridge bus — publishes entity lifecycle events (created, updated, mutual processed)
- Processors — SQS-backed Lambda functions that react to events and maintain denormalized data
- DynamoDB Stream — triggers the replication processor to keep denormalized copies in sync
- Analytics delivery (optional) — a second DynamoDB Stream subscriber normalizes canonical entity and mutual metadata changes, sends buffered history through Firehose to S3, and materializes typed Athena current-state and history tables daily
Analytics path
When MonoriseCore.analytics is enabled, DynamoDB Streams with NEW_AND_OLD_IMAGES are the canonical analytics source. The analytics subscriber records canonical INSERT, MODIFY, and REMOVE changes with available before and after state, excluding derived list, tag, unique, lock, and replication rows. Firehose stores the event history in S3 under daily event_date partitions, with an optional hourly partition for individual datasets. A daily job compacts history to Parquet and merges the latest records into Iceberg current-state tables.
EventBridge is not used for canonical analytics capture because lifecycle events do not provide reliable before-images. It remains available for consumer-defined business-event analytics.
QFunction (processor pattern)
Each event-driven processor (mutual, tag, tree) uses the QFunction pattern — an SQS queue paired with a Lambda function, a Dead Letter Queue for failed messages, and a CloudWatch alarm that publishes to an SNS topic when messages land in the DLQ:

The flow:
- Events arrive in the SQS queue from EventBridge
- Lambda processes the message (e.g., syncs mutual records, recalculates tags)
- If processing fails, the message moves to the DLQ after retry exhaustion
- A CloudWatch Alarm fires when the DLQ depth exceeds 0
- The alarm publishes to the configured SNS topic
Failed messages can be redriven from the DLQ once the issue is resolved.
Key behaviors
- Mutual processor: creates/updates/removes relationship items in both directions with conditional checks and locking.
- Tag processor: calculates tag diffs and syncs tag items.
- Tree processor: walks configured relationship paths and publishes derived mutual updates.
- Replication processor: keeps denormalized copies aligned via stream updates (uses replication indexes).
- Analytics delivery: captures canonical stream changes into durable S3 history and refreshes Athena current-state tables daily when enabled.
API reference
The default Hono API exposes the following routes under /core. All entity routes are validated by entityTypeCheck middleware, and mutual routes by mutualTypeCheck middleware.
Entity endpoints
| Method | Route | Description |
|---|---|---|
GET | /entity/:entityType | List entities (paginated, ?limit=20&query=...) |
POST | /entity/:entityType | Create entity |
GET | /entity/:entityType/unique/:field/:value | Get entity by unique field |
GET | /entity/:entityType/:entityId | Get entity by ID |
PUT | /entity/:entityType/:entityId | Upsert entity (full replacement) |
PATCH | /entity/:entityType/:entityId | Update entity (partial); supports optional named $condition |
DELETE | /entity/:entityType/:entityId | Delete entity |
POST | /entity/:entityType/:entityId/adjust | Atomic numeric adjustment (body: { field: delta }) |
Mutual endpoints
| Method | Route | Description |
|---|---|---|
GET | /mutual/:byEntityType/:byEntityId/:entityType | List mutuals (entities related to a given entity) |
POST | /mutual/:byEntityType/:byEntityId/:entityType/:entityId | Create mutual relationship |
GET | /mutual/:byEntityType/:byEntityId/:entityType/:entityId | Get specific mutual |
PATCH | /mutual/:byEntityType/:byEntityId/:entityType/:entityId | Update mutual data |
DELETE | /mutual/:byEntityType/:byEntityId/:entityType/:entityId | Delete mutual relationship |
Tag endpoints
| Method | Route | Description |
|---|---|---|
GET | /tag/:entityType/:tagName | Query tagged entities (?group=...&query=...&start=...&end=...&limit=...&lastKey=...) |
Transaction endpoint
| Method | Route | Description |
|---|---|---|
POST | /transaction | Execute entity operations atomically |
Custom routes can be mounted under /core/app/* via customRoutes in your monorise config.
Conditional updates ($condition)
Define named updateConditions in the entity config, then send the permitted condition name with the update. The server resolves the name to a DynamoDB ConditionExpression; raw operators never need to be client-facing.
{
"status": "confirmed",
"confirmedAt": "2026-04-13T00:00:00.000Z",
"$condition": "confirm"
}$condition is optional for entity updates, but required for adjustments when the entity defines adjustmentConditions. A failed condition returns 409 CONFLICT. See Entities: Conditional writes for configuration examples.
Response behavior for PATCH:
200 OK: update applied409 CONFLICT: condition failed (CONDITIONAL_CHECK_FAILED); a static condition on a missing entity also lands here404 NOT_FOUND: entity missing without a condition or while resolving a function condition400 BAD_REQUEST: validation errors, unique-value conflicts, or an unknown/undefined condition name
Legacy $where
Raw $where conditions are deprecated and disabled by default. Enable allowLegacyWhere only for trusted compatibility callers; new clients must use named conditions. A compatibility request places the raw clauses under $where, for example { "status": "confirmed", "$where": { "status": { "$eq": "pending" } } }.
Data layout cheat sheet
| Access pattern | Key structure |
|---|---|
| Entity metadata | PK = <entityType>#<entityId>, SK = #METADATA# |
| Entity list | PK = LIST#<entityType>, SK = <entityType>#<entityId> |
| Mutual records | MUTUAL#<id> item + two directional lookups |
| Tag records | PK = TAG#<entityType>#<tagName>[#group], SK = <sortValue?>#<entityType>#<entityId> |
| Unique fields | PK = UNIQUE#<field>#<value>, SK = <entityType> |
The replication indexes (R1PK/R2PK) support fast updates of denormalized items when entity data changes.
