Skip to main content

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

  1. Core Domain

    • users: Platform user accounts and authentication
    • memberships: Organization membership and roles
    • organizations: Multi-tenant organization management
    • roles & permissions: RBAC system
    • files: File storage and access control
    • emails: Email communication
    • temporary_links: Secure temporary access
    • brand_kit: Organization branding assets
  2. Scheduler Domain

    • scheduler_config: Configuration linking booking flows, branding, and initialization settings
    • booking_flow: Configurable booking experiences with step-based flows
    • booking_flow_step: Individual steps in the booking process with type-specific configurations
    • booking_services: Bookable services with vendor integration
    • booking_questions: Questions with associated answers for service selection and data collection
    • booking_question_answers: Responses to questions that can reference services or provide context
    • booking_answer_categories: Grouping mechanism for answers and services
    • booking_session: User session tracking with status-based type discrimination
    • booking_session_event: Event-based tracking of all user interactions within a session
    • booking_flow_routers: URL-based flow routing rules
    • booking_flow_express_links: Direct booking links with prefilled states
    • booking_flow_diagrams: Visual representations of flows and service selection graphs
  3. Accounting Domain

    • subscriptions: Organization subscriptions
    • subscription_change_logs: Subscription history
    • payments: Payment processing
    • product_records: Product configuration
    • stripe_webhook_logs: Payment webhooks
  4. Marketing Domain

    • waitlist_requests: Lead generation tracking
  5. Integrations Domain

    • vendor_credentials: External service configuration
  6. 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:

  • Core Domain

    • User and identity management
    • Organization and membership system
    • Role-based access control
    • File management
    • Communication services
    • Brand management
  • Scheduler Domain

    • Booking flow configuration
    • Service selection graph
    • Integration with service providers
    • Session tracking with event sourcing
    • Comprehensive analytics and journey tracking
  • Marketing Domain

    • 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:

  1. Efficient path traversal
  2. Easy addition or removal of connections
  3. Clear separation between entity data and relationship structure
  4. 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:

  1. Complete traceability of all user interactions
  2. Ability to reconstruct session state at any point in time
  3. Rich analytics capabilities for journey analysis
  4. 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

  1. Additive Changes: Add new fields without modifying existing documents
  2. Transformational Changes: Run migrations that transform document structure
  3. 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

TermDefinition
Answer CategoryA grouping mechanism for organizing related answers or services
Booking FlowA configurable sequence of steps that guides customers through the scheduling process
Booking SessionA record of a customer's interaction with a booking flow using status-based discrimination
Booking Session EventAn immutable record of a specific user interaction within a booking session
ContentConfigA reusable pattern for display information including title, description, and icon
Discriminated UnionA TypeScript pattern used to create type-safe variants of an entity based on a type field
Event SourcingA pattern where all changes to application state are stored as a sequence of immutable events
Express LinkA direct link to a booking flow with prefilled state (e.g., preselected service)
Flow RouterA rule-based system for directing users to specific booking flows based on URL parameters
GraphA structure of nodes (questions, answers, services) connected by edges in the service selection step
Service SelectionA step type that uses a graph-based approach to determine the appropriate service
Scheduler ConfigAn entity that connects a booking flow with visual styling and initialization settings
Step TypeA classification of booking flow steps (zipcode, service_selection, timeslot_selection, etc.)
Vendor DataIntegration 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

  1. Enhanced Analytics: Building on our event-sourcing architecture for deeper user behavior insights
  2. Expanded Integration Options: Support for additional vendor systems beyond ServiceTitan
  3. Dynamic Form Configuration: More flexible data collection capabilities in booking flows
  4. Customer Relationship Management: Extended customer data model for repeat bookings and history
  5. Session Reconstitution: Ability to replay events to recreate user experiences for debugging and optimization

Architectural Evolution

As the platform scales, we anticipate:

  1. Service-Oriented Architecture: Moving toward more granular microservices
  2. Event-Driven Communication: Enhanced inter-domain communication via events
  3. Real-Time Capabilities: Expanded WebSocket support for live updates
  4. Enhanced Caching: Multi-level caching strategy for improved performance
  5. 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:

  1. Starting with user-centered design requirements
  2. Implementing specialized patterns (like event sourcing) where appropriate
  3. Maintaining consistent approaches to common problems
  4. Ensuring strong typing and validation through discriminated unions
  5. Supporting clear separation of concerns between operational and historical data
  6. 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.