Skip to main content

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:

FieldTypePurpose
iduuidUnique identifier
createdAtdatetimeRecord creation timestamp
updatedAtdatetimeLast modification timestamp
deletedAtdatetime (nullable)Soft delete timestamp
versionintegerOptimistic 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:

TypeDescriptionExample
stringText data"John Doe"
numberNumeric data (integer or decimal)199.99
booleanTrue or falsetrue
dateCalendar date without time"2025-03-15"
datetimeDate with time and timezone"2025-03-15T14:30:00Z"
enumOne value from a predefined set"confirmed"
uuidUniversally unique identifier"550e8400-e29b-41d4-a716-446655440000"
jsonArbitrary JSON data{"key": "value"}
referenceForeign key to another EntityReferences 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:

TypeMeaningExample
hasOneThis Entity owns exactly one of the targetUser hasOne Profile
hasManyThis Entity owns many of the targetCustomer hasMany Orders
belongsToThis Entity is owned by the targetOrder belongsTo Customer
manyToManyMany-to-many associationProduct 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.