@startuml
!theme plain
title Turnstone — Storage Architecture

skinparam class {
    BackgroundColor<<protocol>> #E8EAF6
    BackgroundColor<<sqlite>> #C8E6C9
    BackgroundColor<<postgres>> #B3E5FC
    BackgroundColor<<facade>> #FFF9C4
    BackgroundColor<<migration>> #FFE0B2
    BackgroundColor<<schema>> #F3E5F5
}

' -- Protocol --
interface "StorageBackend" as SB <<protocol>> {
    +save_message(ws_id, role, content, ...)
    +load_messages(ws_id) → list[dict]
    +register_workstream(ws_id, node_id, name, state)
    +update_workstream_state(ws_id, state)
    +update_workstream_name(ws_id, name)
    +set_workstream_alias(ws_id, alias) → bool
    +update_workstream_title(ws_id, title)
    +resolve_workstream(alias_or_id) → str | None
    +delete_workstream(ws_id) → bool
    +prune_workstreams(retention_days) → (int, int)
    +list_workstreams(node_id, limit, *, parent_ws_id, kind, user_id) → list
    +save_workstream_config(ws_id, config)
    +load_workstream_config(ws_id) → dict
    +kv_get(key) → str | None
    +kv_set(key, value) → str | None
    +kv_delete(key) → bool
    +kv_list() → list[(str, str)]
    +kv_search(query) → list[(str, str)]
    +search_history(query, limit) → list
    +search_history_recent(limit) → list
    +create_user(user_id, username, display_name, pw_hash)
    +get_user(user_id) / get_user_by_username(username)
    +list_users() / delete_user(user_id)
    +create_api_token(...) / get_api_token_by_hash(hash)
    +list_api_tokens(user_id) / delete_api_token(id)
    +close()
}

' -- Backends --
class "SQLiteBackend" as SQLite <<sqlite>> {
    -_engine: sa.Engine
    -_fts5_available: bool
    +__init__(path: str)
    --
    FTS5 full-text search
    Default pool, check_same_thread=False
}

class "PostgreSQLBackend" as PG <<postgres>> {
    -_engine: sa.Engine
    +__init__(url: str, pool_size: int = 2,\n    max_overflow: int = 3)
    --
    tsvector + ILIKE search
    Connection pooling (5 max per process)
}

' -- Schema --
class "_schema.py" as Schema <<schema>> {
    +metadata: MetaData
    +memories: Table
    +conversations: Table
    +workstreams: Table (node_id, alias, title,\n    state, skill_id)
    +workstream_config: Table
    +users: Table (username, password_hash)
    +api_tokens: Table (token_hash, scopes)
    +channel_users: Table (channel_type)
    +scheduled_tasks: Table (..., skill)
    --
    SQLAlchemy Core
    Single source of truth
}

' -- Migration --
class "_migrate.py" as Migrate <<migration>> {
    +run_migrations(storage, backend)
    -_bootstrap_existing_sqlite()
    --
    Programmatic Alembic
    Auto-bootstrap existing DBs
}

class "migrations/" as Versions <<migration>> {
    001_initial_schema.py
    002_user_identity.py
}

' -- Registry --
class "_registry.py" as Registry {
    -_storage: StorageBackend | None
    +init_storage(backend, path, url) → StorageBackend
    +get_storage() → StorageBackend
    +reset_storage()
    --
    Auto-initializes SQLite
    if not configured
}

' -- Facade --
class "memory.py" as Facade <<facade>> {
    +save_message()
    +load_messages()
    +register_workstream()
    +update_workstream_state()
    +save_workstream_config()
    +save_memory() / delete_memory()
    +search_memories()
    +... (all delegated functions)
    --
    Thin delegation to
    get_storage()
    Silent failure behavior
}

' -- Consumers --
class "session.py\nChatSession" as Session {
}

class "server.py\nWeb UI" as Server {
}

class "cli.py\nTerminal" as CLI {
}

' -- Relationships --
SQLite ..|> SB
PG ..|> SB

SQLite --> Schema : uses
PG --> Schema : uses

Registry --> SB : creates
Registry --> Migrate : calls

Migrate --> Versions : applies
Migrate --> Schema : references

Facade --> Registry : get_storage()

Session --> Facade : imports
Server --> Facade : imports
CLI --> Facade : imports

' -- Config --
note right of Registry
  [database]
  backend = "sqlite" | "postgresql"
  url = "postgresql+psycopg://..."
  path = ".turnstone.db"
  pool_size = 2 (+ 3 overflow)
end note

note bottom of SQLite
  Default backend.
  Zero-config for
  single-node / dev.
end note

note bottom of PG
  Production backend.
  Multi-node / Docker default.
  Use PgBouncer (transaction mode)
  for clusters > 50 nodes.
end note

@enduml
