12 KiB
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 configurationAuthConfig— Authentication configuration (password/key/agent)KeyPair— SSH key pairSnippet— Command snippetProfile— Named configuration profileAppConfig— Application configurationKnownHost— Verified host key
3. V2 Models (NEW)
3.1 HostGroup
Nestable group for organizing hosts. Groups can contain other groups and hosts.
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
{
"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:
// 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.
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.
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
{
"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.
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 widthsplit-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.
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.
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)
{
"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
{
"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
{
"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):
- Read existing
hosts.json— V2 fields default to zero values - Create
groups.jsonif not exists — empty groups - Create
forwards.jsonif not exists — empty forwards - Create
workspaces.jsonif not exists — empty workspaces - Update
config.json— add V2 config fields
No data loss: V1 data is preserved. V2 fields are additive.