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

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. 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, configDefaults.macos.yaml, configDefaults.windows.yaml, or 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 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:

  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:

// 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.

Summary

  • Tabby uses a single YAML file for all configuration, managed by ConfigService in 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.

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 →