Tabby Connection Profiles System Architecture: A Provider-Based Deep Dive
Tabby implements its connection management through a provider pattern where abstract ProfileProvider classes define protocol-specific logic, ProfilesService orchestrates multi-layered configuration merging, and ConfigService persists profiles as plain JavaScript objects conforming to the Profile interface.
The open-source terminal emulator Eugeny/tabby handles SSH, Telnet, Serial, and saved split-layout connections through a modular, extensible architecture built on Angular's dependency injection system. This connection profiles system architecture decouples connection logic from storage and UI concerns, enabling plugin authors to add new protocol support without modifying core code.
Core Architectural Components
The system centers on three interconnected services that separate definition, orchestration, and persistence.
ProfileProvider Abstract Base
Defined in tabby-core/src/api/profileProvider.ts, the ProfileProvider<P extends Profile> abstract class establishes the contract for all connection types. Each provider implementation knows how to list built-in templates, convert a profile into tab-opening parameters, and generate human-readable descriptions. Concrete providers for specific protocols extend either QuickConnectProfileProvider—which adds quick-connect string parsing for formats like user@host:port—or ConnectableProfileProvider—which adds clearServiceMessagesOnConnect behavior for interactive protocols like Serial.
ProfilesService Orchestration
The ProfilesService in tabby-core/src/services/profiles.service.ts acts as the central hub. It aggregates all registered providers via Angular DI constructor injection (line 38), merges default configurations across multiple layers, resolves profile groups, and drives the profile selector UI. This service also generates unique hotkey names for profiles and handles lifecycle operations like creation and deletion.
ConfigService Persistence
While ProfilesService manages runtime logic, ConfigService handles durability. It persists the global configuration object—including the profiles array, profileDefaults, profileGroups, and hotkey mappings—to disk. All profile writes flow through ProfilesService methods that ultimately call config.save().
The Profile Data Model
Every connection in Tabby exists as a profile—a plain JavaScript object implementing the Profile interface.
Interface Structure
The Profile interface in tabby-core/src/api/profileProvider.ts defines the following schema:
export interface Profile {
id: string; // e.g., "ssh:custom:my-host:123e4567-e89b-12d3-a456-426614174000"
type: string; // provider identifier, e.g., "ssh"
name: string; // display name in the UI
group: string; // optional group identifier for organization
options: any; // provider-specific connection parameters
icon?: string;
color?: string;
disableDynamicTitle?: boolean;
behaviorOnSessionEnd?: 'auto'|'keep'|'reconnect'|'close';
weight?: number;
isBuiltin?: boolean; // true for templates provided by the extension
isTemplate?: boolean;
}
The id field follows a namespaced format combining the provider type, a "custom" or "template" marker, a slugified name, and a UUID to ensure uniqueness across imported configurations.
Provider Contract and Concrete Implementations
Each protocol extension implements the abstract contract to integrate with Tabby's UI and connection lifecycle.
Required Provider Methods
Concrete classes must implement:
idandname– Static identifiers used in the selector and configuration.configDefaults– Defaultoptionsvalues merged into every profile of this type.getBuiltinProfiles()– Returns an array ofPartialProfileobjects serving as templates.getNewTabParameters(profile)– Transforms a profile into aNewTabParametersobject containing the component class and inputs required to instantiate the tab.getDescription(profile)– Returns a short descriptive string displayed in the connection selector.
Protocol-Specific Providers
Tabby ships with several built-in provider implementations registered via providedIn: 'root':
- SSH Provider (
tabby-ssh/src/profiles.ts): ExtendsQuickConnectProfileProvider<SSHProfile>, implements parsing foruser@host:portquick-connect strings, and integrates withPasswordStorageServicefor credential management. - Telnet Provider (
tabby-telnet/src/profiles.ts): ExtendsQuickConnectProfileProvider<TelnetProfile>, handling simple host/port parsing and raw socket connections. - Serial Provider (
tabby-serial/src/profiles.ts): ExtendsConnectableProfileProvider<SerialProfile>, dynamically enumerates available serial ports (web or native), and prompts for baud-rate when unspecified. - Split Layout Provider (
tabby-core/src/profiles.ts): ExtendsProfileProvider<SplitLayoutProfile>, saves and restores window layouts via recovery tokens rather than network connections.
Configuration Merging Pipeline
When ProfilesService retrieves a profile, it constructs a layered configuration object through getConfigProxyForProfile() (lines 68-71 in tabby-core/src/services/profiles.service.ts). The system overlays defaults in this priority order:
- Global profile defaults (
profileDefaultsin config) applying to all profiles. - Provider-specific defaults from the provider's
configDefaultsproperty. - Global provider defaults (
profileDefaults[provider.id]) stored in user configuration. - Group defaults defined for the profile's specific group membership.
The merged result is wrapped in a ConfigProxy that provides property access to the combined settings.
Profile Selector and Quick Connect
The showProfileSelector() method (starting at line 94 of tabby-core/src/services/profiles.service.ts) dynamically builds the "New Connection" UI. It constructs SelectorOption objects for every available profile, including recent connections and quick-connect entries. Providers extending QuickConnectProfileProvider contribute additional selector items that parse free-text queries (like root@192.168.1.1:2222) into temporary profiles without requiring manual template creation.
Persistence and Lifecycle Management
User-created profiles live in ConfigService.store.profiles as an array of PartialProfile objects. The ProfilesService exposes explicit lifecycle methods:
newProfile(profile): Validates and appends a new profile to the store.writeProfile(profile): Updates an existing profile by ID.deleteProfile(profile): Removes the profile from the array and optionally cleans associated hotkey bindings.
All mutations trigger config.save() to persist changes to disk immediately.
Extending the System: Implementation Examples
Listing User-Defined Profiles
import { ProfilesService } from 'tabby-core';
async function listProfiles(profilesService: ProfilesService) {
const profiles = await profilesService.getProfiles({ includeBuiltin: false });
console.log('User profiles:', profiles);
}
Launching a Profile Programmatically
async function launch(
profile: PartialProfile<Profile>,
profilesService: ProfilesService
) {
await profilesService.launchProfile(profile);
}
Creating a Custom SSH Profile
import { ProfilesService } from 'tabby-core';
import { v4 as uuidv4 } from 'uuid';
import slugify from 'slugify';
async function createCustomSSH(profilesService: ProfilesService) {
const profile = {
id: `ssh:custom:${slugify('my-host')}:${uuidv4()}`,
type: 'ssh',
name: 'My Host',
options: {
host: 'my-host.example.com',
port: 22,
user: 'alice',
auth: null,
},
isBuiltin: false,
isTemplate: false,
};
await profilesService.newProfile(profile);
await profilesService.config.save();
}
Implementing a New Provider
import { Injectable } from '@angular/core';
import { QuickConnectProfileProvider, NewTabParameters, PartialProfile } from 'tabby-core';
import { MQTTTabComponent } from './components/mqttTab.component';
import { MQTTProfileSettingsComponent } from './components/mqttProfileSettings.component';
@Injectable({ providedIn: 'root' })
export class MQTTProfileProvider extends QuickConnectProfileProvider<MQTTProfile> {
id = 'mqtt';
name = 'MQTT';
settingsComponent = MQTTProfileSettingsComponent;
configDefaults = {
options: { host: '', port: 1883, clientId: '' },
clearServiceMessagesOnConnect: false,
};
async getBuiltinProfiles(): Promise<PartialProfile<MQTTProfile>[]> {
return [{
id: `mqtt:template`,
type: this.id,
name: 'MQTT Broker',
icon: 'fas fa-broadcast-tower',
isBuiltin: true,
isTemplate: true,
}];
}
async getNewTabParameters(profile: MQTTProfile): Promise<NewTabParameters<MQTTTabComponent>> {
return { type: MQTTTabComponent, inputs: { profile } };
}
getDescription(profile: MQTTProfile): string {
return `${profile.options.host}:${profile.options.port}`;
}
quickConnect(query: string): PartialProfile<MQTTProfile> {
const [host, port] = query.split(':');
return {
name: query,
type: this.id,
options: { host, port: Number(port) || 1883 },
};
}
}
After registration, Angular's DI automatically injects the provider into ProfilesService, making the new protocol available in the connection selector without further configuration.
Summary
- Provider Pattern Architecture: Tabby uses abstract
ProfileProviderclasses to decouple protocol logic from core code, with specific implementations for SSH, Telnet, Serial, and split layouts. - Layered Configuration: The system merges four levels of defaults—global, provider-specific, provider-global, and group—through
ProfilesService.getConfigProxyForProfile(). - Persistence Model: Profiles are plain JavaScript objects stored in
ConfigService.store.profiles, manipulated throughProfilesServicelifecycle methods. - Extensibility: New connection types require only a provider class extending
ProfileProviderorQuickConnectProfileProvider, registered via Angular DI. - Quick Connect Integration: Providers implementing
QuickConnectProfileProviderenable instant connections from free-text input without pre-defined templates.
Frequently Asked Questions
How does Tabby handle secure storage of connection passwords?
Passwords are not stored within the profile object itself. The SSH provider (tabby-ssh/src/profiles.ts) delegates credential storage to PasswordStorageService, which encrypts and persists sensitive data separately from the configuration file, while the profile retains only non-sensitive connection parameters like host and port.
What distinguishes QuickConnectProfileProvider from ConnectableProfileProvider?
QuickConnectProfileProvider adds methods for parsing connection strings (like user@host:port) into temporary profiles for immediate use, ideal for SSH and Telnet. ConnectableProfileProvider extends the base class with clearServiceMessagesOnConnect behavior, designed for interactive terminal protocols like Serial that require service message clearing upon connection establishment.
Can I organize connections into custom groups?
Yes. The Profile interface includes a group string field. The ProfilesService resolves group-specific defaults from ConfigService.store.profileGroups during the configuration merge process, allowing you to apply shared settings (like common SSH keys or Serial baud rates) to entire collections of profiles.
How do I open a connection programmatically without the UI selector?
Inject ProfilesService into your component or service and call await profilesService.launchProfile(profile), passing either a complete Profile object or a PartialProfile with at minimum the type and options fields populated. The service handles provider lookup, default merging, and tab instantiation automatically.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →