Data Flow
Device Registration
sequenceDiagram
autonumber
participant Operator
participant Client as nixstasis client
participant Identity as identity store
participant Phoenix
participant Devices as Devices context
participant Domain as Ash domain
Operator->>Client: nixstasis register
Client->>Client: Detect MAC/IP and product metadata
Client->>Phoenix: POST /api/v1/devices/register
Phoenix->>Devices: register_device(params)
Devices->>Domain: register_device(params)
Domain-->>Devices: device record and approval state
Devices-->>Phoenix: registration result
Phoenix-->>Client: 201 data.id and optional api_token
Client->>Identity: Save UUID
- Operator or service invokes
nixstasis register. - Client detects primary MAC and IP through
internal/identity. - Client generates a device name from the MAC address.
- Client sends
POST /api/v1/devices/registerwithmac_address, optionalproduct_name, and optionalmetadata. - Phoenix
DeviceController.register/2callsNixstasis.Devices.register_device/1. Devices.register_device/1validates any supplied schema definition and callsNixstasis.Domain.register_device/1.- Server responds
201withdata.idand includesdata.api_tokenwhen the device is approved. - Client stores UUID through
identity.Store.SaveUUIDatconfig.IdentityPath()and uses the issued token for runtime API calls.
Traceable references:
packages/client/cmd/nixstasis/register.go:28-93packages/client/internal/transport/client.go:84-121packages/server/lib/nixstasis_web/controllers/device_controller.ex:31-37packages/server/lib/nixstasis/devices.ex:51-83
Polling and Telemetry
sequenceDiagram
autonumber
participant Client as nixstasis poll
participant Scripts as Starlark scripts
participant FRP as FRP manager
participant Phoenix
participant Monitoring
participant Commands as Command handler
Client->>Client: Load UUID and runtime config
loop Every poll interval
Client->>Scripts: Discover and execute latest scripts
Scripts-->>Client: telemetry reports/errors
Client->>FRP: Read current connection status
FRP-->>Client: connection_status
Client->>Phoenix: POST heartbeat with telemetry/status
Phoenix->>Monitoring: heartbeat(device, payload)
Monitoring-->>Phoenix: remote_access_token and commands
Phoenix-->>Client: Poll response
Client->>Commands: Hydrate payloads and execute commands
Commands-->>Phoenix: POST command_results
Client->>FRP: Start/stop/restart from remote_access_token
end
- Client invokes
nixstasis poll. - Client loads stored UUID from
/etc/nixstasis/id. - Client creates:
- HTTP transport client.
- Starlark script executor.
- FRP manager.
- server-command handler.
- Client runs
pollOnceimmediately and then on the configured ticker interval. pollOncere-detects MAC/IP identity details.- Client discovers scripts from configured script directory.
- Client executes latest script versions and collects script reports/errors.
- Client reads current FRP status.
- Client sends
POST /api/v1/devices/:uuid/heartbeat?api_key=...with telemetry and connection status. - Phoenix
HeartbeatController.create/2loads device and requiresapproval_status == :approved. Nixstasis.Monitoring.heartbeat/2updateslast_seen_at, persists telemetry, evaluates rules, and pops pending commands.- Server returns optional
remote_access_tokenand optional command list. - Client hydrates deferred command payloads, executes commands, and posts command results.
- Client starts, stops, or restarts FRPC according to
remote_access_tokenand current FRP status.
Traceable references:
packages/client/cmd/nixstasis/poll.go:35-157packages/client/internal/script/executor.go:23-93packages/client/internal/transport/client.go:170-212packages/server/lib/nixstasis_web/controllers/heartbeat_controller.ex:7-27packages/server/lib/nixstasis/monitoring.ex:15-28
Command Delivery and Results
- Server-side code queues a command with
Nixstasis.Devices.queue_command/2. - On heartbeat,
Nixstasis.Devices.pop_pending_commands/1claims queued commands transactionally. - Heartbeat response serializes commands to the client.
- Client receives commands in
PollResponse.Commands. - Client fetches any deferred payload with
GET /api/v1/devices/:uuid/command_payloads/:ref. - Client command handler executes supported commands:
list_scripts,install_script,remove_script, andssh_authorize. - Client posts results to
POST /api/v1/devices/:uuid/command_results?api_key=.... - Phoenix
DeviceCommandController.command_results/2callsDevices.acknowledge_command_results/2.
Observable error paths:
- Missing or duplicate command IDs produce failed command results client-side.
- Unsupported command types produce failed command results client-side.
- Missing command result list returns HTTP
400from server. - Invalid command results return HTTP
422from server.
Traceable references:
packages/server/lib/nixstasis/devices.ex:265-348packages/client/cmd/nixstasis/poll.go:198-249packages/client/internal/commands/handler.go:27-230packages/server/lib/nixstasis_web/controllers/device_command_controller.ex:6-39
Remote Access Through FRP
sequenceDiagram
autonumber
participant Browser
participant LiveView as DeviceLive.Show
participant Phoenix
participant Client as nixstasis client
participant FRPC as frpc
participant FRPS as frps
participant Caddy
Browser->>LiveView: Open /devices/:id
LiveView->>Phoenix: Mark remote_access_requested
Client->>Phoenix: Heartbeat
Phoenix-->>Client: remote_access_token
Client->>FRPC: Start transient systemd unit
FRPC->>FRPS: Connect with token
Browser->>Caddy: Request wildcard device host
Caddy->>FRPS: Proxy HTTP vhost traffic
Browser-->>Caddy: Close device view
LiveView->>Phoenix: Clear remote_access_requested
Client->>Phoenix: Next heartbeat
Phoenix-->>Client: No remote_access_token
Client->>FRPC: Stop transient unit
- Browser opens
/devices/:id. DeviceLive.Show.handle_params/3loads device.- If device is online,
setup_device_view/3setsremote_access_requestedto true when not already requested. - Next client heartbeat receives a non-empty
remote_access_token. - Client starts FRPC through the FRP manager when FRP is inactive.
- FRPC connects to FRPS with rendered configuration and the heartbeat-provided token.
- Caddy wildcard host routes
*.{$BASE_DOMAIN}to FRPS HTTP vhost port. - When LiveView terminates,
DeviceLive.Show.terminate/2setsremote_access_requestedto false. - Next client heartbeat omits
remote_access_token, so the client can stop FRPC.
Traceable references:
packages/server/lib/nixstasis_web/live/device_live/show.ex:13-31packages/server/lib/nixstasis_web/live/device_live/show.ex:93-145packages/client/cmd/nixstasis/poll.go:138-154packages/client/internal/frp/manager.go:47-169deploy/compose/caddy/Caddyfile:68-75
Browser Terminal Session
sequenceDiagram
autonumber
participant Browser
participant LiveView as DeviceLive.Show
participant Devices
participant Client as nixstasis client
participant Socket as TerminalChannel
participant SSH as SshClient
participant FRP as FRP TCP mux
Browser->>LiveView: start_ssh_session
LiveView->>Devices: Generate SSH key pair
LiveView->>Devices: Queue ssh_authorize command
Client->>Devices: Heartbeat claims command
Client->>Client: Install authorized key
Browser->>Socket: Join terminal:<device_id>
Socket->>Devices: Resolve terminal session ref
Socket->>SSH: Start SSH Port with ncat proxy
SSH->>FRP: Connect through FRP TCP mux
Browser->>Socket: Terminal input
Socket->>SSH: send_data(input)
SSH-->>Socket: output events
Socket-->>Browser: terminal output
- Browser triggers
start_ssh_sessioninDeviceLive.Show. - Server generates an SSH key pair with
SshKeyManager.generate_key_pair/0. - Server queues an
ssh_authorizecommand for the device containing the public key. - Server stores private key material behind an opaque terminal session ref and signs a Phoenix socket token containing the device ID.
- Browser connects to
UserSocketwith socket token. - Browser joins topic
terminal:<device_id>with the terminal session ref. TerminalChannel.join/3resolves the session ref, verifies device binding, and startsNixstasis.Devices.SshClient.SshClientwrites private key to a temp file and opens ansshPort usingncatas HTTP proxy to the FRP TCP mux endpoint.- Browser input is sent to
SshClient.send_data/2. - SSH process output is pushed back as channel
outputevents. - Session stops on SSH exit, idle timeout, or max duration.
Traceable references:
packages/server/lib/nixstasis_web/live/device_live/show.ex:57-80packages/server/lib/nixstasis_web/channels/user_socket.ex:37-64packages/server/lib/nixstasis_web/channels/terminal_channel.ex:20-113packages/server/lib/nixstasis/devices/ssh_client.ex:17-94
LiveView Event Cycle
- Browser requests a LiveView route through Phoenix
:browserpipeline. - LiveView
mount/3initializes socket assigns. - LiveView
handle_params/3loads route-specific data when present. - Browser events invoke
handle_event/3callbacks. - Callback updates assigns, streams, flash, or navigation state.
- LiveView diffs update the browser over LiveView transport.
Observable event sets:
- Device list: search, filter, sort, selection, bulk approve/reject.
- Device detail: change tab, retry session, start SSH session.
- Alerts: validate/save rules, modal discard confirmation, rule deletion, sorting/filtering.
- Reports: sort, filter, delete confirmation, report detail filters.
- Settings: save monitoring and notification settings.
Traceable references:
packages/server/lib/nixstasis_web/router.ex:30-45packages/server/lib/nixstasis_web/live/device_live/index.expackages/server/lib/nixstasis_web/live/device_live/show.expackages/server/lib/nixstasis_web/live/alerts/index_live.expackages/server/lib/nixstasis_web/live/reports/index_live.expackages/server/lib/nixstasis_web/live/settings_live.ex
E2E Run Lifecycle
stateDiagram-v2
[*] --> Requested: POST /e2e/runs
Requested --> Reused: idempotency hit
Requested --> Rejected: policy/protocol/registration error
Requested --> Locked: environment lock acquired
Locked --> Seeded: seed script succeeds
Locked --> SeedFailed: seed script fails
Seeded --> Running: run and journey rows persisted
Running --> ResultsSubmitted: POST results
ResultsSubmitted --> Completed: final aggregate status
ResultsSubmitted --> Failed: failed aggregate status
Running --> Cancelled: POST cancel
Completed --> Retained: logs available
Failed --> Retained: logs available
Cancelled --> Retained: logs available
Retained --> Pruned: retention worker
Reused --> [*]
Rejected --> [*]
SeedFailed --> [*]
Pruned --> [*]
- Client E2E runner loads config and journey specs.
- Client sends
POST /e2e/runswithX-E2E-Protocol-Version. - Server validates legacy fields, environment policy, protocol version, suite/journey selection, and action/expect registrations.
- Server enforces idempotency for
(environment_label, idempotency_key). - Server acquires an environment lock for new runs.
- Server runs the configured seed script.
- Server persists run and queued journey result rows.
- Client executes journeys and writes JSONL logs.
- Client submits results to
POST /e2e/runs/:id/results. - Server updates journey result rows, computes aggregate status, and releases environment lock on final status.
- Logs are fetched through
GET /e2e/runs/:id/results/:journey_id/log. - Retention worker periodically prunes old runs/logs according to retention policy.
Observable error paths:
409 environment_lockedfor overlapping active environment runs.422 protocol_mismatchfor invalid protocol version.400 invalid_action_expectationfor unregistered action/expect pairs.422 seed_failedfor seed failures.410 log_unavailablesemantics for missing/pruned logs, as documented in README.
Traceable references:
README.md:96-135packages/server/lib/nixstasis/e2e.ex:61-100packages/server/lib/nixstasis/e2e.ex:201-227packages/server/lib/nixstasis/e2e.ex:320-407packages/server/lib/nixstasis_web/controllers/e2e_run_controller.ex:19-62packages/server/lib/nixstasis/e2e/retention_worker.ex:25-40