# Tabby Configuration Storage and Migration: A Deep Dive into Schema Evolution

> Explore Tabby's configuration storage and migration. Learn how ConfigService manages YAML files and automatically migrates schemas to the latest version 8 with stepwise transformations.

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

---

**Tabby stores all user settings in a single YAML-based configuration file managed by `ConfigService`, which automatically migrates older schemas to the current version 8 through a series of stepwise transformations at startup.**

The open-source terminal emulator Tabby (Eugeny/tabby) centralizes its configuration in a structured YAML file that evolves through explicit schema versions. Understanding how Tabby configuration storage and migration works is essential for developers extending the application or troubleshooting profile issues across updates.

## Configuration File Structure and Schema

### Top-Level Configuration Keys

The configuration object follows a strict schema defined in [`tabby-core/src/config.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/config.ts). At runtime, `ConfigService` merges these with platform-specific defaults from `tabby-core/src/configDefaults.*.yaml` (Linux, macOS, Windows, Web).

- **`version`**: A number tracking the schema version (currently 8)
- **`profiles`**: Array of connection profiles with `id`, `type` (`local`, `ssh`, `serial`), `name`, optional `group` (ID), `icon`, `color`, and an `options` object containing profile-specific settings
- **`groups`**: Array of group objects (`{ id: string, name: string }`) used to organize profiles
- **`vault`**: Object storing encrypted secrets (e.g., stored passwords)
- **`encrypted`**: Boolean flag indicating if the vault requires decryption on load
- **`configSync`**: Object containing remote synchronization settings (`host`, `token`, etc.)
- **`profileDefaults`**: Default values applied to new profiles (e.g., `ssh.clearServiceMessagesOnConnect`)
- **`pluginBlacklist`**, **`providerBlacklist`**: Arrays of disabled plugins/providers consulted by `ConfigService.enabledServices`
- **Legacy keys** (`terminal`, `ssh`, `serial`): Hold pre-migration data including old connection lists and flags that are transformed into modern profile objects

### Platform-Specific Defaults

Tabby merges runtime defaults using `ConfigService.mergeDefaults()`, loading platform-specific values from [`tabby-core/src/configDefaults.linux.yaml`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/configDefaults.linux.yaml), [`configDefaults.macos.yaml`](https://github.com/Eugeny/tabby/blob/main/configDefaults.macos.yaml), [`configDefaults.windows.yaml`](https://github.com/Eugeny/tabby/blob/main/configDefaults.windows.yaml), or [`configDefaults.web.yaml`](https://github.com/Eugeny/tabby/blob/main/configDefaults.web.yaml). These files provide fallback values for any user-undefined settings.

## Migration Logic and Version Management

When Tabby starts, `ConfigService.migrate()` in [`tabby-core/src/services/config.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/services/config.service.ts) performs sequential upgrades. Each migration block checks `config.version` and transforms the data in-place until reaching version 8.

The stepwise migration pipeline includes:

1. **Version < 1**: Migrates `ssh.privateKey` (string) to `ssh.privateKeys` (array)
2. **Version < 2**: Introduces top-level `profiles`, moves legacy terminal settings, renames `sessionOptions` to `options`, and assigns new UUID-based IDs
3. **Version < 3**: Converts `ssh.recentConnections` and serial connections into proper profile entries, clears legacy `localStorage` cache
4. **Version < 4**: Ensures every profile has a valid `id` (generating custom UUIDs where missing)
5. **Version < 5**: Converts textual profile groups into structured group objects and migrates UI state from `localStorage.profileGroupCollapsed`
6. **Version < 6**: Moves `ssh.clearServiceMessagesOnConnect` into `profileDefaults`
7. **Version < 7**: Removes the default sync host (`https://api.tabby.sh`) and any associated tokens
8. **Version < 8**: Normalizes compression algorithms to array format `['none']`

## ConfigService Architecture and Data Flow

The `ConfigService` orchestrates configuration persistence through a five-stage pipeline defined in [`tabby-core/src/services/config.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/services/config.service.ts):

1. **Load**: `platform.loadConfig()` reads the YAML file. If missing, it creates a minimal object `{ version: LATEST_VERSION }`
2. **Decrypt**: `maybeDecryptConfig()` handles vault decryption if `encrypted: true`, requesting the passphrase from the Vault service
3. **Migrate**: `migrate(this._store)` upgrades legacy schemas to version 8 through the stepwise transformations
4. **Proxy**: A `ConfigProxy` wraps the store, exposing defaults as transparent properties while tracking user-overridden values separately
5. **Save**: `save()` serializes the store back to YAML (encrypting if enabled) via `platform.saveConfig`

## Programmatic Configuration Access

Developers interact with the configuration system through Angular dependency injection:

```typescript
// In an Angular component or service (TypeScript)
import { Injectable } from '@angular/core';
import { ConfigService } from 'tabby-core';

@Injectable({ providedIn: 'root' })
export class ProfileManager {
  constructor(private config: ConfigService) {}

  // Read a setting with fallback to default
  get recoverTabs(): boolean {
    return this.config.store.recoverTabs ?? false;
  }

  // Add a new SSH profile programmatically
  async addSshProfile() {
    const newProfile = {
      id: `ssh:${crypto.randomUUID()}`,
      type: 'ssh',
      name: 'Production Server',
      options: {
        host: 'prod.example.com',
        port: 22,
        username: 'admin'
      }
    };
    this.config.store.profiles.push(newProfile);
    await this.config.save(); // Persists to YAML (encrypted if enabled)
  }
}

```

The `ConfigProxy` ensures that reads fall back to platform defaults while writes mark values as user-overridden. Plugins can also contribute defaults by implementing the `ConfigProvider` interface from [`tabby-core/src/api/configProvider.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/api/configProvider.ts).

## Summary

- Tabby uses a **single YAML file** for all configuration, managed by `ConfigService` in [`tabby-core/src/services/config.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/services/config.service.ts)
- A **`version`** field tracks schema compatibility, with automatic **migrations** upgrading data from older versions to the current version 8 at startup
- The system handles **encryption** transparently through the Vault service when `encrypted: true`, decrypting via `maybeDecryptConfig()` and re-encrypting on `save()`
- **Platform-specific defaults** from `configDefaults.*.yaml` files merge at runtime via `mergeDefaults()`, with `ConfigProxy` providing transparent fallback access
- Legacy connection lists from `ssh`, `serial`, and `terminal` keys automatically convert to modern **profile objects** during migration
- Changes are persisted by calling `config.save()`, which serializes the store (optionally encrypted) back to disk

## Frequently Asked Questions

### Where does Tabby store its configuration file?

Tabby stores its configuration in a YAML file located in the platform-specific user data directory. The exact path is determined by the platform's `loadConfig()` implementation—typically `%APPDATA%/Tabby/config.yaml` on Windows, `~/Library/Application Support/tabby/config.yaml` on macOS, and `~/.config/tabby/config.yaml` on Linux.

### How does Tabby handle configuration changes when updating to a new version?

Tabby automatically migrates configurations through `ConfigService.migrate()` during application startup. This function checks the `version` field and applies sequential transformations—such as converting legacy SSH connection lists to modern profile objects—until the configuration reaches the current schema version 8. This process is transparent, preserves user data, and modifies the store in-place before the `ConfigProxy` wraps it.

### Can I encrypt sensitive data in Tabby's configuration?

Yes. Tabby supports encrypting the `vault` field containing sensitive data like stored passwords. When encryption is enabled, the `encrypted` flag is set to `true`, and the `maybeDecryptConfig` method prompts for the Vault passphrase during the loading phase. The configuration is automatically re-encrypted when saved via `config.save()`.

### How do I programmatically access or modify Tabby's configuration from a plugin?

Plugins can inject `ConfigService` from `tabby-core` to read and write configuration values. The service exposes a `store` property wrapped in a `ConfigProxy`, allowing direct property access that falls back to platform defaults defined in `tabby-core/src/configDefaults.*.yaml`. Always call `await config.save()` after modifications to persist changes to the YAML file.