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
| Property | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable entity name |
key | string | Yes | Unique identifier (used in API endpoints) |
pluralisedName | string | Yes | Plural form of the name |
icon | string | No | Icon component name |
allowComments | boolean | No | Enable comments on records |
allowTasks | boolean | No | Enable tasks on records |
allowAttachments | boolean | No | Enable file attachments |
maxAttachmentSizeAllowed | number | No | Max attachment size in bytes |
acceptedAttachmentFileTypes | string[] | No | Allowed 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:
| Property | Type | Required | Description |
|---|---|---|---|
label | string | Yes | Display label |
key | string | Yes | Database column name (snake_case) |
type | string | Yes | Field type |
placeholder | string | No | Placeholder text |
description | string | No | Help text |
defaultValue | any | No | Default value for new records |
validateRules | object | No | Validation rules |
behaviourOptions | object | No | UI behavior options |
typeOptions | object | No | Type-specific options |
index | boolean | No | Create 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:
| Field | Type | Description |
|---|---|---|
id | integer | Primary key (auto-increment) |
_created_at | timestamp | Creation timestamp |
_updated_at | timestamp | Last update timestamp |
_created_by | integer | User who created the record |
_updated_by | integer | User who last updated the record |
_is_deleted | boolean | Soft 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
}
6. Group Related Fields Logically
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:
- Screens - Create UIs for your entities
- Entity Hooks - Add custom business logic
- Query Library - Query your entity data
- Rest API - Using the Rest APIs