Entity
An Entity is Fascia's core business object abstraction. It encapsulates a domain concept — such as a Reservation, Customer, Order, or Payment — with typed fields, relationships to other Entities, a status machine governing lifecycle transitions, and invariants enforcing business rules.
You define an Entity through a structured spec. Fascia automatically derives the database storage model (tables, columns, indexes) from that spec. You never write SQL schemas by hand.
What an Entity Includes
Every Entity spec contains:
- Fields -- Typed data attributes that hold the Entity's state
- Relationships -- Connections to other Entities (foreign keys, join tables)
- Status Machine -- Allowed states and the transitions between them
- Invariants -- Business rules that must always be true
- Row-Level Access -- Ownership-based filtering for multi-tenant security
System Fields
Every Entity automatically includes these system-managed fields. Do not define them manually in your spec:
| Field | Type | Purpose |
|---|---|---|
id | uuid | Unique identifier |
createdAt | datetime | Record creation timestamp |
updatedAt | datetime | Last modification timestamp |
deletedAt | datetime (nullable) | Soft delete timestamp |
version | integer | Optimistic locking counter |
Fascia uses soft deletes exclusively. When a record is "deleted," its deletedAt field is set rather than removing the row. The version field enables optimistic concurrency control -- updates that conflict with a concurrent modification are safely rejected.
Field Types
Entity fields support the following types:
| Type | Description | Example |
|---|---|---|
string | Text data | "John Doe" |
number | Numeric data (integer or decimal) | 199.99 |
boolean | True or false | true |
date | Calendar date without time | "2025-03-15" |
datetime | Date with time and timezone | "2025-03-15T14:30:00Z" |
enum | One value from a predefined set | "confirmed" |
uuid | Universally unique identifier | "550e8400-e29b-41d4-a716-446655440000" |
json | Arbitrary JSON data | {"key": "value"} |
reference | Foreign key to another Entity | References a Customer's id |
Each field can be marked as required and can have a default value.
Relationships
Entities connect to each other through four relationship types:
| Type | Meaning | Example |
|---|---|---|
hasOne | This Entity owns exactly one of the target | User hasOne Profile |
hasMany | This Entity owns many of the target | Customer hasMany Orders |
belongsTo | This Entity is owned by the target | Order belongsTo Customer |
manyToMany | Many-to-many association | Product manyToMany Category |
Relationships are defined by specifying the target Entity and the foreign key field.
Status Machine
Every Entity must have a status machine, even if it is simple (for example, active to deleted). The status machine defines:
- States -- The allowed statuses an Entity can be in
- Initial State -- The status assigned when the Entity is first created
- Transitions -- Which state changes are permitted
- Guards (optional) -- Value DSL boolean expressions that must be true for a transition to occur
Transitions must be explicitly defined. There are no wildcard transitions. If a transition is not listed, it is not allowed.
Invariants
Invariants are business rules that must always hold true for an Entity. They are expressed as Value DSL boolean expressions and are enforced at step 6 of the Execution Contract -- after the flow graph executes but before the transaction commits.
If any invariant evaluates to false, the entire transaction rolls back.
Row-Level Access Control
When rowLevelAccess is enabled and an ownerField is specified, Fascia automatically filters query results so that users can only see records they own. Admin and staff roles can be configured to bypass this restriction.
Example: Reservation Entity
{
"name": "Reservation",
"version": 1,
"description": "A booking made by a customer for a specific time slot and resource",
"fields": {
"customerId": { "type": "reference", "referenceTo": "Customer", "required": true },
"resourceId": { "type": "reference", "referenceTo": "Resource", "required": true },
"startDate": { "type": "datetime", "required": true },
"endDate": { "type": "datetime", "required": true },
"totalPrice": { "type": "number", "required": true },
"depositAmount": { "type": "number", "required": false, "default": 0 },
"notes": { "type": "string", "required": false },
"cancellationReason": { "type": "string", "required": false }
},
"relationships": {
"customer": { "type": "belongsTo", "target": "Customer", "foreignKey": "customerId" },
"resource": { "type": "belongsTo", "target": "Resource", "foreignKey": "resourceId" },
"payments": { "type": "hasMany", "target": "Payment" }
},
"statusMachine": {
"states": ["pending", "confirmed", "checked_in", "completed", "cancelled", "no_show"],
"initialState": "pending",
"transitions": [
{ "from": "pending", "to": "confirmed" },
{ "from": "pending", "to": "cancelled" },
{ "from": "confirmed", "to": "checked_in" },
{ "from": "confirmed", "to": "cancelled", "guard": "now() < reservation.startDate" },
{ "from": "confirmed", "to": "no_show", "guard": "now() > addHours(reservation.startDate, 1)" },
{ "from": "checked_in", "to": "completed" }
]
},
"invariants": [
{ "name": "endDateAfterStart", "expression": "reservation.endDate > reservation.startDate", "message": "End date must be after start date" },
{ "name": "totalPricePositive", "expression": "reservation.totalPrice > 0", "message": "Total price must be positive" },
{ "name": "depositNotExceedTotal", "expression": "reservation.depositAmount <= reservation.totalPrice", "message": "Deposit cannot exceed total price" }
],
"rowLevelAccess": true,
"ownerField": "customerId"
}
This spec defines a Reservation with six lifecycle states, three invariants enforced on every write, and row-level access control so customers can only see their own reservations. Notice the transition guards -- a confirmed reservation can only be cancelled before its start date, and it is only marked as a no-show if more than one hour has passed since the start time.