# Tabby Connection Profiles System Architecture: A Provider-Based Deep Dive

> Explore the Tabby connection profiles system architecture. Understand the provider pattern, ProfilesService orchestration, and ConfigService persistence for seamless connection management. Learn how Tabby simplifies profile con...

- Repository: [Eugene/tabby](https://github.com/Eugeny/tabby)
- Tags: architecture
- Published: 2026-03-03

---

**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](https://github.com/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`](https://github.com/Eugeny/tabby/blob/main/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`](https://github.com/Eugeny/tabby/blob/main/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`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/api/profileProvider.ts) defines the following schema:

```typescript
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`](https://github.com/Eugeny/tabby/blob/main/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`](https://github.com/Eugeny/tabby/blob/main/tabby-telnet/src/profiles.ts)): Extends `QuickConnectProfileProvider<TelnetProfile>`, handling simple host/port parsing and raw socket connections.
- **Serial Provider** ([`tabby-serial/src/profiles.ts`](https://github.com/Eugeny/tabby/blob/main/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`](https://github.com/Eugeny/tabby/blob/main/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`](https://github.com/Eugeny/tabby/blob/main/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`](https://github.com/Eugeny/tabby/blob/main/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

```typescript
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

```typescript
async function launch(
    profile: PartialProfile<Profile>, 
    profilesService: ProfilesService
) {
    await profilesService.launchProfile(profile);
}

```

### Creating a Custom SSH Profile

```typescript
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

```typescript
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`](https://github.com/Eugeny/tabby/blob/main/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.