Data Models Overview
Welcome to the Wrkbelt Data Models documentation. This section provides a comprehensive overview of our platform's data architecture and relationships.
Data Model Topology
Our data model is organized into distinct domains, each handling specific platform capabilities:
Domain Responsibilities
-
Core Domain
users: Platform user accounts and authenticationmemberships: Organization membership and rolesorganizations: Multi-tenant organization managementroles&permissions: RBAC systemfiles: File storage and access controlemails: Email communicationtemporary_links: Secure temporary accessbrand_kit: Organization branding assets
-
Scheduler Domain
scheduler_config: Configuration linking booking flows, branding, and initialization settingsbooking_flow: Configurable booking experiences with step-based flowsbooking_flow_step: Individual steps in the booking process with type-specific configurationsbooking_services: Bookable services with vendor integrationbooking_questions: Questions with associated answers for service selection and data collectionbooking_question_answers: Responses to questions that can reference services or provide contextbooking_answer_categories: Grouping mechanism for answers and servicesbooking_session: User session tracking with status-based type discriminationbooking_session_event: Event-based tracking of all user interactions within a sessionbooking_flow_routers: URL-based flow routing rulesbooking_flow_express_links: Direct booking links with prefilled statesbooking_flow_diagrams: Visual representations of flows and service selection graphs
-
Accounting Domain
subscriptions: Organization subscriptionssubscription_change_logs: Subscription historypayments: Payment processingproduct_records: Product configurationstripe_webhook_logs: Payment webhooks
-
Marketing Domain
waitlist_requests: Lead generation tracking
-
Integrations Domain
vendor_credentials: External service configuration
-
Data Migrations Domain
data_migrations: Schema version control
Key Design Decisions
Multi-Tenant Architecture
The platform is built with multi-tenancy as a foundational principle:
- Organization Isolation: Each organization operates in a secure, isolated context
- Resource Sharing: Efficient infrastructure utilization across tenants
- Domain Separation: Clear boundaries between tenant resources
- Access Control: Tenant-aware data access patterns
Identity and Access
We implement a robust RBAC (Role-Based Access Control) system:
- Multi-Organization: Users can belong to multiple organizations
- Role Hierarchy: Flexible permission inheritance
- Context Awareness: Organization-specific roles and permissions
- Granular Control: Fine-grained access to features and resources
Data Integrity
Strong data integrity is maintained through:
- Schema Validation: Type and format validation at the schema level
- Business Rules: Domain-specific validation in the service layer
- Referential Integrity: Proper relationship management
- Audit Trail: Comprehensive change tracking
Integration Patterns
Standardized approach to external integrations:
- Provider Agnostic: Flexible integration interfaces
- Secure Storage: Encrypted credential management
- Event-Driven: Real-time data synchronization
- Extensible: Easy addition of new integrations
Event Sourcing
Our enhanced session tracking employs event sourcing for comprehensive visibility:
- Events as Source of Truth: All user interactions captured as immutable events
- State Derivation: Current state derived from the event stream
- Chronological Record: Complete history of user interactions preserved
- Analytical Depth: Rich insights into user behavior patterns
Entity Relationship Diagram
The following diagram shows the relationships between our core entities:
See our living Entity Relationship Diagram for a more detailed overview of our data models.
Implementation Notes
MongoDB & Mongoose
While our ERD uses SQL-like notation for clarity, our platform is built on MongoDB:
- Schema Definition: We use Mongoose for schema definition and validation
- References: Relationships are implemented using MongoDB ObjectIds
- Query Patterns: Nested fields use dot notation in queries
- Indexing: Strategic indexes for performance optimization
Type Safety
Our models are strongly typed using TypeScript:
- Interfaces: All schemas have corresponding TypeScript interfaces
- Enums: Predefined value sets are strictly typed
- Validation: Rules are enforced at both runtime and compile-time
- Type Guards: Custom type guards for complex validations
Common Data Patterns
We use several consistent patterns across our data model:
- ContentConfig Pattern: A reusable structure for display information (title, description, icon) used in various entities
- Discriminated Unions: Type-specific configurations based on entity type fields
- JSON Schema Approach: Complex nested structures with well-defined schemas
- Organization Context: All business entities include organization_id for multi-tenant isolation
- Event Sourcing: Capturing all user interactions as immutable events for complete journey tracking
- Status-Based Type Discrimination: Using status fields to determine entity structure with proper type safety
Domain Documentation
For detailed information about specific domains, refer to:
-
- User and identity management
- Organization and membership system
- Role-based access control
- File management
- Communication services
- Brand management
-
- Booking flow configuration
- Service selection graph
- Integration with service providers
- Session tracking with event sourcing
- Comprehensive analytics and journey tracking
-
- Waitlist management
- Lead tracking
Additional domain documentation coming soon:
- Accounting Domain
- Integrations Domain
Working with the Data Model
Best Practices
When interacting with the Wrkbelt data model, follow these best practices to ensure consistency, performance, and maintainability:
1. Respect Domain Boundaries
- Keep domain-specific logic within its respective service layer
- Don't directly modify data across domain boundaries
- Use defined interfaces or events for cross-domain communication
2. Use Type Guards for Complex Entities
TypeScript's type system helps ensure correctness, especially for entities with discriminated unions:
// Example type guard for BookingSession status types
function isActiveSession(
session: BookingSession
): session is ActiveBookingSession {
return session.booking_outcome.status === BookingSessionStatus.ACTIVE;
}
// Usage
if (isActiveSession(session)) {
// TypeScript knows this is an ActiveBookingSession
const currentStepId = session.current_state.step_id;
} else if (isCompletedSession(session)) {
// TypeScript knows this is a CompletedBookingSession
const bookingId = session.booking_outcome.booking_id;
const completedAt = session.booking_outcome.completed_at;
}
3. Optimize Query Patterns
MongoDB performs best with certain query patterns:
- Use projection to limit returned fields when retrieving large documents
- Create compound indexes for common query patterns
- Use aggregation pipelines for complex data transformations
- Implement pagination for large result sets
4. Maintain Referential Integrity
While MongoDB doesn't enforce foreign key constraints, application code should:
- Implement cascade operations where appropriate
- Validate references before creation
- Handle orphaned references gracefully
Graph-Based Data Patterns
The Scheduler domain's service selection step demonstrates how to implement graph structures in MongoDB:
Representing Directed Acyclic Graphs (DAGs)
// Node representation
type Node = {
_id: string;
type: NodeType;
// Type-specific fields...
};
// Edge representation
type Edge = {
_id: string;
source_node_id: string;
target_node_id: string;
};
This approach separates node entities from their connections, enabling:
- Efficient path traversal
- Easy addition or removal of connections
- Clear separation between entity data and relationship structure
- Simplified validation of graph integrity
Graph Traversal Patterns
When working with the service selection graph:
// Find all possible next nodes from a given node
function findNextNodes(nodeId: string, edges: Edge[], nodes: Node[]): Node[] {
const outgoingEdges = edges.filter(edge => edge.source_node_id === nodeId);
return outgoingEdges.map(edge =>
nodes.find(node => node._id === edge.target_node_id)
).filter(Boolean);
}
// Validate that all paths contain exactly one service
function validatePaths(rootNodeId: string, edges: Edge[], nodes: Node[]): boolean {
// Implementation would use recursive depth-first search
// to ensure each possible path contains exactly one service node
}
Event-Sourcing Patterns
Our booking session tracking system uses event sourcing to capture complete user journeys:
// Process an incoming event
async function processEvent(eventData) {
// Find the session
const session = await bookingSessionRepository.findById(eventData.booking_session_id);
// Create and save the event
const savedEvent = await bookingSessionEventRepository.save({
...eventData,
timestamp: new Date()
});
// Update session with reference to this new event
session.current_state.last_event_id = savedEvent._id;
// Add event to the array of references
session.booking_session_event_ids.push(savedEvent._id);
// Update the session based on event type
await updateSessionState(session, savedEvent);
// Save the updated session
await bookingSessionRepository.save(session);
return savedEvent;
}
// Status transition example (active to completed)
async function completeSession(session, bookingResult) {
// Create completion event
const completionEvent = await bookingSessionEventRepository.save({
booking_session_id: session._id,
organization_id: session.organization_id,
event_type: BookingSessionEventType.SESSION_COMPLETED,
timestamp: new Date(),
step_id: session.current_state.step_id,
data: {
booking_id: bookingResult.bookingId
}
});
// Transform active session to completed session
const completedSession = {
...session,
booking_outcome: {
status: BookingSessionStatus.COMPLETED,
completed_at: new Date(),
booking_id: bookingResult.bookingId,
selected_service_id: session.current_state.temp_selections?.service_id,
selected_timeslot: session.current_state.temp_selections?.timeslot
},
current_state: {
...session.current_state,
// Remove temporary selections
temp_selections: undefined
}
};
// Add the completion event to events array
completedSession.booking_session_event_ids.push(completionEvent._id);
// Update and save the transformed session
return bookingSessionRepository.save(completedSession);
}
This pattern gives us:
- Complete traceability of all user interactions
- Ability to reconstruct session state at any point in time
- Rich analytics capabilities for journey analysis
- Clear separation between operational state and outcome data
Data Migration Strategies
As the platform evolves, we manage data model changes through carefully planned migrations:
MongoDB-Specific Approaches
- Additive Changes: Add new fields without modifying existing documents
- Transformational Changes: Run migrations that transform document structure
- Schema Version Tracking: Use the DataMigration collection to track applied changes
Migration Implementation
// Example Permissions Seeding Migration
import { Injectable, Logger } from '@nestjs/common';
import { AllPermissionsMetadata, roleMap } from '@wrkbelt/shared/types-data';
import { MigrationInterface } from '@wrkbelt/utils-api';
import { PermissionService } from '../../core/permission/permission.service';
@Injectable()
export default class SeedInternalUsersAndRBAC implements MigrationInterface {
logger = new Logger(SeedInternalUsersAndRBAC.name);
constructor(
private permissionService: PermissionService,
) {}
public readonly unique_name = '001_seed-permissions-for-xyz';
public readonly description =
'Seeds permissions for XYZ';
async up(): Promise<void> {
// Seed permissions
const permissions = await this.seedPermissions();
}
async down(): Promise<void> {
// Delete permissions
await this.deletePermissions();
}
private async seedPermissions(): Promise<{ [key: string]: string }> {
this.logger.log('Starting to seed permissions...');
const permissionIds: { [key: string]: string } = {};
for (const key in AllPermissionsMetadata) {
const permMetadata =
AllPermissionsMetadata[key as keyof typeof AllPermissionsMetadata];
try {
const createdPerm = await this.permissionService.create({
name: permMetadata.name,
description: permMetadata.description,
});
permissionIds[permMetadata.name] = createdPerm._id;
this.logger.log(`Seeded permission: ${permMetadata.name}`);
} catch (error) {
this.logger.error(
`Error seeding permission ${permMetadata.name}: ${
(error as Error).message
}`
);
}
}
this.logger.log('Finished seeding permissions.');
return permissionIds;
}
async deletePermissions(): Promise<void> {
// Delete all permissions
try {
const permissions = await this.permissionService.findAll({
name: { $in: Object.keys(AllPermissionsMetadata) },
});
this.logger.log(`Deleting ${permissions.length} permissions`);
for (const permission of permissions) {
try {
this.logger.log(`Deleting permission: ${permission.name}`);
await this.permissionService.delete(permission._id.toString());
this.logger.log(`Deleted permission: ${permission.name}`);
} catch (error) {
this.logger.error(
`Error deleting permission: ${permission.name}`,
(error as Error).message
);
}
}
} catch (error) {
this.logger.error(
`Error deleting permissions: ${(error as Error).message}`
);
}
}
}
Performance Considerations
Document Size Management
MongoDB works best with reasonably sized documents:
- Keep documents under 16MB (MongoDB's document size limit)
- For BookingSessions with potentially many events:
- Store events in separate BookingSessionEvent documents
- Maintain an array of references in the session document
- Consider limiting the reference array to recent or significant events
// Function to manage booking_session_event_ids array size
function manageEventReferences(session) {
// If array is getting too large, implement a sliding window approach
if (session.booking_session_event_ids.length > 500) {
// Keep the first event for context
const firstEvent = session.booking_session_event_ids[0];
// Keep only the most recent 100 events plus the first event
session.booking_session_event_ids = [
firstEvent,
...session.booking_session_event_ids.slice(-100)
];
}
}
Indexing Strategy
Strategic indexing improves query performance:
// Example indexing strategy for BookingSession
db.booking_sessions.createIndex({ organization_id: 1, 'booking_outcome.status': 1, created_at: -1 });
db.booking_sessions.createIndex({ 'booking_outcome.booking_id': 1 }, { sparse: true });
db.booking_sessions.createIndex({ 'booking_outcome.status': 1, 'booking_outcome.drop_off_step_id': 1 });
// BookingSessionEvent indexing
db.booking_session_events.createIndex({ booking_session_id: 1, timestamp: 1 });
db.booking_session_events.createIndex({ organization_id: 1, event_type: 1, timestamp: 1 });
Denormalization for Read Performance
In specific high-read scenarios, we use controlled denormalization:
- Store frequently accessed related fields directly in documents
- Maintain a single source of truth for writable operations
- Use event-based systems to keep denormalized data in sync
Cross-Domain Relationships
The Scheduler domain interacts with other domains in several key ways:
Scheduler and Core Domain
- BrandKit Integration: Scheduler configs reference BrandKit entities for consistent visual styling
- Organization Context: All scheduler entities maintain organization_id for multi-tenant isolation
- File Access: Media uploads in additional details steps use the File entity from the core domain
Scheduler and Integrations Domain
- Vendor Connectivity: BookingService entities link to vendor services via integration data
- Credential Access: Service synchronization uses VendorCredential entities for authentication
- Bidirectional Sync: Updates from vendor systems propagate to booking services
Glossary of Terms
| Term | Definition |
|---|---|
| Answer Category | A grouping mechanism for organizing related answers or services |
| Booking Flow | A configurable sequence of steps that guides customers through the scheduling process |
| Booking Session | A record of a customer's interaction with a booking flow using status-based discrimination |
| Booking Session Event | An immutable record of a specific user interaction within a booking session |
| ContentConfig | A reusable pattern for display information including title, description, and icon |
| Discriminated Union | A TypeScript pattern used to create type-safe variants of an entity based on a type field |
| Event Sourcing | A pattern where all changes to application state are stored as a sequence of immutable events |
| Express Link | A direct link to a booking flow with prefilled state (e.g., preselected service) |
| Flow Router | A rule-based system for directing users to specific booking flows based on URL parameters |
| Graph | A structure of nodes (questions, answers, services) connected by edges in the service selection step |
| Service Selection | A step type that uses a graph-based approach to determine the appropriate service |
| Scheduler Config | An entity that connects a booking flow with visual styling and initialization settings |
| Step Type | A classification of booking flow steps (zipcode, service_selection, timeslot_selection, etc.) |
| Vendor Data | Integration information that connects Wrkbelt services to external systems like ServiceTitan |
Future Directions
The Wrkbelt data model continues to evolve to support new capabilities:
Planned Enhancements
- Enhanced Analytics: Building on our event-sourcing architecture for deeper user behavior insights
- Expanded Integration Options: Support for additional vendor systems beyond ServiceTitan
- Dynamic Form Configuration: More flexible data collection capabilities in booking flows
- Customer Relationship Management: Extended customer data model for repeat bookings and history
- Session Reconstitution: Ability to replay events to recreate user experiences for debugging and optimization
Architectural Evolution
As the platform scales, we anticipate:
- Service-Oriented Architecture: Moving toward more granular microservices
- Event-Driven Communication: Enhanced inter-domain communication via events
- Real-Time Capabilities: Expanded WebSocket support for live updates
- Enhanced Caching: Multi-level caching strategy for improved performance
- Analytics Projections: Materialized views derived from event streams for performance-optimized analytics
Conclusion
The Wrkbelt data model represents a thoughtful balance between flexibility and structure. By organizing entities into well-defined domains with clear responsibilities, we maintain a system that can evolve while preserving data integrity and performance.
The recent enhancements to the Scheduler domain's session tracking exemplify our approach to data modeling:
- Starting with user-centered design requirements
- Implementing specialized patterns (like event sourcing) where appropriate
- Maintaining consistent approaches to common problems
- Ensuring strong typing and validation through discriminated unions
- Supporting clear separation of concerns between operational and historical data
- Enabling rich analytical capabilities through comprehensive event tracking
The BookingSession entity now follows a more structured architecture with:
- A clear current state container for operational data
- A booking outcome container with status-based discrimination
- Enhanced event reference management
- Improved step progression and metrics tracking
These changes provide both improved developer experience through type safety and better analytical capabilities through structured, queryable data patterns.
This documentation serves as a living reference that will continue to evolve alongside the platform's capabilities.