Architectural design and technical specifications of the Portalt platform
Portalt's architecture implements a layered, service-oriented design that integrates virtual reality experiences with AI capabilities. The system architecture consists of four primary layers that handle distinct responsibilities while maintaining clear interfaces for inter-layer communication:
This architecture enforces separation of concerns while facilitating efficient inter-layer communication through well-defined interfaces and protocols. Each layer operates independently while providing essential services to adjacent layers through standardized APIs.
The system architecture diagram shows how different components interact across layers
The Compute Layer serves as the core processing engine of the system, handling real-time communication, AI services, and data processing. It's built on a modular architecture using several design patterns to ensure scalability, maintainability, and extensibility.
Central multiplayer server for Unity communication implemented with NodeJS.
Modular service architecture for AI capabilities, built on the ServiceController base class.
Advanced speech processing service with standalone integration.
Quantized local LLM processing (3.2B parameters) with REST API interface.
Purpose: Illustrates the process of creating and configuring a new scene in the system.
Flow Description:
Purpose: Illustrates the process of generating and validating pairing codes for organization access.
Flow Description:
Purpose: Demonstrates the flow of voice interaction and AI response generation in the system.
Components and Flow:
The Ubiq-Genie framework implements several design patterns to create a flexible and maintainable system. Design patterns are proven solutions to common software design problems, helping us write more organized and scalable code. Let's explore each pattern and understand how it contributes to our system:
What is it? The Observer pattern is like a subscription system - components can subscribe to events and get notified when those events occur.
How we use it: In our system, this pattern enables real-time communication between different parts of the application. For example, when a user speaks, multiple components need to know about it:
Benefits:
The Observer pattern showing how events flow through the system
What is it? Think of this pattern like a cooking recipe with some steps that must be followed exactly and others that can be customized. It defines a skeleton of operations while allowing some steps to be overridden.
How we use it: Our ApplicationController provides a standard way to initialize and run services while allowing each service to define its own specific behavior:
The Template Method pattern showing standard service lifecycle
What is it? The Factory pattern is like a specialized workshop that knows how to create different types of components. Instead of creating objects directly, we ask the factory to create them for us.
How we use it: When the ConversationalAgent needs different services (speech, text, etc.), it uses factories to create them:
The Factory pattern showing how components are created
What is it? The Strategy pattern is like having interchangeable tools for the same job. It allows us to switch between different algorithms or approaches at runtime.
How we use it: Our AI services use this pattern to support different implementation strategies:
The Strategy pattern showing interchangeable service implementations
What is it? Think of the Mediator pattern like an air traffic controller - instead of planes communicating directly with each other, they all communicate through the controller.
How we use it: Our NetworkScene acts as a mediator for all network communication:
The Mediator pattern showing centralized communication control
These patterns complement each other to create a robust and flexible system:
How different design patterns interact in our system
Purpose: Abstracts data persistence operations
Implementation: Collection class in db.ts
class Collection<T extends { _id?: string }> {
private tableName: string;
constructor(tableName: string) {
this.tableName = tableName;
this.ensureTableExists();
this.ensureIndexes();
}
// CRUD Operations
findOne(query: Partial<T>): T | null
find(query: Partial<T> = {}): T[]
insertOne(doc: T): { insertedId: string }
updateOne(filter: Partial<T>, update: UpdateOptions): UpdateResult
deleteOne(filter: Partial<T>): { deletedCount: number }
}
Purpose: Ensures single database connection instance
Implementation: SQLite database connection in sqlite.ts
// Development mode singleton
if (process.env.NODE_ENV === "development") {
let globalWithSqlite = global as typeof globalThis & {
_sqliteDatabase?: BetterSqlite3.Database;
};
if (!globalWithSqlite._sqliteDatabase) {
globalWithSqlite._sqliteDatabase = new BetterSqlite3(DB_PATH);
initDatabase(globalWithSqlite._sqliteDatabase);
}
db = globalWithSqlite._sqliteDatabase;
}
Purpose: Creates database client instances
Implementation: getDbClient function in db.ts
export function getDbClient() {
return {
db: function(dbName: string) {
return {
collection: function<T extends { _id?: string }>(collectionName: string) {
return new Collection<T>(collectionName);
}
};
}
};
}
Purpose: Handles different types of database operations
Implementation: Relations class for handling relationships
export class Relations {
static async linkDocumentToActivity(documentId: string, activityId: string): Promise<void>
static async unlinkDocumentFromActivity(documentId: string, activityId: string): Promise<void>
static getDocumentsByActivityId(activityId: string): string[]
static getActivitiesByDocumentId(documentId: string): string[]
}
Purpose: Adapts MongoDB-like operations to SQLite
Implementation: Collection class methods mimicking MongoDB operations
// MongoDB-like operations
updateOne(
filter: Partial<T>,
update: {
$set?: Partial<T>,
$addToSet?: Record<string, any>,
$pull?: Record<string, any>
}
): { matchedCount: number, modifiedCount: number }
Our database schema is designed to support the complex relationships between different entities in the system while maintaining flexibility for future extensions. Let's explore how the data is organized:
Entity Relationship diagram showing how different data entities are connected
The database implements a hybrid approach combining SQLite's ACID compliance with document-based storage flexibility, enabling both structured relationships and dynamic data storage.