Creating Entities
Learn how to define custom entities in Stack9 - the foundation of your application's data model. This guide covers all entity configuration options, field types, validation rules, and best practices for building robust data structures.
What You'll Learn
- ✅ Entity structure and configuration
- ✅ All available field types and their options
- ✅ Validation rules and constraints
- ✅ Relationships between entities
- ✅ Entity features (comments, tasks, attachments)
- ✅ Workflow definitions
- ✅ Best practices for entity design
Time Required: 30-45 minutes
Prerequisites
- Stack9 instance initialized
- Basic understanding of JSON
- Text editor or IDE
Understanding Entities
Entities in Stack9 are the core building blocks of your application. They define:
- Data structure - What fields and types your records contain
- Validation rules - What data is valid and required
- Relationships - How entities connect to each other
- Behavior - Comments, tasks, attachments, workflows
- Database schema - Automatically generated tables and indexes
Stack9 entities are configuration-driven: you define them in JSON files, and Stack9 automatically generates:
- Database tables and migrations
- TypeScript types and models
- REST API endpoints
- Admin UI forms and screens
- Validation logic
Entity File Structure
Entities are defined in src/entities/custom/ directory:
src/
└── entities/
└── custom/
├── customer.json
├── product.json
└── order.json
Basic Entity Definition
Let's create a simple Contact entity:
File: src/entities/custom/contact.json
{
"head": {
"name": "Contact",
"key": "contact",
"pluralisedName": "contacts",
"icon": "UserOutlined",
"allowComments": false,
"allowTasks": false,
"isActive": true
},
"fields": [
{
"label": "First Name",
"key": "first_name",
"type": "TextField",
"placeholder": "Enter first name",
"description": "Contact's first name",
"validateRules": {
"required": true,
"maxLength": 100,
"minLength": 2
},
"index": true
},
{
"label": "Last Name",
"key": "last_name",
"type": "TextField",
"placeholder": "Enter last name",
"validateRules": {
"required": true,
"maxLength": 100
},
"index": true
},
{
"label": "Email",
"key": "email",
"type": "TextField",
"placeholder": "email@example.com",
"validateRules": {
"required": true,
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
},
"index": true
},
{
"label": "Phone",
"key": "phone",
"type": "TextField",
"placeholder": "+1 (555) 123-4567"
},
{
"label": "Status",
"key": "status",
"type": "OptionSet",
"typeOptions": {
"values": ["Active", "Inactive", "Archived"]
},
"defaultValue": "Active",
"validateRules": {
"required": true
}
}
],
"hooks": []
}
Head Configuration
The head section defines entity metadata:
| Property | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name (singular) |
key | string | Yes | Unique identifier (snake_case) |
pluralisedName | string | Yes | Plural form for lists |
icon | string | No | Ant Design icon name |
allowComments | boolean | No | Enable comments feature |
allowTasks | boolean | No | Enable tasks feature |
allowAttachments | boolean | No | Enable file attachments |
isActive | boolean | No | Whether entity is active (default: true) |
maxAttachmentSizeAllowed | number | No | Max attachment size in bytes |
acceptedAttachmentFileTypes | array | No | Allowed MIME types |
All Field Types
1. TextField
Single-line text input.
{
"label": "Title",
"key": "title",
"type": "TextField",
"placeholder": "Enter title",
"description": "Short title or heading",
"validateRules": {
"required": true,
"maxLength": 200,
"minLength": 3,
"pattern": "^[a-zA-Z0-9 ]+$"
},
"behaviourOptions": {
"readOnly": false
},
"index": true
}
2. NumericField
Number input with decimal support.
{
"label": "Price",
"key": "price",
"type": "NumericField",
"placeholder": "0.00",
"description": "Product price in dollars",
"validateRules": {
"required": true,
"min": 0,
"max": 9999999
},
"typeOptions": {
"decimals": 2,
"allowNegative": false
},
"defaultValue": 0
}
typeOptions:
decimals- Number of decimal places (default: 0)allowNegative- Allow negative numbers (default: true)
3. DateField
Date/time picker.
{
"label": "Birth Date",
"key": "birth_date",
"type": "DateField",
"validateRules": {
"required": true
},
"typeOptions": {
"time": false
}
}
typeOptions:
time- Include time picker (default: false)
4. Checkbox
Boolean true/false field.
{
"label": "Is Active",
"key": "is_active",
"type": "Checkbox",
"defaultValue": true,
"description": "Whether this record is active"
}
5. OptionSet
Dropdown with predefined options.
{
"label": "Priority",
"key": "priority",
"type": "OptionSet",
"typeOptions": {
"values": ["Low", "Medium", "High", "Urgent"]
},
"defaultValue": "Medium",
"validateRules": {
"required": true
}
}
6. RichTextEditor
Multi-line HTML editor.
{
"label": "Description",
"key": "description",
"type": "RichTextEditor",
"description": "Detailed description with formatting"
}
7. FileField
File upload field.
{
"label": "Profile Picture",
"key": "profile_picture",
"type": "FileField",
"typeOptions": {
"accept": ["image/jpeg", "image/jpg", "image/png"],
"isPublic": false
}
}
typeOptions:
accept- Array of allowed MIME typesisPublic- Whether file is publicly accessible
8. SingleDropDown
Reference to another entity (foreign key).
{
"label": "Category",
"key": "category_id",
"type": "SingleDropDown",
"relationshipOptions": {
"ref": "category"
},
"typeOptions": {
"label": "name"
},
"validateRules": {
"required": true
}
}
relationshipOptions:
ref- Entity key to reference
typeOptions:
label- Field to display from related entity
9. MultiDropDown
Multiple references (many-to-many).
{
"label": "Tags",
"key": "tags",
"type": "MultiDropDown",
"relationshipOptions": {
"ref": "tag"
},
"typeOptions": {
"label": "name",
"value": "id"
}
}
10. Grid
Related entity list (one-to-many).
{
"label": "Order Items",
"key": "order_items",
"type": "Grid",
"relationshipOptions": {
"ref": "order_item"
},
"typeOptions": {
"relationshipField": "order_id"
}
}
typeOptions:
relationshipField- Foreign key field in child entity
Validation Rules
All field types support these validation rules:
{
"validateRules": {
"required": true,
"maxLength": 200,
"minLength": 3,
"max": 100,
"min": 0,
"pattern": "^[A-Z0-9-]+$"
}
}
| Rule | Applies To | Description |
|---|---|---|
required | All | Field must have a value |
maxLength | Text | Maximum character count |
minLength | Text | Minimum character count |
max | Numeric | Maximum value |
min | Numeric | Minimum value |
pattern | Text | Regular expression pattern |
Behavior Options
Control field behavior:
{
"behaviourOptions": {
"readOnly": true,
"hidden": false
}
}
| Option | Description |
|---|---|
readOnly | Field cannot be edited after creation |
hidden | Field is hidden in UI (still in database) |
Entity with Attachments
Enable file attachments on an entity:
{
"head": {
"name": "Document",
"key": "document",
"pluralisedName": "documents",
"allowAttachments": true,
"maxAttachmentSizeAllowed": 10000000,
"acceptedAttachmentFileTypes": [
"application/pdf",
"image/jpeg",
"image/png",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
]
},
"fields": []
}
Entity with Comments and Tasks
Enable collaboration features:
{
"head": {
"name": "Project",
"key": "project",
"pluralisedName": "projects",
"allowComments": true,
"allowTasks": true
},
"fields": []
}
Advanced Example: Order Entity
Complete example with multiple field types and relationships:
File: src/entities/custom/order.json
{
"head": {
"name": "Order",
"key": "order",
"pluralisedName": "orders",
"icon": "ShoppingCartOutlined",
"allowComments": true,
"allowTasks": true,
"allowAttachments": true,
"isActive": true
},
"fields": [
{
"label": "Order Number",
"key": "order_number",
"type": "TextField",
"placeholder": "ORD-20250111-0001",
"description": "Unique order identifier",
"validateRules": {
"required": true,
"pattern": "^ORD-[0-9]{8}-[0-9]{4}$"
},
"behaviourOptions": {
"readOnly": true
},
"index": true
},
{
"label": "Customer",
"key": "customer_id",
"type": "SingleDropDown",
"relationshipOptions": {
"ref": "customer"
},
"typeOptions": {
"label": "{{name}} ({{email}})"
},
"validateRules": {
"required": true
}
},
{
"label": "Order Date",
"key": "order_date",
"type": "DateField",
"typeOptions": {
"time": true
},
"validateRules": {
"required": true
}
},
{
"label": "Status",
"key": "status",
"type": "OptionSet",
"typeOptions": {
"values": ["Draft", "Pending", "Processing", "Shipped", "Delivered", "Cancelled"]
},
"defaultValue": "Draft",
"validateRules": {
"required": true
}
},
{
"label": "Order Items",
"key": "order_items",
"type": "Grid",
"relationshipOptions": {
"ref": "order_item"
},
"typeOptions": {
"relationshipField": "order_id"
}
},
{
"label": "Subtotal",
"key": "subtotal",
"type": "NumericField",
"typeOptions": {
"decimals": 2,
"allowNegative": false
},
"behaviourOptions": {
"readOnly": true
},
"defaultValue": 0
},
{
"label": "Tax Amount",
"key": "tax_amount",
"type": "NumericField",
"typeOptions": {
"decimals": 2,
"allowNegative": false
},
"behaviourOptions": {
"readOnly": true
},
"defaultValue": 0
},
{
"label": "Total",
"key": "total",
"type": "NumericField",
"typeOptions": {
"decimals": 2,
"allowNegative": false
},
"behaviourOptions": {
"readOnly": true
},
"defaultValue": 0
},
{
"label": "Notes",
"key": "notes",
"type": "RichTextEditor",
"description": "Internal notes about the order"
}
],
"hooks": []
}
Entity Folders
Organize entities into folders for better management:
File: src/entities/entity-folders.json
{
"folders": [
{
"key": "crm",
"name": "CRM",
"order": 1
},
{
"key": "products",
"name": "Products",
"order": 2
},
{
"key": "orders",
"name": "Orders",
"order": 3
}
]
}
Then reference folder in entity:
{
"head": {
"name": "Customer",
"key": "customer"
},
"fields": [],
"folderKey": "crm"
}
Testing Your Entity
After creating an entity:
- Restart Stack9:
npm run dev
- Check Database: Stack9 automatically creates:
- Table:
customers(plural of entity key) - Columns for each field
- Indexes where specified
- Foreign key constraints
- Test via API:
# Create a contact
curl -X POST http://localhost:3000/api/contact \
-H "Content-Type: application/json" \
-d '{
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"phone": "+1 555-123-4567",
"status": "Active"
}'
# Get contact
curl http://localhost:3000/api/contact/1
# Search contacts
curl -X POST http://localhost:3000/api/contact/search \
-H "Content-Type: application/json" \
-d '{
"$where": {
"status": "Active"
},
"$orderBy": [
{"column": "last_name", "order": "asc"}
]
}'
Best Practices
1. Naming Conventions
// ✅ Good
{
"key": "customer", // snake_case
"name": "Customer", // PascalCase
"pluralisedName": "customers" // lowercase plural
}
// ❌ Bad
{
"key": "Customer", // Don't use PascalCase
"name": "customer", // Don't use lowercase
"pluralisedName": "Customers" // Don't capitalize
}
2. Field Keys
// ✅ Good - snake_case
"key": "first_name"
"key": "order_date"
"key": "customer_id"
// ❌ Bad - avoid camelCase or special characters
"key": "firstName"
"key": "order-date"
"key": "customer.id"
3. Index Frequently Queried Fields
{
"label": "Email",
"key": "email",
"type": "TextField",
"index": true // ✅ Index fields used in searches
}
4. Use Descriptive Labels and Descriptions
{
"label": "SKU",
"key": "sku",
"type": "TextField",
"description": "Stock Keeping Unit - unique product identifier", // ✅ Helpful
"placeholder": "e.g., PROD-001" // ✅ Shows expected format
}
5. Set Appropriate Default Values
{
"label": "Status",
"key": "status",
"type": "OptionSet",
"typeOptions": {
"values": ["Draft", "Active", "Archived"]
},
"defaultValue": "Draft" // ✅ Good default
}
6. Make Read-Only Calculated Fields
{
"label": "Total",
"key": "total",
"type": "NumericField",
"behaviourOptions": {
"readOnly": true // ✅ Calculated in hooks
}
}
Common Patterns
Pattern 1: Audit Fields
{
"fields": [
{
"label": "Created By",
"key": "_created_by",
"type": "SingleDropDown",
"relationshipOptions": {
"ref": "user"
},
"behaviourOptions": {
"readOnly": true
}
},
{
"label": "Updated By",
"key": "_updated_by",
"type": "SingleDropDown",
"relationshipOptions": {
"ref": "user"
},
"behaviourOptions": {
"readOnly": true
}
}
]
}
Pattern 2: Status with Colors
{
"label": "Status",
"key": "status",
"type": "OptionSet",
"typeOptions": {
"values": ["Draft", "Active", "Completed", "Cancelled"]
}
}
Pattern 3: Hierarchical Data
{
"label": "Parent Category",
"key": "parent_category_id",
"type": "SingleDropDown",
"relationshipOptions": {
"ref": "category"
},
"typeOptions": {
"label": "name"
}
}
Troubleshooting
Entity Not Appearing
Problem: Created entity but not seeing it in UI or API
Solutions:
- Restart Stack9:
npm run dev - Check file is in
src/entities/custom/ - Validate JSON syntax
- Check logs for errors
- Ensure
isActive: truein head
Field Not Saving
Problem: Field value doesn't persist
Solutions:
- Check field key is unique
- Verify field type is correct
- Check validation rules aren't too strict
- Look for errors in browser console
Relationship Not Working
Problem: SingleDropDown or MultiDropDown not showing options
Solutions:
- Verify
relationshipOptions.refmatches target entity key - Ensure target entity exists and has records
- Check
typeOptions.labelreferences valid field
Validation Failing
Problem: Cannot save valid data
Solutions:
- Check regex pattern is correct (escape special characters)
- Verify min/max values are appropriate
- Test pattern with online regex tester
- Check for conflicts between rules
Next Steps
Now that you've mastered creating entities:
- Entity Relationships - Connect entities together
- Entity Hooks - Add validation and business logic
- Building a CRUD Application - Build complete features
- Custom Queries - Query your entities efficiently
Summary
You now understand:
✅ Entity structure and configuration ✅ All available field types ✅ Validation rules and constraints ✅ Relationships (SingleDropDown, MultiDropDown, Grid) ✅ Entity features (comments, tasks, attachments) ✅ Best practices for entity design
Entities are the foundation of your Stack9 application. Well-designed entities make everything else easier!