An entity is a persisted thing your business keeps — a product, an order, a shipment. Every entity in Vishwakarma is an aggregate root: an identity-bearing object that owns a consistency boundary. You declare the noun, its fields, its indexes, and its relationships, and the platform generates the table, the repository, the API, and the typed client. The running example throughout is Chowk, a small marketplace.
An aggregate root
The aggregate_root modifier marks an entity as the entry point to its own consistency boundary — the thing commands write to and events are emitted from. Here is Chowk's order:
entity Order aggregate_root global {
doc "A customer's order — the root of the checkout lifecycle."
systems [OrderingSystem]
audit timestamps
indexes [
{ name "uk_order_id" kind unique fields [id] }
{ name "ix_order_customer" kind standard fields [customer_id] }
]
id uuid pk filterable[eq]
customer_id uuid required immutable filterable[eq]
status OrderStatus required
total Money required
shipping_address Address required
gift_message GiftMessage?
created_at timestamp immutable sortable
updated_at timestamp
}Two modifiers sit before the brace. aggregate_root is the boundary marker. global means the entity is not tenant-isolated — one shared dataset rather than per-tenant rows; a tenant-scoped entity drops global and declares isolation_axes [tenant_id] instead (see Sub-domains, isolation, and the caller). audit timestamps asks the framework to manage created_at/updated_at, and indexes declares the access paths — a unique index on id, a lookup index on customer_id.
The boundary is composed by reference, not by nesting
There is no nested child-entity construct. An order line is conceptually part of an order, but it is its own aggregate root, tied to its order by a foreign-key relationship:
entity OrderLine aggregate_root global {
doc "A single line on an order — a product and a quantity. Owned by its Order."
systems [OrderingSystem]
audit timestamps
invariants [OrderOpenToAddLine]
indexes [
{ name "uk_order_line_id" kind unique fields [id] }
{ name "ix_order_line_order" kind standard fields [order_id] }
]
id uuid pk filterable[eq]
order_id uuid required immutable filterable[eq] relates Order many_to_one on_delete cascade
product_id uuid required immutable filterable[eq]
quantity int32 required
unit_price Money required
}The line composes into the order through relates Order many_to_one on_delete cascade. The cascade is what makes the ownership real: delete the order and its lines go with it. The immutable on order_id says a line never re-parents; the ix_order_line_order index is the "list the lines of this order" path. This is how an aggregate is assembled — separate roots joined by an owning foreign key, not lexical containment.
Owning versus referencing: on_delete
Not every relationship is ownership. A shipment references the order it fulfils, but it does not own it — you should not be able to delete an order out from under a shipment that ships it. That intent is expressed with on_delete restrict:
entity Shipment aggregate_root global {
doc "A shipment opened to fulfil an order."
systems [FulfillmentSystem]
audit timestamps
indexes [
{ name "uk_shipment_id" kind unique fields [id] }
{ name "ix_shipment_order" kind standard fields [order_id] }
]
id uuid pk filterable[eq]
order_id uuid required immutable filterable[eq] relates Order many_to_one on_delete restrict
status ShipmentStatus required
created_at timestamp immutable sortable
updated_at timestamp
}OrderLine and Shipment both relate to Order, but they mean different things — and the single word after on_delete carries the whole difference:
| Relationship | on_delete | Meaning |
|---|---|---|
OrderLine → Order | cascade | Owned. The line is part of the order; deleting the order deletes its lines. |
Shipment → Order | restrict | Referenced. The shipment points at the order; the order cannot be deleted while a shipment references it. |
The other behaviours are set_null (clear a nullable foreign key when the target goes away) and no_action (defer to the database). Choosing the right one is a modeling decision — you are stating whether one thing is part of another or merely points at it.
What you get for declaring it
From these declarations the platform generates the Postgres table with its columns, indexes, and foreign keys; the repository to load and save it; the HTTP surface; and the typed client. You never write the schema, the data-access layer, or the wire types — they are derived from the entity, and they stay correct as the entity changes. The next page covers identity and the field vocabulary an entity is built from.