Step 3: Create the data structures
Inventory data lives in a module-scoped MikroORM entity. Keep multi-tenancy in mind by including both tenant_id and organization_id.
1. Define the entity
mkdir -p apps/saasframe/src/modules/inventory/data
touch apps/saasframe/src/modules/inventory/data/entities.ts
touch apps/saasframe/src/modules/inventory/data/validators.ts
import { Entity, Property, PrimaryKey } from '@mikro-orm/core';
import { v4 as uuid } from 'uuid';
@Entity({ tableName: 'inventory_items' })
export class InventoryItemEntity {
@PrimaryKey({ columnType: 'uuid' })
id = uuid();
@Property({ columnType: 'uuid' })
tenant_id!: string;
@Property({ columnType: 'uuid' })
organization_id!: string;
@Property()
sku!: string;
@Property()
name!: string;
@Property({ columnType: 'integer' })
quantity!: number;
@Property({ columnType: 'text', nullable: true })
location?: string;
@Property({ columnType: 'timestamptz', defaultRaw: 'now()' })
created_at: Date = new Date();
@Property({ columnType: 'timestamptz', defaultRaw: 'now()', onUpdate: () => new Date() })
updated_at: Date = new Date();
@Property({ columnType: 'timestamptz', nullable: true })
deleted_at?: Date | null;
}
Each column follows the multi-tenant conventions: tenant_id, organization_id, timestamps, and deleted_at for soft deletes.
Database columns use snake_case (tenant_id, organization_id, created_at). The makeCrudRoute factory expects the corresponding ORM field names in its configuration -- MikroORM handles the mapping automatically when the entity property name differs from the column name. If you use camelCase property names in your entity class, set columnType explicitly and MikroORM will derive the snake_case column name.
2. Add validators
import { z } from 'zod';
export const upsertInventoryItemSchema = z.object({
sku: z.string().min(1).max(64),
name: z.string().min(1).max(128),
quantity: z.number().int().min(0),
location: z.string().max(128).optional(),
});
export type UpsertInventoryItemInput = z.infer<typeof upsertInventoryItemSchema>;
Use the same schema for create and update flows -- call it from API handlers, CLI commands, and forms to guarantee consistent validation.
- Do not include
tenantIdororganizationIdin your request validator. These are derived from the authenticated session by the CRUD factory and injected automatically -- accepting them from user input would be a security risk. - Derive TypeScript types with
z.infer<typeof schema>to keep types and runtime validation in sync.
3. Generate migrations
Run the generators and create a migration for the new entity:
yarn generate
yarn db:generate
Migrations appear under apps/saasframe/src/modules/inventory/migrations/ (for app-level modules with from: '@app'). Commit them alongside your entity. When you run yarn db:migrate, the new table will be created across environments.
yarn db:migrate