Session Store
All session state is persisted in PostgreSQL under the session schema. Every session opened via the SSH Proxy or the Console is written to the session.sessions table, updated throughout its lifetime, and closed when the connection ends or when the janitor reaps it.
Session record
| Column | Description |
|---|---|
session_id | Unique identifier for the session. Generated by the caller (SSH Proxy or API Server) and used as the primary key. |
username | The authenticated user who opened the session. |
workspace | The workspace the session is attached to. |
blueprint | The blueprint used to provision the workspace at the time the session was created. |
client | The SSH client identifier string reported during the SSH handshake. |
client_ip | Source IP address of the client connection. |
k8shelld_ver | Version of the k8shelld daemon inside the workspace at session creation time. |
channels | List of SSH channels opened during the session (e.g. shell, exec, port-forward). |
bytes_in / bytes_out | Running totals of ingress and egress bytes, updated on each upsert. |
start_time | When the session was opened. Defaults to NOW() if not supplied by the caller. |
end_time | Set when the session closes. NULL indicates the session is still active. |
updated_at | Timestamp of the last upsert. Used by the janitor to detect stale sessions. |
Session lifecycle
Sessions move through three operations:
Create / update (UpsertSession) — the SSH Proxy or API Server calls upsert at session open and periodically throughout the session to report updated byte counters and channel lists. The operation uses INSERT ... ON CONFLICT DO UPDATE, so the same call handles both creation and incremental updates. start_time is set only on insert; end_time is only advanced if the caller supplies a value.
End (EndSession) — called at disconnect. Sets end_time = NOW() and returns the final record. Once ended a session is immutable.
Query (GetSessions) — returns sessions filtered by any combination of username, workspace, or session_id, with configurable pagination and sort direction. Used by the API Server to serve the session list in the Console and support resume workflows.
Janitor
When a session is opened normally, the caller explicitly ends it on disconnect. However, if the SSH Proxy or API Server crashes, a session can be left with end_time = NULL and no further upserts — an open session with a stale updated_at.
The janitor is a background goroutine that periodically sweeps for these stale sessions and closes them automatically. It runs on a configurable interval and ends any session whose updated_at has not advanced within the configured TTL:
UPDATE session.sessions
SET end_time = updated_at,
updated_at = NOW()
WHERE end_time IS NULL
AND updated_at < NOW() - <ttl>::interval
ORDER BY updated_at
LIMIT <batchSize>
FOR UPDATE SKIP LOCKED
end_time is set to updated_at rather than NOW() so the recorded close time reflects the last known activity of the session, not the sweep time.
Advisory lock
When the Session Service runs as multiple replicas, every instance runs its own janitor goroutine. To prevent concurrent sweeps from racing on the same rows, the janitor acquires a PostgreSQL advisory lock before each sweep using pg_try_advisory_lock. If another instance already holds the lock, the sweep is skipped without error. The lock is released immediately after the sweep completes.
Janitor settings (ttl, interval, batchSize) are configured under the janitor block. See Configuration.