Getting Started
Monorise is an open-source DynamoDB single-table toolkit that powers the core data layer for applications built on DynamoDB. It provides a shared data model (schemas + relationships), a ready-made API surface (entities, mutuals, tags), and background processors to keep denormalized access patterns in sync.
What it solves
- Single-table DynamoDB modeling without hand-writing complex queries.
- Relational-style access via
Entity,Mutual, andTagconcepts. - Event-driven maintenance (mutual/tag/tree processors + replication).
- Zero schema drift — One Zod config drives DB, API, and frontend types.
monorise devauto-regenerates on every change.
Prerequisites
- Node.js 20+
- npm 10+
- AWS account/infrastructure context for runtime integration (SST + DynamoDB)
Quickstart
1. Create a new project
npx monorise init --name my-appThis single command creates a production-ready monorepo:
my-app/
├── apps/web/ # Next.js frontend (Tailwind CSS)
│ ├── app/
│ │ ├── layout.tsx # GlobalInitializer + GlobalLoader wired in
│ │ ├── page.tsx # Example page with useEntities + createEntity
│ │ ├── globals.css # Shadcn theme variables (oklch)
│ │ └── api/
│ │ ├── proxy-request.ts # Rewrites /api/* to monorise backend
│ │ └── [...proxy]/route.ts # Catch-all route (GET/POST/PUT/PATCH/DELETE)
│ ├── components/
│ │ ├── global-initializer.tsx # Monorise store configuration
│ │ ├── global-loader.tsx # Full-screen interruptive loading overlay
│ │ └── ui/ # Shadcn UI components (button, card, input, label)
│ └── lib/utils.ts # cn() — clsx + tailwind-merge helper
├── services/core/ # Hono backend routes
│ └── routes.ts
├── monorise/configs/ # Entity definitions
│ ├── user.ts # Starter User entity — mutual with Team
│ └── team.ts # Starter Team entity — mutual with User
├── monorise/mutuals/ # Shared mutual relationship configs
│ └── team-membership.ts # createMutualConfig — referenced by both sides
├── monorise.config.ts # Points to configs dir + custom routes
├── sst.config.ts # SST v4 + Monorise module configured
├── tsconfig.json # Path aliases (#/monorise, #/monorise/*, #/*)
└── .monorise/ # Generated types + handlers (do not edit)
├── config.ts # Entity enum, types, EntityConfig
├── index.ts # Re-exports config.ts — import from '#/monorise'
└── handle.tsWhat's included out of the box
| Feature | Description |
|---|---|
| Shadcn UI | Pre-installed button, card, input, label components with theme variables |
| Global Loader | useInterruptiveLoadStore → full-screen loading overlay via portal |
| Global Initializer | Calls Monorise.config() with your entity config on app mount |
| API Proxy | Next.js catch-all route at /api/* that proxies requests to monorise backend |
| Mutual relationship | User <-> Team via createMutualConfig, showing a shared, validated mutual schema |
| Path Aliases | #/monorise (or #/monorise/*) for generated types, #/* for app-local imports |
2. Start development
cd my-app
npx sst devThat's it! Open http://localhost:3000 to see the example app.
Understanding the structure
Entity config (monorise/configs/user.ts)
Define your data model with Zod:
import { createEntityConfig } from 'monorise/base';
import { z } from 'zod/v4';
const baseSchema = z
.object({
displayName: z.string().min(1),
email: z.string().email(),
})
.partial();
const createSchema = z.object({
displayName: z.string().min(1),
email: z.string().email(),
});
const config = createEntityConfig({
name: 'user',
displayName: 'User',
baseSchema,
createSchema,
searchableFields: ['displayName', 'email'],
uniqueFields: ['email'],
});
export default config;Frontend page (apps/web/app/page.tsx)
Use the React hooks to interact with your data:
'use client';
import { useEntities, createEntity } from 'monorise/react';
import { Entity } from '#/monorise';
export default function Home() {
const { entities: users, isLoading } = useEntities(Entity.USER);
const handleCreate = async () => {
await createEntity(Entity.USER, {
displayName: 'John Doe',
email: 'john@example.com',
});
// The list automatically updates via the store!
};
return (
<div>
{users?.map((user) => (
<div key={user.entityId}>
{user.data.displayName} — {user.data.email}
</div>
))}
</div>
);
}Mutual relationship (monorise/mutuals/team-membership.ts)
The scaffold also demonstrates createMutualConfig — a schema defined once and referenced from both sides of a relationship:
import { createMutualConfig } from 'monorise/base';
import type { Entity } from 'monorise/base';
import { z } from 'zod/v4';
const teamMembership = createMutualConfig({
entities: ['user', 'team'] as unknown as [Entity, Entity],
mutualDataSchema: z.object({
role: z.enum(['member', 'admin']),
}),
});
export default teamMembership;user.ts and team.ts both import teamMembership and reference it in their mutual.mutualFields, so mutualData (the role field) is validated identically from either direction. Entity types are referenced as plain strings cast to Entity ('team' as unknown as Entity) rather than imported from #/monorise — that avoids a circular import, since the generated Entity enum is itself built from these config files.
Build and watch commands
Generate types from entity configs:
npx monorise buildWatch mode for development:
npx monorise devProject config (monorise.config.ts)
The monorise.config.ts file at your project root tells the CLI where to find your entity configs and custom routes:
export default {
// Directory containing your entity config files
configDir: './monorise/configs',
// (Optional) Hono app for custom API routes (mounted at /core/app/*)
customRoutes: './services/core/routes.ts',
};| Field | Type | Required | Description |
|---|---|---|---|
configDir | string | Yes | Path to directory containing entity config .ts files |
customRoutes | string | No | Path to a Hono app module for custom routes |
TIP
The CLI auto-detects whether your project uses the combined monorise package or scoped @monorise/* packages and generates the correct import paths in .monorise/handle.ts accordingly.
Entity config directory
The configDir should contain one .ts file per entity, each exporting a default createEntityConfig result:
monorise/configs/
user.ts
organisation.ts
order.tsCustom routes
The customRoutes file must default-export a Hono app instance. These routes are mounted under /core/app/*:
import { Hono } from 'hono';
const app = new Hono();
app.get('/health', (c) => c.json({ status: 'ok' }));
app.post('/custom-action', async (c) => {
// Access DI container, entity services, etc.
const body = await c.req.json();
return c.json({ result: 'done' });
});
export default app;Generated code and type safety
When you run monorise build or monorise dev, the CLI generates .monorise/config.ts which includes:
- An
Entityenum with all your entity names - TypeScript types inferred from each entity's
baseSchema - An
EntitySchemaMapinterface mapping entity types to their schemas - Module augmentation declarations that extend
monorise/basetypes
The module augmentation is what makes the entire system type-safe. For example, when you call useEntity(Entity.USER, id), the returned entity.data is strongly typed with your user schema fields — not any.
// Auto-generated in .monorise/config.ts
declare module 'monorise/base' {
export enum Entity {
USER = 'user',
}
export type UserType = z.infer<typeof userConfig.finalSchema>;
export interface EntitySchemaMap {
[Entity.USER]: UserType;
}
}INFO
You never need to write this manually — the CLI generates it from your entity configs.
SST Configuration Reference
The monorise.module.Core construct provisions everything you need — API Gateway, DynamoDB table, EventBridge bus, SQS queues for processors, and DynamoDB streams for replication — in a single construct.
What monorise.module.Core creates
| Resource | Description |
|---|---|
api | API Gateway v2 with CORS, routing to Hono Lambda |
table | DynamoDB single table with GSIs and replication indexes |
bus | EventBridge bus for entity events |
alarmTopic | SNS topic for processor error alerts |
| Mutual processor | SQS + Lambda for mutual relationship sync |
| Tag processor | SQS + Lambda for tag index sync |
| Tree processor | SQS + Lambda for computed relationship sync |
| Replication processor | DynamoDB stream + Lambda for denormalized data sync |
| CloudWatch dashboard | Pre-built dashboard with Lambda metrics, DLQ depth, and table stats |
Configuration options
new monorise.module.Core('core', {
allowOrigins: ['https://myapp.com'], // CORS origins
allowHeaders: ['x-custom-header'], // Additional CORS headers
configRoot: './services/api', // Custom config root path
cloudwatchLogRetention: '1 week', // Lambda log retention period
cloudwatchDashboard: { enabled: $app.stage === 'production' }, // Dashboard only on prod
});The DynamoDB table's TTL attribute is always expiresAt — see Entities: TTL for how to set an expiry on an entity.
For the full SST SDK reference including QFunction, see the SST SDK page.
Development
SST's dev mode works seamlessly with monorise. The monorise.module.Core constructor automatically registers a dev command that runs monorise dev in watch mode:
npx sst devThis starts your local dev environment with live Lambda functions and auto-regenerating monorise config.
Deployment
Deploy to production using SST:
npx sst deploy --stage prodFor comprehensive deployment guides, environment management, and CI/CD setup, see the SST documentation.
Before you start building
Read the Best Practices guide first — especially the edge-auth proxy pattern. How you connect your frontend to the monorise API Gateway has significant security implications.
CLI Commands
npx monorise init # scaffold a new project
npx monorise dev # watch mode — regenerates on config changes
npx monorise build # one-time buildExamples
For a complete reference app, see the Ledger example.
It demonstrates date-range tag queries, mutual relationships, and a custom aggregation route (/core/app/summary) built on top of the generated API.
