# Hostkeeper V2 — Extended Data Models > **Status**: V2 Planning Complete > **Last Updated**: 2026-06-29 > **V1 models**: `internal/models/models.go` — frozen, no changes. --- ## 1. Overview V2 extends V1 models with new entities for groups, vault, port forwarding, workspaces, and sessions. All V1 models remain unchanged. New models are added in `app/backend/models/` (or `internal/models/` with backward-compatible additions). --- ## 2. V1 Models (FROZEN — do not modify) These exist in `internal/models/models.go` and are used as-is: - `Host` — SSH host connection configuration - `AuthConfig` — Authentication configuration (password/key/agent) - `KeyPair` — SSH key pair - `Snippet` — Command snippet - `Profile` — Named configuration profile - `AppConfig` — Application configuration - `KnownHost` — Verified host key --- ## 3. V2 Models (NEW) ### 3.1 HostGroup Nestable group for organizing hosts. Groups can contain other groups and hosts. ```go type HostGroup struct { ID string `json:"id" bson:"_id,omitempty"` Name string `json:"name"` ParentID *string `json:"parent_id,omitempty"` // nil = root group Color string `json:"color,omitempty"` // hex color e.g. "#e78a4e" Icon string `json:"icon,omitempty"` // icon name e.g. "server", "globe" Order int `json:"order"` // sort order within parent Children []*HostGroup `json:"children,omitempty"` // populated at runtime, not stored HostCount int `json:"host_count,omitempty"` // populated at runtime CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } ``` **Storage**: `groups.json` ```json { "groups": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Production", "parent_id": null, "color": "#e78a4e", "icon": "server", "order": 0, "created_at": "2024-06-22T10:00:00Z", "updated_at": "2024-06-22T10:00:00Z" } ] } ``` **Rules**: - Maximum nesting depth: 5 levels - Root groups have `parent_id: null` - Deleting a group moves its hosts to parent (or ungrouped) - Group names must be unique within the same parent --- ### 3.2 Extended Host New fields added to the existing `Host` struct: ```go // V2 additions to Host struct type HostV2 struct { // ... all V1 fields ... GroupID *string `json:"group_id,omitempty"` // reference to HostGroup.ID IsFavorite bool `json:"is_favorite"` // quick access Color string `json:"color,omitempty"` // override group color Order int `json:"order"` // sort order within group StartupSnippets []string `json:"startup_snippets,omitempty"` // snippet IDs to run on connect Proxy *ProxyConfig `json:"proxy,omitempty"` // proxy configuration JumpHost *string `json:"jump_host_id,omitempty"` // jump host ID Keepalive int `json:"keepalive,omitempty"` // keepalive interval in seconds } ``` **Note**: V1 Host fields are untouched. V2 fields are added with `omitempty` for backward compatibility. --- ### 3.3 ProxyConfig Proxy configuration for SSH connections through proxy servers. ```go type ProxyConfig struct { Type string `json:"type"` // "socks5", "http", "command" Host string `json:"host"` // proxy host Port int `json:"port"` // proxy port Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` Command string `json:"command,omitempty"` // for "command" type (netcat-style) } ``` --- ### 3.4 PortForward SSH port forwarding configuration. ```go type PortForward struct { ID string `json:"id" bson:"_id,omitempty"` Name string `json:"name"` HostID string `json:"host_id"` // reference to Host.ID Type string `json:"type"` // "local", "remote", "dynamic" LocalAddr string `json:"local_addr"` // e.g. "127.0.0.1:8080" RemoteAddr string `json:"remote_addr"` // e.g. "localhost:3000" Status string `json:"status"` // "stopped", "running", "error" AutoStart bool `json:"auto_start"` // start tunnel on app launch CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } ``` **Storage**: `forwards.json` ```json { "forwards": [ { "id": "forward-uuid", "name": "Local Redis", "host_id": "host-uuid", "type": "local", "local_addr": "127.0.0.1:6379", "remote_addr": "localhost:6379", "status": "stopped", "auto_start": false, "created_at": "2024-06-22T10:00:00Z", "updated_at": "2024-06-22T10:00:00Z" } ] } ``` **Forward Types**: - **Local**: `ssh -L localPort:remoteHost:remotePort` — forward local port to remote - **Remote**: `ssh -R remotePort:localHost:localPort` — forward remote port to local - **Dynamic**: `ssh -D localPort` — SOCKS5 proxy through SSH --- ### 3.5 Workspace Group multiple terminal sessions in a layout. ```go type Workspace struct { ID string `json:"id" bson:"_id,omitempty"` Name string `json:"name"` Layout string `json:"layout"` // "single", "split-h", "split-v", "grid-2x2" Tabs []Tab `json:"tabs"` // terminal tabs in this workspace IsActive bool `json:"is_active"` // currently active workspace CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type Tab struct { ID string `json:"id"` HostID string `json:"host_id"` Type string `json:"type"` // "terminal", "sftp" Title string `json:"title"` // display title (host name) Cols int `json:"cols"` Rows int `json:"rows"` } ``` **Storage**: `workspaces.json` **Layout Types**: - `single` — one terminal, full width - `split-h` — two terminals side by side (horizontal split) - `split-v` — two terminals stacked (vertical split) - `grid-2x2` — four terminals in a 2x2 grid --- ### 3.6 Session (Runtime only, not persisted) Active terminal session tracking. ```go type Session struct { ID string `json:"id"` HostID string `json:"host_id"` StartedAt time.Time `json:"started_at"` LastActive time.Time `json:"last_active"` Status string `json:"status"` // "connecting", "connected", "disconnected", "error" Cols int `json:"cols"` Rows int `json:"rows"` } ``` **Note**: Sessions are in-memory only. Not stored to disk. --- ### 3.7 VaultStatus (Runtime only) Vault lock/unlock state tracking. ```go type VaultStatus struct { Locked bool `json:"locked"` EncryptionEnabled bool `json:"encryption_enabled"` AutoLockEnabled bool `json:"auto_lock_enabled"` AutoLockMinutes int `json:"auto_lock_minutes"` LastActivity time.Time `json:"last_activity"` UnlockAttempts int `json:"unlock_attempts"` // failed attempts LockedUntil *time.Time `json:"locked_until,omitempty"` // temporary lock after too many attempts } ``` **Auto-lock rules**: - Default: lock after 5 minutes of inactivity - After 5 failed unlock attempts: temporary lock for 30 seconds - Lock on system sleep/suspend - Lock on window minimize (configurable) --- ## 4. Storage Schema ### File Structure ``` ~/.hostkeeper/ ├── hosts.json # V1 + V2 host data ├── keys.json # V1 + V2 key pairs ├── snippets.json # V1 + V2 snippets ├── groups.json # V2: host groups ├── forwards.json # V2: port forwards ├── workspaces.json # V2: workspaces ├── config.json # V1 + V2 app config ├── known_hosts.json # V1 + V2 known hosts ├── profiles.json # V2: profiles (if separated from config) └── sessions.json # V2: session history (optional) ``` ### JSON Schema Examples #### hosts.json (extended) ```json { "hosts": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Production Server", "hostname": "192.168.1.100", "port": 22, "username": "admin", "auth": { "type": "key", "key_id": "key-uuid" }, "group_id": "group-uuid", "tags": ["production", "linux"], "is_favorite": true, "color": "#e78a4e", "order": 0, "notes": "Main production server", "startup_snippets": ["snippet-uuid-1"], "proxy": { "type": "socks5", "host": "10.0.0.1", "port": 1080 }, "jump_host_id": "jump-host-uuid", "keepalive": 60, "created_at": "2024-06-22T10:00:00Z", "updated_at": "2024-06-22T10:00:00Z", "last_used_at": "2024-06-22T15:30:00Z" } ] } ``` #### groups.json ```json { "groups": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Production", "parent_id": null, "color": "#e78a4e", "icon": "server", "order": 0, "created_at": "2024-06-22T10:00:00Z", "updated_at": "2024-06-22T10:00:00Z" }, { "id": "660e8400-e29b-41d4-a716-446655440001", "name": "Web Servers", "parent_id": "550e8400-e29b-41d4-a716-446655440000", "color": "#a9b665", "icon": "globe", "order": 0, "created_at": "2024-06-22T10:00:00Z", "updated_at": "2024-06-22T10:00:00Z" } ] } ``` #### forwards.json ```json { "forwards": [ { "id": "forward-uuid", "name": "Local Redis", "host_id": "host-uuid", "type": "local", "local_addr": "127.0.0.1:6379", "remote_addr": "localhost:6379", "status": "stopped", "auto_start": false, "created_at": "2024-06-22T10:00:00Z", "updated_at": "2024-06-22T10:00:00Z" } ] } ``` --- ## 5. Relationships ``` HostGroup (1) ──── (*) Host │ │ │ parent_id │ group_id │ (self-ref) │ key_id │ │ startup_snippets (*) │ │ jump_host_id │ │ HostGroup (1) ──── (*) HostGroup (children) │ └── max depth: 5 Host (1) ──── (*) PortForward │ └── host_id Workspace (1) ──── (*) Tab │ └── tabs[].host_id Host (1) ──── (*) Snippet (via startup_snippets) │ └── startup_snippets[] = snippet IDs ``` --- ## 6. Migration from V1 V2 data is backward-compatible with V1. New fields are optional (`omitempty`). **Migration steps** (automatic on first V2 launch): 1. Read existing `hosts.json` — V2 fields default to zero values 2. Create `groups.json` if not exists — empty groups 3. Create `forwards.json` if not exists — empty forwards 4. Create `workspaces.json` if not exists — empty workspaces 5. Update `config.json` — add V2 config fields **No data loss**: V1 data is preserved. V2 fields are additive.