Introduction
Atomix native remote management
Nixstasis
State synchronization
Project
- Nixstasis is an IoT monitoring and remote access platform.
- The Go client registers managed devices, collects telemetry through Stary/Starlark scripts, polls the server, and manages FRP client tunnels.
- The Elixir/Phoenix server provides the control plane, LiveView UI, device API, E2E API, reporting, alerting, and TLS approval endpoint.
- The supported server deployment path is Docker Compose under
deploy/compose.
Traceable references:
README.md:3-17packages/client/README.md:1-12packages/server/README.md:1-17deploy/compose/README.md:1-20
Primary Use Cases
- Device registration and persistent device identity.
- Device telemetry polling and server-side heartbeat processing.
- Approval and monitoring of devices through a Phoenix LiveView UI.
- Alert-rule configuration and alert review.
- Custom report creation and report result browsing.
- On-demand remote access through FRP tunnels fronted by Caddy.
- Browser-based SSH terminal sessions through Phoenix Channels and FRP TCP muxing.
- Client/server E2E validation runs using
/e2eendpoints and client-side journey execution.
Entry Points
Phoenix HTTP Endpoints
- Browser LiveView routes in
packages/server/lib/nixstasis_web/router.ex://devices/devices/new/devices/:id/alerts/alerts/new/alerts/:id/edit/alerts/rules/reports/reports/new/reports/:id/edit/reports/:id/settings
- JSON device/API routes in
packages/server/lib/nixstasis_web/router.ex:GET /api/v1/builder-schemasGET /api/v1/builder-schemas/:schema_id/versions/:schema_version/optionsPOST /api/v1/builder-configurations/validateGET /api/v1/devicesPOST /api/v1/devices/registerPOST /api/v1/devices/:device_id/heartbeatPOST /api/v1/devices/:device_id/command_resultsGET /api/v1/devices/:device_id/command_payloads/:refGET /api/v1/check_domain
- Ash JSON:API routes are forwarded under
/api/jsonthroughNixstasisWeb.AshJsonApiRouter. - E2E routes are under
/e2eand useNixstasisWeb.Plugs.E2EEnabled.
LiveView Entry Points
NixstasisWeb.DashboardLive.IndexNixstasisWeb.DeviceLive.IndexNixstasisWeb.DeviceLive.ShowNixstasisWeb.AlertLive.IndexNixstasisWeb.AlertLive.RulesNixstasisWeb.ReportLive.IndexNixstasisWeb.ReportLive.ShowNixstasisWeb.SettingsLive
Traceable reference:
packages/server/lib/nixstasis_web/router.ex:30-45
Go Client Commands
nixstasis register: detects device identity and registers with the server.nixstasis poll: starts the telemetry polling loop.nixstasis script install <path>: installs a Stary script.nixstasis script list: lists installed scripts.nixstasis script remove: removes installed scripts.nixstasis script test: tests scripts.nixstasis script repl: starts the script REPL.
Traceable references:
packages/client/cmd/nixstasis/main.gopackages/client/cmd/nixstasis/register.gopackages/client/cmd/nixstasis/poll.gopackages/client/cmd/nixstasis/script.gopackages/client/cmd/nixstasis/install_script.gopackages/client/cmd/nixstasis/list_scripts.gopackages/client/cmd/nixstasis/remove_script.gopackages/client/cmd/nixstasis/test_script.gopackages/client/cmd/nixstasis/repl.go
Architecture Overview
High-Level Architecture
flowchart TB
Browser[Browser/User] -->|HTTPS/WSS| Caddy[Caddy + AuthCrunch]
Caddy -->|reverse_proxy| Phoenix[Phoenix Server]
Caddy -->|wildcard proxy| FRPS[FRPS]
Client[nixstasis Go client] -->|HTTP JSON| Phoenix
Client -->|starts/stops| FRPC[frpc]
FRPC -->|FRP tunnel| FRPS
Phoenix -->|queries| DB[(PostgreSQL)]
Components
- Server application:
- Elixir OTP application
:nixstasis. - Phoenix HTTP endpoint and LiveView UI.
- Ash domain and Ash resources for devices, commands, alerts, telemetry, reports, and settings.
- Ecto/PostgreSQL persistence through
Nixstasis.Repo.
- Elixir OTP application
- Client application:
- Go CLI binary
nixstasisusing Cobra. - Device registration, telemetry polling, script execution, command handling, and FRP lifecycle management.
- Embedded Starlark runtime for telemetry scripts.
- Go CLI binary
- Edge layer:
- Caddy handles public HTTPS ingress, on-demand TLS, AuthCrunch authentication/authorization, and reverse proxying.
- FRPS accepts FRPC tunnels and exposes device services through Caddy-routed wildcard hosts and TCP muxing.
- Deployment layer:
deploy/compose/docker-compose.ymldefinesnixstasis,caddy,frps, and optionalpostgresservices.
Repository Structure
Nixstasis is organized around deployable runtime boundaries rather than one monolithic application tree.
packages/server: Phoenix, LiveView, Ash, Ecto/PostgreSQL, OTP workers, device APIs, E2E APIs, and the browser UI.packages/client: Go CLI/client runtime for registration, polling, local identity, Stary/Starlark scripts, command execution, FRPC lifecycle, and E2E journeys.deploy/compose: supported server deployment path and runtime contract for Phoenix, Caddy, FRPS, and PostgreSQL.packages/caddy: Caddy build with the AuthCrunch plugin used by the public edge.packages/frp: FRP image/package build assets and pinned FRP acquisition.packages/shared/e2e_log_viewer: shared static E2E report/log viewer assets.docs/src/features: docs-driven feature designs and task history.
See Project Structure for path-by-path details.
Component Relationships
- Caddy routes
nixstasis.<base-domain>to Phoenix atnixstasis:4000. - Caddy routes
frp-admin.<base-domain>to the FRPS dashboard port. - Caddy routes
*.{$BASE_DOMAIN}to FRPS HTTP vhost port. - Caddy on-demand TLS calls
http://nixstasis:4000/api/v1/check_domain. - The Go client calls Phoenix JSON endpoints under
/api/v1/devices/.... - The Go client starts
frpcwhen the server heartbeat response includes a non-emptyremote_access_token. - Phoenix queues device commands as pending commands and returns them in heartbeat responses.
- The Go client executes supported command types and posts command results back to Phoenix.
- Browser terminal sessions connect through Phoenix Channels on
terminal:*and server-sideNixstasis.Devices.SshClientopens an SSH process through FRP TCP muxing.
Product Data Model
The server treats managed devices as long-lived identities with dynamic telemetry payloads.
- Registration is keyed by device identity such as MAC address and product context; re-registration updates the existing device rather than creating a duplicate identity.
- Unknown or unapproved devices enter the pending-approval workflow and do not receive runtime API credentials until approved.
- Approved devices receive a persistent runtime API token used by heartbeat, command-result, and deferred command-payload endpoints.
- Device telemetry is stored as dynamic JSON payloads so Stary/Starlark scripts and product schemas can evolve without one table per product type.
- Alert rules and report builders use schema-aware fields where practical, while runtime telemetry remains flexible enough for product-specific payloads.
Device lifecycle details live in Data Flow, API payloads live in Client-Server Interface, and package internals live in Server Devices and Client Identity.
API And Authentication Surfaces
Nixstasis intentionally has multiple API surfaces with different consumers.
- Browser routes and LiveView sockets are reached through Caddy/AuthCrunch in the supported deployment. Caddy is the public authentication edge.
- Device runtime APIs live under
/api/v1/devices/...and are used by the Go client. Registration issues credentials; heartbeat, command results, and payload fetches use the approved device API token. - Caddy on-demand TLS approval calls
GET /api/v1/check_domainfrom inside the Compose network. - Ash JSON:API routes live under
/api/jsonand have generated OpenAPI inpackages/server/priv/static/openapi.yaml. - E2E harness APIs live under
/e2eand are gated byNixstasisWeb.Plugs.E2EEnabled.
The current docs distinguish these contracts in API & Runtime Contracts. Future consolidation of practical bespoke APIs into Ash-backed generated OpenAPI is tracked as planned work, not as current architecture.
AuthCrunch claim and role mapping is deployment-sensitive and remains an area to document more explicitly as the authorization model hardens.
Traceable references:
deploy/compose/caddy/Caddyfile:42-75deploy/compose/docker-compose.yml:1-90packages/client/internal/transport/client.go:84-212packages/client/cmd/nixstasis/poll.go:128-154packages/server/lib/nixstasis_web/router.ex:50-79packages/server/lib/nixstasis_web/live/device_live/show.ex:57-80packages/server/lib/nixstasis_web/channels/terminal_channel.ex:20-50packages/server/lib/nixstasis/devices/ssh_client.ex:26-63
System Boundaries
- HTTP boundary:
NixstasisWeb.EndpointandNixstasisWeb.Routerexpose browser, API, JSON:API, channel, and E2E surfaces.
- Domain boundary:
Nixstasis.Domaindefines Ash resources and domain APIs.- Context modules (
Nixstasis.Devices,Nixstasis.Monitoring,Nixstasis.Reporting,Nixstasis.E2E) call Ash domain APIs and Ecto where needed.
- Client boundary:
internal/transport.Clientis the Go client HTTP boundary to Phoenix.internal/script.Runtimeis the Starlark execution boundary.internal/frp.Manageris the process boundary forfrpc.
- Edge boundary:
- Caddy is the public ingress point in the supported Compose deployment.
- FRPS is externally exposed for FRPC tunnel transport ports and internally exposed to Caddy for dashboard and HTTP vhost traffic.
Key Design Patterns
OTP
- The server starts supervised children from
Nixstasis.Application. - Supervised processes include
NixstasisWeb.Telemetry,Nixstasis.Repo, optionalNixstasis.E2E.RetentionWorker,DNSCluster,Phoenix.PubSub,Nixstasis.Monitoring.OfflineChecker, andNixstasisWeb.Endpoint. - GenServers present in application code:
Nixstasis.Monitoring.OfflineCheckerNixstasis.E2E.RetentionWorkerNixstasis.Devices.SshClient
Traceable references:
packages/server/lib/nixstasis/application.ex:9-29packages/server/lib/nixstasis/monitoring/offline_checker.ex:1-32packages/server/lib/nixstasis/e2e/retention_worker.ex:1-51packages/server/lib/nixstasis/devices/ssh_client.ex:1-125
LiveView Interaction Model
- Browser routes use the
:browserpipeline with session fetch, LiveView flash, CSRF protection, and secure browser headers. - LiveViews implement
mount/3,handle_params/3, andhandle_event/3for UI state and user interactions. - Observable event examples:
- Device list search/filter/sort/bulk approval in
DeviceLive.Index. - Device detail tab change, retry session, and SSH session start in
DeviceLive.Show. - Alert rule validation/save/delete in alert LiveViews.
- Report sorting/filtering/deletion in report LiveViews.
- Device list search/filter/sort/bulk approval in
Traceable references:
packages/server/lib/nixstasis_web/router.ex:4-11packages/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.ex
Ash Domain Layering
Nixstasis.DomainusesAsh.DomainwithAshJsonApi.DomainandAshPhoenixextensions.- JSON:API routes are declared for devices, pending commands, alerts, alert rules, telemetry events, custom reports, and system settings.
- Domain-level functions are defined for common resource actions such as
list_devices,get_device,register_device,create_pending_command,list_alerts, andlist_custom_reports. - Context modules call
Nixstasis.Domainfunctions and Ash queries to implement application behavior.
Traceable references:
packages/server/lib/nixstasis/domain.ex:1-122packages/server/lib/nixstasis/devices.expackages/server/lib/nixstasis/monitoring.expackages/server/lib/nixstasis/reporting.ex
Repository Structure
Root
README.md: project overview, architecture notes, E2E harness documentation, and FRP/Caddy/AuthCrunch context.AGENTS.md: repository instructions for automated agents.package.json,hk.pkl,.pre-commit-config.yaml: repository-level commit and hook tooling.mise.toml: local tool version/configuration entry point.prod.env: shared production version pins referenced by release/deployment workflows.book.toml: mdBook configuration.docs/src: mdBook documentation source tree.book/: generated mdBook output.
Server: packages/server
- Language: Elixir.
- Framework/runtime: Phoenix, Phoenix LiveView, Ash, Ecto/PostgreSQL, OTP.
- Major paths:
lib/nixstasis/application.ex: OTP application supervision tree.lib/nixstasis/domain.ex: Ash domain and JSON:API resource routing.lib/nixstasis/*.ex: context modules for devices, monitoring, reporting, dashboard, settings, alerts, E2E, deployment utilities.lib/nixstasis/*/*.ex: Ash resources, support modules, GenServers, SSH and schema utilities.lib/nixstasis_web/router.ex: Phoenix route definitions.lib/nixstasis_web/controllers: JSON controllers.lib/nixstasis_web/live: LiveView modules and LiveComponents.lib/nixstasis_web/channels: Phoenix Channels for terminal sessions.lib/nixstasis_web/components: function components and layouts.lib/nixstasis_web/live_dashboard: LiveDashboard E2E pages and hooks.priv/repo/migrations: database migrations.priv/static/openapi.yaml: generated Ash JSON:API OpenAPI output.config/*.exs: compile-time and runtime configuration.Dockerfile: server OCI image build.
Client: packages/client
- Language: Go.
- Runtime: compiled CLI/service-style binary with Starlark execution and FRPC process management.
- Major paths:
cmd/nixstasis: Cobra CLI commands.internal/config: config loading and default paths.internal/identity: local device identity detection and stored runtime credentials.internal/transport: HTTP client for Phoenix/api/v1device endpoints.internal/telemetry: telemetry payload types.internal/script: Stary/Starlark parsing, validation, execution, builtins, reports, and REPL support.internal/commands: server-issued command execution.internal/frp: FRPC lifecycle and config rendering.internal/e2e: E2E runner, journey executor, selector, API client, and runtime scripts.scripts/e2e: E2E shell entrypoints, config, journey specs, and scaffolding.scripts/mock_api: mock API used by client workflows/tests.build/root-dir: package filesystem assets, including systemd units and config templates.
Infrastructure and Edge
deploy/compose:- Supported server deployment path.
- Defines Compose services for
nixstasis,caddy,frps, and optionalpostgres. - Includes runtime contract validation and Compose rendering scripts.
packages/caddy:- Caddy Dockerfile and build script for Caddy with the AuthCrunch plugin.
packages/frp:- FRP image/package build assets and Dockerfile.
packages/shared/e2e_log_viewer:- Shared static E2E log viewer assets.
Features and Workflows
docs/src/features: docs-driven feature designs and task history..github/workflows: build, release, E2E Pages, and config-check workflows.
Separation of Concerns
- Server code is isolated under
packages/server. - Client code is isolated under
packages/client. - Infrastructure and deployment configuration is isolated under
deploy/compose,packages/caddy, andpackages/frp. - Cross-cutting feature docs and repository automation live under
docs/src/featuresand.github.
Runtime Boundaries
Process Boundaries
Elixir/OTP Processes
Nixstasis.Applicationstarts the OTP supervision tree.NixstasisWeb.Endpointowns HTTP, WebSocket, LiveView, and Channel request handling.Nixstasis.Repoowns database connections.Phoenix.PubSubis supervised asNixstasis.PubSub.Nixstasis.Monitoring.OfflineCheckeris a named GenServer that schedules:checkmessages every 60 seconds.Nixstasis.E2E.RetentionWorkeris a named GenServer that schedules E2E retention pruning.Nixstasis.Devices.SshClientis a GenServer per terminal session and wraps an OSsshprocess through an Elixir Port.
Traceable references:
packages/server/lib/nixstasis/application.ex:10-29packages/server/lib/nixstasis/monitoring/offline_checker.ex:13-31packages/server/lib/nixstasis/e2e/retention_worker.ex:14-50packages/server/lib/nixstasis/devices/ssh_client.ex:9-94
Go Runtime
- The client binary entry point is
cmd/nixstasis/main.go. - The root command is a Cobra command named
nixstasis. - Client configuration is loaded during
PersistentPreRunE, except forscript testandscript replcommand paths. runMainstarts a Go runtime flight recorder before executing the root command.pollcreates a ticker from configured poll interval and repeatedly callspollOnce.script.Executorruns discovered scripts concurrently with goroutines and async.WaitGroup.commands.Handlerexecutes batches concurrently where command type allows it.frp.Managerlaunches anixstasis-frpctransient systemd unit withsystemd-run; that unit runs the hiddennixstasis frp-sessionsubcommand, which starts the bundledfrpcprocess with a one-hour timeout.
Traceable references:
packages/client/cmd/nixstasis/main.go:20-98packages/client/cmd/nixstasis/poll.go:35-83packages/client/internal/script/executor.go:23-48packages/client/internal/commands/handler.go:27-76packages/client/internal/frp/manager.gopackages/client/cmd/nixstasis/frp_session.go
Starlark Execution Environment
script.Runtimeexecutes Starlark scripts usinggo.starlark.net/starlark.- Runtime builtins include
pub_and_get,exec_cmd, andjson. Runtime.Executecreates a Starlark thread namedstaryand executes a parsed script body.- Scripts must define a callable
main(). main()output is converted from Starlark values to Go values and must be a dictionary when non-null.- Runtime execution is bounded by
RuntimeConfig.Timeout; timeout cancels the Starlark thread.
Traceable references:
packages/client/internal/script/runtime.go:20-47packages/client/internal/script/runtime.go:73-128packages/client/internal/script/runtime.go:130-179packages/client/internal/script/builtins_exec.gopackages/client/internal/script/builtins_mqtt.go
Trust Boundaries
User Input
- Browser form and event inputs enter through Phoenix LiveViews and controllers.
- Device API inputs enter through Phoenix JSON controllers under
/api/v1. - Ash JSON:API inputs enter through
/api/jsonforwarded toNixstasisWeb.AshJsonApiRouter. - E2E API inputs enter through
/e2eroutes when E2E is enabled. - Caddy on-demand TLS sends domain approval input to
GET /api/v1/check_domain.
Traceable references:
packages/server/lib/nixstasis_web/router.ex:22-79packages/server/lib/nixstasis_web/controllers/tls_controller.ex:7-27
Script Execution
- Stary/Starlark scripts are external script content read from configured script directories or installed command payloads.
- Script installation validates front matter and JSON schema before writing installed script files.
- Script execution runs with Starlark builtins that can interact with MQTT.
- OS command execution through
exec_cmdis deny-by-default and only available when the client runtime configuration maps a requested command name to an absolute allowlisted executable path. - Script results become telemetry payload fields sent to the server.
Traceable references:
packages/client/internal/script/executor.go:50-93packages/client/internal/script/runtime.go:40-44packages/client/internal/commands/handler.go:132-187packages/client/cmd/nixstasis/poll.go:105-126
External Access Through FRP
- FRPC runs on managed devices and connects to FRPS.
- FRPS exposes tunnel transport ports from the Compose deployment.
- Caddy proxies wildcard
*.{$BASE_DOMAIN}traffic to FRPS HTTP vhost port. - Caddy proxies
frp-admin.{$BASE_DOMAIN}to the FRPS dashboard port. - Server-side SSH terminal sessions use
sshwith anncatHTTP proxy command pointed at the configured FRP host and TCP mux port. - Development laptop mode uses the same Caddy, Phoenix, FRPS, FRPC, and SSH
process boundaries with
localhostas the base domain and Caddy internal/local certificates for TLS. - The
clientcontainer is a device simulator running systemd as PID 1 with sshd, frpc, and the Go client binary; FRPC connects through FRP and the browser terminal reaches SSH through FRPS TCP mux.
Traceable references:
deploy/compose/docker-compose.yml:33-66deploy/compose/frps/frps.toml:1-15deploy/compose/caddy/Caddyfile:59-75packages/server/lib/nixstasis/devices/ssh_client.ex:30-49
Network Boundaries
Caddy to Phoenix
- Public host
nixstasis.{$BASE_DOMAIN}terminates TLS at Caddy and reverse proxies tonixstasis:4000. - Caddy on-demand TLS asks Phoenix at
http://nixstasis:4000/api/v1/check_domain. - Compose publishes only Caddy ports
80and443for the main HTTP ingress. - Default laptop mode maps the same host pattern to
.localhostnames:nixstasis.localhost,auth.localhost,frp-admin.localhost, andatom-<normalized-device-id>.localhost. - Laptop mode also publishes Phoenix on
127.0.0.1:4000for local-only validation diagnostics; deployment-shaped browser access still goes through Caddy.
Traceable references:
deploy/compose/caddy/Caddyfile:8-10deploy/compose/caddy/Caddyfile:50-57deploy/compose/docker-compose.yml:14-31
Client to Server
- The Go client uses HTTP JSON requests to the configured
api.url. - Default client API URL is
http://localhost:4000. - Packaged configuration documentation uses
https://nixstasis.example.comas the public Caddy host.
Traceable references:
packages/client/internal/config/config.go:62-64packages/client/README.md:115-128packages/client/internal/transport/client.go:27-35
Internal Services
nixstasisservice listens onPORT=4000and publishes it to the host for dev-lab and CI access; Caddy is the production HTTP(S) ingress.postgresis always included; production can overrideDATABASE_URLto use an external managed database.frpsis reached by Caddy on internal service ports and by FRPC on published FRP ports.
Traceable references:
deploy/compose/docker-compose.yml:1-129deploy/compose/README.md:1-117
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
Modules
This section documents repository modules by runtime context.
Server Modules
- Server Application
- Server Domain
- Server Web
- Server Devices
- Server Monitoring
- Server Reporting
- Server E2E
Client Modules
- Client CLI
- Client Transport
- Client Identity
- Client Starlark Runtime
- Client Command Handler
- Client FRP Manager
- Client E2E Harness
Infrastructure Modules
Deployment-facing operation docs are surfaced under Operations in the book summary; this module index stays organized by runtime context.
Server Application
Language
- Elixir.
Runtime Context
- Server.
- OTP application and supervision tree.
Purpose
- Starts and supervises the Phoenix server runtime, database repository, telemetry, PubSub, periodic workers, and endpoint.
Key Files
packages/server/mix.exspackages/server/lib/nixstasis/application.expackages/server/lib/nixstasis/repo.expackages/server/lib/nixstasis_web/endpoint.expackages/server/lib/nixstasis_web/telemetry.expackages/server/config/config.exspackages/server/config/runtime.exs
Public Interfaces
Nixstasis.Application.start/2Nixstasis.Application.config_change/3- Mix aliases:
mix setupmix ecto.setupmix phx.servermix testmix openapi.generatemix precommit
Dependencies
Internal
NixstasisWeb.TelemetryNixstasis.RepoNixstasis.E2E.RetentionWorkerNixstasis.Monitoring.OfflineCheckerNixstasisWeb.Endpoint
External
- Phoenix
- Phoenix LiveView
- Ecto/PostgreSQL
- Ash/AshPostgres/AshJsonApi/AshPhoenix
- Bandit
- DNSCluster
- Telemetry
Runtime Notes
Nixstasis.E2E.RetentionWorkeris included only when E2E retention is enabled in app config.- The supervision strategy is
:one_for_onewith supervisor nameNixstasis.Supervisor.
Traceable references:
packages/server/mix.exs:1-113packages/server/lib/nixstasis/application.ex:9-51
Server Domain
Language
- Elixir.
Runtime Context
- Server domain/data layer.
- Ash domain with JSON:API and Phoenix integration.
Purpose
- Defines domain resources, resource action functions, and Ash JSON:API routes.
Key Files
packages/server/lib/nixstasis/domain.expackages/server/lib/nixstasis/devices/device.expackages/server/lib/nixstasis/devices/pending_command.expackages/server/lib/nixstasis/monitoring/alert.expackages/server/lib/nixstasis/monitoring/alert_rule.expackages/server/lib/nixstasis/monitoring/telemetry.expackages/server/lib/nixstasis/reporting/custom_report.expackages/server/lib/nixstasis/system_setting.expackages/server/lib/nixstasis_web/ash_json_api_router.expackages/server/priv/static/openapi.yaml
Public Interfaces
- Ash domain APIs defined in
Nixstasis.Domain:list_devicesget_deviceget_device_by_maccreate_deviceregister_deviceupdate_devicedestroy_devicelist_pending_commandscreate_pending_commandupdate_pending_commanddestroy_pending_commandlist_alertscreate_alertupdate_alertdestroy_alertlist_rulesget_rulecreate_ruleupdate_ruledestroy_rulelist_telemetry_eventscreate_telemetry_eventlist_custom_reportsget_custom_reportcreate_custom_reportupdate_custom_reportdestroy_custom_reportget_setting_by_keycreate_settingupdate_setting
Dependencies
Internal
- Ash resources under
Nixstasis.Devices,Nixstasis.Monitoring,Nixstasis.Reporting, andNixstasis.SystemSetting. NixstasisWeb.AshJsonApiRouter.
External
- Ash
- AshJsonApi
- AshPhoenix
- AshPostgres
Client-Server Interaction Details
- Ash JSON:API routes are exposed under
/api/json. - Resource route groups:
/api/json/devices/api/json/pending_commands/api/json/alerts/api/json/alert_rules/api/json/telemetry_events/api/json/custom_reports/api/json/system_settings
- Swagger UI is forwarded at
/api/json/swaggerui.
Traceable references:
packages/server/lib/nixstasis/domain.ex:1-122packages/server/lib/nixstasis_web/router.ex:22-28packages/server/priv/static/openapi.yaml:3190
Server Web
Language
- Elixir.
Runtime Context
- Server HTTP/UI boundary.
- Phoenix Router, controllers, LiveView, channels, components, and LiveDashboard extensions.
Purpose
- Exposes browser UI routes, JSON API routes, Ash JSON:API forwarding, E2E API routes, LiveDashboard pages, and terminal WebSocket channels.
Key Files
packages/server/lib/nixstasis_web/router.expackages/server/lib/nixstasis_web/endpoint.expackages/server/lib/nixstasis_web/controllers/*.expackages/server/lib/nixstasis_web/live/**/*.expackages/server/lib/nixstasis_web/channels/user_socket.expackages/server/lib/nixstasis_web/channels/terminal_channel.expackages/server/lib/nixstasis_web/components/*.expackages/server/lib/nixstasis_web/live_dashboard/*.ex
Public Interfaces
Browser Routes
//devices/devices/new/devices/:id/alerts/alerts/new/alerts/:id/edit/alerts/rules/reports/reports/new/reports/:id/edit/reports/:id/settings
JSON API Routes
GET /api/v1/builder-schemasGET /api/v1/builder-schemas/:schema_id/versions/:schema_version/optionsPOST /api/v1/builder-configurations/validateGET /api/v1/devicesPOST /api/v1/devices/registerPOST /api/v1/devices/:device_id/heartbeatPOST /api/v1/devices/:device_id/command_resultsGET /api/v1/devices/:device_id/command_payloads/:refGET /api/v1/reports/:id/resultsGET /api/v1/check_domain
E2E Routes
GET /e2e/suitesGET /e2e/runsPOST /e2e/runsGET /e2e/runs/:idPOST /e2e/runs/:id/cancelGET /e2e/runs/:id/resultsPOST /e2e/runs/:id/resultsGET /e2e/runs/:id/results/:journey_id/log
Channels
- Socket channel topic pattern:
terminal:*. - Socket connect requires a Phoenix token signed for
terminal_socket. - Terminal channel join uses an opaque, expiring server-side session ref.
Dependencies
Internal
Nixstasis.DevicesNixstasis.MonitoringNixstasis.ReportingNixstasis.E2ENixstasis.DeploymentNixstasisWeb.Plugs.E2EEnabled
External
- Phoenix
- Phoenix LiveView
- Phoenix Channels
- Phoenix LiveDashboard
- OpenApiSpex
- Plug/Swoosh development routes
Client-Server Interaction Details
- Go client device traffic uses JSON routes under
/api/v1/devices. - Browser UI uses Phoenix LiveView over HTTP and LiveView WebSocket transport.
- Device detail uses the
/devices/:idLiveView route and may render as a modal overlay over the Devices list; the old REST modal API is not part of the supported surface. - Terminal UI uses Phoenix Channels over WebSocket.
- Caddy on-demand TLS calls
/api/v1/check_domain. - E2E harness calls
/e2eroutes withX-E2E-Protocol-Versionon run creation.
Traceable references:
packages/server/lib/nixstasis_web/router.ex:1-117packages/server/lib/nixstasis_web/channels/user_socket.ex:37-64packages/server/lib/nixstasis_web/channels/terminal_channel.ex:20-113packages/server/lib/nixstasis_web/controllers/e2e_run_controller.ex:7-62
Server Devices
Language
- Elixir.
Runtime Context
- Server context and Ash resource layer.
Purpose
- Manages device registration, listing, approval, remote-access flags, pending command queueing, command delivery, command acknowledgement, command payload retrieval, and SSH terminal process support.
Key Files
packages/server/lib/nixstasis/devices.expackages/server/lib/nixstasis/devices/device.expackages/server/lib/nixstasis/devices/pending_command.expackages/server/lib/nixstasis/devices/schema_validator.expackages/server/lib/nixstasis/devices/ssh_key_manager.expackages/server/lib/nixstasis/devices/ssh_client.expackages/server/lib/nixstasis_web/controllers/device_controller.expackages/server/lib/nixstasis_web/controllers/heartbeat_controller.expackages/server/lib/nixstasis_web/controllers/device_command_controller.expackages/server/lib/nixstasis_web/live/device_live/index.expackages/server/lib/nixstasis_web/live/device_live/show.ex
Public Interfaces
- Context functions:
Nixstasis.Devices.count_all/0Nixstasis.Devices.count_by_status/1Nixstasis.Devices.count_pending_approvals/0Nixstasis.Devices.register_device/1Nixstasis.Devices.update_last_seen/1Nixstasis.Devices.list_pending_devices/0Nixstasis.Devices.approve_device/1Nixstasis.Devices.list_devices/1Nixstasis.Devices.requesting_remote_access?/1Nixstasis.Devices.approve_devices/1Nixstasis.Devices.reject_devices/1Nixstasis.Devices.set_remote_access/2Nixstasis.Devices.get_device!/1Nixstasis.Devices.create_device/1Nixstasis.Devices.update_device/2Nixstasis.Devices.delete_device/1Nixstasis.Devices.change_device/2Nixstasis.Devices.queue_command/2Nixstasis.Devices.pop_pending_commands/1Nixstasis.Devices.acknowledge_command_results/2Nixstasis.Devices.get_command_payload/2Nixstasis.Devices.online?/1
- GenServer/process interfaces:
Nixstasis.Devices.SshClient.start_link/1Nixstasis.Devices.SshClient.send_data/2Nixstasis.Devices.SshClient.ssh_host/1
Dependencies
Internal
Nixstasis.DomainNixstasis.RepoNixstasis.Devices.DeviceNixstasis.Devices.PendingCommandNixstasis.Devices.SchemaValidator
External
- Ash
- AshPhoenix
- Ecto/PostgreSQL
- Elixir Port for
ssh ncatthrough SSHProxyCommand
Client-Server Interaction Details
POST /api/v1/devices/registercallsDevices.register_device/1.POST /api/v1/devices/:device_id/heartbeatcallsMonitoring.heartbeat/2, which updates last seen and returns pending commands.POST /api/v1/devices/:device_id/command_resultscallsDevices.acknowledge_command_results/2.GET /api/v1/devices/:device_id/command_payloads/:refcallsDevices.get_command_payload/2.- Device detail LiveView queues
ssh_authorizecommands before terminal session startup. - Device detail is reached through
/devices/:id; opening remote-access tabs may setremote_access_requested, and close/cleanup paths must clear stale remote access intent. - PCP metrics, Cockpit links, and terminal sessions are detail-view concerns and should degrade gracefully when FRP, SSH, or device data is unavailable.
Data Model Notes
- Device records carry stable identity and operational fields such as MAC address, product/product name, account number, approval status, schema definition, last-seen/last-polled timestamps, metadata, and remote-access intent.
- Telemetry and schema data intentionally remain dynamic JSON structures so product-specific Stary scripts can evolve without one relational table per product payload.
- Detailed historical product requirements live in IoT Device Monitoring and Device Detail Page; runtime API payloads live in Client-Server Interface.
Traceable references:
packages/server/lib/nixstasis/devices.ex:1-360packages/server/lib/nixstasis_web/controllers/device_controller.ex:31-65packages/server/lib/nixstasis_web/controllers/heartbeat_controller.ex:7-27packages/server/lib/nixstasis_web/controllers/device_command_controller.ex:6-39packages/server/lib/nixstasis_web/live/device_live/show.ex:57-80
Server Monitoring
Language
- Elixir.
Runtime Context
- Server context, Ash resources, and periodic worker.
Purpose
- Processes device heartbeats, persists telemetry events, evaluates alert rules, creates alerts, checks offline devices, and provides alert/rule APIs for LiveView screens.
Key Files
packages/server/lib/nixstasis/monitoring.expackages/server/lib/nixstasis/monitoring/offline_checker.expackages/server/lib/nixstasis/monitoring/rule_evaluator.expackages/server/lib/nixstasis/monitoring/telemetry.expackages/server/lib/nixstasis/monitoring/alert.expackages/server/lib/nixstasis/monitoring/alert_rule.expackages/server/lib/nixstasis_web/live/alerts/index_live.expackages/server/lib/nixstasis_web/live/alerts/rules_live.ex
Public Interfaces
Nixstasis.Monitoring.heartbeat/2Nixstasis.Monitoring.check_offline_devices/1Nixstasis.Monitoring.evaluate_telemetry/2Nixstasis.Monitoring.list_rules/0Nixstasis.Monitoring.get_rule!/1Nixstasis.Monitoring.create_rule/1Nixstasis.Monitoring.update_rule/2Nixstasis.Monitoring.delete_rule/1Nixstasis.Monitoring.list_rules_for_product/1Nixstasis.Monitoring.OfflineChecker.start_link/1
Dependencies
Internal
Nixstasis.DevicesNixstasis.DomainNixstasis.SettingsNixstasis.Monitoring.RuleEvaluatorNixstasis.Monitoring.AlertNixstasis.Monitoring.AlertRule
External
- Ash
- GenServer
Client-Server Interaction Details
- Heartbeat controller passes client telemetry and connection status into
Monitoring.heartbeat/2. Monitoring.heartbeat/2updates devicelast_seen_at, persists telemetry, evaluates rules, and returns queued commands to the client.- Offline checking uses
Settings.get_offline_window/0and runs periodically throughOfflineChecker. - Offline timing is runtime-configured through settings instead of hard-coded in the module docs. See Data Flow for the heartbeat and offline-check sequence.
Traceable references:
packages/server/lib/nixstasis/monitoring.ex:15-148packages/server/lib/nixstasis/monitoring/offline_checker.ex:1-32packages/server/lib/nixstasis_web/controllers/heartbeat_controller.ex:7-27
Server Reporting
Language
- Elixir.
Runtime Context
- Server context, Ash resource layer, LiveView UI support.
Purpose
- Manages custom report CRUD operations, report list/detail view data, table filtering/sorting, and session-scoped view preferences.
Key Files
packages/server/lib/nixstasis/reporting.expackages/server/lib/nixstasis/reporting/custom_report.expackages/server/lib/nixstasis/reporting/query_builder.expackages/server/lib/nixstasis/reporting/table_filters.expackages/server/lib/nixstasis_web/live/reports/index_live.expackages/server/lib/nixstasis_web/live/reports/show_live.expackages/server/lib/nixstasis_web/live/reports/form_component.ex
Public Interfaces
Nixstasis.Reporting.list_custom_reports/0Nixstasis.Reporting.list_custom_reports_with_view/1Nixstasis.Reporting.get_custom_report!/1Nixstasis.Reporting.create_custom_report/1Nixstasis.Reporting.custom_report_name_taken?/1Nixstasis.Reporting.update_custom_report/2Nixstasis.Reporting.delete_custom_report/1Nixstasis.Reporting.save_view_preferences/3Nixstasis.Reporting.load_view_preferences/2Nixstasis.Reporting.change_custom_report/2
Dependencies
Internal
Nixstasis.DomainNixstasis.RepoNixstasis.Reporting.CustomReportNixstasis.Reporting.TableFilters
External
- Ecto.Query
- AshPhoenix
- Postgres-backed
report_view_preferencestable for persisted view preferences
Client-Server Interaction Details
- Reporting is primarily used by browser LiveView routes under
/reports. - Custom reports are also exposed through Ash JSON:API under
/api/json/custom_reports. - Report list/detail interaction requirements, including filtering, sorting, delete confirmation, and saved view preferences, are captured in Report View Improvements.
- Schema-aware alert/report builder option lookup and invalid-selection clearing behavior is captured in Schema-Driven Builder Dropdowns.
Traceable references:
packages/server/lib/nixstasis/reporting.ex:1-200packages/server/lib/nixstasis/domain.ex:52-58packages/server/lib/nixstasis_web/router.ex:41-44
Server E2E
Language
- Elixir.
Runtime Context
- Server E2E control API, data lifecycle, log retention, LiveDashboard reporting.
Purpose
- Manages E2E test run lifecycle, suite listing, protocol validation, idempotency, environment locks, seed execution, journey result ingestion, log storage, log retrieval, and retention pruning.
Key Files
packages/server/lib/nixstasis/e2e.expackages/server/lib/nixstasis/e2e/run.expackages/server/lib/nixstasis/e2e/run_result.expackages/server/lib/nixstasis/e2e/protocol.expackages/server/lib/nixstasis/e2e/journey_selection.expackages/server/lib/nixstasis/e2e/expectation_registry.expackages/server/lib/nixstasis/e2e/environment_locks.expackages/server/lib/nixstasis/e2e/log_store.expackages/server/lib/nixstasis/e2e/retention_worker.expackages/server/lib/nixstasis_web/controllers/e2e_run_controller.expackages/server/lib/nixstasis_web/controllers/e2e_run_result_controller.expackages/server/lib/nixstasis_web/plugs/e2e_enabled.expackages/server/lib/nixstasis_web/live_dashboard/e2e_page.expackages/server/lib/mix/tasks/e2e.export_static.ex
Public Interfaces
Nixstasis.E2E.list_runs/0Nixstasis.E2E.list_suites/0Nixstasis.E2E.get_run!/1Nixstasis.E2E.get_run/1Nixstasis.E2E.prune_retention/1Nixstasis.E2E.list_results/1Nixstasis.E2E.create_run/1Nixstasis.E2E.cancel_run/1Nixstasis.E2E.delete_runs/1Nixstasis.E2E.record_result/3Nixstasis.E2E.submit_results/2Nixstasis.E2E.store_log/3Nixstasis.E2E.fetch_result_log/2Nixstasis.E2E.RetentionWorker.start_link/1
Dependencies
Internal
Nixstasis.E2E.DataPolicyNixstasis.E2E.EnvironmentLocksNixstasis.E2E.ExpectationRegistryNixstasis.E2E.JourneySelectionNixstasis.E2E.LogStoreNixstasis.E2E.ProtocolNixstasis.E2E.RunNixstasis.E2E.RunResultNixstasis.Repo
External
- Ecto.Query
- GenServer
- Phoenix Controller rendering
- LiveDashboard extension page
Client-Server Interaction Details
POST /e2e/runsreadsX-E2E-Protocol-Version, validates protocol/environment/suite/journeys/action-expect pairs, runs configured seed script, creates run rows, and returns201on success.- Run creation can return typed errors including
environment_locked,protocol_mismatch,invalid_action_expectation,seed_failed,invalid_request, anddatabase_error. POST /e2e/runs/:id/resultsstores journey outcomes and updates run status.GET /e2e/runs/:id/results/:journey_id/logretrieves log content or typed log-unavailable errors.- Production deployments disable E2E endpoints by default through
NixstasisWeb.Plugs.E2EEnabledunlessNIXSTASIS_E2E_ENABLED=true.
Traceable references:
packages/server/lib/nixstasis/e2e.ex:1-420packages/server/lib/nixstasis_web/controllers/e2e_run_controller.ex:1-93packages/server/lib/nixstasis_web/router.ex:68-79deploy/compose/README.md:12-14
Client CLI
Language
- Go.
Runtime Context
- Client compiled binary.
- Cobra-based CLI command tree.
Purpose
- Provides the
nixstasisexecutable for registration, polling, and script management.
Key Files
packages/client/cmd/nixstasis/main.gopackages/client/cmd/nixstasis/register.gopackages/client/cmd/nixstasis/poll.gopackages/client/cmd/nixstasis/script.gopackages/client/cmd/nixstasis/install_script.gopackages/client/cmd/nixstasis/list_scripts.gopackages/client/cmd/nixstasis/remove_script.gopackages/client/cmd/nixstasis/test_script.gopackages/client/cmd/nixstasis/repl.go
Public Interfaces
- CLI commands:
nixstasis registernixstasis pollnixstasis script install <path>nixstasis script listnixstasis script removenixstasis script testnixstasis script repl
- Go functions:
mainrunMainrunshouldSkipConfigrunRegisterrunPollpollOncepollInterval
Dependencies
Internal
internal/configinternal/logginginternal/identityinternal/transportinternal/scriptinternal/frpinternal/commandsinternal/telemetry
External
github.com/spf13/cobragithub.com/spf13/viper- Go
runtime/traceflight recorder.
Client-Server Interaction Details
registercalls the transport client registration endpoint.pollsends telemetry heartbeats, processes server commands, sends command results, and starts/stops FRPC based on the heartbeat response.
Traceable references:
packages/client/cmd/nixstasis/main.go:20-98packages/client/cmd/nixstasis/register.go:16-93packages/client/cmd/nixstasis/poll.go:21-249packages/client/cmd/nixstasis/script.go:5-12
Client Transport
Language
- Go.
Runtime Context
- Client HTTP boundary to Phoenix.
Purpose
- Encapsulates JSON HTTP requests for device registration, heartbeat polling, command-result submission, and deferred command-payload retrieval.
Key Files
packages/client/internal/transport/client.gopackages/client/internal/transport/register_test.gopackages/client/internal/transport/client_runtime_test.godocs/src/client-server-interface.md
Public Interfaces
- Types:
ClientPollRequestCommandStatusCommandRequestCommandPayloadCommandResultPollResponseCommandResultsRequest
- Constants:
CommandStatusOKCommandStatusFailed
- Functions and methods:
NewClient(*Client).RegisterDevice(*Client).Poll(*Client).SendCommandResults(*Client).FetchCommandPayload
Dependencies
Internal
internal/configinternal/frpinternal/identityinternal/telemetry
External
- Go
net/http - Go experimental
encoding/json/v2
Client-Server Interaction Details
RegisterDevice:POST {baseURL}/api/v1/devices/register- Sends
mac_address, optionalproduct_name, and optionalmetadata. - Expects
201and responsedata.id. - Approved devices receive
data.api_token; pending devices omit it until approval. - Re-registering the same MAC address updates the existing device record rather than creating a duplicate identity.
Poll:POST {baseURL}/api/v1/devices/{uuid}/heartbeat- Sends
telemetryandconnection_status. - Requires the issued device token as
api_keyquery parameter. - Expects
200or202and optional responsedata.remote_access_tokenplus optionaldata.commands. - HTTP
429indicates the server rate limit rejected the heartbeat.
SendCommandResults:POST {baseURL}/api/v1/devices/{uuid}/command_results- Sends
resultsarray. - Requires the issued device token as
api_keyquery parameter. - Expects
200or202.
FetchCommandPayload:GET {baseURL}/api/v1/devices/{uuid}/command_payloads/{ref}- Requires the issued device token as
api_keyquery parameter. - Expects
200and aCommandPayload.
Traceable references:
packages/client/internal/transport/client.go:21-212docs/src/client-server-interface.md
Client Identity
Language
- Go.
Runtime Context
- Client local host identity detection and persistence.
Purpose
- Detects primary MAC/IP identity, generates device names, persists server-assigned runtime credentials, and loads those credentials for polling.
Key Files
packages/client/internal/identity/types.gopackages/client/internal/identity/detect.gopackages/client/internal/identity/store.gopackages/client/internal/identity/detect_test.gopackages/client/internal/identity/store_test.gopackages/client/cmd/nixstasis/register.gopackages/client/cmd/nixstasis/poll.gopackages/client/internal/config/config.go
Public Interfaces
- Types:
DeviceIdentityCredentialsStore
- Functions and methods:
GetPrimaryMACGetPrimaryIPGenerateDeviceNameNewStore(*Store).Load(*Store).LoadUUID(*Store).Save(*Store).SaveUUIDconfig.IdentityPath
Dependencies
Internal
internal/configinternal/transport
External
- Go standard library networking and filesystem APIs.
Client-Server Interaction Details
registerdetects MAC/IP and sends identity data toPOST /api/v1/devices/register.- Approved registration responses include an API token. The client stores UUID
and token together as JSON at
config.IdentityPath()with owner-only file permissions. - Legacy identity files that contain only a UUID are still readable, but runtime heartbeat, command-result, and command-payload requests require the stored API token once the device is approved.
pollloads stored credentials from/etc/nixstasis/idviaconfig.IdentityPath()before sending heartbeat requests.
Traceable references:
packages/client/cmd/nixstasis/register.go:28-93packages/client/cmd/nixstasis/poll.go:38-47packages/client/internal/identity/store.go:18-157packages/client/internal/config/config.go:116-119
Client Starlark Runtime
Language
- Go.
Runtime Context
- Client dynamic script execution boundary.
Purpose
- Parses, validates, installs, discovers, executes, and reports Stary/Starlark telemetry scripts.
Key Files
packages/client/internal/script/runtime.gopackages/client/internal/script/executor.gopackages/client/internal/script/types.gopackages/client/internal/script/discovery.gopackages/client/internal/script/validator.gopackages/client/internal/script/report.gopackages/client/internal/script/repl.gopackages/client/internal/script/format.gopackages/client/internal/script/version.gopackages/client/internal/script/builtins_exec.gopackages/client/internal/script/builtins_mqtt.gopackages/client/cmd/nixstasis/install_script.godocs/src/features/starlark-script-system/design.mdpackages/client/README.md
Public Interfaces
- Types:
RuntimeRuntimeConfigExecutorScriptInfoScriptResultScriptErrorScriptWarningFrontMatter
- Functions and methods:
NewRuntime(*Runtime).Builtins(*Runtime).Close(*Runtime).ExecuteNewExecutor(*Executor).ExecuteScriptsDiscoverScriptsSelectLatestScriptsParseStaryFileParseStaryContentCompileSchemaValidateOutputToReportDefaultInstallDirInstallFilenameParseVersionNumberMaxVersion
Dependencies
Internal
internal/telemetry- CLI commands under
cmd/nixstasis/script*.
External
go.starlark.net/starlarkgo.starlark.net/syntaxgo.starlark.net/lib/jsongithub.com/eclipse/paho.mqtt.golanggithub.com/santhosh-tekuri/jsonschema/v5
Client-Server Interaction Details
- Script outputs are transformed into telemetry reports during
pollOnceand sent inside the heartbeattelemetryobject. - Server-issued commands can install or remove scripts through
internal/commands.Handler. .staryscripts contain YAML front matter plus a Starlark body. Validation compiles front matter schemas before installed scripts can contribute telemetry.- Script execution is bounded: executions have a five-second timeout and emit a slow-script warning after three seconds.
script testprints normalized YAML output on success and exits non-zero without telemetry output when validation or execution fails.- Server command batches are correlated by command ID. Duplicate command IDs in a
batch are ignored after the first occurrence and reported as failed with a
duplicate_command_idreason.
Traceable references:
packages/client/internal/script/runtime.go:20-179packages/client/internal/script/executor.go:13-133packages/client/cmd/nixstasis/poll.go:105-126packages/client/cmd/nixstasis/install_script.go:16-77
Client Command Handler
Language
- Go.
Runtime Context
- Client server-command execution.
Purpose
- Executes supported commands returned by the server in heartbeat responses and produces command result payloads.
Key Files
packages/client/internal/commands/handler.gopackages/client/internal/commands/fs.gopackages/client/internal/commands/handler_test.gopackages/client/cmd/nixstasis/poll.gopackages/client/internal/transport/client.go
Public Interfaces
- Types:
Handler
- Functions and methods:
NewHandler(*Handler).ExecuteBatch
Dependencies
Internal
internal/scriptinternal/transport
External
- Go
context - Go
sync - Go filesystem APIs.
Client-Server Interaction Details
- Commands originate in
PollResponse.CommandsfromPOST /api/v1/devices/:device_id/heartbeat. - Supported command types are
list_scripts,install_script,remove_script, andssh_authorize. - Commands with deferred payload references are hydrated through
FetchCommandPayloadbefore execution. - Results are sent to
POST /api/v1/devices/:device_id/command_results.
Traceable references:
packages/client/internal/commands/handler.go:17-230packages/client/cmd/nixstasis/poll.go:198-249packages/client/internal/transport/client.go:140-212
Client FRP Manager
Language
- Go.
Runtime Context
- Client launcher for the bundled
frpctransient systemd unit.
Purpose
- Starts/stops the FRPC transient systemd unit, passes runtime template values to frpc, checks connection state through systemd, and reports state to heartbeat payloads.
Key Files
packages/client/internal/frp/manager.gopackages/client/internal/frp/types.gopackages/client/internal/frp/manager_test.gopackages/client/cmd/nixstasis/frp_session.gopackages/client/cmd/nixstasis/frp_session_test.gopackages/client/build/root-dir/usr/share/nixstasis/frpc.tomlpackages/client/internal/config/config.gopackages/client/cmd/nixstasis/poll.go
Public Interfaces
- Types:
ManagerConnectionStatus
- Functions and methods:
NewManager(*Manager).Start(*Manager).Stop(*Manager).IsActive(*Manager).GetStatusconfig.FRPCBinaryPathconfig.FRPCConfigPath
Dependencies
Internal
internal/config
External
- OS process execution via
os/exec. - systemd transient units via
systemd-runandsystemctl.
Client-Server Interaction Details
- Heartbeat responses include
remote_access_tokenonly while remote access is requested for the device. - If
remote_access_tokenis non-empty and FRP is inactive,pollOncestarts thenixstasis-frpctransient unit using configured non-secret FRP values and the heartbeat token. - If
remote_access_tokenis absent or empty and FRP is active,pollOncestops FRPC. - If the heartbeat token changes while FRP is active,
pollOncerestarts FRPC with the current token. - FRP status is included in subsequent heartbeat requests as
connection_status. frpc.tomlremains client-owned in/usr/share/nixstasis/frpc.tomland frpc expands{{ .Envs.* }}placeholders from the session environment.- FRPS auth is passed to the transient unit as a systemd credential and converted
to
FRPS_AUTH_TOKENinsidefrp-session, avoiding token exposure insystemd-run --setenvmetadata.
Traceable references:
packages/client/internal/frp/manager.gopackages/client/cmd/nixstasis/frp_session.gopackages/client/cmd/nixstasis/poll.gopackages/client/internal/config/config.go
Client E2E Harness
Language
- Go and shell scripts.
Runtime Context
- Client-side E2E runner for validating client/server integration.
Purpose
- Loads E2E configuration and journey specs, creates server-side E2E runs, executes journeys, writes JSONL logs, and submits results.
Key Files
packages/client/scripts/e2e/runpackages/client/scripts/e2e/run_all_suitespackages/client/scripts/e2e/scaffoldpackages/client/scripts/e2e/main.gopackages/client/scripts/e2e/config.example.yamlpackages/client/scripts/e2e/journeys/*.yamlpackages/client/internal/e2e/api.gopackages/client/internal/e2e/runner.gopackages/client/internal/e2e/journey.gopackages/client/internal/e2e/journey_executor.gopackages/client/internal/e2e/selector.gopackages/client/internal/e2e/runtime_scripts.gopackages/server/lib/nixstasis/e2e.ex
Public Interfaces
- CLI scripts:
scripts/e2e/runscripts/e2e/run_all_suitesscripts/e2e/scaffold
- Go E2E package interfaces:
api.goAPI client for/e2eendpoints.runner.gorun orchestration.journey_executor.goaction execution and JSONL log emission.selector.gosuite/journey selection.
Dependencies
Internal
internal/e2einternal/transportinternal/script- Server E2E API and configuration.
External
- YAML parsing through
go.yaml.in/yaml/v3. - Runtime container fallback mentioned in
packages/client/README.md: Apple Container, Docker, then Podman for non-Linux runtime E2E.
Client-Server Interaction Details
- Creates runs through
POST /e2e/runs. - Uses
X-E2E-Protocol-Version. - Fetches suite catalog through
GET /e2e/suites. - Submits journey outcomes through
POST /e2e/runs/:id/results. - Logs can be inspected through
GET /e2e/runs/:id/results/:journey_id/log.
Traceable references:
README.md:53-225packages/client/README.md:42-114packages/client/internal/e2e/api.gopackages/client/internal/e2e/runner.gopackages/client/internal/e2e/journey_executor.go
Edge Caddy
Language
- Caddyfile configuration and Docker build assets.
Runtime Context
- Edge reverse proxy, TLS termination, AuthCrunch authentication/authorization, and routing.
Purpose
- Terminates public HTTPS traffic, performs on-demand TLS approval, hosts AuthCrunch portal, authorizes protected hosts, and reverse proxies to Phoenix and FRPS.
Key Files
deploy/compose/caddy/Caddyfilepackages/caddy/Dockerfilepackages/caddy/bin/build_caddy.sh
Public Interfaces
- Public hosts:
auth.{$BASE_DOMAIN}nixstasis.{$BASE_DOMAIN}frp-admin.{$BASE_DOMAIN}*.{$BASE_DOMAIN}
- Caddy on-demand TLS ask endpoint:
http://nixstasis:4000/api/v1/check_domain
Dependencies
Internal
- Phoenix service
nixstasis:4000. - FRPS service ports.
- Compose environment variables.
External
- Caddy.
- AuthCrunch/Caddy security plugin.
- Azure OAuth identity provider configuration.
- ACME/on-demand TLS.
Client-Server Interaction Details
- Browser and client HTTPS traffic to
nixstasis.<base-domain>is routed to Phoenix. - Wildcard device traffic is routed to FRPS HTTP vhost port.
- FRPS dashboard traffic is routed through
frp-admin.<base-domain>. - TLS certificate issuance calls Phoenix
GET /api/v1/check_domainto approve domains.
Traceable references:
deploy/compose/caddy/Caddyfile:1-75README.md:319-350deploy/compose/README.md:7-20
Edge FRP
Language
- TOML configuration, Docker build assets, and Go client process integration.
Runtime Context
- FRPS server in Compose deployment.
- FRPC process managed by the Go client on devices.
Purpose
- Provides reverse proxy tunneling for managed devices so remote access can be exposed through server-side infrastructure.
Key Files
deploy/compose/frps/frps.tomldeploy/compose/docker-compose.ymlpackages/frp/Dockerfilepackages/client/internal/frp/manager.gopackages/client/build/root-dir/usr/share/nixstasis/frpc.tomlpackages/server/lib/nixstasis/devices/ssh_client.ex
Public Interfaces
- FRPS published ports from Compose:
${FRPS_BIND_PORT}${FRPS_HTTP_PORT}${FRPS_TCPMUX_PORT}
- FRPS internal dashboard port:
${FRPS_DASHBOARD_PORT}
- FRPS config fields:
bindPortauth.method = "token"auth.tokenwebServer.portwebServer.userwebServer.passwordtcpmuxHTTPConnectPortvhostHTTPPortsubDomainHost
Dependencies
Internal
- Caddy wildcard and dashboard reverse proxying.
- Go client FRPC manager.
- Server SSH terminal client.
External
- FRP
frpsandfrpcbinaries. sshandncatfor terminal sessions.
Client-Server Interaction Details
- The server stores remote-access intent on devices and exposes the active FRPS
token to clients only through heartbeat
remote_access_tokenresponses. - Client polling reads heartbeat
remote_access_tokenvalues and starts/stops FRPC through a transient systemd unit. A missing or empty token means FRPC should stop or remain stopped. - FRPC reads
/usr/share/nixstasis/frpc.tomldirectly; frpc expands runtime{{ .Envs.* }}placeholders from the session environment. - The FRPS auth token from the heartbeat response is passed from the launcher to
frp-sessionas a systemd credential rather than as asystemd-run --setenvvalue. - Caddy proxies wildcard HTTP traffic to FRPS HTTP vhost port.
- Server SSH terminal uses FRP TCP mux through
ncat --proxy-type http.
Traceable references:
deploy/compose/frps/frps.toml:1-15deploy/compose/docker-compose.yml:33-66packages/client/internal/frp/manager.go:47-137packages/server/lib/nixstasis/devices/ssh_client.ex:30-49
Shared E2E Log Viewer
Language
- JavaScript and CSS.
Runtime Context
- Shared static assets for E2E log viewing/report output.
Purpose
- Provides client-side static viewer behavior and styling for exported E2E logs/pages.
Key Files
packages/shared/e2e_log_viewer/viewer.jspackages/shared/e2e_log_viewer/viewer.css.github/workflows/e2e-pages.ymlpackages/server/lib/mix/tasks/e2e.export_static.ex
Public Interfaces
- Static asset files consumed by E2E report/export workflows.
Dependencies
Internal
- Server E2E static export Mix task.
- GitHub Pages E2E workflow.
External
- Browser JavaScript and CSS runtime.
Client-Server Interaction Details
- Exported static pages are generated from E2E run data and logs.
- The root E2E Pages index loads
runs.jsonclient-side according to repository README documentation.
Traceable references:
README.md:187-203packages/shared/e2e_log_viewer/viewer.jspackages/shared/e2e_log_viewer/viewer.css
Development
Development documentation covers workflows that exist to build, validate, or exercise Nixstasis locally. These docs are not production operating procedures.
Local Stack
- Compose Dev Harness describes the local deployment-shaped stack for validating Caddy TLS approval, FRP, managed device simulation, and browser-launched SSH terminal flows.
- Production deployment belongs in Deployment Compose; this section is for local validation and developer feedback loops.
- Default laptop mode uses local hostnames and local/internal Caddy certificates so developers can exercise application behavior without public DNS or public certificate issuance.
- Optional public-fidelity validation can use DuckDNS or an operator-owned domain when public ACME behavior needs to be tested.
Accessibility
All user-facing features must meet WCAG 2.1 AA accessibility targets. This includes contrast ratios, keyboard focus visibility, form labels, error announcements, and modal dialog behavior.
Validation Boundaries
- Local development should preserve the production-shaped boundary where browser traffic reaches Phoenix through Caddy.
- Terminal testing should exercise LiveView, Phoenix Channels, server-side SSH, FRP TCP mux, and the managed client path instead of direct shell shortcuts.
- Development-only certificates, local keys, generated Compose overrides, and runtime state stay out of source control.
Open Questions
- Operational Unknowns tracks open implementation and operations questions that should be resolved before promoting a workflow to production guidance.
Compose Dev Harness
Feature Name
compose-dev-harness
Goal
Provide a repeatable Compose development harness that can validate the Nixstasis remote-access stack without a public domain by default. The workflow must run the server-side stack, exercise Caddy dynamic TLS approval with local certificates, register or simulate a managed device, connect FRPC to FRPS, and launch an SSH terminal from the Phoenix UI through the FRP path.
Optional public-fidelity validation may use DuckDNS or a real operator-owned domain to test DNS-based ACME behavior, but that path must not be required for normal development.
Source Of Intent
docs/src/planned-features.md, featurecompose-dev-harnessdocs/src/runtime-boundaries.mddocs/src/modules/deployment-compose.mddocs/src/modules/server-web.md
Users
- Developers validating remote-access behavior from a laptop.
- Maintainers reviewing TLS approval and terminal regressions before release.
- Operators who need clear separation between local validation and production deployment guidance.
Requirements
- Provide documented startup and teardown commands for a default laptop mode.
- Default laptop mode must use local host routing and Caddy internal CA/local certificates rather than public DNS or public certificate issuance.
- Default laptop mode must exercise Caddy on-demand approval through Phoenix
GET /api/v1/check_domain. - Provide a managed test-device path that registers with the server, runs or simulates FRPC, and exposes SSH through FRP so the UI terminal can connect.
- Provide validation steps for opening a terminal from
/devices/:idand running a harmless command through the browser UI. - Provide optional public-fidelity guidance for DuckDNS or a real domain using DNS-based ACME validation.
- Clearly separate development-only shortcuts from production Compose guidance.
Constraints
- Do not weaken production ingress or authentication requirements.
- Do not require public DNS, public ingress, ngrok, localtunnel, or similar tunnel providers for default laptop mode.
- Preserve Compose-file composition as the development override mechanism.
- Keep generated certificates, local keys, DNS tokens, and runtime state out of source control.
- The Phoenix app remains reached through Caddy in deployment-shaped flows.
- SSH terminal validation must exercise the browser UI, Phoenix Channels, server-side SSH process boundary, and FRP TCP mux path.
Non-Goals
- Replacing the supported production Compose deployment path.
- Making production Let’s Encrypt validation mandatory for local development.
- Building a hosted staging environment.
- Load, performance, or high-availability validation.
- Replacing existing E2E API protocol validation.
Proposed Design
One-Command Dev Lab
The fastest local path is a single-command dev lab (dev-lab.sh up --devices N)
that starts the server stack and seeds N pre-approved virtual devices via release
RPC. Virtual devices are seeded idempotently by MAC address and bypass
registration, polling, and FRPC entirely. This path validates server UI, database,
and API behavior but does not exercise the Go client or FRP tunnel path. The dev
lab uses a tracked dev.env with hardcoded development defaults (no template
secrets), uses NIXSTASIS_FORCE_SSL=false, and the Compose development harness
with docker compose --env-file dev.env.
Default Laptop Mode
Default laptop mode is local-first and deterministic:
- Use a single
docker-compose.ymlwith environment-file-driven configuration. - Run Phoenix, Caddy, FRPS, PostgreSQL, and client containers using the same
compose file with
docker compose --env-file dev.env. - Use Caddy local certificates or internal CA for HTTPS.
- Use local host routing for reserved app hosts and device wildcard hosts.
- Configure Caddy on-demand TLS with the existing Phoenix ask endpoint so domain approval remains part of the flow.
- Run a test device using a containerized client with systemd, sshd, frpc, and the Go client binary — matching real device lifecycle.
- The client container acts as both the Go client and the SSH target reachable through FRP.
- Set
NIXSTASIS_FORCE_SSL=falseso Phoenix does not enforce SSL redirects in local mode.
Optional Public-Fidelity Mode
Public-fidelity mode should be documented as a separate validation path:
- DuckDNS may be used for low-cost DNS and TXT-record ACME challenge testing.
- A real operator-owned domain may be used when available.
- DNS provider credentials must stay outside source control.
- Public-fidelity mode validates DNS challenge behavior and public certificate issuance, but it does not replace default laptop mode.
Hostnames
Default laptop mode reserves these local hostnames:
nixstasis.localhostfor the Phoenix app through Caddy.auth.localhostfor AuthCrunch through Caddy.frp-admin.localhostfor the FRPS dashboard through Caddy.atom-<normalized-device-id>.localhostfor device HTTP routes through FRPS and Caddy.
These names intentionally mirror the existing production reserved-host pattern of
nixstasis.<base-domain>, auth.<base-domain>, frp-admin.<base-domain>, and
wildcard device hosts while keeping default routing local-only. Scripts, examples,
and validation steps must reuse these names consistently.
Managed Test Device
The managed-device path uses a containerized client that runs Ubuntu with systemd
as PID 1, sshd for remote access, frpc for tunnel connectivity, and the Go client
binary started via systemd units. This matches the real device lifecycle including
registration, polling, FRPC process management, and SSH key authorization. Scale
client containers with --clients N or docker compose --scale client=N.
TLS Observation Diagnostics
A development-only TLS observation system records Caddy ask calls in an
ETS-backed GenServer (Nixstasis.TLSObservations). Observations are exposed via
/_nixstasis/laptop/tls_observations (GET to list, DELETE to clear) and gated by
NIXSTASIS_TLS_OBSERVATIONS_ENABLED (routed through runtime.exs into app
config) and NIXSTASIS_TLS_OBSERVATIONS_TOKEN. The observation store is capped at
50 entries and is not persisted. Validation scripts use this endpoint to
programmatically confirm Caddy reached Phoenix for domain approval.
Terminal Smoke Coverage
The minimum terminal smoke test must launch the terminal from /devices/:id, run a
harmless command such as whoami or printf nixstasis-smoke, close the session,
and reopen a terminal for the same test device. This is implemented as an ExUnit
LiveView integration test using a fake SSH client, covering command execution and
session lifecycle behavior without requiring a running FRP tunnel or browser
automation.
Public-Fidelity Guidance
DuckDNS and real-domain public-fidelity support should be documented as optional manual setup guidance for this feature. Do not add a DNS-provider abstraction until there is a concrete implementation need beyond documenting validation steps.
Risks And Tradeoffs
- Local TLS can prove Caddy and approval plumbing without proving public CA issuance.
- DuckDNS improves public-fidelity coverage but adds account tokens, DNS propagation delays, and external availability risk.
- Device simulation can hide packaging or client defects if it bypasses the Go client and FRPC process model.
- Host routing varies across macOS, Linux, Docker, Podman, and Apple Container.
- Tunnel providers such as ngrok are useful for reachability demos but can mask Caddy-owned TLS behavior if TLS terminates before Caddy.
Dependencies
deploy/compose/docker-compose.ymldeploy/compose/dev.envdeploy/compose/.env.exampledeploy/compose/caddy/Caddyfiledeploy/compose/caddy/Caddyfile.laptopdeploy/compose/frps/frps.tomldeploy/compose/scripts/dev-lab.shdeploy/compose/scripts/check_runtime_contract.shdeploy/compose/scripts/validate_stack.shpackages/client/Dockerfilepackages/server/Dockerfilepackages/server/lib/nixstasis_web/controllers/tls_controller.expackages/server/lib/nixstasis/tls_observations.expackages/server/lib/nixstasis/deployment.expackages/server/lib/nixstasis_web/channels/terminal_channel.expackages/server/lib/nixstasis/devices/ssh_client.expackages/client/internal/frp/manager.gopackages/client/internal/config/config.gopackages/client/cmd/nixstasis/register.gopackages/client/cmd/nixstasis/poll.go
Likely Affected Docs
docs/src/planned-features.mddocs/src/modules/deployment-compose.mddocs/src/runtime-boundaries.mddocs/src/modules/server-web.mddeploy/compose/README.mdpackages/client/README.mdpackages/server/README.md
Validation
- Static validation for generated Compose development overrides.
- Local smoke test confirming Caddy reaches Phoenix TLS approval.
- Local smoke test confirming Caddy serves local certificates through default laptop hostnames.
- Local smoke test confirming FRPC connects to FRPS using development config.
- ExUnit LiveView integration test that launches a terminal and runs a harmless command through a fake SSH client.
- Dev-lab one-command flow seeding virtual devices and confirming server UI accessibility.
- Optional DuckDNS or real-domain validation that documents certificate issuance, DNS challenge behavior, and expected failure modes.
Operational Unknowns
This page records missing, ambiguous, or conflicting operational signals observable from repository files. It does not prescribe changes.
Missing Docs
- No single generated OpenAPI document covers Ash JSON:API,
/api/v1controller endpoints, and/e2eendpoints. The current contract split is explained in API & Runtime Contracts.
Ambiguities
- Production AuthCrunch role and claim mapping is future work tracked in Planned Features, not an unresolved operational unknown.
Conflicting Signals Between Code and Specs
- Ash JSON:API OpenAPI output covers
/api/jsonresources, while the Go client uses/api/v1controller endpoints. Future API unification is tracked in Planned Features.
Areas Where Intent Is Unclear
- Whether the separate production authentication contract for Phoenix-internal APIs should be a docs page, a design spec, or generated API documentation.
Traceable references:
README.md:341-350deploy/compose/caddy/Caddyfile:32-38
Deployment Compose
Language
- Docker Compose YAML and shell scripts.
Runtime Context
- Supported production server deployment path.
Purpose
- Defines and validates the deployable server stack composed of Phoenix, Caddy, FRPS, and optional PostgreSQL.
Key Files
deploy/compose/docker-compose.ymldeploy/compose/.env.exampledeploy/compose/dev.envdeploy/compose/README.mddeploy/compose/caddy/Caddyfile.laptopdeploy/compose/scripts/dev-lab.shdeploy/compose/scripts/check_runtime_contract.shdeploy/compose/scripts/validate_stack.shprod.env
Public Interfaces
- Services:
nixstasiscaddyfrpspostgresclient
- Public published ports:
- Caddy
80:80 - Caddy
443:443 - FRPS bind, HTTP vhost, and TCP mux ports.
- Caddy
- Required operator inputs documented in
deploy/compose/README.md:DATABASE_URLSECRET_KEY_BASEPHX_HOSTPORTBASE_DOMAINCLIENT_IDCLIENT_SECRETTENANT_IDJWT_KEYFRPS_BIND_PORTFRPS_AUTH_TOKENFRPS_HTTP_PORTFRPS_DASHBOARD_PORTFRPS_DASHBOARD_USERFRPS_DASHBOARD_PASSWORDFRPS_TCPMUX_PORT
Runtime Contract
DATABASE_URL: PostgreSQL connection URL consumed by the Phoenixnixstasisservice. It may point at bundled PostgreSQL or an external PostgreSQL host.SECRET_KEY_BASE: Phoenix release secret consumed bynixstasis.PHX_HOST: Public Phoenix host behind Caddy.PORT: Phoenix container port. The supported Compose deployment uses4000.BASE_DOMAIN: Root domain used fornixstasis,auth,frp-admin, and wildcard device hostnames.CLIENT_ID: Entra application client identifier consumed by Caddy auth.CLIENT_SECRET: Entra application secret consumed by Caddy auth.TENANT_ID: Entra tenant identifier consumed by Caddy auth.JWT_KEY: Caddy auth JWT signing key.FRPS_BIND_PORT: FRPS bind port for client tunnel connections.FRPS_AUTH_TOKEN: Shared FRPS auth token consumed byfrps,nixstasis, and managed clients when remote access is requested.FRPS_HTTP_PORT: FRPS HTTP virtual host port used by Caddy wildcard proxying.FRPS_DASHBOARD_PORT: FRPS dashboard port used behind authenticated Caddy ingress.FRPS_DASHBOARD_USER: FRPS dashboard username.FRPS_DASHBOARD_PASSWORD: FRPS dashboard password.FRPS_TCPMUX_PORT: FRPS TCP mux port for TCP remote access.
Hostnames
nixstasis.<base-domain>: public Phoenix application host behind Caddy.auth.<base-domain>: AuthCrunch/OIDC callback and auth host.frp-admin.<base-domain>: authenticated FRPS dashboard host.atom-<normalized-device-id>.<base-domain>: device remote-access host pattern routed through Caddy wildcard TLS and FRPS HTTP vhost support.
Endpoints
check_domain: Phoenix ask endpoint used by Caddy on-demand TLS to authorize wildcard device hostnames before proxying to FRPS.
Artifact Rules
- Server startup and database migrations are separate operations; application startup must not implicitly run migrations.
- Externally sourced runtime artifacts must be pinned by digest or checksum.
- Client release artifacts install bundled
frpcat/usr/libexec/nixstasis/frpcso managed devices do not depend on a separate FRP package.
Dependencies
Internal
packages/server/Dockerfilepackages/caddy/Dockerfilepackages/frp/Dockerfiledeploy/compose/caddy/Caddyfiledeploy/compose/frps/frps.tomlpackages/frp/bin/download_frp.sh
External
- Docker Compose or rendered config for Apple Container
container-compose. - PostgreSQL image (always included; production can override
DATABASE_URL). - Pinned release image references from Compose configuration.
Client-Server Interaction Details
- Compose deployment exposes the Phoenix app only through Caddy for HTTP ingress.
- Client configuration points at the public Caddy host.
- Bundled PostgreSQL starts automatically; production can override
DATABASE_URLto point at an external managed database. - Release image references are pinned in Compose configuration; local development
builds images locally with
devtags. packages/frpcurrently provides FRPS image build assets and the shared FRP binary acquisition script used by server/client packaging flows.- E2E endpoints are disabled by default in production and can be enabled for staging validation with
NIXSTASIS_E2E_ENABLED=true. - Development laptop mode uses the same single
docker-compose.ymlwith a trackeddev.envfile passed viadocker compose --env-file dev.env. deploy/compose/scripts/dev-lab.shstarts the full stack, runs migrations, and seeds virtual devices for UI testing.deploy/compose/caddy/Caddyfile.laptopprovides Caddy internal/local certificates for local HTTPS without public DNS.- The
clientservice is a device simulator running Ubuntu with systemd as PID 1, sshd, frpc, and the Go client binary — matching real device lifecycle. - Default laptop mode uses
BASE_DOMAIN=localhostwithnixstasis.localhost,auth.localhost,frp-admin.localhost, andatom-<normalized-device-id>.localhost. - Laptop-mode TLS uses Caddy internal/local certificates while preserving the same
Phoenix ask endpoint at
GET /api/v1/check_domain. - Environment variables are passed to containers via explicit
environment:blocks in the compose file;--env-filehandles compose-time interpolation.
Traceable references:
deploy/compose/docker-compose.yml:1-129deploy/compose/README.md:1-117deploy/compose/scripts/check_runtime_contract.sh
Server-Client E2E Tests
Feature Name
server-client-e2e-tests
Goal
Provide a repeatable client-driven end-to-end harness that validates Nixstasis client/server compatibility before release.
Users
- Release owners validating release readiness.
- Developers running targeted journeys during feature work.
- Stakeholders reviewing auditable integration results.
Requirements
- Run a full suite or selected journey subset from manual or CI triggers.
- Use synthetic test data only.
- Require
X-E2E-Protocol-Versionand reject legacy client/server version fields. - Record runs, journeys, results, metadata, logs, and failure points.
- Enforce environment locks so only one active run uses a given environment at a time.
- Provide per-journey logs and summary reports through API/UI/static export paths.
- Support idempotent run creation for repeated invocations.
- Provide retention and unavailable-log behavior.
Proposed Design
The client owns journey specs and executes steps. The server validates run contracts, seeds baseline data, stores runs/results/log references, exposes run APIs, and renders LiveDashboard/static reports. Operational usage is documented in the top-level README and module docs.
Validation
- Full suite completes under the documented target in standard environments.
- Targeted journeys run without executing unrelated journeys.
- Unsupported protocol versions are rejected.
- Environment locking prevents overlapping runs.
- Logs remain traceable or return typed unavailable states after pruning.
Self-Extracting Installer
Feature Name
self-extracting-installer
Goal
Produce a single .run self-extracting archive per supported architecture as
part of the client release pipeline. The archive bundles the client binary,
arch-matched frpc, configuration templates, systemd units, and an artifact
manifest so that operators on systemd Linux distros without dpkg or rpm can
install Nixstasis with one command and no manual file placement. Operators
should invoke downloaded installers with sh nixstasis-<version>-linux-<arch>.run
because GitHub Release downloads do not preserve the executable bit.
Source Of Intent
docs/src/planned-features.md, featureself-extracting-installer- Prior review findings M6 and Q3 from
nistasis.issues_resolved.md
Users
- Operators running systemd Linux distros without native deb/rpm support.
- CI pipelines that need a single download artifact for fleet provisioning.
- Developers validating the install experience without building from source.
Requirements
- Produce a
.runself-extracting archive for each release architecture (linux/amd64,linux/arm64). - Each archive contains a flat staging directory with:
nixstasisbinary (copied from GoReleaserdist/build output)frpcbinary (arch-matched, frombuild/root-dir/usr/libexec/nixstasis/)frpc.toml(frombuild/root-dir/usr/share/nixstasis/)config.example.yaml(frombuild/root-dir/usr/share/nixstasis/)nixstasis-poll.service(frombuild/root-dir/lib/systemd/system/)nixstasis-poll.path(frombuild/root-dir/lib/systemd/system/)nixstasis-registration.service(frombuild/root-dir/lib/systemd/system/)install.sh(FHS placement script)artifacts.json(manifest)
install.shmaps flat archive files to their FHS paths:
nixstasis->/usr/bin/nixstasisfrpc->/usr/libexec/nixstasis/frpcfrpc.toml->/usr/share/nixstasis/frpc.toml(always replaced on upgrade; client-owned, not operator-edited)config.example.yaml->/usr/share/nixstasis/config.example.yaml- Seeds
/etc/nixstasis/config.yamlfromconfig.example.yamlif not already present (matching nfpm postinstall behavior) nixstasis-poll.service->/lib/systemd/system/nixstasis-poll.servicenixstasis-poll.path->/lib/systemd/system/nixstasis-poll.pathnixstasis-registration.service->/lib/systemd/system/nixstasis-registration.service
install.shmust be idempotent and safe for upgrades:
- Overwrite binaries and systemd units unconditionally.
- Preserve existing
/etc/nixstasis/config.yamlunless--force-configis passed. - Print installed file paths and versions to stdout.
artifacts.jsoncontains:
version: release version string (sourced from GoReleaserdist/metadata.json)arch: target architecturebuild_date: ISO 8601 timestampfiles: array of{path, sha256, mode}entries for every bundled file (paths are flat archive-relative names, not FHS destinations)
- The release workflow produces
.runarchives intodist/afterverify_artifacts.shpasses, and uploads them alongside existing release artifacts. verify_artifacts.shis extended to validate.runarchive contents and manifest integrity.frpcis consumed frombuild/root-dir/usr/libexec/nixstasis/frpc_<arch>(already staged byfetch_frpc.shbefore GoReleaser runs), not downloaded separately.
Constraints
- Do not embed
frpcin the Go client binary. FRPS_SERVER_ADDRremains a runtime env var, not baked into the archive.- Systemd units must retain
PrivateTmp=true. build/root-dirstays as the GoReleaser staging source for file templates.packages/frpremains the shared source of truth for FRP version and checksums.- The self-extracting archive is an additional release artifact; it does not
replace
.deb,.rpm, or.tar.gzoutputs. makeselfis the archive tool. It is available in Ubuntu 24.04 viaapt-get install makeselfand produces POSIX-compatible.runfiles.
Non-Goals
- Replacing
.debor.rpmpackaging for distros that support them. - Interactive TUI installer or configuration wizard.
- Automatic
systemctl enableorsystemctl starton install. - Uninstall support (can be added later).
- macOS or Windows support.
- Signing the
.runarchive (can be added later with GPG).
Design
Archive Assembly
A new script packages/client/scripts/release/build_installer.sh assembles
the .run archive:
- Accept
DIST_DIR(GoReleaser dist directory, defaultdist) andARCH(amd64orarm64) as inputs. - Create a temporary staging directory.
- Copy the compiled
nixstasisbinary from the GoReleaser build output indist/nixstasis_linux_<arch>/nixstasis. - Copy
frpcfrombuild/root-dir/usr/libexec/nixstasis/frpc_<arch>and rename tofrpc. - Copy config files from
build/root-dir/:usr/share/nixstasis/frpc.toml->frpc.tomlusr/share/nixstasis/config.example.yaml->config.example.yaml
- Copy systemd units from
build/root-dir/lib/systemd/system/:nixstasis-poll.servicenixstasis-poll.pathnixstasis-registration.service
- Copy
install.shfromscripts/release/install.sh. - Read version from
dist/metadata.json(GoReleaser output). - Generate
artifacts.jsonby computing sha256 and recording mode for each file in staging. - Run
makeself --nox11 <staging> <output> <label>to producenixstasis-<version>-linux-<arch>.runintodist/.
Install Script
packages/client/scripts/release/install.sh is a POSIX shell script that:
- Checks for root privileges (
id -uequals 0; avoids$EUIDwhich is bash-only). - Requires a running systemd host.
- Creates target directories if they do not exist.
- Installs binaries and systemd units with correct permissions.
- Conditionally installs config files:
- Always install
/usr/share/nixstasis/frpc.tomlfrom the archive so client upgrades can update the FRP template. - Seed
/etc/nixstasis/config.yamlfromconfig.example.yamlif it does not exist (unless--force-config, which overwrites both).
- Prints a summary of installed files and a reminder to configure
/etc/nixstasis/config.yamland runsystemctl enable.
Manifest Format
{
"version": "0.1.0",
"arch": "amd64",
"build_date": "2026-05-13T12:00:00Z",
"files": [
{"path": "nixstasis", "sha256": "abc123...", "mode": "0755"},
{"path": "frpc", "sha256": "def456...", "mode": "0755"},
{"path": "frpc.toml", "sha256": "...", "mode": "0644"},
{"path": "config.example.yaml", "sha256": "...", "mode": "0644"},
{"path": "nixstasis-poll.service", "sha256": "...", "mode": "0644"},
{"path": "nixstasis-poll.path", "sha256": "...", "mode": "0644"},
{"path": "nixstasis-registration.service", "sha256": "...", "mode": "0644"},
{"path": "install.sh", "sha256": "...", "mode": "0755"}
]
}
CI Integration
In release_client.yml, after verify_artifacts.sh:
apt-get install -y makeself- Run
build_installer.shforamd64andarm64. .runfiles are written todist/.- For snapshot builds:
dist/is already uploaded asnixstasis-client-snapshot. - For tag builds: GoReleaser builds artifacts with publishing skipped, then the
workflow creates the GitHub release with the verified files from
dist/.
Verification Extension
verify_artifacts.sh gains a new section that:
- Finds all
.runfiles in$DIST_DIR. - Extracts each to a temp directory with
--noexec --target <dir>. - Validates
artifacts.jsonexists and is valid JSON (usingjqorpython3 -m json.tool). - Validates every file listed in
artifacts.jsonexists and its sha256 matches. - Validates the archive contains
install.sh,nixstasis, andfrpc. - Requires at least one
.runfile only whenVERIFY_INSTALLERS=true, so the existing pre-installer artifact verification step can still validate tar, deb, and rpm outputs before installers are built.
Risks And Tradeoffs
makeselfis a CI runtime dependency; pinning its version prevents surprising format changes. This feature uses the Ubuntu 24.04 package first; explicit version pinning can be added later if release reproducibility needs it.- Self-extracting archives are less auditable than plain tarballs; operators
who prefer inspection can use
--noexec --target <dir>to extract without running. install.shconfig preservation adds conditional logic that must be tested for both fresh install and upgrade paths.- No uninstall script means operators must manually remove files or wait for a future feature.
EUIDis bash-only;install.shusesid -ufor POSIX compatibility.
Dependencies
packages/frp/bin/download_frp.sh(shared FRP acquisition, already used)packages/client/scripts/fetch_frpc.sh(stages frpc into build/root-dir).github/workflows/release_client.yml(release pipeline)packages/client/scripts/release/verify_artifacts.sh(artifact validation)packages/client/build/root-dir/(FHS layout source)prod.env(FRP version pins)packages/client/.goreleaser.yaml(archive structure reference)
Affected Docs
packages/client/README.md(document.runinstaller usage)docs/src/planned-features.md(keep feature status and delivered behavior reconciled as the feature moves fromin-spectoin-progressandcompleted)
Suggested Validation
- CI step that builds
.runfrom snapshot artifacts, extracts, and verifies manifest integrity. - Extraction test on Ubuntu 24.04 (CI runner) confirming all files land.
- Manual smoke test on a systemd distro without
dpkg/rpmto confirm FHS placement. - Upgrade test: install v1, then install v2, confirm binaries are replaced but config is preserved.
- Config seeding test: fresh install seeds
config.yamlfrom example; upgrade preserves existingconfig.yaml.
Packaging And Deployment Migration
Feature Name
packaging-deployment-migration
Goal
Establish one supported deployment path for the server stack and one supported native packaging/release path for the client while standardizing Nixstasis naming and runtime contracts.
Users
- Platform operators deploying the server stack.
- Device administrators installing the client.
- Maintainers producing release artifacts.
Requirements
- Provide one supported server deployment source of truth under
deploy/compose. - Include the required public ingress/authentication layer in the supported stack.
- Support bundled PostgreSQL and externally managed database modes.
- Keep client artifacts host-installable for supported Linux targets.
- Bundle the FRPC helper with client release artifacts in a product-owned path.
- Install user-facing client command, configuration templates, and service assets together.
- Apply Nixstasis naming consistently across release-facing assets.
- Separate application startup from database migration execution.
- Pin externally sourced runtime artifacts reproducibly.
- Fully document operator-supplied runtime settings for supported deployment.
Proposed Design
The supported server path is Compose-based and documented through deploy/compose.
Client packaging uses GoReleaser outputs and shared FRP acquisition scripts. The
runtime contract defines domains, ports, secrets, database configuration, TLS
approval paths, and artifact version pins.
Operator commands and validation procedures belong in deploy/compose/README.md,
the top-level README, and deployment module docs.
Edge Cases
- Operators attempt legacy server package instructions.
- Client hosts lack separately installed tunnel tooling.
- Operators use externally managed databases.
- Required secrets or domain settings are missing.
- External artifacts resolve differently across environments.
Validation
- Clean server deployment follows only
deploy/composeguidance. - Runtime contract identifies all required operator-supplied settings.
- Client artifacts install command, configs, services, and bundled FRPC.
- Release validation verifies pinned external artifacts and package contents.
Specifications
Feature docs are the durable design and task history for delivered,
in-progress, or planned work. Each feature directory contains a design.md and
tasks.md file.
Product And UI
- IoT Device Monitoring
- Dashboard Home
- Phoenix UI Polish
- Device Detail Page
- Add Rule Modal Improvements
Client And Runtime
Reporting And Builders
Builder/report UI details, including dropdown option normalization, invalid selection clearing, report filter operators, delete confirmation, and saved view preferences, live in these feature designs rather than the architecture pages.
Operations And Development
Current top-level book placement groups these same specs by reader intent: Development, Operations, and Reference pages link to the same feature designs where they are most useful.
IoT Device Monitoring
Feature Name
iot-device-monitoring
Goal
Provide the core server-side monitoring system for Nixstasis-managed devices. Devices self-register with dynamic product schemas, move through an approval workflow, send authenticated heartbeats with telemetry, receive pending commands, and feed alerting and reporting workflows.
Users
- Device integrators registering hardware with Nixstasis.
- Administrators approving devices and monitoring fleet health.
- Operators delivering commands and remote-access intent through heartbeat responses.
- Analysts building alert rules and custom reports from device telemetry.
Requirements
- Devices register with a unique MAC address, product name, schema, and optional metadata.
- Registration schemas must include top-level
product; missing or empty schemas are rejected. - Re-registration by the same MAC address updates the existing device schema and metadata.
- Registration returns persistent API credentials for subsequent device calls.
- Unknown devices are recorded as pending until approved by an administrator.
- Heartbeats update
last_seen_at, accept telemetry, and return pending commands. - Heartbeats are authenticated with the persistent API token and rate limited per device.
- Offline alerts are generated from the configured heartbeat window.
- Alert rules evaluate dynamic telemetry fields for configured products.
- Alert notifications can be surfaced in the dashboard and dispatched through email or webhook destinations.
- Custom reports can select JSONB-backed telemetry fields across products.
- Report builder field-type conflicts require explicit user handling before save.
Proposed Design
Registration And Approval
Registration stores each device in devices with mac_address, product_name,
schema_definition, metadata, approval state, API token hash, and last-seen state.
Unknown devices enter pending approval. Approved devices can complete the normal
heartbeat and command workflow.
Heartbeat And Commands
Approved devices send authenticated heartbeats to the device-specific heartbeat
endpoint. The server updates connectivity state, stores telemetry when present,
evaluates alert rules, and returns queued commands. Empty command queues return a
lightweight acknowledgement. When remote access is requested and configured, the
heartbeat response can include remote_access_token for FRPC authentication.
Alerts And Reports
Offline monitoring compares last_seen_at with the configured offline window.
Data-driven alert rules bind product, JSON path, operator, and threshold.
Reports query telemetry payloads through JSONB paths and save reusable report
definitions.
Data Model
devices: identity, product, approval status, schema, metadata, token hash, remote-access state, andlast_seen_at.telemetry_events: device reference, timestamp, and JSONB payload.pending_commands: device reference, command payload, status, and delivery timestamps.alert_rules: product, field path, operator, threshold, and rule metadata.alerts: device reference, optional rule reference, type, status, message, and trigger time.custom_reports: saved report configuration and query metadata.system_settings: global monitoring windows and notification destinations.
Edge Cases
- Duplicate or conflicting schema keys must not silently break saved reports.
- Heartbeats for unregistered or unauthorized devices are rejected.
- Heartbeat surges are rate limited with HTTP 429 responses.
- Missing telemetry fields must not crash alert evaluation or report rendering.
- Incompatible report field types require explicit user resolution.
Validation
- Registration accepts valid schemas and rejects missing
product. - Pending devices appear in the approval workflow and can be approved.
- Heartbeats update last-seen state, return commands, enforce token auth, and rate limit overload.
- Offline alerts are generated within the configured threshold behavior.
- Alert rules and reports operate over dynamic telemetry fields.
Dashboard Home
Feature Name
dashboard-home
Goal
Provide a LiveView homepage that gives operators immediate situational awareness for the device fleet and direct navigation into core workflows.
Users
- IoT operators monitoring fleet health.
- Administrators reviewing pending approvals and active alerts.
Requirements
- Show total device count.
- Show online and offline device counts using the same heartbeat-window logic as offline alerts.
- Show pending approval count.
- Show active alert count.
- Provide prominent navigation to Devices, Approvals, Alerts, and Reports.
- Make relevant stat cards clickable and route to the corresponding filtered/detail view where supported.
- Update stats in real time without requiring a page reload.
- Render sensible zero-data, loading, and degraded-data states.
Proposed Design
The dashboard home is the default landing page. It reads aggregated summary data from the dashboard/server contexts and renders compact statistic cards plus workflow navigation. LiveView updates keep the snapshot fresh as devices register, heartbeats arrive, approvals change, and alerts resolve.
The feature assumes one visible role for this iteration: users who can load the dashboard see the full summary.
Validation
- Seed devices, approvals, and alerts; verify displayed counts match database state.
- Verify navigation links reach the expected routes.
- Verify empty-state counts display as zero rather than errors.
- Verify updates arrive without manual page reload.
Phoenix UI Polish
Feature Name
phoenix-ui-polish
Goal
Make the Phoenix application look and feel professional across desktop and mobile, with consistent layout, typography, responsive navigation, accessible states, and clear notifications.
Users
- Operators using the web UI daily.
- Administrators managing devices, alerts, and reports.
- Keyboard and mobile users who need reliable interaction behavior.
Requirements
- Use a consistent application layout with a responsive header and collapsible desktop sidebar.
- Provide mobile navigation that avoids horizontal scrolling and keeps primary destinations reachable.
- Establish readable typography, spacing, and content max-widths.
- Provide visible hover, focus, active, loading, and error states for interactive controls.
- Use data-first table/list styling for dense data.
- Support a manual light/dark theme toggle that respects system preference by default.
- Persist manual theme choice across sessions.
- Render flash messages as floating toast notifications.
- Preserve WCAG AA contrast and keyboard focus visibility.
Proposed Design
The UI polish work is a cross-cutting feature over Phoenix layouts, components, and page templates. The design favors a structured shell, responsive content areas, consistent form controls, readable tables, and feedback patterns that make status and errors obvious without distracting from operational data.
Edge Cases
- Mobile landscape should not break navigation.
- Long headings and table cells should wrap or truncate without layout overflow.
- 200% browser zoom should remain usable without horizontal scrolling.
Validation
- Verify primary pages at desktop and mobile widths.
- Verify keyboard focus movement and visible focus states.
- Verify main text contrast meets WCAG AA.
- Verify toasts, theme toggle, tables, forms, and navigation use consistent styling.
Go Client Rewrite
Feature Name
go-client-rewrite
Goal
Replace the original shell-based client with a maintainable Go CLI and service that handles device identity, registration, polling, telemetry collection, server command handling, and FRP lifecycle management.
Users
- Device administrators installing and configuring managed devices.
- Operators relying on client telemetry and remote access behavior.
- Developers maintaining client/server protocol compatibility.
Requirements
- Provide one
nixstasisbinary with subcommands for registration, polling, scripts, and support workflows. - Load primary configuration from a config file with environment overrides.
- Detect device identity from network interfaces and persist assigned device ID.
- Register with the server and persist API credentials.
- Poll the server for heartbeat responses, commands, and remote-access tokens.
- Collect telemetry from the supported extension mechanism and merge it into heartbeat payloads.
- Manage FRPC process lifecycle for requested remote access.
- Enforce process timeouts and avoid blocking the main poll loop on slow extensions or commands.
- Produce supported release artifacts for Linux hosts.
Proposed Design
The Go client is organized into focused internal packages: configuration, identity, transport, telemetry/script execution, command handling, FRP management, and E2E support. The CLI entrypoints orchestrate these packages while keeping protocol details inside typed transport code.
The original plugin assumptions were superseded by the Starlark script system and
server-provided FRPS token flow. Durable client behavior is documented in
packages/client/README.md, docs/src/modules/client-*, and
docs/src/client-server-interface.md.
Edge Cases
- Missing or corrupt local identity should trigger safe re-registration.
- Network failures should log and retry without crashing normal service operation.
- Hanging telemetry scripts or commands must time out.
- Duplicate command IDs in one batch must produce deterministic command results.
- FRPC token rotation must restart active FRPC safely.
Validation
- Unit tests for identity, config, transport, command handling, scripts, and FRP manager behavior.
- Integration tests against mock server protocol responses.
GOEXPERIMENT=jsonv2 go test ./...inpackages/client.- Release artifact validation for supported packaging outputs.
Starlark Script System
Feature Name
starlark-script-system
Goal
Provide a Starlark-based client extension system using stary files with YAML
front matter and declared output schemas.
Users
- Device integrators extending telemetry and command behavior.
- Developers testing scripts locally before enabling them in polling flows.
Requirements
- Accept user-authored
staryfiles with YAML front matter and Starlark body. - Require front matter to declare an output schema.
- Validate script output against the declared schema with field-level errors.
- Allow scripts to be selected by path or unique name; conflicting names require path selection.
- Support install, remove, list, test, and REPL workflows from the CLI.
- Provide Starlark builtins including MQTT-style
pub_and_getand deny-by-default command execution. - Execute heartbeat command batches and send aggregated command results back to the server.
- Correlate command results by
command_idand handle duplicate IDs deterministically. - Time out scripts and commands that exceed configured execution windows.
Proposed Design
The client embeds a Starlark runtime with a parser for stary files, schema
validation, execution result reporting, and CLI helpers for local development.
Command execution is restricted and documented as deny-by-default with an
allowlist model for executable paths.
Durable CLI usage belongs in packages/client/README.md; runtime architecture
belongs in docs/src/modules/client-starlark-runtime.md and
docs/src/modules/client-command-handler.md.
Edge Cases
- Missing or invalid YAML front matter.
- YAML front matter without a script body.
- Output missing required schema fields.
- Duplicate script names.
- Long-running scripts or commands.
- Duplicate
command_idvalues in a heartbeat batch.
Validation
nixstasis script test <path>prints YAML for valid output and exits non-zero on parse/execution/validation failures.- REPL starts with supported builtins available.
- Command batches run with timeout and duplicate handling.
- Aggregated command results are sent promptly after batch completion.
Schema-Driven Builder Dropdowns
Feature Name
schema-driven-builder-dropdowns
Goal
Generate alert and report builder dropdown options from available schemas so users select valid fields instead of typing fragile field names manually.
Users
- Users creating alert rules.
- Users building custom reports.
Requirements
- Generate alert builder options from the explicitly selected schema version.
- Generate report builder options from the explicitly selected schema version.
- Keep alert and report schema selections independent.
- Refresh dropdown options when schema context changes.
- Clear invalidated selections and show inline reselect-required feedback.
- Block save when current selections do not match the active schema.
- Provide clear empty, missing, unreadable, and unauthorized states.
- Disambiguate duplicate display labels while preserving selectability.
- Load typical schema option sets within 2 seconds.
- Expose schema reference/options and builder validation endpoints where needed by the UI.
Proposed Design
Server-side schema option services normalize schema metadata into UI options, validate selected slots against the active schema, and return cleared slot IDs when selections become invalid. Alert and report builders consume the service without sharing mutable schema-selection state.
Validation
- Builder dropdowns populate from selected schemas.
- Switching schemas clears invalid prior selections and preserves valid ones.
- Missing schema data disables affected controls and explains recovery steps.
- Authorization loss blocks save until access returns.
Report View Improvements
Feature Name
report-view-improvements
Goal
Make custom report list and detail pages easier to scan, filter, navigate, and reuse.
Users
- Report consumers reviewing generated results.
- Operators managing saved custom reports.
Requirements
- Separate report metadata, controls, results, and status messaging.
- Support column sorting and per-column filtering.
- Support clearing active filters and sort state.
- Preserve report view state during an active session.
- Support saved report view preferences when available.
- Fall back safely when saved preferences no longer match report structure.
- Respect report field permissions in every view state.
- Provide useful empty and error states.
- Provide explicit View, Edit, and Delete actions in the custom report list.
- Require explicit confirmation before deleting a custom report.
- Support numeric filter operators
gt,gte,eq,lte,ltand string operatorscontains,not_contains,is,is_not.
Proposed Design
Report view improvements refine the existing reporting LiveViews and reporting context rather than introducing a new reporting model. Filter and sorting semantics belong in reporting module docs when they are current API behavior.
Validation
- Large reports remain scannable with stable headers/controls.
- Filtering and sorting update predictably.
- Invalid saved preferences fall back with a user-visible message.
- Delete requires confirmation.
Add Rule Modal Improvements
Feature Name
add-rule-modal-improvements
Goal
Bring Add Rule modal behavior to parity with the Create Report modal for layout, validation, keyboard interaction, and accessible feedback.
Users
- Users creating or editing alert rules.
- Keyboard-only and accessibility-focused users.
Requirements
- Match Create Report modal structure, action placement, validation placement, and close affordances.
- Use one primary save action and a clear cancel path.
- Tie save availability to validation state.
- Focus the first actionable rule-building control on open.
- Keep focus order logical, visible, and contained while modal is open.
- Support
Ctrl+Enter/Cmd+Enterto save. - Do not let plain Enter in text fields force modal submission.
- Confirm close/cancel only when unsaved changes exist.
- In edit mode, keep only rule name immutable.
- Success feedback auto-dismisses; error feedback persists until user action or correction.
- Meet WCAG 2.1 AA expectations for modal dialogs and form validation feedback.
Proposed Design
The feature refines the existing alert rule LiveViews and modal component state. It focuses on interaction quality and validation recovery rather than changing alert-rule semantics.
Validation
- Keyboard-only users can complete create/edit flows.
- Invalid inputs show inline guidance and preserve entered values.
- Duplicate/rapid submits process only one save.
- Unsaved changes prompt before close; unchanged modals close immediately.
Device Detail Page
Feature Name
device-detail-page
Goal
Expose device details from the Devices page workflow without the obsolete modal REST endpoints, preserving list context while operators inspect individual devices.
Users
- Operations users browsing the device fleet.
- Administrators inspecting individual device status and ownership details.
Requirements
- Present the Devices page with key attributes needed for monitoring and selection.
- Apply additive AND filtering across Product, Account Number, and Status values.
- Support individual filter-chip removal and clear-all behavior.
- Provide an explicit MAC Address entrypoint for each device row to open details.
- Open device details through the route-backed
/devices/:idLiveView flow while preserving active filters and return context. - Do not restore the obsolete modal open/close REST endpoints.
- Display meaningful empty, loading, and error states.
- Prevent unauthorized users from seeing restricted device details.
- Keep detail content current at open/refresh time.
- Keep behavior consistent across desktop and mobile.
Proposed Design
Device detail is owned by the existing Devices LiveView flow and browser route,
not the obsolete /api/v1/devices/:device_id/modal API surface. The list page
stays optimized for finding devices, while the route-backed detail view may be
rendered as a modal overlay so deeper inspection and action context do not lose
the user’s filter state.
Remote-access detail behavior is shared with the older device-list management backlog: detail views can expose PCP metrics, terminal access, and Cockpit links, but those tabs must show degraded/retry states when the device or tunnel is not available.
Edge Cases
- Empty device list.
- Device deleted before or during detail navigation.
- Large device lists requiring efficient rendering.
- Network loss during detail load.
- User loses access to sensitive device details.
Validation
- Users can find a target device from the list quickly.
- Opening
/devices/:idshows the correct device and supports return navigation or modal close back to the filtered list. - Missing/deleted/unauthorized devices show clear recovery states.
Server-Provided FRPS Token
Summary
Replace the remote-access heartbeat response boolean with a token-bearing contract. When the server wants a managed client to open remote access, the heartbeat response includes the shared FRPS auth token. The client treats a non-empty token as the start signal, passes that token to the transient unit as a systemd credential, and stops FRPC when no token is provided.
Goals
- Make the heartbeat response the source of truth for both remote-access intent and the FRPS auth token needed to satisfy the current upstream FRPS token auth deployment.
- Keep the device runtime API token separate from the FRPS auth token.
- Avoid persisting the FRPS token in client config or identity files.
- Keep the client-owned
frpc.tomltemplate and frpc-native{{ .Envs.FRPS_AUTH_TOKEN }}expansion model. - Keep FRPC lifecycle owned by the
nixstasis-frpctransient systemd unit.
Non-Goals
- Implementing per-device FRPS tokens.
- Adding an FRPS auth plugin or replacing upstream token authentication.
- Changing UI permissions for opening or closing remote access.
- Changing the terminal channel or browser authorization model.
- Persisting the FRPS token on client hosts.
Current Behavior
- The Phoenix heartbeat response includes
remote_access_requested: boolean. - The Go client starts FRPC when
remote_access_requestedis true. - FRPC receives its auth token from client runtime config (
frp.auth_token). - Compose configures FRPS with a single shared
FRPS_AUTH_TOKEN. - The Phoenix
nixstasisservice does not currently receiveFRPS_AUTH_TOKEN.
Proposed Contract
Heartbeat response data changes from:
{
"data": {
"remote_access_requested": true,
"commands": []
}
}
to:
{
"data": {
"remote_access_token": "<shared-frps-token>",
"commands": []
}
}
Semantics:
remote_access_tokennon-empty: start or keep FRPC running with that token.remote_access_tokenomitted: stop FRPC if active; otherwise remain stopped.remote_access_tokenmay decode as an empty string on older or malformed responses; the client treats that the same as omission.remote_access_requestedis removed from the client response contract because no release has shipped with the current branch behavior.
Server Design
- Compose passes
FRPS_AUTH_TOKENto both services:frps: uses it infrps.tomlasauth.token.nixstasis: uses it only to populate authenticated heartbeat responses whendevice.remote_access_requestedis true.
- Add a small helper that resolves the heartbeat FRPS token from
FRPS_AUTH_TOKENonly whendevice.remote_access_requestedis true. - Keep
HeartbeatJSON.show/1focused on rendering response data. It receives or calls the helper result and conditionally includesremote_access_token:
Remote access token rendering cases:
- Absent when
device.remote_access_requestedis false. - Configured
FRPS_AUTH_TOKENwhendevice.remote_access_requestedis true. - If remote access is requested but
FRPS_AUTH_TOKENis missing, the heartbeat should continue returning telemetry/command responses and omit the token. The helper should log a clear error so the UI timeout is diagnosable.
Client Design
transport.PollResponsereplacesRemoteAccessRequested boolwithRemoteAccessToken string.pollOncestarts FRPC whenresp.RemoteAccessToken != "".- Before
Manager.Start,pollOncederives runtime FRP config from the local config and MAC address, then setsfrpConfig.AuthToken = resp.RemoteAccessToken. runtimeFRPConfigderives only dynamic client-owned values, such as FRP proxy name from MAC whenfrp.nameis not configured.pollOncestops FRPC whenresp.RemoteAccessToken == ""and current FRP status is active.Manager.Startcontinues validating that the final FRP config has a non-empty auth token before invokingsystemd-run.Manager.Startcontinues passing the token to the transient unit throughLoadCredential=FRPS_AUTH_TOKEN:<path>; it must not pass the token throughsystemd-run --setenvmetadata.
Security Notes
- The shared FRPS token is exposed to authenticated managed devices only when an operator has requested remote access for that device.
- This preserves the current upstream FRPS token auth model, but shared-token revocation remains all-or-nothing.
- Per-device revocation and token audit are explicitly deferred to a future FRPS auth-plugin feature.
Docs And Contracts Affected
docs/src/client-server-interface.mddocs/src/modules/deployment-compose.mddeploy/compose/scripts/check_runtime_contract.shdeploy/compose/docker-compose.ymldeploy/compose/README.mdpackages/server/README.mdpackages/client/README.mdpackages/client/scripts/mock_api/main.gopackages/client/internal/frp/manager.godocs/src/modules/client-frp-manager.mddocs/src/modules/edge-frp.mddocs/src/runtime-boundaries.mddocs/src/planned-features.md
Validation
- Server tests prove heartbeat omits
remote_access_tokenwhen remote access is false. - Server tests prove heartbeat includes
FRPS_AUTH_TOKENwhen remote access is true and the env var is configured. - Server tests prove heartbeat omits
remote_access_tokenand logs a clear error when remote access is requested butFRPS_AUTH_TOKENis missing. - Client tests prove a non-empty
remote_access_tokenstarts FRPC using that token. - Client tests prove an empty/missing token stops FRPC when active.
- Runtime contract checks prove the Phoenix service receives
FRPS_AUTH_TOKEN. - Client README no longer tells operators to configure a static
frp.auth_tokenfor normal server-requested remote access. - Mock API flags and fixtures use
remote_access_tokenfor client-side testing. - FRP manager comments distinguish non-secret template values passed by
--setenvfrom the secret token passed through systemd credentials. - Docs and module pages no longer describe
remote_access_requestedas the heartbeat response trigger after implementation. - Existing client tests continue to pass with
GOEXPERIMENT=jsonv2 go test -race ./.... - Server precommit checks pass with
mix precommit.
Reconciliation Bookends
- Before implementation, rebase or merge
mainso the completedself-extracting-installerdocs and systemd credential model are present. - During implementation, update server, client, deployment contract, and docs in
the same unit of work so
remote_access_requestedis no longer documented as a heartbeat response field. - Before completion, rerun the affected docs search for
remote_access_requested,remote_access_token, andFRPS_AUTH_TOKENand reconcile any remaining stale references.
Planned Features
Project Overview
Nixstasis needs a Compose development harness that exercises the same remote-access boundaries operators rely on in deployment. Local development must be able to test dynamic TLS approval and browser-driven SSH terminal flows without waiting for a production-like environment.
Goals
- Provide a repeatable Compose development harness for end-to-end remote-access validation.
- Make Caddy on-demand TLS approval testable from local development workflows.
- Make UI-launched SSH terminal sessions testable from local development workflows.
- Keep the development workflow aligned with documented runtime boundaries for Phoenix, Caddy, FRPS, FRPC, and managed-device identity.
- Support an optional public-fidelity mode using DuckDNS or a real operator-owned domain when developers need to validate public ACME behavior.
Non-Goals
- Replacing the supported production Compose deployment path.
- Changing production TLS policy or public ingress requirements.
- Building a full hosted staging environment.
- Making local development depend on public DNS or public certificate issuance when a local equivalent can validate the same application behavior.
- Requiring ngrok, localtunnel, or another third-party tunnel provider for the default development workflow.
Global Constraints
- The supported production deployment path remains
deploy/compose. - Development overrides should use Compose file composition, not mutable release
image tags in
.env. - The Phoenix application remains public only through Caddy in deployment-shaped flows.
- SSH terminal testing must exercise the browser UI, Phoenix Channels, server-side SSH process boundary, and FRP TCP mux path rather than bypassing them with direct shell access.
- TLS testing must exercise
GET /api/v1/check_domainapproval behavior. - Default laptop mode uses Caddy’s internal CA/local certificates and local host routing so development does not require public DNS or public ingress.
- Optional public-fidelity mode may use DuckDNS or a real domain with DNS-based ACME validation to test publicly trusted certificate behavior.
Cross-Cutting Decisions
- Treat local dynamic TLS and UI SSH validation as one development-environment feature because both depend on the same local ingress, DNS/host routing, FRPS, and device-client simulation shape.
- Default development mode uses local-only trust and routing mechanisms instead of public DNS or real public certificate issuance.
- DuckDNS or a real domain is an optional validation path for public ACME fidelity, not a prerequisite for local feature development.
- Keep production Compose docs and Compose dev-harness docs explicitly separated so test affordances do not become accidental production guidance.
Resolved Questions
- The managed test device runs as a containerized client with systemd, sshd, frpc, and the Go client binary — matching real device lifecycle.
- Reserved local hostnames:
nixstasis.localhost,auth.localhost,frp-admin.localhost, andatom-<normalized-device-id>.localhost. - Terminal smoke test covers session open, command execution, session close, and reopen via ExUnit LiveView integration test with a fake SSH client.
- DuckDNS support is documented as optional manual setup guidance; no DNS-provider abstraction was added.
Backlog
- Document
exec_cmdintent as deny-by-default and allowlist-gated by absolute executable path. - Move bespoke Phoenix controller APIs under Ash-backed actions/resources where
practical so their OpenAPI contracts can be generated from the same source of
truth as the
/api/jsonsurface. - Define the production AuthCrunch/Phoenix role and claim contract, including header mapping, LiveView authorization behavior, and operator role semantics.
- Add production operations runbooks for backup/restore, secret rotation, incident response, HA expectations, and production monitoring.
- Add richer API examples for common success, validation-error, authorization, and edge-case responses across maintained API contracts.
Feature Map
compose-dev-harness
- Status: delivered
- Overview:
- Create a default Compose development harness that can run the server-side stack, register or simulate a managed client, validate Caddy dynamic TLS approval with local certificates, and open SSH terminal sessions from the Phoenix UI through the FRP path. Add optional guidance for DuckDNS or a real domain when public ACME fidelity is required.
- Requirements:
- Provide documented startup and teardown commands for the local development environment.
- Provide local host routing/DNS guidance for the app host, AuthCrunch host, FRPS admin host, and device wildcard hostnames.
- Provide a local TLS validation path that exercises Caddy on-demand approval via
Phoenix
GET /api/v1/check_domainusing Caddy internal CA/local certificates by default. - Provide optional DuckDNS or real-domain guidance for validating public ACME behavior without making that path required for normal development.
- Provide a test managed-device path that can register, connect FRPC to FRPS, and expose SSH in a way the UI terminal can reach.
- Provide validation steps for opening a terminal from
/devices/:idand running a harmless command through the browser UI. - Document how the development workflow differs from production Compose.
- Constraints:
- Must not weaken or bypass production ingress/authentication requirements.
- Must not require public DNS or public internet exposure for the core local validation path.
- Must keep DuckDNS and real-domain support optional and clearly marked as public-fidelity validation.
- Must preserve Compose-file-composition as the development override mechanism.
- Must keep generated certificates, local keys, and runtime state out of source control.
- Non-goals:
- Making production certificate issuance validation mandatory for local development.
- Multi-user staging operations.
- Load, performance, or HA validation.
- Replacing existing E2E API protocol validation.
- Success criteria:
- A developer can start the local stack from a clean checkout using documented commands and local-only configuration.
- Visiting the local app through Caddy with local certificates causes TLS approval behavior to be exercised and observable.
- Optional DuckDNS or real-domain instructions identify how to run a higher fidelity public ACME validation path and how it differs from default laptop mode.
- A test device appears in the UI with remote-access state sufficient to launch a terminal.
- A browser-launched terminal session can run a harmless command through FRP and SSH without direct shell shortcuts.
- The docs clearly separate development-only shortcuts from production deployment guidance.
- Risks and tradeoffs:
- Local TLS can become misleading if it validates only certificate plumbing and not domain approval behavior.
- Device simulation can hide real packaging/client defects if it bypasses the Go client and FRPC process model.
- Hostname routing can become fragile across macOS, Linux, Docker, Podman, and Apple Container unless the spec chooses supported paths explicitly.
- Using real public DNS would increase fidelity but adds cost, secrets, and operational burden for developers.
- DuckDNS reduces domain cost but adds account tokens, DNS propagation delay, and external-service availability concerns.
- Tunnel providers such as ngrok or localtunnel are useful for reachability demos but can mask Caddy-owned TLS behavior if they terminate TLS before Caddy.
- Dependencies:
deploy/compose/docker-compose.ymldeploy/compose/caddy/Caddyfiledeploy/compose/frps/frps.tomlpackages/server/lib/nixstasis_web/controllers/tls_controller.expackages/server/lib/nixstasis_web/channels/terminal_channel.expackages/server/lib/nixstasis/devices/ssh_client.expackages/client/internal/frp/manager.gopackages/client/cmd/nixstasis/register.gopackages/client/cmd/nixstasis/poll.go- Suggested validation:
- Static validation for generated Compose development overrides.
- A local smoke test that confirms Caddy reaches Phoenix TLS approval.
- A local smoke test that confirms Caddy serves local certificates through the default laptop hostnames.
- A local smoke test that confirms FRPC connects to FRPS using the development configuration.
- Optional DuckDNS or real-domain validation that documents certificate issuance, DNS challenge behavior, and expected failure modes.
- A browser/UI or E2E journey that launches a terminal and executes a harmless command through SSH.
- Suggested first workflow command:
/start-feature compose-dev-harness
self-extracting-installer
- Status: completed
- Overview:
- Build a CI-produced self-extracting installer archive for non-Nix, non-deb,
non-rpm installs. The archive bundles the client binary, arch-matched
frpc, configs, systemd units, and an artifact manifest into a single.runfile that extracts and installs to FHS paths. - Requirements:
- Produce a self-extracting archive per supported architecture as part of the client release workflow.
- Include an install script that copies files to
/usr/bin/,/usr/libexec/nixstasis/,/etc/nixstasis/,/usr/share/nixstasis/, and/lib/systemd/system/. - Include an
artifacts.jsonmanifest with version, arch, sha256 per file, file modes, and build timestamp. - Consume
frpcfrompackages/frpvia the shared acquisition path, not a separate download. - Extend client release CI so
.runfiles are produced and verified alongside existing archives,.deb, and.rpmpackages. - Extend
verify_artifacts.shto validate.runarchive contents and manifest integrity. - Constraints:
- Do not embed
frpcin the Go client binary. FRPS_SERVER_ADDRremains a runtime env var, not baked into the archive.- Systemd units must use
PrivateTmp=true. build/root-dirstays as the GoReleaser staging source.packages/frpremains the shared source of truth for FRP version and checksums.- Non-goals:
- Replacing
.debor.rpmpackaging for distros that support them. - Interactive TUI installer or configuration wizard.
- Automatic service enablement or start on install.
- Uninstall support.
- Success criteria:
- A
.runfile for each release architecture is published to GitHub Releases. - Running the
.runfile on a clean Linux system installs all required files to their FHS paths. - Existing
/etc/nixstasis/config.yamlis preserved on upgrade unless the installer is explicitly forced to replace it; client-ownedfrpc.tomlis updated on every upgrade from/usr/share/nixstasis/frpc.toml. artifacts.jsonin the archive matches the installed bundle contents by sha256.verify_artifacts.shcatches content or manifest drift in CI.- Risks and tradeoffs:
makeselfadds a release CI dependency.- Self-extracting archives are less auditable than plain tarballs, so the installer must support no-exec extraction for inspection.
- Install script upgrade behavior must avoid overwriting operator-owned config.
- Dependencies:
packages/frp/bin/download_frp.shpackages/client/scripts/fetch_frpc.sh.github/workflows/release_client.ymlpackages/client/scripts/release/verify_artifacts.shpackages/client/build/root-dir/prod.env- Suggested validation:
- CI step that builds the
.runarchive from snapshot artifacts and verifies extraction plus manifest integrity. - Fresh-install and upgrade tests for file placement, modes, and config preservation.
- Manual smoke test on Alpine or Arch to confirm FHS placement without
dpkg/rpm. - Suggested first workflow command:
/start-feature self-extracting-installer
server-provided-frps-token
- Status: implemented
- Overview:
- Move the remote-access trigger from a boolean heartbeat response flag to a server-provided FRPS auth token. When the server wants a client to open remote access, the heartbeat response includes the shared FRPS token. The client treats the presence of that token as the start signal, passes it to the transient unit through a systemd credential, and stops FRPC when the token is absent.
- Requirements:
- Replace
remote_access_requestedin the device heartbeat response contract withremote_access_token. - Include
remote_access_tokenonly when remote access is currently requested for the device. - Make the Phoenix server read the shared FRPS token from deployment configuration and expose it only to authenticated device heartbeat responses that need remote access.
- Make the Compose
nixstasisservice receive the sameFRPS_AUTH_TOKENas thefrpsservice. - Make the Go client start FRPC when
remote_access_tokenis non-empty and use that value asFRPS_AUTH_TOKENfor frpc template expansion. - Make the Go client stop FRPC when
remote_access_tokenis absent or empty. - Keep the device runtime API token separate from the FRPS auth token.
- Constraints:
- The current FRPS deployment uses upstream FRP token auth with one shared
FRPS_AUTH_TOKEN. - Do not add an FRPS authentication plugin or per-device FRPS tokens in this feature.
- Do not persist the FRPS token in client config or identity files.
- The client-owned
frpc.tomlcontinues to use{{ .Envs.FRPS_AUTH_TOKEN }}and frpc-native environment expansion. - The client continues launching FRPC through the
nixstasis-frpctransient systemd unit. - Non-goals:
- Replacing FRP token authentication.
- Implementing per-device FRPS auth or revocation.
- Changing browser terminal authorization.
- Changing how operators request or close remote access in the UI beyond the heartbeat response payload.
- Success criteria:
- A heartbeat for a device without requested remote access returns no FRPS token and the client stops or leaves FRPC stopped.
- A heartbeat for a device with requested remote access returns the configured FRPS token and the client starts FRPC with that token in the transient unit credential path.
- The device API token is never used as the FRPS token.
- Server and client tests cover both token-present and token-absent response paths.
- Runtime contract documentation identifies
FRPS_AUTH_TOKENas consumed by bothfrpsandnixstasis. - Risks and tradeoffs:
- Returning a shared FRPS token to a device exposes that token to the managed host during the active remote-access lease.
- A shared token keeps FRPS deployment simple but cannot revoke a single device independently at the FRPS layer.
- If
FRPS_AUTH_TOKENis missing from the server environment while remote access is requested, clients cannot open FRPC even though the UI requested access. - Dependencies:
packages/server/lib/nixstasis_web/controllers/heartbeat_json.expackages/server/test/nixstasis_web/controllers/heartbeat_controller_test.exspackages/client/internal/transport/client.gopackages/client/cmd/nixstasis/poll.gopackages/client/cmd/nixstasis/poll_test.gopackages/client/internal/frp/manager.godeploy/compose/docker-compose.ymldeploy/compose/scripts/check_runtime_contract.shdocs/src/client-server-interface.mddocs/src/modules/deployment-compose.md- Suggested validation:
- Server controller tests for
remote_access_tokenomitted when remote access is false and present when true withFRPS_AUTH_TOKENconfigured. - Client transport/poll tests for starting FRPC with heartbeat-provided token.
- Client poll tests for stopping FRPC when the token is absent.
- Runtime contract check proving the Phoenix service receives
FRPS_AUTH_TOKEN. - Suggested first workflow command:
/start-feature server-provided-frps-token
ash-api-contract-unification
- Status: planned
- Overview:
- Rework the custom Phoenix controller APIs that represent durable product contracts so they are exposed through Ash actions/resources where practical, allowing OpenAPI generation to become the source of truth for those APIs. Keep explicitly workflow-only endpoints as Phoenix controllers only when Ash would make the contract less clear.
- Requirements:
- Inventory every bespoke route under
/api/v1and/e2eand classify it as resource/action-oriented or workflow-only. - Move resource/action-oriented device, builder, and E2E APIs to Ash-backed actions/resources or Ash JSON:API routes where the behavior maps cleanly.
- Preserve current wire contracts for the Go client, Caddy
check_domain, and E2E harness unless a deliberate versioned contract change is documented. - Generate OpenAPI docs for the migrated Ash-backed APIs and remove duplicate hand-maintained OpenAPI sections when the generated docs cover them fully.
- Keep any remaining hand-written Phoenix controller contracts under
docs/src/reference/openapi/with an explicit reason why they are not Ash-generated. - Constraints:
- Do not break existing Go client registration, heartbeat, command result, or command payload behavior without a versioned migration plan.
- Do not force terminal, Caddy TLS approval, or E2E workflow endpoints into Ash if a controller boundary is clearer or safer.
- Maintain authentication and authorization semantics for device API keys, Caddy/AuthCrunch, and E2E enablement gates.
- Non-goals:
- Replacing Ash JSON:API with a separate OpenAPI generator.
- Converting browser LiveView routes to API routes.
- Changing the public deployment or FRP runtime contract.
- Success criteria:
- Generated OpenAPI covers every API route that is implemented as an Ash-backed product contract.
- Remaining bespoke OpenAPI files only document endpoints that intentionally stay outside Ash, with rationale in the reference docs.
- Client/server integration tests prove the Go client and E2E harness still work against the migrated API surface.
- The Reference section clearly distinguishes generated Ash OpenAPI from any retained bespoke contracts.
- Risks and tradeoffs:
- Some endpoints are protocol workflows rather than CRUD resources, and forcing them into Ash may obscure behavior or complicate error handling.
- Moving stable client APIs can create compatibility risk unless responses, status codes, and auth failures remain byte-for-byte compatible or versioned.
- Dependencies:
packages/server/lib/nixstasis_web/router.expackages/server/lib/nixstasis_web/controllers/device_controller.expackages/server/lib/nixstasis_web/controllers/heartbeat_controller.expackages/server/lib/nixstasis_web/controllers/device_command_controller.expackages/server/lib/nixstasis_web/controllers/e2e_run_controller.expackages/server/lib/nixstasis_web/controllers/e2e_run_result_controller.expackages/server/lib/nixstasis_web/controllers/builder_schema_controller.expackages/server/lib/nixstasis_web/controllers/builder_config_validation_controller.expackages/server/lib/nixstasis/domain.expackages/server/priv/static/openapi.yamldocs/src/reference/openapi/- Suggested validation:
- Diff generated OpenAPI before/after and confirm migrated paths appear in the generated document.
- Run Go client transport tests and server controller/domain tests for each migrated route.
- Run the E2E harness against the migrated
/e2econtract if E2E routes are moved or wrapped by Ash. - Run
mdbook build docsand ensure Reference links describe the final contract source of truth. - Suggested first workflow command:
/start-feature ash-api-contract-unification
authcrunch-role-contract
- Status: planned
- Overview:
- Define the production authorization contract between Caddy/AuthCrunch and the Phoenix application. Document which claims or headers Caddy forwards, how roles/groups map to operator capabilities, and whether LiveView screens use those claims for role-aware behavior.
- Requirements:
- Inventory AuthCrunch-related Caddy configuration, Phoenix request handling, LiveView session data, and any existing role/group assumptions.
- Define the canonical forwarded headers or session fields Phoenix may trust after Caddy/AuthCrunch authentication.
- Define operator roles and the capabilities each role grants for dashboard, devices, remote access, alerts, reports, settings, and E2E surfaces.
- Document behavior for missing, malformed, or insufficient claims.
- Decide whether role-aware UI behavior belongs in LiveView assigns, plugs, policies, or a separate authorization module.
- Update operations docs so deployment operators know which AuthCrunch groups or claims must be configured.
- Constraints:
- Do not weaken the requirement that public browser traffic reaches Phoenix through Caddy/AuthCrunch in the supported deployment.
- Do not treat device API tokens, E2E enablement, or terminal session tokens as substitutes for browser/operator authorization.
- Keep local development shortcuts clearly separate from production role enforcement.
- Non-goals:
- Replacing AuthCrunch as the browser authentication edge.
- Changing the device runtime API authentication contract.
- Implementing a full multi-tenant RBAC product unless the role inventory proves it is required.
- Success criteria:
- Operators can configure AuthCrunch groups/claims and know which Nixstasis capabilities each role enables.
- Phoenix behavior for missing or insufficient role claims is documented and tested.
- The docs clearly distinguish browser/operator authorization from device API, E2E, and terminal-session authentication.
- Risks and tradeoffs:
- Browser authorization rules can drift if they are encoded only in Caddy and not visible to Phoenix UI logic.
- Over-modeling roles too early can add complexity before production operator needs are proven.
- Dependencies:
deploy/compose/caddy/Caddyfilepackages/server/lib/nixstasis_web/router.expackages/server/lib/nixstasis_web/controllers/packages/server/lib/nixstasis_web/live/docs/src/modules/edge-caddy.mddocs/src/client-server-interface.md- Suggested validation:
- Add request/LiveView tests for allowed and denied role scenarios once the contract is implemented.
- Run
mdbook build docsand ensure Architecture, Operations, and Reference pages all point to the final authorization contract. - Suggested first workflow command:
/start-feature authcrunch-role-contract
production-operations-runbooks
- Status: planned
- Overview:
- Add production operations runbooks that go beyond the Compose deployment contract. Cover backup/restore, secret rotation, incident response, monitoring, upgrade checks, and explicit HA/non-HA expectations for the supported deployment shape.
- Requirements:
- Document PostgreSQL backup and restore workflows for bundled and external database modes.
- Document secret rotation procedures for Phoenix secrets, AuthCrunch/OIDC values, JWT key material, FRPS auth/dashboard credentials, and database credentials.
- Document operational health checks for Phoenix, Caddy, FRPS, PostgreSQL, device heartbeat freshness, E2E retention, and remote-access availability.
- Document incident-response playbooks for failed migrations, broken TLS approval, FRPS token exposure, device credential compromise, and E2E retention/log failures.
- Document upgrade and rollback validation steps for Compose services and client release artifacts.
- State HA/scaling boundaries clearly: what the supported Compose deployment does and does not guarantee.
- Constraints:
- Do not imply unsupported HA or clustered deployment semantics unless they are implemented and tested.
- Keep production runbooks separate from local development harness guidance.
- Preserve
deploy/composeas the supported server deployment path. - Non-goals:
- Building a hosted operations platform.
- Replacing operator-specific backup tooling.
- Implementing HA as part of the documentation feature.
- Success criteria:
- A production operator can restore service from backup using documented steps.
- A production operator can rotate each documented secret without guessing which services must restart.
- The docs identify observable symptoms, immediate mitigations, and validation checks for common incidents.
- HA and scaling expectations are explicit rather than implied.
- Risks and tradeoffs:
- Runbooks can become stale if they duplicate scripts without linking to source validation.
- Over-prescriptive backup tooling can conflict with an operator’s managed database platform.
- Dependencies:
deploy/compose/docker-compose.ymldeploy/compose/scripts/check_runtime_contract.shdeploy/compose/README.mddocs/src/modules/deployment-compose.mddocs/src/runtime-boundaries.md- Suggested validation:
- Exercise backup/restore against a disposable Compose stack.
- Run runtime contract checks before and after documented secret rotation steps.
- Run
mdbook build docsand verify Operations navigation points to the new runbooks. - Suggested first workflow command:
/start-feature production-operations-runbooks
rich-api-examples
- Status: planned
- Overview:
- Add example-rich API documentation for maintained HTTP contracts. Provide
representative requests and responses for successful calls, validation
errors, authorization failures, conflict/locking behavior, and important
edge cases across
/api/v1,/e2e, builder APIs, and retained bespoke OpenAPI files. - Requirements:
- Add examples for device registration, pending approval, approved credential
issuance, heartbeat, command delivery, command results, deferred payload
fetches, and Caddy
check_domaindecisions. - Add examples for E2E run creation, idempotent reuse, environment lock conflicts, protocol mismatch, seed failures, result submission, cancellation, and missing/pruned logs.
- Add examples for builder schema option lookup, validation success, validation failure, stale selections, missing schemas, and authorization failures where applicable.
- Add examples for report and alert-rule API surfaces that remain hand-maintained outside generated Ash OpenAPI.
- Keep OpenAPI examples and prose examples synchronized, or link one canonical source from the other.
- Constraints:
- Do not invent behavior that is not implemented or tested.
- Distinguish Ash-generated
/api/jsonexamples from bespoke/api/v1and/e2econtroller examples. - Keep secrets, real tokens, hostnames, and operator data out of examples.
- Non-goals:
- Replacing generated Ash OpenAPI.
- Changing API behavior.
- Creating exhaustive API tutorials for every LiveView browser route.
- Success criteria:
- API consumers can copy representative request/response shapes for each durable runtime contract.
- Error examples cover the common failure classes operators and client authors must handle.
mdbook build docssucceeds and OpenAPI references remain linked from the Reference section.- Risks and tradeoffs:
- Examples can drift unless they are derived from tests or reviewed when controllers change.
- Too many examples can obscure the canonical contract if not organized by API surface.
- Dependencies:
docs/src/client-server-interface.mddocs/src/reference/openapi/packages/server/priv/static/openapi.yamlpackages/server/lib/nixstasis_web/controllers/packages/client/internal/transport/client.go- Suggested validation:
- Compare examples against controller tests and client transport tests.
- Run
mdbook build docsand, where practical, OpenAPI validation for example payloads. - Suggested first workflow command:
/start-feature rich-api-examples
API & Runtime Contracts
This reference collects the durable contract-style documentation that replaced the retired spec-kit contract files.
HTTP APIs
- Architecture Overview: high-level API and authentication surfaces for browser, device, Caddy, Ash JSON:API, and E2E consumers.
- Client-Server Interface: Go client
/api/v1, E2E/e2e, authentication, response shapes, and error handling. - OpenAPI Contracts: maintained OpenAPI definitions for bespoke Phoenix controller APIs that are not generated by Ash.
- Server Web: browser routes, JSON routes, E2E routes, and terminal channel surface.
- Server E2E: E2E run/result behavior and protocol expectations.
Client Contracts
- Client Transport: typed Go client HTTP boundary for registration, heartbeat, command results, and command payloads.
- Client Command Handler: server-issued command execution boundary.
- Client Starlark Runtime:
.staryscript parsing, validation, execution, and telemetry output behavior. - Client FRP Manager: FRPC lifecycle and remote-access token handling.
Server Contracts
- Server Devices: registration, approval, listing, remote access flags, pending commands, and terminal support.
- Server Monitoring: heartbeat, telemetry, alerts, and offline checks.
- Server Reporting: custom report and query builder behavior.
Runtime And Deployment
- Deployment Compose: Compose services, required operator inputs, hostname contract, Caddy ask endpoint, and artifact rules.
- Runtime Boundaries: process, network, data, secret, and test-only boundaries.
Generated OpenAPI
packages/server/priv/static/openapi.yamldocuments the generated Ash JSON:API surface under/api/json.- The bespoke Phoenix controller APIs under
/api/v1and/e2eare documented by the OpenAPI files in OpenAPI Contracts and the human-readable references above; they are not covered by the Ash generated OpenAPI document.
OpenAPI Contracts
The Ash-generated OpenAPI file at packages/server/priv/static/openapi.yaml
documents the Ash JSON:API surface under /api/json. The Phoenix controller
APIs used by the Go client, builder UI, TLS approval, and E2E harness are
bespoke routes, so their contracts live here.
Contracts
- Device API: registration, heartbeat, command results, deferred command payloads, device list filtering, and TLS domain approval.
- Builder API: schema option lookup and builder selection validation.
- Report API: custom report result preview data.
- E2E API: E2E run lifecycle, results, logs, cancellation, and protocol-version requirements.
Maintenance Notes
- Keep these contracts aligned with
docs/src/client-server-interface.mdand the Phoenix router/controller modules. - Do not duplicate Ash JSON:API resources here unless a bespoke
/api/v1or/e2econtroller owns the route. - If an API is reference-only or planned, keep it out of these files until it is part of the final implementation contract.
Task Reference
This page links to retained final-state task records for completed or active features. Superseded historical task lists are intentionally removed from the book.
Core Product
Client And Runtime
Reporting And Builders
Operations And Delivery
- Server-Client E2E Tests
- Packaging And Deployment Migration
- Compose Dev Harness
- Self-Extracting Installer
Agent Workflows
This repository includes native OpenCode workflow commands under:
.opencode/commands/
Available OpenCode commands:
/plan-features/start-feature/review-feature-spec/implement-feature/close-feature/project-alignment-review/project-alignment-execute/project-alignment-land
Intent
These command files execute the repository workflow consistently.
They assume the native OpenCode runtime for execution, including runtime tools
such as task when a command explicitly requires a second-agent review step.
The repository policy still lives in:
docs/src/development.mddocs/src/features/index.mdAGENTS.md
Workflow Roles
/plan-features is for creating, refining, and inspecting the planned feature roadmap.
/start-feature is for opening a feature branch with initial design.md and
tasks.md.
/review-feature-spec is for checking whether a feature spec is
implementation-ready.
/implement-feature is for executing the feature tasks.
/close-feature is for reconciling delivered implementation with the docs.
/project-alignment-review, /project-alignment-execute, and
/project-alignment-land are the deterministic repository-wide alignment
pipeline.
Workflow Mapping
Recommended sequence:
- Use
/plan-featuresto create or inspect the planned feature roadmap. - Use
/start-featurewhen creating a feature. - Use
/review-feature-specbefore major implementation work. - Use
/implement-featureto execute the feature tasks. - Use
/close-featurebefore opening or finalizing the pull request. - Use
/project-alignment-review,/project-alignment-execute, and/project-alignment-landwhen you want the repository-wide deterministic alignment pipeline.
Enforcement Boundary
hk hooks enforce structural repository rules.
The command files help with consistency, but they do not replace human review after the initial release when pull requests are required.
E2E Results
Published E2E run reports are stored separately from the mdBook site and linked here during the docs deployment.
| Ref | Commit | Timestamp | Report |
|---|---|---|---|
main | cbed931 | 2026-05-23T15:40:16Z | Open report |
main | 0142b85 | 2026-05-23T15:01:33Z | Open report |
main | 0b46fa9 | 2026-05-23T14:49:20Z | Open report |
v0.0.3 | 7cc1bb0 | 2026-05-17T14:07:47Z | Open report |
main | 7cc1bb0 | 2026-05-17T14:03:49Z | Open report |
v0.0.3 | 1dd6e9a | 2026-05-17T13:57:30Z | Open report |
main | 1dd6e9a | 2026-05-17T13:53:42Z | Open report |
v0.0.3 | 71ff625 | 2026-05-17T13:23:13Z | Open report |
main | 71ff625 | 2026-05-17T13:18:06Z | Open report |
v0.0.3 | b0ca7bb | 2026-05-17T13:07:13Z | Open report |
main | b0ca7bb | 2026-05-17T13:03:20Z | Open report |
Open the full E2E report index.
Client-Server Interface
Communication Methods
- Go client to Phoenix server:
- HTTP JSON requests under
/api/v1.
- HTTP JSON requests under
- Browser to Phoenix LiveView:
- HTTP for initial requests.
- LiveView WebSocket transport for stateful UI updates.
- Browser terminal to Phoenix:
- Phoenix Channels over WebSocket on topic
terminal:*.
- Phoenix Channels over WebSocket on topic
- Caddy to Phoenix:
- Reverse proxy to
nixstasis:4000. - HTTP ask endpoint for on-demand TLS approval.
- Reverse proxy to
- E2E client to Phoenix:
- HTTP JSON requests under
/e2e.
- HTTP JSON requests under
- Client FRPC to FRPS:
- FRP tunnel protocol through configured FRPS ports.
Rate Limiting
All /api/v1 and /api/json requests are rate-limited per device (or per
remote IP when no device identity is available).
| Scope | Default Limit | Window |
|---|---|---|
Heartbeat (POST .../heartbeat) | 30 requests | 60 seconds |
| Other API requests | 120 requests | 60 seconds |
When the limit is exceeded the server responds with HTTP 429 and body:
{"error": {"code": "rate_limited", "message": "Rate limit exceeded"}}
Limits are configurable via application config (:nixstasis, :rate_limit
keyword list with :limit and :window_ms keys).
Traceable references:
packages/server/lib/nixstasis_web/plugs/rate_limiter.expackages/server/lib/nixstasis_web/rate_limiter_store.ex
Go Client to Phoenix Endpoint Mapping
| Go Method | HTTP Endpoint | Server Handler | Purpose |
|---|---|---|---|
RegisterDevice | POST /api/v1/devices/register | DeviceController.register/2 | Register device and receive UUID |
Poll | POST /api/v1/devices/:id/heartbeat?api_key=... | HeartbeatController.create/2 | Submit telemetry and receive remote-access/command directives |
SendCommandResults | POST /api/v1/devices/:id/command_results?api_key=... | DeviceCommandController.command_results/2 | Acknowledge command execution results |
FetchCommandPayload | GET /api/v1/devices/:id/command_payloads/:ref?api_key=... | DeviceCommandController.command_payload/2 | Fetch deferred command payload |
Traceable references:
packages/client/internal/transport/client.go:84-212packages/server/lib/nixstasis_web/router.ex:58-65packages/server/lib/nixstasis_web/controllers/device_controller.ex:31-37packages/server/lib/nixstasis_web/controllers/heartbeat_controller.ex:7-27packages/server/lib/nixstasis_web/controllers/device_command_controller.ex:6-39
Request and Response Formats
Device Registration
Device registration is the credential issuance boundary. The request identifies the host by MAC address and product metadata; the server may create a pending device record or update an existing record for the same MAC address. Approved devices receive a persistent API token in the response. Pending devices do not receive a token until an operator approves them.
Request:
{
"mac_address": "00:11:22:33:44:55",
"product_name": "atom-001122334455",
"metadata": {
"ip_address": "192.0.2.10",
"client_uuid": "optional-existing-uuid"
}
}
Response shape:
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"api_token": "issued-only-after-approval"
}
}
Traceable references:
packages/client/internal/transport/client.go:86-120docs/src/features/go-client-rewrite/design.md
Heartbeat
Heartbeat requests authenticate with the registration-issued device API token.
Rate-limited devices receive HTTP 429. Heartbeats update the server’s
last_seen_at view of the device, submit telemetry, and return any command or
remote-access directives.
Request:
{
"telemetry": {},
"connection_status": {
"active": true,
"connection_string": "...",
"pid": 1234,
"start_time": "2026-05-06T14:00:00Z"
}
}
Response shape:
{
"data": {
"remote_access_token": "shared-frps-token",
"commands": [
{
"command_id": "...",
"type": "list_scripts",
"args": [],
"payload_ref": "..."
}
]
}
}
Traceable references:
packages/client/internal/transport/client.go:123-187docs/src/features/go-client-rewrite/design.md
Command Results
Command results are correlated by command_id. A heartbeat or command-result
batch that repeats a command identifier should not execute duplicate work; later
duplicates are reported as FAILED with a duplicate_command_id reason.
Request:
{
"results": [
{
"command_id": "...",
"status": "OK",
"output": {}
}
]
}
Response shape:
{
"data": {
"acknowledged_count": 1
}
}
Traceable references:
packages/client/internal/transport/client.go:189-200packages/server/lib/nixstasis_web/controllers/device_command_controller.ex:6-19docs/src/features/go-client-rewrite/design.md
Command Payload
Response shape:
{
"content_type": "...",
"name": "...",
"data": "..."
}
Traceable references:
packages/client/internal/transport/client.go:202-212packages/server/lib/nixstasis_web/controllers/device_command_controller.ex:28-38docs/src/features/go-client-rewrite/design.md
E2E API Mapping
| Endpoint | Handler | Purpose |
|---|---|---|
GET /e2e/suites | E2ERunController.suites/2 | List configured suites |
GET /e2e/runs | E2ERunController.index/2 | List runs |
POST /e2e/runs | E2ERunController.create/2 | Create or reuse run |
GET /e2e/runs/:id | E2ERunController.show/2 | Fetch run |
POST /e2e/runs/:id/cancel | E2ERunController.cancel/2 | Cancel run |
GET /e2e/runs/:id/results | E2ERunResultController.index/2 | List results |
POST /e2e/runs/:id/results | E2ERunResultController.create/2 | Submit results |
GET /e2e/runs/:id/results/:journey_id/log | E2ERunResultController.log/2 | Fetch journey log |
Run creation requires X-E2E-Protocol-Version; protocol version 1 is the
default supported version. Legacy client_version/server_version fields are
not accepted as the version-pairing contract.
Traceable references:
packages/server/lib/nixstasis_web/router.ex:68-79packages/server/lib/nixstasis_web/controllers/e2e_run_controller.ex:7-93README.md:96-116
Authentication and Session Handling
- Browser routes use Phoenix browser pipeline:
fetch_sessionfetch_live_flashprotect_from_forgeryput_secure_browser_headers
- Supported Compose ingress places Caddy/AuthCrunch in front of Phoenix for public hosts.
- Caddy authorization policy injects headers with claims and validates bearer header according to Caddyfile config.
- The high-level split between browser, device, Caddy, Ash JSON:API, and E2E API surfaces is summarized in Architecture Overview.
- Terminal sockets require Phoenix tokens:
- socket token signed for
terminal_socket. - join payload contains an opaque server-side terminal session ref.
- socket token signed for
- Runtime device heartbeat, command-result, and command-payload requests require the registration-issued device token as an
api_keyquery parameter. - E2E routes are gated by
NixstasisWeb.Plugs.E2EEnabled. - Initial device registration does not attach a device API key because it is the credential issuance step.
Traceable references:
packages/server/lib/nixstasis_web/router.ex:4-20deploy/compose/caddy/Caddyfile:25-38packages/server/lib/nixstasis_web/channels/user_socket.ex:37-64packages/server/lib/nixstasis_web/channels/terminal_channel.ex:20-50packages/client/internal/transport/client.go:47-54
Error Handling Patterns
- Go transport treats any unexpected status as
API returned non-success status: <status>. - Go transport allows empty response bodies when a response body target was provided and EOF is returned.
- Runtime device API requests without
api_keyreturn HTTP401with codemissing_api_key. - Runtime device API requests with an invalid
api_keyreturn HTTP401with codeinvalid_api_key. - Heartbeat rejects unapproved devices with HTTP
403and codedevice_not_approved. - Command results without a results list return HTTP
400. - Command-result processing errors return HTTP
422. - Command payload lookup returns HTTP
404for missing payloads. - TLS domain denial returns HTTP
401and{"error":"The host is not permitted"}. - E2E create returns typed error codes in an
errorobject.
Traceable references:
packages/client/internal/transport/client.go:37-82packages/server/lib/nixstasis_web/controllers/heartbeat_controller.ex:16-25packages/server/lib/nixstasis_web/controllers/device_command_controller.ex:15-37packages/server/lib/nixstasis_web/controllers/tls_controller.ex:21-27packages/server/lib/nixstasis_web/controllers/e2e_run_controller.ex:33-62
Versioning Strategy
- Device API is documented by this interface page and transport/controller tests.
- E2E API run creation requires protocol version header
X-E2E-Protocol-Version. - E2E JSONL logs use schema
e2e_log.v1according to README. - Repository tooling currently installs Go
1.26.2throughmise.toml; the client module target isgo 1.26ingo.mod.
Traceable references:
docs/src/features/go-client-rewrite/design.mdpackages/server/lib/nixstasis_web/controllers/e2e_run_controller.ex:7-25README.md:123-135packages/client/go.mod:1-13