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:

  • id and name – Static identifiers used in the selector and configuration.
  • configDefaults – Default options values merged into every profile of this type.
  • getBuiltinProfiles() – Returns an array of PartialProfile objects serving as templates.
  • getNewTabParameters(profile) – Transforms a profile into a NewTabParameters object 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): Extends QuickConnectProfileProvider<SSHProfile>, implements parsing for user@host:port quick-connect strings, and integrates with PasswordStorageService for credential management.
  • Telnet Provider (tabby-telnet/src/profiles.ts): Extends QuickConnectProfileProvider<TelnetProfile>, handling simple host/port parsing and raw socket connections.
  • Serial Provider (tabby-serial/src/profiles.ts): Extends ConnectableProfileProvider<SerialProfile>, dynamically enumerates available serial ports (web or native), and prompts for baud-rate when unspecified.
  • Split Layout Provider (tabby-core/src/profiles.ts): Extends ProfileProvider<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:

  1. Global profile defaults (profileDefaults in config) applying to all profiles.
  2. Provider-specific defaults from the provider's configDefaults property.
  3. Global provider defaults (profileDefaults[provider.id]) stored in user configuration.
  4. 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 ProfileProvider classes 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 through ProfilesService lifecycle methods.
  • Extensibility: New connection types require only a provider class extending ProfileProvider or QuickConnectProfileProvider, registered via Angular DI.
  • Quick Connect Integration: Providers implementing QuickConnectProfileProvider enable 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →