Skip to main content

Entities

Entities are the foundation of every Stack9 application. An entity represents a business object (like Customer, Order, Product) and defines its data structure, validation rules, and relationships.

What is an Entity?

An entity is a JSON configuration file that tells Stack9:

  • What fields the entity has
  • What type each field is (text, number, date, etc.)
  • How to validate the data
  • What relationships exist with other entities
  • What UI behaviors to apply

When you define an entity, Stack9 automatically:

  • ✅ Creates a database table
  • ✅ Generates REST API endpoints (CRUD)
  • ✅ Creates TypeScript models
  • ✅ Handles validation
  • ✅ Manages relationships
  • ✅ Provides audit logging

Entity File Structure

Entities are defined in src/entities/custom/{entity_name}.json:

{
"head": {
// Entity metadata
},
"fields": [
// Field definitions
],
"hooks": [
// Optional validation hooks
]
}

Entity Head Configuration

The head section defines entity-level metadata:

{
"head": {
"name": "Customer", // Display name (singular)
"key": "customer", // Unique identifier (lowercase, no spaces)
"pluralisedName": "customers", // Plural form
"icon": "UserIcon", // Icon for UI (optional)
"allowComments": true, // Enable comments feature
"allowTasks": true, // Enable tasks feature
"allowAttachments": true, // Enable file attachments
"isActive": true, // Entity is active
"maxAttachmentSizeAllowed": 30000000, // 30MB max file size
"acceptedAttachmentFileTypes": [ // Allowed MIME types
"image/jpg",
"image/jpeg",
"image/png",
"application/pdf"
]
}
}

Key Properties

PropertyTypeRequiredDescription
namestringYesHuman-readable entity name
keystringYesUnique identifier (used in API endpoints)
pluralisedNamestringYesPlural form of the name
iconstringNoIcon component name
allowCommentsbooleanNoEnable comments on records
allowTasksbooleanNoEnable tasks on records
allowAttachmentsbooleanNoEnable file attachments
maxAttachmentSizeAllowednumberNoMax attachment size in bytes
acceptedAttachmentFileTypesstring[]NoAllowed file MIME types

Field Types

Stack9 supports various field types to model your data:

1. TextField

Single-line text input.

{
"label": "Email Address",
"key": "email",
"type": "TextField",
"placeholder": "Enter email address",
"description": "Primary contact email",
"validateRules": {
"required": true,
"maxLength": 100,
"minLength": 5
},
"typeOptions": {
"format": "email" // or "phone", "url"
},
"behaviourOptions": {
"readOnly": false
}
}

Type Options:

  • format: Predefined format (email, phone, url)

2. NumericField

Number input with validation.

{
"label": "Age",
"key": "age",
"type": "NumericField",
"validateRules": {
"required": true,
"min": 18,
"max": 120
},
"typeOptions": {
"decimals": 0, // Number of decimal places
"allowNegative": false
}
}

3. DateField

Date or datetime picker.

{
"label": "Date of Birth",
"key": "dob",
"type": "DateField",
"placeholder": "Select date",
"typeOptions": {
"time": false // Set to true for datetime
},
"validateRules": {
"required": true
}
}

4. Checkbox

Boolean field (true/false).

{
"label": "Is Active",
"key": "is_active",
"type": "Checkbox",
"defaultValue": true,
"description": "Whether the customer is active"
}

5. OptionSet

Dropdown with predefined values.

{
"label": "Customer Type",
"key": "customer_type",
"type": "OptionSet",
"typeOptions": {
"values": ["Individual", "Business"]
},
"validateRules": {
"required": true
}
}

6. SingleDropDown

Foreign key relationship (Many-to-One).

{
"label": "Sales Channel",
"key": "sales_channel_id",
"type": "SingleDropDown",
"relationshipOptions": {
"ref": "sales_channel" // Related entity key
},
"typeOptions": {
"label": "name" // Field to display in dropdown
},
"validateRules": {
"required": false
}
}

7. MultiDropDown

Many-to-Many relationship.

{
"label": "Available Times",
"key": "available_times",
"type": "MultiDropDown",
"relationshipOptions": {
"ref": "available_time"
},
"typeOptions": {
"label": "{{period}}", // Template for display
"value": "{{id}}" // Value field
}
}

8. Grid

One-to-Many relationship (child records).

{
"label": "Alternative Emails",
"key": "alternative_emails",
"type": "Grid",
"relationshipOptions": {
"ref": "alternative_email" // Related entity
},
"typeOptions": {
"relationshipField": "customer_id" // FK field in child entity
}
}

9. RichTextEditor

HTML content editor.

{
"label": "Notes",
"key": "notes",
"type": "RichTextEditor",
"description": "Additional notes about the customer"
}

Field Properties

All fields share these common properties:

PropertyTypeRequiredDescription
labelstringYesDisplay label
keystringYesDatabase column name (snake_case)
typestringYesField type
placeholderstringNoPlaceholder text
descriptionstringNoHelp text
defaultValueanyNoDefault value for new records
validateRulesobjectNoValidation rules
behaviourOptionsobjectNoUI behavior options
typeOptionsobjectNoType-specific options
indexbooleanNoCreate database index

Validation Rules

{
"validateRules": {
"required": true, // Field is required
"maxLength": 100, // Max string length
"minLength": 5, // Min string length
"max": 999, // Max numeric value
"min": 0, // Min numeric value
"pattern": "^[A-Z0-9]+$" // Regex pattern
}
}

Behaviour Options

{
"behaviourOptions": {
"readOnly": true, // Field cannot be edited
"hidden": false // Hide field in forms
}
}

Relationships

One-to-Many (Parent → Children)

Use the Grid field type in the parent entity:

Parent Entity (customer.json):

{
"label": "Sales Orders",
"key": "sales_orders",
"type": "Grid",
"relationshipOptions": {
"ref": "sales_order"
},
"typeOptions": {
"relationshipField": "customer_id"
}
}

Child Entity (sales_order.json):

{
"label": "Customer",
"key": "customer_id",
"type": "SingleDropDown",
"relationshipOptions": {
"ref": "customer"
},
"typeOptions": {
"label": "name"
}
}

Many-to-One (Child → Parent)

Use the SingleDropDown field type:

{
"label": "Customer",
"key": "customer_id",
"type": "SingleDropDown",
"relationshipOptions": {
"ref": "customer"
},
"typeOptions": {
"label": "name"
}
}

Many-to-Many

Use the MultiDropDown field type:

{
"label": "Tags",
"key": "tags",
"type": "MultiDropDown",
"relationshipOptions": {
"ref": "tag"
},
"typeOptions": {
"label": "name",
"value": "id"
}
}

Stack9 automatically creates a junction table for many-to-many relationships.

Real-World Example

Here's a simplified Customer entity from a production Stack9 app:

{
"head": {
"name": "Customer",
"key": "customer",
"pluralisedName": "customers",
"allowComments": true,
"allowTasks": true,
"maxAttachmentSizeAllowed": 30000000,
"acceptedAttachmentFileTypes": [
"image/jpg",
"image/jpeg",
"image/png",
"application/pdf"
]
},
"fields": [
{
"label": "CRN",
"key": "crn",
"type": "NumericField",
"description": "Unique customer reference number",
"behaviourOptions": {
"readOnly": true
}
},
{
"label": "Customer Type",
"key": "customer_type",
"type": "OptionSet",
"typeOptions": {
"values": ["Individual", "Business"]
},
"validateRules": {
"required": true
}
},
{
"label": "First Name",
"key": "name",
"type": "TextField",
"placeholder": "First name or Business Name",
"validateRules": {
"required": true,
"maxLength": 100,
"minLength": 1
}
},
{
"label": "Last Name",
"key": "last_name",
"type": "TextField",
"validateRules": {
"maxLength": 80
}
},
{
"label": "Email",
"key": "email_address",
"type": "TextField",
"typeOptions": {
"format": "email"
},
"validateRules": {
"required": true,
"maxLength": 100
}
},
{
"label": "Phone",
"key": "phone",
"type": "TextField",
"typeOptions": {
"format": "phone"
},
"validateRules": {
"maxLength": 13
}
},
{
"label": "Date of Birth",
"key": "dob",
"type": "DateField",
"typeOptions": {
"time": false
}
},
{
"label": "Is Active",
"key": "is_active",
"type": "Checkbox",
"defaultValue": true
},
{
"label": "Sales Channel",
"key": "sales_channel_id",
"type": "SingleDropDown",
"relationshipOptions": {
"ref": "sales_channel"
},
"typeOptions": {
"label": "name"
}
},
{
"label": "Sales Orders",
"key": "sales_orders",
"type": "Grid",
"relationshipOptions": {
"ref": "sales_order"
},
"typeOptions": {
"relationshipField": "customer_id"
}
}
],
"hooks": []
}

Auto-Generated Features

When you create this entity, Stack9 automatically generates:

Database Table

CREATE TABLE customers (
id SERIAL PRIMARY KEY,
crn INTEGER,
customer_type VARCHAR(50),
name VARCHAR(100) NOT NULL,
last_name VARCHAR(80),
email_address VARCHAR(100) NOT NULL,
phone VARCHAR(13),
dob DATE,
is_active BOOLEAN DEFAULT true,
sales_channel_id INTEGER REFERENCES sales_channels(id),
_created_at TIMESTAMP DEFAULT NOW(),
_updated_at TIMESTAMP DEFAULT NOW(),
_created_by INTEGER,
_updated_by INTEGER,
_is_deleted BOOLEAN DEFAULT false
);

REST API Endpoints

GET    /api/customer/:id          - Get customer by ID
POST /api/customer - Create customer
PUT /api/customer/:id - Update customer
DELETE /api/customer/:id - Soft delete customer
POST /api/customer/search - Search customers
POST /api/customer/query - Query with aggregations

TypeScript Models

interface Customer {
id: number;
crn?: number;
customer_type?: 'Individual' | 'Business';
name: string;
last_name?: string;
email_address: string;
phone?: string;
dob?: Date;
is_active?: boolean;
sales_channel_id?: number;
_created_at?: Date;
_updated_at?: Date;
_created_by?: number;
_updated_by?: number;
_is_deleted?: boolean;
}

System Fields

Every entity automatically includes these system fields:

FieldTypeDescription
idintegerPrimary key (auto-increment)
_created_attimestampCreation timestamp
_updated_attimestampLast update timestamp
_created_byintegerUser who created the record
_updated_byintegerUser who last updated the record
_is_deletedbooleanSoft delete flag

Best Practices

1. Use Consistent Naming

  • Entity keys: customer, sales_order (lowercase, underscore)
  • Field keys: first_name, email_address (snake_case)
  • Label everything clearly for non-technical users

2. Add Descriptions

{
"label": "CRN",
"key": "crn",
"description": "Unique customer reference number - automatically generated"
}

3. Set Appropriate Validation

{
"validateRules": {
"required": true,
"maxLength": 100,
"minLength": 1
}
}

4. Use Read-Only Fields for System-Generated Values

{
"key": "crn",
"behaviourOptions": {
"readOnly": true
}
}

5. Index Frequently Queried Fields

{
"key": "email_address",
"index": true
}

Organize fields in the JSON in a logical order:

  • Basic info first (name, email)
  • Contact details next
  • Relationships at the end

Common Patterns

Auto-Generated Sequence Numbers

{
"label": "Order Number",
"key": "order_number",
"type": "NumericField",
"behaviourOptions": {
"readOnly": true
}
}

Then use an entity hook to generate the sequence:

// entity-hooks/order.vat.ts
const orderNumber = await db.sequence.nextVal('sales_order', 1, 999999);

Conditional Fields

Use behaviourOptions to show/hide fields based on other field values:

{
"label": "Business ABN",
"key": "business_abn",
"type": "TextField",
"conditionalDisplay": {
"field": "customer_type",
"operator": "equals",
"value": "Business"
}
}

Calculated Fields

Define fields that are calculated in entity hooks:

{
"label": "Age",
"key": "age",
"type": "NumericField",
"behaviourOptions": {
"readOnly": true
}
}

Calculate in hook:

const age = dayjs().diff(dayjs(entity.dob), 'year');

Next Steps

Now that you understand entities, learn about: