Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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-17
  • packages/client/README.md:1-12
  • packages/server/README.md:1-17
  • deploy/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 /e2e endpoints 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-schemas
    • GET /api/v1/builder-schemas/:schema_id/versions/:schema_version/options
    • POST /api/v1/builder-configurations/validate
    • GET /api/v1/devices
    • POST /api/v1/devices/register
    • POST /api/v1/devices/:device_id/heartbeat
    • POST /api/v1/devices/:device_id/command_results
    • GET /api/v1/devices/:device_id/command_payloads/:ref
    • GET /api/v1/check_domain
  • Ash JSON:API routes are forwarded under /api/json through NixstasisWeb.AshJsonApiRouter.
  • E2E routes are under /e2e and use NixstasisWeb.Plugs.E2EEnabled.

LiveView Entry Points

  • NixstasisWeb.DashboardLive.Index
  • NixstasisWeb.DeviceLive.Index
  • NixstasisWeb.DeviceLive.Show
  • NixstasisWeb.AlertLive.Index
  • NixstasisWeb.AlertLive.Rules
  • NixstasisWeb.ReportLive.Index
  • NixstasisWeb.ReportLive.Show
  • NixstasisWeb.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.go
  • packages/client/cmd/nixstasis/register.go
  • packages/client/cmd/nixstasis/poll.go
  • packages/client/cmd/nixstasis/script.go
  • packages/client/cmd/nixstasis/install_script.go
  • packages/client/cmd/nixstasis/list_scripts.go
  • packages/client/cmd/nixstasis/remove_script.go
  • packages/client/cmd/nixstasis/test_script.go
  • packages/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.
  • Client application:
    • Go CLI binary nixstasis using Cobra.
    • Device registration, telemetry polling, script execution, command handling, and FRP lifecycle management.
    • Embedded Starlark runtime for telemetry scripts.
  • 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.yml defines nixstasis, caddy, frps, and optional postgres services.

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 at nixstasis: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 frpc when the server heartbeat response includes a non-empty remote_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-side Nixstasis.Devices.SshClient opens 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_domain from inside the Compose network.
  • Ash JSON:API routes live under /api/json and have generated OpenAPI in packages/server/priv/static/openapi.yaml.
  • E2E harness APIs live under /e2e and are gated by NixstasisWeb.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-75
  • deploy/compose/docker-compose.yml:1-90
  • packages/client/internal/transport/client.go:84-212
  • packages/client/cmd/nixstasis/poll.go:128-154
  • packages/server/lib/nixstasis_web/router.ex:50-79
  • packages/server/lib/nixstasis_web/live/device_live/show.ex:57-80
  • packages/server/lib/nixstasis_web/channels/terminal_channel.ex:20-50
  • packages/server/lib/nixstasis/devices/ssh_client.ex:26-63

System Boundaries

  • HTTP boundary:
    • NixstasisWeb.Endpoint and NixstasisWeb.Router expose browser, API, JSON:API, channel, and E2E surfaces.
  • Domain boundary:
    • Nixstasis.Domain defines 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.Client is the Go client HTTP boundary to Phoenix.
    • internal/script.Runtime is the Starlark execution boundary.
    • internal/frp.Manager is the process boundary for frpc.
  • 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, optional Nixstasis.E2E.RetentionWorker, DNSCluster, Phoenix.PubSub, Nixstasis.Monitoring.OfflineChecker, and NixstasisWeb.Endpoint.
  • GenServers present in application code:
    • Nixstasis.Monitoring.OfflineChecker
    • Nixstasis.E2E.RetentionWorker
    • Nixstasis.Devices.SshClient

Traceable references:

  • packages/server/lib/nixstasis/application.ex:9-29
  • packages/server/lib/nixstasis/monitoring/offline_checker.ex:1-32
  • packages/server/lib/nixstasis/e2e/retention_worker.ex:1-51
  • packages/server/lib/nixstasis/devices/ssh_client.ex:1-125

LiveView Interaction Model

  • Browser routes use the :browser pipeline with session fetch, LiveView flash, CSRF protection, and secure browser headers.
  • LiveViews implement mount/3, handle_params/3, and handle_event/3 for 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.

Traceable references:

  • packages/server/lib/nixstasis_web/router.ex:4-11
  • packages/server/lib/nixstasis_web/live/device_live/index.ex
  • packages/server/lib/nixstasis_web/live/device_live/show.ex
  • packages/server/lib/nixstasis_web/live/alerts/index_live.ex
  • packages/server/lib/nixstasis_web/live/reports/index_live.ex

Ash Domain Layering

  • Nixstasis.Domain uses Ash.Domain with AshJsonApi.Domain and AshPhoenix extensions.
  • 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, and list_custom_reports.
  • Context modules call Nixstasis.Domain functions and Ash queries to implement application behavior.

Traceable references:

  • packages/server/lib/nixstasis/domain.ex:1-122
  • packages/server/lib/nixstasis/devices.ex
  • packages/server/lib/nixstasis/monitoring.ex
  • packages/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/v1 device 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 optional postgres.
    • 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, and packages/frp.
  • Cross-cutting feature docs and repository automation live under docs/src/features and .github.

Runtime Boundaries

Process Boundaries

Elixir/OTP Processes

  • Nixstasis.Application starts the OTP supervision tree.
  • NixstasisWeb.Endpoint owns HTTP, WebSocket, LiveView, and Channel request handling.
  • Nixstasis.Repo owns database connections.
  • Phoenix.PubSub is supervised as Nixstasis.PubSub.
  • Nixstasis.Monitoring.OfflineChecker is a named GenServer that schedules :check messages every 60 seconds.
  • Nixstasis.E2E.RetentionWorker is a named GenServer that schedules E2E retention pruning.
  • Nixstasis.Devices.SshClient is a GenServer per terminal session and wraps an OS ssh process through an Elixir Port.

Traceable references:

  • packages/server/lib/nixstasis/application.ex:10-29
  • packages/server/lib/nixstasis/monitoring/offline_checker.ex:13-31
  • packages/server/lib/nixstasis/e2e/retention_worker.ex:14-50
  • packages/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 for script test and script repl command paths.
  • runMain starts a Go runtime flight recorder before executing the root command.
  • poll creates a ticker from configured poll interval and repeatedly calls pollOnce.
  • script.Executor runs discovered scripts concurrently with goroutines and a sync.WaitGroup.
  • commands.Handler executes batches concurrently where command type allows it.
  • frp.Manager launches a nixstasis-frpc transient systemd unit with systemd-run; that unit runs the hidden nixstasis frp-session subcommand, which starts the bundled frpc process with a one-hour timeout.

Traceable references:

  • packages/client/cmd/nixstasis/main.go:20-98
  • packages/client/cmd/nixstasis/poll.go:35-83
  • packages/client/internal/script/executor.go:23-48
  • packages/client/internal/commands/handler.go:27-76
  • packages/client/internal/frp/manager.go
  • packages/client/cmd/nixstasis/frp_session.go

Starlark Execution Environment

  • script.Runtime executes Starlark scripts using go.starlark.net/starlark.
  • Runtime builtins include pub_and_get, exec_cmd, and json.
  • Runtime.Execute creates a Starlark thread named stary and 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-47
  • packages/client/internal/script/runtime.go:73-128
  • packages/client/internal/script/runtime.go:130-179
  • packages/client/internal/script/builtins_exec.go
  • packages/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/json forwarded to NixstasisWeb.AshJsonApiRouter.
  • E2E API inputs enter through /e2e routes 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-79
  • packages/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_cmd is 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-93
  • packages/client/internal/script/runtime.go:40-44
  • packages/client/internal/commands/handler.go:132-187
  • packages/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 ssh with an ncat HTTP 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 localhost as the base domain and Caddy internal/local certificates for TLS.
  • The client container 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-66
  • deploy/compose/frps/frps.toml:1-15
  • deploy/compose/caddy/Caddyfile:59-75
  • packages/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 to nixstasis:4000.
  • Caddy on-demand TLS asks Phoenix at http://nixstasis:4000/api/v1/check_domain.
  • Compose publishes only Caddy ports 80 and 443 for the main HTTP ingress.
  • Default laptop mode maps the same host pattern to .localhost names: nixstasis.localhost, auth.localhost, frp-admin.localhost, and atom-<normalized-device-id>.localhost.
  • Laptop mode also publishes Phoenix on 127.0.0.1:4000 for local-only validation diagnostics; deployment-shaped browser access still goes through Caddy.

Traceable references:

  • deploy/compose/caddy/Caddyfile:8-10
  • deploy/compose/caddy/Caddyfile:50-57
  • deploy/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.com as the public Caddy host.

Traceable references:

  • packages/client/internal/config/config.go:62-64
  • packages/client/README.md:115-128
  • packages/client/internal/transport/client.go:27-35

Internal Services

  • nixstasis service listens on PORT=4000 and publishes it to the host for dev-lab and CI access; Caddy is the production HTTP(S) ingress.
  • postgres is always included; production can override DATABASE_URL to use an external managed database.
  • frps is reached by Caddy on internal service ports and by FRPC on published FRP ports.

Traceable references:

  • deploy/compose/docker-compose.yml:1-129
  • deploy/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
  1. Operator or service invokes nixstasis register.
  2. Client detects primary MAC and IP through internal/identity.
  3. Client generates a device name from the MAC address.
  4. Client sends POST /api/v1/devices/register with mac_address, optional product_name, and optional metadata.
  5. Phoenix DeviceController.register/2 calls Nixstasis.Devices.register_device/1.
  6. Devices.register_device/1 validates any supplied schema definition and calls Nixstasis.Domain.register_device/1.
  7. Server responds 201 with data.id and includes data.api_token when the device is approved.
  8. Client stores UUID through identity.Store.SaveUUID at config.IdentityPath() and uses the issued token for runtime API calls.

Traceable references:

  • packages/client/cmd/nixstasis/register.go:28-93
  • packages/client/internal/transport/client.go:84-121
  • packages/server/lib/nixstasis_web/controllers/device_controller.ex:31-37
  • packages/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
  1. Client invokes nixstasis poll.
  2. Client loads stored UUID from /etc/nixstasis/id.
  3. Client creates:
    • HTTP transport client.
    • Starlark script executor.
    • FRP manager.
    • server-command handler.
  4. Client runs pollOnce immediately and then on the configured ticker interval.
  5. pollOnce re-detects MAC/IP identity details.
  6. Client discovers scripts from configured script directory.
  7. Client executes latest script versions and collects script reports/errors.
  8. Client reads current FRP status.
  9. Client sends POST /api/v1/devices/:uuid/heartbeat?api_key=... with telemetry and connection status.
  10. Phoenix HeartbeatController.create/2 loads device and requires approval_status == :approved.
  11. Nixstasis.Monitoring.heartbeat/2 updates last_seen_at, persists telemetry, evaluates rules, and pops pending commands.
  12. Server returns optional remote_access_token and optional command list.
  13. Client hydrates deferred command payloads, executes commands, and posts command results.
  14. Client starts, stops, or restarts FRPC according to remote_access_token and current FRP status.

Traceable references:

  • packages/client/cmd/nixstasis/poll.go:35-157
  • packages/client/internal/script/executor.go:23-93
  • packages/client/internal/transport/client.go:170-212
  • packages/server/lib/nixstasis_web/controllers/heartbeat_controller.ex:7-27
  • packages/server/lib/nixstasis/monitoring.ex:15-28

Command Delivery and Results

  1. Server-side code queues a command with Nixstasis.Devices.queue_command/2.
  2. On heartbeat, Nixstasis.Devices.pop_pending_commands/1 claims queued commands transactionally.
  3. Heartbeat response serializes commands to the client.
  4. Client receives commands in PollResponse.Commands.
  5. Client fetches any deferred payload with GET /api/v1/devices/:uuid/command_payloads/:ref.
  6. Client command handler executes supported commands: list_scripts, install_script, remove_script, and ssh_authorize.
  7. Client posts results to POST /api/v1/devices/:uuid/command_results?api_key=....
  8. Phoenix DeviceCommandController.command_results/2 calls Devices.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 400 from server.
  • Invalid command results return HTTP 422 from server.

Traceable references:

  • packages/server/lib/nixstasis/devices.ex:265-348
  • packages/client/cmd/nixstasis/poll.go:198-249
  • packages/client/internal/commands/handler.go:27-230
  • packages/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
  1. Browser opens /devices/:id.
  2. DeviceLive.Show.handle_params/3 loads device.
  3. If device is online, setup_device_view/3 sets remote_access_requested to true when not already requested.
  4. Next client heartbeat receives a non-empty remote_access_token.
  5. Client starts FRPC through the FRP manager when FRP is inactive.
  6. FRPC connects to FRPS with rendered configuration and the heartbeat-provided token.
  7. Caddy wildcard host routes *.{$BASE_DOMAIN} to FRPS HTTP vhost port.
  8. When LiveView terminates, DeviceLive.Show.terminate/2 sets remote_access_requested to false.
  9. 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-31
  • packages/server/lib/nixstasis_web/live/device_live/show.ex:93-145
  • packages/client/cmd/nixstasis/poll.go:138-154
  • packages/client/internal/frp/manager.go:47-169
  • deploy/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
  1. Browser triggers start_ssh_session in DeviceLive.Show.
  2. Server generates an SSH key pair with SshKeyManager.generate_key_pair/0.
  3. Server queues an ssh_authorize command for the device containing the public key.
  4. Server stores private key material behind an opaque terminal session ref and signs a Phoenix socket token containing the device ID.
  5. Browser connects to UserSocket with socket token.
  6. Browser joins topic terminal:<device_id> with the terminal session ref.
  7. TerminalChannel.join/3 resolves the session ref, verifies device binding, and starts Nixstasis.Devices.SshClient.
  8. SshClient writes private key to a temp file and opens an ssh Port using ncat as HTTP proxy to the FRP TCP mux endpoint.
  9. Browser input is sent to SshClient.send_data/2.
  10. SSH process output is pushed back as channel output events.
  11. Session stops on SSH exit, idle timeout, or max duration.

Traceable references:

  • packages/server/lib/nixstasis_web/live/device_live/show.ex:57-80
  • packages/server/lib/nixstasis_web/channels/user_socket.ex:37-64
  • packages/server/lib/nixstasis_web/channels/terminal_channel.ex:20-113
  • packages/server/lib/nixstasis/devices/ssh_client.ex:17-94

LiveView Event Cycle

  1. Browser requests a LiveView route through Phoenix :browser pipeline.
  2. LiveView mount/3 initializes socket assigns.
  3. LiveView handle_params/3 loads route-specific data when present.
  4. Browser events invoke handle_event/3 callbacks.
  5. Callback updates assigns, streams, flash, or navigation state.
  6. 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-45
  • packages/server/lib/nixstasis_web/live/device_live/index.ex
  • packages/server/lib/nixstasis_web/live/device_live/show.ex
  • packages/server/lib/nixstasis_web/live/alerts/index_live.ex
  • packages/server/lib/nixstasis_web/live/reports/index_live.ex
  • packages/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 --> [*]
  1. Client E2E runner loads config and journey specs.
  2. Client sends POST /e2e/runs with X-E2E-Protocol-Version.
  3. Server validates legacy fields, environment policy, protocol version, suite/journey selection, and action/expect registrations.
  4. Server enforces idempotency for (environment_label, idempotency_key).
  5. Server acquires an environment lock for new runs.
  6. Server runs the configured seed script.
  7. Server persists run and queued journey result rows.
  8. Client executes journeys and writes JSONL logs.
  9. Client submits results to POST /e2e/runs/:id/results.
  10. Server updates journey result rows, computes aggregate status, and releases environment lock on final status.
  11. Logs are fetched through GET /e2e/runs/:id/results/:journey_id/log.
  12. Retention worker periodically prunes old runs/logs according to retention policy.

Observable error paths:

  • 409 environment_locked for overlapping active environment runs.
  • 422 protocol_mismatch for invalid protocol version.
  • 400 invalid_action_expectation for unregistered action/expect pairs.
  • 422 seed_failed for seed failures.
  • 410 log_unavailable semantics for missing/pruned logs, as documented in README.

Traceable references:

  • README.md:96-135
  • packages/server/lib/nixstasis/e2e.ex:61-100
  • packages/server/lib/nixstasis/e2e.ex:201-227
  • packages/server/lib/nixstasis/e2e.ex:320-407
  • packages/server/lib/nixstasis_web/controllers/e2e_run_controller.ex:19-62
  • packages/server/lib/nixstasis/e2e/retention_worker.ex:25-40

Modules

This section documents repository modules by runtime context.

Server Modules

Client Modules

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.exs
  • packages/server/lib/nixstasis/application.ex
  • packages/server/lib/nixstasis/repo.ex
  • packages/server/lib/nixstasis_web/endpoint.ex
  • packages/server/lib/nixstasis_web/telemetry.ex
  • packages/server/config/config.exs
  • packages/server/config/runtime.exs

Public Interfaces

  • Nixstasis.Application.start/2
  • Nixstasis.Application.config_change/3
  • Mix aliases:
    • mix setup
    • mix ecto.setup
    • mix phx.server
    • mix test
    • mix openapi.generate
    • mix precommit

Dependencies

Internal

  • NixstasisWeb.Telemetry
  • Nixstasis.Repo
  • Nixstasis.E2E.RetentionWorker
  • Nixstasis.Monitoring.OfflineChecker
  • NixstasisWeb.Endpoint

External

  • Phoenix
  • Phoenix LiveView
  • Ecto/PostgreSQL
  • Ash/AshPostgres/AshJsonApi/AshPhoenix
  • Bandit
  • DNSCluster
  • Telemetry

Runtime Notes

  • Nixstasis.E2E.RetentionWorker is included only when E2E retention is enabled in app config.
  • The supervision strategy is :one_for_one with supervisor name Nixstasis.Supervisor.

Traceable references:

  • packages/server/mix.exs:1-113
  • packages/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.ex
  • packages/server/lib/nixstasis/devices/device.ex
  • packages/server/lib/nixstasis/devices/pending_command.ex
  • packages/server/lib/nixstasis/monitoring/alert.ex
  • packages/server/lib/nixstasis/monitoring/alert_rule.ex
  • packages/server/lib/nixstasis/monitoring/telemetry.ex
  • packages/server/lib/nixstasis/reporting/custom_report.ex
  • packages/server/lib/nixstasis/system_setting.ex
  • packages/server/lib/nixstasis_web/ash_json_api_router.ex
  • packages/server/priv/static/openapi.yaml

Public Interfaces

  • Ash domain APIs defined in Nixstasis.Domain:
    • list_devices
    • get_device
    • get_device_by_mac
    • create_device
    • register_device
    • update_device
    • destroy_device
    • list_pending_commands
    • create_pending_command
    • update_pending_command
    • destroy_pending_command
    • list_alerts
    • create_alert
    • update_alert
    • destroy_alert
    • list_rules
    • get_rule
    • create_rule
    • update_rule
    • destroy_rule
    • list_telemetry_events
    • create_telemetry_event
    • list_custom_reports
    • get_custom_report
    • create_custom_report
    • update_custom_report
    • destroy_custom_report
    • get_setting_by_key
    • create_setting
    • update_setting

Dependencies

Internal

  • Ash resources under Nixstasis.Devices, Nixstasis.Monitoring, Nixstasis.Reporting, and Nixstasis.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-122
  • packages/server/lib/nixstasis_web/router.ex:22-28
  • packages/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.ex
  • packages/server/lib/nixstasis_web/endpoint.ex
  • packages/server/lib/nixstasis_web/controllers/*.ex
  • packages/server/lib/nixstasis_web/live/**/*.ex
  • packages/server/lib/nixstasis_web/channels/user_socket.ex
  • packages/server/lib/nixstasis_web/channels/terminal_channel.ex
  • packages/server/lib/nixstasis_web/components/*.ex
  • packages/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-schemas
  • GET /api/v1/builder-schemas/:schema_id/versions/:schema_version/options
  • POST /api/v1/builder-configurations/validate
  • GET /api/v1/devices
  • POST /api/v1/devices/register
  • POST /api/v1/devices/:device_id/heartbeat
  • POST /api/v1/devices/:device_id/command_results
  • GET /api/v1/devices/:device_id/command_payloads/:ref
  • GET /api/v1/reports/:id/results
  • GET /api/v1/check_domain

E2E Routes

  • GET /e2e/suites
  • GET /e2e/runs
  • POST /e2e/runs
  • GET /e2e/runs/:id
  • POST /e2e/runs/:id/cancel
  • GET /e2e/runs/:id/results
  • POST /e2e/runs/:id/results
  • GET /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.Devices
  • Nixstasis.Monitoring
  • Nixstasis.Reporting
  • Nixstasis.E2E
  • Nixstasis.Deployment
  • NixstasisWeb.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/:id LiveView 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 /e2e routes with X-E2E-Protocol-Version on run creation.

Traceable references:

  • packages/server/lib/nixstasis_web/router.ex:1-117
  • packages/server/lib/nixstasis_web/channels/user_socket.ex:37-64
  • packages/server/lib/nixstasis_web/channels/terminal_channel.ex:20-113
  • packages/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.ex
  • packages/server/lib/nixstasis/devices/device.ex
  • packages/server/lib/nixstasis/devices/pending_command.ex
  • packages/server/lib/nixstasis/devices/schema_validator.ex
  • packages/server/lib/nixstasis/devices/ssh_key_manager.ex
  • packages/server/lib/nixstasis/devices/ssh_client.ex
  • packages/server/lib/nixstasis_web/controllers/device_controller.ex
  • packages/server/lib/nixstasis_web/controllers/heartbeat_controller.ex
  • packages/server/lib/nixstasis_web/controllers/device_command_controller.ex
  • packages/server/lib/nixstasis_web/live/device_live/index.ex
  • packages/server/lib/nixstasis_web/live/device_live/show.ex

Public Interfaces

  • Context functions:
    • Nixstasis.Devices.count_all/0
    • Nixstasis.Devices.count_by_status/1
    • Nixstasis.Devices.count_pending_approvals/0
    • Nixstasis.Devices.register_device/1
    • Nixstasis.Devices.update_last_seen/1
    • Nixstasis.Devices.list_pending_devices/0
    • Nixstasis.Devices.approve_device/1
    • Nixstasis.Devices.list_devices/1
    • Nixstasis.Devices.requesting_remote_access?/1
    • Nixstasis.Devices.approve_devices/1
    • Nixstasis.Devices.reject_devices/1
    • Nixstasis.Devices.set_remote_access/2
    • Nixstasis.Devices.get_device!/1
    • Nixstasis.Devices.create_device/1
    • Nixstasis.Devices.update_device/2
    • Nixstasis.Devices.delete_device/1
    • Nixstasis.Devices.change_device/2
    • Nixstasis.Devices.queue_command/2
    • Nixstasis.Devices.pop_pending_commands/1
    • Nixstasis.Devices.acknowledge_command_results/2
    • Nixstasis.Devices.get_command_payload/2
    • Nixstasis.Devices.online?/1
  • GenServer/process interfaces:
    • Nixstasis.Devices.SshClient.start_link/1
    • Nixstasis.Devices.SshClient.send_data/2
    • Nixstasis.Devices.SshClient.ssh_host/1

Dependencies

Internal

  • Nixstasis.Domain
  • Nixstasis.Repo
  • Nixstasis.Devices.Device
  • Nixstasis.Devices.PendingCommand
  • Nixstasis.Devices.SchemaValidator

External

  • Ash
  • AshPhoenix
  • Ecto/PostgreSQL
  • Elixir Port for ssh
  • ncat through SSH ProxyCommand

Client-Server Interaction Details

  • POST /api/v1/devices/register calls Devices.register_device/1.
  • POST /api/v1/devices/:device_id/heartbeat calls Monitoring.heartbeat/2, which updates last seen and returns pending commands.
  • POST /api/v1/devices/:device_id/command_results calls Devices.acknowledge_command_results/2.
  • GET /api/v1/devices/:device_id/command_payloads/:ref calls Devices.get_command_payload/2.
  • Device detail LiveView queues ssh_authorize commands before terminal session startup.
  • Device detail is reached through /devices/:id; opening remote-access tabs may set remote_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-360
  • packages/server/lib/nixstasis_web/controllers/device_controller.ex:31-65
  • packages/server/lib/nixstasis_web/controllers/heartbeat_controller.ex:7-27
  • packages/server/lib/nixstasis_web/controllers/device_command_controller.ex:6-39
  • packages/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.ex
  • packages/server/lib/nixstasis/monitoring/offline_checker.ex
  • packages/server/lib/nixstasis/monitoring/rule_evaluator.ex
  • packages/server/lib/nixstasis/monitoring/telemetry.ex
  • packages/server/lib/nixstasis/monitoring/alert.ex
  • packages/server/lib/nixstasis/monitoring/alert_rule.ex
  • packages/server/lib/nixstasis_web/live/alerts/index_live.ex
  • packages/server/lib/nixstasis_web/live/alerts/rules_live.ex

Public Interfaces

  • Nixstasis.Monitoring.heartbeat/2
  • Nixstasis.Monitoring.check_offline_devices/1
  • Nixstasis.Monitoring.evaluate_telemetry/2
  • Nixstasis.Monitoring.list_rules/0
  • Nixstasis.Monitoring.get_rule!/1
  • Nixstasis.Monitoring.create_rule/1
  • Nixstasis.Monitoring.update_rule/2
  • Nixstasis.Monitoring.delete_rule/1
  • Nixstasis.Monitoring.list_rules_for_product/1
  • Nixstasis.Monitoring.OfflineChecker.start_link/1

Dependencies

Internal

  • Nixstasis.Devices
  • Nixstasis.Domain
  • Nixstasis.Settings
  • Nixstasis.Monitoring.RuleEvaluator
  • Nixstasis.Monitoring.Alert
  • Nixstasis.Monitoring.AlertRule

External

  • Ash
  • GenServer

Client-Server Interaction Details

  • Heartbeat controller passes client telemetry and connection status into Monitoring.heartbeat/2.
  • Monitoring.heartbeat/2 updates device last_seen_at, persists telemetry, evaluates rules, and returns queued commands to the client.
  • Offline checking uses Settings.get_offline_window/0 and runs periodically through OfflineChecker.
  • 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-148
  • packages/server/lib/nixstasis/monitoring/offline_checker.ex:1-32
  • packages/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.ex
  • packages/server/lib/nixstasis/reporting/custom_report.ex
  • packages/server/lib/nixstasis/reporting/query_builder.ex
  • packages/server/lib/nixstasis/reporting/table_filters.ex
  • packages/server/lib/nixstasis_web/live/reports/index_live.ex
  • packages/server/lib/nixstasis_web/live/reports/show_live.ex
  • packages/server/lib/nixstasis_web/live/reports/form_component.ex

Public Interfaces

  • Nixstasis.Reporting.list_custom_reports/0
  • Nixstasis.Reporting.list_custom_reports_with_view/1
  • Nixstasis.Reporting.get_custom_report!/1
  • Nixstasis.Reporting.create_custom_report/1
  • Nixstasis.Reporting.custom_report_name_taken?/1
  • Nixstasis.Reporting.update_custom_report/2
  • Nixstasis.Reporting.delete_custom_report/1
  • Nixstasis.Reporting.save_view_preferences/3
  • Nixstasis.Reporting.load_view_preferences/2
  • Nixstasis.Reporting.change_custom_report/2

Dependencies

Internal

  • Nixstasis.Domain
  • Nixstasis.Repo
  • Nixstasis.Reporting.CustomReport
  • Nixstasis.Reporting.TableFilters

External

  • Ecto.Query
  • AshPhoenix
  • Postgres-backed report_view_preferences table 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-200
  • packages/server/lib/nixstasis/domain.ex:52-58
  • packages/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.ex
  • packages/server/lib/nixstasis/e2e/run.ex
  • packages/server/lib/nixstasis/e2e/run_result.ex
  • packages/server/lib/nixstasis/e2e/protocol.ex
  • packages/server/lib/nixstasis/e2e/journey_selection.ex
  • packages/server/lib/nixstasis/e2e/expectation_registry.ex
  • packages/server/lib/nixstasis/e2e/environment_locks.ex
  • packages/server/lib/nixstasis/e2e/log_store.ex
  • packages/server/lib/nixstasis/e2e/retention_worker.ex
  • packages/server/lib/nixstasis_web/controllers/e2e_run_controller.ex
  • packages/server/lib/nixstasis_web/controllers/e2e_run_result_controller.ex
  • packages/server/lib/nixstasis_web/plugs/e2e_enabled.ex
  • packages/server/lib/nixstasis_web/live_dashboard/e2e_page.ex
  • packages/server/lib/mix/tasks/e2e.export_static.ex

Public Interfaces

  • Nixstasis.E2E.list_runs/0
  • Nixstasis.E2E.list_suites/0
  • Nixstasis.E2E.get_run!/1
  • Nixstasis.E2E.get_run/1
  • Nixstasis.E2E.prune_retention/1
  • Nixstasis.E2E.list_results/1
  • Nixstasis.E2E.create_run/1
  • Nixstasis.E2E.cancel_run/1
  • Nixstasis.E2E.delete_runs/1
  • Nixstasis.E2E.record_result/3
  • Nixstasis.E2E.submit_results/2
  • Nixstasis.E2E.store_log/3
  • Nixstasis.E2E.fetch_result_log/2
  • Nixstasis.E2E.RetentionWorker.start_link/1

Dependencies

Internal

  • Nixstasis.E2E.DataPolicy
  • Nixstasis.E2E.EnvironmentLocks
  • Nixstasis.E2E.ExpectationRegistry
  • Nixstasis.E2E.JourneySelection
  • Nixstasis.E2E.LogStore
  • Nixstasis.E2E.Protocol
  • Nixstasis.E2E.Run
  • Nixstasis.E2E.RunResult
  • Nixstasis.Repo

External

  • Ecto.Query
  • GenServer
  • Phoenix Controller rendering
  • LiveDashboard extension page

Client-Server Interaction Details

  • POST /e2e/runs reads X-E2E-Protocol-Version, validates protocol/environment/suite/journeys/action-expect pairs, runs configured seed script, creates run rows, and returns 201 on success.
  • Run creation can return typed errors including environment_locked, protocol_mismatch, invalid_action_expectation, seed_failed, invalid_request, and database_error.
  • POST /e2e/runs/:id/results stores journey outcomes and updates run status.
  • GET /e2e/runs/:id/results/:journey_id/log retrieves log content or typed log-unavailable errors.
  • Production deployments disable E2E endpoints by default through NixstasisWeb.Plugs.E2EEnabled unless NIXSTASIS_E2E_ENABLED=true.

Traceable references:

  • packages/server/lib/nixstasis/e2e.ex:1-420
  • packages/server/lib/nixstasis_web/controllers/e2e_run_controller.ex:1-93
  • packages/server/lib/nixstasis_web/router.ex:68-79
  • deploy/compose/README.md:12-14

Client CLI

Language

  • Go.

Runtime Context

  • Client compiled binary.
  • Cobra-based CLI command tree.

Purpose

  • Provides the nixstasis executable for registration, polling, and script management.

Key Files

  • packages/client/cmd/nixstasis/main.go
  • packages/client/cmd/nixstasis/register.go
  • packages/client/cmd/nixstasis/poll.go
  • packages/client/cmd/nixstasis/script.go
  • packages/client/cmd/nixstasis/install_script.go
  • packages/client/cmd/nixstasis/list_scripts.go
  • packages/client/cmd/nixstasis/remove_script.go
  • packages/client/cmd/nixstasis/test_script.go
  • packages/client/cmd/nixstasis/repl.go

Public Interfaces

  • CLI commands:
    • nixstasis register
    • nixstasis poll
    • nixstasis script install <path>
    • nixstasis script list
    • nixstasis script remove
    • nixstasis script test
    • nixstasis script repl
  • Go functions:
    • main
    • runMain
    • run
    • shouldSkipConfig
    • runRegister
    • runPoll
    • pollOnce
    • pollInterval

Dependencies

Internal

  • internal/config
  • internal/logging
  • internal/identity
  • internal/transport
  • internal/script
  • internal/frp
  • internal/commands
  • internal/telemetry

External

  • github.com/spf13/cobra
  • github.com/spf13/viper
  • Go runtime/trace flight recorder.

Client-Server Interaction Details

  • register calls the transport client registration endpoint.
  • poll sends 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-98
  • packages/client/cmd/nixstasis/register.go:16-93
  • packages/client/cmd/nixstasis/poll.go:21-249
  • packages/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.go
  • packages/client/internal/transport/register_test.go
  • packages/client/internal/transport/client_runtime_test.go
  • docs/src/client-server-interface.md

Public Interfaces

  • Types:
    • Client
    • PollRequest
    • CommandStatus
    • CommandRequest
    • CommandPayload
    • CommandResult
    • PollResponse
    • CommandResultsRequest
  • Constants:
    • CommandStatusOK
    • CommandStatusFailed
  • Functions and methods:
    • NewClient
    • (*Client).RegisterDevice
    • (*Client).Poll
    • (*Client).SendCommandResults
    • (*Client).FetchCommandPayload

Dependencies

Internal

  • internal/config
  • internal/frp
  • internal/identity
  • internal/telemetry

External

  • Go net/http
  • Go experimental encoding/json/v2

Client-Server Interaction Details

  • RegisterDevice:
    • POST {baseURL}/api/v1/devices/register
    • Sends mac_address, optional product_name, and optional metadata.
    • Expects 201 and response data.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 telemetry and connection_status.
    • Requires the issued device token as api_key query parameter.
    • Expects 200 or 202 and optional response data.remote_access_token plus optional data.commands.
    • HTTP 429 indicates the server rate limit rejected the heartbeat.
  • SendCommandResults:
    • POST {baseURL}/api/v1/devices/{uuid}/command_results
    • Sends results array.
    • Requires the issued device token as api_key query parameter.
    • Expects 200 or 202.
  • FetchCommandPayload:
    • GET {baseURL}/api/v1/devices/{uuid}/command_payloads/{ref}
    • Requires the issued device token as api_key query parameter.
    • Expects 200 and a CommandPayload.

Traceable references:

  • packages/client/internal/transport/client.go:21-212
  • docs/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.go
  • packages/client/internal/identity/detect.go
  • packages/client/internal/identity/store.go
  • packages/client/internal/identity/detect_test.go
  • packages/client/internal/identity/store_test.go
  • packages/client/cmd/nixstasis/register.go
  • packages/client/cmd/nixstasis/poll.go
  • packages/client/internal/config/config.go

Public Interfaces

  • Types:
    • DeviceIdentity
    • Credentials
    • Store
  • Functions and methods:
    • GetPrimaryMAC
    • GetPrimaryIP
    • GenerateDeviceName
    • NewStore
    • (*Store).Load
    • (*Store).LoadUUID
    • (*Store).Save
    • (*Store).SaveUUID
    • config.IdentityPath

Dependencies

Internal

  • internal/config
  • internal/transport

External

  • Go standard library networking and filesystem APIs.

Client-Server Interaction Details

  • register detects MAC/IP and sends identity data to POST /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.
  • poll loads stored credentials from /etc/nixstasis/id via config.IdentityPath() before sending heartbeat requests.

Traceable references:

  • packages/client/cmd/nixstasis/register.go:28-93
  • packages/client/cmd/nixstasis/poll.go:38-47
  • packages/client/internal/identity/store.go:18-157
  • packages/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.go
  • packages/client/internal/script/executor.go
  • packages/client/internal/script/types.go
  • packages/client/internal/script/discovery.go
  • packages/client/internal/script/validator.go
  • packages/client/internal/script/report.go
  • packages/client/internal/script/repl.go
  • packages/client/internal/script/format.go
  • packages/client/internal/script/version.go
  • packages/client/internal/script/builtins_exec.go
  • packages/client/internal/script/builtins_mqtt.go
  • packages/client/cmd/nixstasis/install_script.go
  • docs/src/features/starlark-script-system/design.md
  • packages/client/README.md

Public Interfaces

  • Types:
    • Runtime
    • RuntimeConfig
    • Executor
    • ScriptInfo
    • ScriptResult
    • ScriptError
    • ScriptWarning
    • FrontMatter
  • Functions and methods:
    • NewRuntime
    • (*Runtime).Builtins
    • (*Runtime).Close
    • (*Runtime).Execute
    • NewExecutor
    • (*Executor).ExecuteScripts
    • DiscoverScripts
    • SelectLatestScripts
    • ParseStaryFile
    • ParseStaryContent
    • CompileSchema
    • ValidateOutput
    • ToReport
    • DefaultInstallDir
    • InstallFilename
    • ParseVersionNumber
    • MaxVersion

Dependencies

Internal

  • internal/telemetry
  • CLI commands under cmd/nixstasis/script*.

External

  • go.starlark.net/starlark
  • go.starlark.net/syntax
  • go.starlark.net/lib/json
  • github.com/eclipse/paho.mqtt.golang
  • github.com/santhosh-tekuri/jsonschema/v5

Client-Server Interaction Details

  • Script outputs are transformed into telemetry reports during pollOnce and sent inside the heartbeat telemetry object.
  • Server-issued commands can install or remove scripts through internal/commands.Handler.
  • .stary scripts 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 test prints 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_id reason.

Traceable references:

  • packages/client/internal/script/runtime.go:20-179
  • packages/client/internal/script/executor.go:13-133
  • packages/client/cmd/nixstasis/poll.go:105-126
  • packages/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.go
  • packages/client/internal/commands/fs.go
  • packages/client/internal/commands/handler_test.go
  • packages/client/cmd/nixstasis/poll.go
  • packages/client/internal/transport/client.go

Public Interfaces

  • Types:
    • Handler
  • Functions and methods:
    • NewHandler
    • (*Handler).ExecuteBatch

Dependencies

Internal

  • internal/script
  • internal/transport

External

  • Go context
  • Go sync
  • Go filesystem APIs.

Client-Server Interaction Details

  • Commands originate in PollResponse.Commands from POST /api/v1/devices/:device_id/heartbeat.
  • Supported command types are list_scripts, install_script, remove_script, and ssh_authorize.
  • Commands with deferred payload references are hydrated through FetchCommandPayload before execution.
  • Results are sent to POST /api/v1/devices/:device_id/command_results.

Traceable references:

  • packages/client/internal/commands/handler.go:17-230
  • packages/client/cmd/nixstasis/poll.go:198-249
  • packages/client/internal/transport/client.go:140-212

Client FRP Manager

Language

  • Go.

Runtime Context

  • Client launcher for the bundled frpc transient 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.go
  • packages/client/internal/frp/types.go
  • packages/client/internal/frp/manager_test.go
  • packages/client/cmd/nixstasis/frp_session.go
  • packages/client/cmd/nixstasis/frp_session_test.go
  • packages/client/build/root-dir/usr/share/nixstasis/frpc.toml
  • packages/client/internal/config/config.go
  • packages/client/cmd/nixstasis/poll.go

Public Interfaces

  • Types:
    • Manager
    • ConnectionStatus
  • Functions and methods:
    • NewManager
    • (*Manager).Start
    • (*Manager).Stop
    • (*Manager).IsActive
    • (*Manager).GetStatus
    • config.FRPCBinaryPath
    • config.FRPCConfigPath

Dependencies

Internal

  • internal/config

External

  • OS process execution via os/exec.
  • systemd transient units via systemd-run and systemctl.

Client-Server Interaction Details

  • Heartbeat responses include remote_access_token only while remote access is requested for the device.
  • If remote_access_token is non-empty and FRP is inactive, pollOnce starts the nixstasis-frpc transient unit using configured non-secret FRP values and the heartbeat token.
  • If remote_access_token is absent or empty and FRP is active, pollOnce stops FRPC.
  • If the heartbeat token changes while FRP is active, pollOnce restarts FRPC with the current token.
  • FRP status is included in subsequent heartbeat requests as connection_status.
  • frpc.toml remains client-owned in /usr/share/nixstasis/frpc.toml and 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_TOKEN inside frp-session, avoiding token exposure in systemd-run --setenv metadata.

Traceable references:

  • packages/client/internal/frp/manager.go
  • packages/client/cmd/nixstasis/frp_session.go
  • packages/client/cmd/nixstasis/poll.go
  • packages/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/run
  • packages/client/scripts/e2e/run_all_suites
  • packages/client/scripts/e2e/scaffold
  • packages/client/scripts/e2e/main.go
  • packages/client/scripts/e2e/config.example.yaml
  • packages/client/scripts/e2e/journeys/*.yaml
  • packages/client/internal/e2e/api.go
  • packages/client/internal/e2e/runner.go
  • packages/client/internal/e2e/journey.go
  • packages/client/internal/e2e/journey_executor.go
  • packages/client/internal/e2e/selector.go
  • packages/client/internal/e2e/runtime_scripts.go
  • packages/server/lib/nixstasis/e2e.ex

Public Interfaces

  • CLI scripts:
    • scripts/e2e/run
    • scripts/e2e/run_all_suites
    • scripts/e2e/scaffold
  • Go E2E package interfaces:
    • api.go API client for /e2e endpoints.
    • runner.go run orchestration.
    • journey_executor.go action execution and JSONL log emission.
    • selector.go suite/journey selection.

Dependencies

Internal

  • internal/e2e
  • internal/transport
  • internal/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-225
  • packages/client/README.md:42-114
  • packages/client/internal/e2e/api.go
  • packages/client/internal/e2e/runner.go
  • packages/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/Caddyfile
  • packages/caddy/Dockerfile
  • packages/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_domain to approve domains.

Traceable references:

  • deploy/compose/caddy/Caddyfile:1-75
  • README.md:319-350
  • deploy/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.toml
  • deploy/compose/docker-compose.yml
  • packages/frp/Dockerfile
  • packages/client/internal/frp/manager.go
  • packages/client/build/root-dir/usr/share/nixstasis/frpc.toml
  • packages/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:
    • bindPort
    • auth.method = "token"
    • auth.token
    • webServer.port
    • webServer.user
    • webServer.password
    • tcpmuxHTTPConnectPort
    • vhostHTTPPort
    • subDomainHost

Dependencies

Internal

  • Caddy wildcard and dashboard reverse proxying.
  • Go client FRPC manager.
  • Server SSH terminal client.

External

  • FRP frps and frpc binaries.
  • ssh and ncat for 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_token responses.
  • Client polling reads heartbeat remote_access_token values 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.toml directly; frpc expands runtime {{ .Envs.* }} placeholders from the session environment.
  • The FRPS auth token from the heartbeat response is passed from the launcher to frp-session as a systemd credential rather than as a systemd-run --setenv value.
  • 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-15
  • deploy/compose/docker-compose.yml:33-66
  • packages/client/internal/frp/manager.go:47-137
  • packages/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.js
  • packages/shared/e2e_log_viewer/viewer.css
  • .github/workflows/e2e-pages.yml
  • packages/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.json client-side according to repository README documentation.

Traceable references:

  • README.md:187-203
  • packages/shared/e2e_log_viewer/viewer.js
  • packages/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, feature compose-dev-harness
  • docs/src/runtime-boundaries.md
  • docs/src/modules/deployment-compose.md
  • docs/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/:id and 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.yml with 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=false so 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.localhost for the Phoenix app through Caddy.
  • auth.localhost for AuthCrunch through Caddy.
  • frp-admin.localhost for the FRPS dashboard through Caddy.
  • atom-<normalized-device-id>.localhost for 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.yml
  • deploy/compose/dev.env
  • deploy/compose/.env.example
  • deploy/compose/caddy/Caddyfile
  • deploy/compose/caddy/Caddyfile.laptop
  • deploy/compose/frps/frps.toml
  • deploy/compose/scripts/dev-lab.sh
  • deploy/compose/scripts/check_runtime_contract.sh
  • deploy/compose/scripts/validate_stack.sh
  • packages/client/Dockerfile
  • packages/server/Dockerfile
  • packages/server/lib/nixstasis_web/controllers/tls_controller.ex
  • packages/server/lib/nixstasis/tls_observations.ex
  • packages/server/lib/nixstasis/deployment.ex
  • packages/server/lib/nixstasis_web/channels/terminal_channel.ex
  • packages/server/lib/nixstasis/devices/ssh_client.ex
  • packages/client/internal/frp/manager.go
  • packages/client/internal/config/config.go
  • packages/client/cmd/nixstasis/register.go
  • packages/client/cmd/nixstasis/poll.go

Likely Affected Docs

  • docs/src/planned-features.md
  • docs/src/modules/deployment-compose.md
  • docs/src/runtime-boundaries.md
  • docs/src/modules/server-web.md
  • deploy/compose/README.md
  • packages/client/README.md
  • packages/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/v1 controller endpoints, and /e2e endpoints. 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/json resources, while the Go client uses /api/v1 controller 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-350
  • deploy/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.yml
  • deploy/compose/.env.example
  • deploy/compose/dev.env
  • deploy/compose/README.md
  • deploy/compose/caddy/Caddyfile.laptop
  • deploy/compose/scripts/dev-lab.sh
  • deploy/compose/scripts/check_runtime_contract.sh
  • deploy/compose/scripts/validate_stack.sh
  • prod.env

Public Interfaces

  • Services:
    • nixstasis
    • caddy
    • frps
    • postgres
    • client
  • Public published ports:
    • Caddy 80:80
    • Caddy 443:443
    • FRPS bind, HTTP vhost, and TCP mux ports.
  • Required operator inputs documented in deploy/compose/README.md:
    • DATABASE_URL
    • SECRET_KEY_BASE
    • PHX_HOST
    • PORT
    • BASE_DOMAIN
    • CLIENT_ID
    • CLIENT_SECRET
    • TENANT_ID
    • JWT_KEY
    • FRPS_BIND_PORT
    • FRPS_AUTH_TOKEN
    • FRPS_HTTP_PORT
    • FRPS_DASHBOARD_PORT
    • FRPS_DASHBOARD_USER
    • FRPS_DASHBOARD_PASSWORD
    • FRPS_TCPMUX_PORT

Runtime Contract

  • DATABASE_URL: PostgreSQL connection URL consumed by the Phoenix nixstasis service. It may point at bundled PostgreSQL or an external PostgreSQL host.
  • SECRET_KEY_BASE: Phoenix release secret consumed by nixstasis.
  • PHX_HOST: Public Phoenix host behind Caddy.
  • PORT: Phoenix container port. The supported Compose deployment uses 4000.
  • BASE_DOMAIN: Root domain used for nixstasis, 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 by frps, 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 frpc at /usr/libexec/nixstasis/frpc so managed devices do not depend on a separate FRP package.

Dependencies

Internal

  • packages/server/Dockerfile
  • packages/caddy/Dockerfile
  • packages/frp/Dockerfile
  • deploy/compose/caddy/Caddyfile
  • deploy/compose/frps/frps.toml
  • packages/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_URL to point at an external managed database.
  • Release image references are pinned in Compose configuration; local development builds images locally with dev tags.
  • packages/frp currently 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.yml with a tracked dev.env file passed via docker compose --env-file dev.env.
  • deploy/compose/scripts/dev-lab.sh starts the full stack, runs migrations, and seeds virtual devices for UI testing.
  • deploy/compose/caddy/Caddyfile.laptop provides Caddy internal/local certificates for local HTTPS without public DNS.
  • The client service 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=localhost with nixstasis.localhost, auth.localhost, frp-admin.localhost, and atom-<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-file handles compose-time interpolation.

Traceable references:

  • deploy/compose/docker-compose.yml:1-129
  • deploy/compose/README.md:1-117
  • deploy/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-Version and 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, feature self-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

  1. Produce a .run self-extracting archive for each release architecture (linux/amd64, linux/arm64).
  2. Each archive contains a flat staging directory with:
  • nixstasis binary (copied from GoReleaser dist/ build output)
  • frpc binary (arch-matched, from build/root-dir/usr/libexec/nixstasis/)
  • frpc.toml (from build/root-dir/usr/share/nixstasis/)
  • config.example.yaml (from build/root-dir/usr/share/nixstasis/)
  • nixstasis-poll.service (from build/root-dir/lib/systemd/system/)
  • nixstasis-poll.path (from build/root-dir/lib/systemd/system/)
  • nixstasis-registration.service (from build/root-dir/lib/systemd/system/)
  • install.sh (FHS placement script)
  • artifacts.json (manifest)
  1. install.sh maps flat archive files to their FHS paths:
  • nixstasis -> /usr/bin/nixstasis
  • frpc -> /usr/libexec/nixstasis/frpc
  • frpc.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.yaml from config.example.yaml if not already present (matching nfpm postinstall behavior)
  • nixstasis-poll.service -> /lib/systemd/system/nixstasis-poll.service
  • nixstasis-poll.path -> /lib/systemd/system/nixstasis-poll.path
  • nixstasis-registration.service -> /lib/systemd/system/nixstasis-registration.service
  1. install.sh must be idempotent and safe for upgrades:
  • Overwrite binaries and systemd units unconditionally.
  • Preserve existing /etc/nixstasis/config.yaml unless --force-config is passed.
  • Print installed file paths and versions to stdout.
  1. artifacts.json contains:
  • version: release version string (sourced from GoReleaser dist/metadata.json)
  • arch: target architecture
  • build_date: ISO 8601 timestamp
  • files: array of {path, sha256, mode} entries for every bundled file (paths are flat archive-relative names, not FHS destinations)
  1. The release workflow produces .run archives into dist/ after verify_artifacts.sh passes, and uploads them alongside existing release artifacts.
  2. verify_artifacts.sh is extended to validate .run archive contents and manifest integrity.
  3. frpc is consumed from build/root-dir/usr/libexec/nixstasis/frpc_<arch> (already staged by fetch_frpc.sh before GoReleaser runs), not downloaded separately.

Constraints

  • Do not embed frpc in the Go client binary.
  • FRPS_SERVER_ADDR remains a runtime env var, not baked into the archive.
  • Systemd units must retain PrivateTmp=true.
  • build/root-dir stays as the GoReleaser staging source for file templates.
  • packages/frp remains 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.gz outputs.
  • makeself is the archive tool. It is available in Ubuntu 24.04 via apt-get install makeself and produces POSIX-compatible .run files.

Non-Goals

  • Replacing .deb or .rpm packaging for distros that support them.
  • Interactive TUI installer or configuration wizard.
  • Automatic systemctl enable or systemctl start on install.
  • Uninstall support (can be added later).
  • macOS or Windows support.
  • Signing the .run archive (can be added later with GPG).

Design

Archive Assembly

A new script packages/client/scripts/release/build_installer.sh assembles the .run archive:

  1. Accept DIST_DIR (GoReleaser dist directory, default dist) and ARCH (amd64 or arm64) as inputs.
  2. Create a temporary staging directory.
  3. Copy the compiled nixstasis binary from the GoReleaser build output in dist/nixstasis_linux_<arch>/nixstasis.
  4. Copy frpc from build/root-dir/usr/libexec/nixstasis/frpc_<arch> and rename to frpc.
  5. Copy config files from build/root-dir/:
    • usr/share/nixstasis/frpc.toml -> frpc.toml
    • usr/share/nixstasis/config.example.yaml -> config.example.yaml
  6. Copy systemd units from build/root-dir/lib/systemd/system/:
    • nixstasis-poll.service
    • nixstasis-poll.path
    • nixstasis-registration.service
  7. Copy install.sh from scripts/release/install.sh.
  8. Read version from dist/metadata.json (GoReleaser output).
  9. Generate artifacts.json by computing sha256 and recording mode for each file in staging.
  10. Run makeself --nox11 <staging> <output> <label> to produce nixstasis-<version>-linux-<arch>.run into dist/.

Install Script

packages/client/scripts/release/install.sh is a POSIX shell script that:

  1. Checks for root privileges (id -u equals 0; avoids $EUID which is bash-only).
  2. Requires a running systemd host.
  3. Creates target directories if they do not exist.
  4. Installs binaries and systemd units with correct permissions.
  5. Conditionally installs config files:
  • Always install /usr/share/nixstasis/frpc.toml from the archive so client upgrades can update the FRP template.
  • Seed /etc/nixstasis/config.yaml from config.example.yaml if it does not exist (unless --force-config, which overwrites both).
  1. Prints a summary of installed files and a reminder to configure /etc/nixstasis/config.yaml and run systemctl 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:

  1. apt-get install -y makeself
  2. Run build_installer.sh for amd64 and arm64.
  3. .run files are written to dist/.
  4. For snapshot builds: dist/ is already uploaded as nixstasis-client-snapshot.
  5. 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:

  1. Finds all .run files in $DIST_DIR.
  2. Extracts each to a temp directory with --noexec --target <dir>.
  3. Validates artifacts.json exists and is valid JSON (using jq or python3 -m json.tool).
  4. Validates every file listed in artifacts.json exists and its sha256 matches.
  5. Validates the archive contains install.sh, nixstasis, and frpc.
  6. Requires at least one .run file only when VERIFY_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

  • makeself is 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.sh config 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.
  • EUID is bash-only; install.sh uses id -u for 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 .run installer usage)
  • docs/src/planned-features.md (keep feature status and delivered behavior reconciled as the feature moves from in-spec to in-progress and completed)

Suggested Validation

  • CI step that builds .run from 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/rpm to 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.yaml from example; upgrade preserves existing config.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/compose guidance.
  • 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

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, and last_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 nixstasis binary 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 ./... in packages/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 stary files 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_get and deny-by-default command execution.
  • Execute heartbeat command batches and send aggregated command results back to the server.
  • Correlate command results by command_id and 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_id values 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, lt and string operators contains, 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+Enter to 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/:id LiveView 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/:id shows 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.toml template and frpc-native {{ .Envs.FRPS_AUTH_TOKEN }} expansion model.
  • Keep FRPC lifecycle owned by the nixstasis-frpc transient 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_requested is 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 nixstasis service does not currently receive FRPS_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_token non-empty: start or keep FRPC running with that token.
  • remote_access_token omitted: stop FRPC if active; otherwise remain stopped.
  • remote_access_token may decode as an empty string on older or malformed responses; the client treats that the same as omission.
  • remote_access_requested is removed from the client response contract because no release has shipped with the current branch behavior.

Server Design

  • Compose passes FRPS_AUTH_TOKEN to both services:
    • frps: uses it in frps.toml as auth.token.
    • nixstasis: uses it only to populate authenticated heartbeat responses when device.remote_access_requested is true.
  • Add a small helper that resolves the heartbeat FRPS token from FRPS_AUTH_TOKEN only when device.remote_access_requested is true.
  • Keep HeartbeatJSON.show/1 focused on rendering response data. It receives or calls the helper result and conditionally includes remote_access_token:

Remote access token rendering cases:

  • Absent when device.remote_access_requested is false.
  • Configured FRPS_AUTH_TOKEN when device.remote_access_requested is true.
  • If remote access is requested but FRPS_AUTH_TOKEN is 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.PollResponse replaces RemoteAccessRequested bool with RemoteAccessToken string.
  • pollOnce starts FRPC when resp.RemoteAccessToken != "".
  • Before Manager.Start, pollOnce derives runtime FRP config from the local config and MAC address, then sets frpConfig.AuthToken = resp.RemoteAccessToken.
  • runtimeFRPConfig derives only dynamic client-owned values, such as FRP proxy name from MAC when frp.name is not configured.
  • pollOnce stops FRPC when resp.RemoteAccessToken == "" and current FRP status is active.
  • Manager.Start continues validating that the final FRP config has a non-empty auth token before invoking systemd-run.
  • Manager.Start continues passing the token to the transient unit through LoadCredential=FRPS_AUTH_TOKEN:<path>; it must not pass the token through systemd-run --setenv metadata.

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.md
  • docs/src/modules/deployment-compose.md
  • deploy/compose/scripts/check_runtime_contract.sh
  • deploy/compose/docker-compose.yml
  • deploy/compose/README.md
  • packages/server/README.md
  • packages/client/README.md
  • packages/client/scripts/mock_api/main.go
  • packages/client/internal/frp/manager.go
  • docs/src/modules/client-frp-manager.md
  • docs/src/modules/edge-frp.md
  • docs/src/runtime-boundaries.md
  • docs/src/planned-features.md

Validation

  • Server tests prove heartbeat omits remote_access_token when remote access is false.
  • Server tests prove heartbeat includes FRPS_AUTH_TOKEN when remote access is true and the env var is configured.
  • Server tests prove heartbeat omits remote_access_token and logs a clear error when remote access is requested but FRPS_AUTH_TOKEN is missing.
  • Client tests prove a non-empty remote_access_token starts 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_token for normal server-requested remote access.
  • Mock API flags and fixtures use remote_access_token for client-side testing.
  • FRP manager comments distinguish non-secret template values passed by --setenv from the secret token passed through systemd credentials.
  • Docs and module pages no longer describe remote_access_requested as 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 main so the completed self-extracting-installer docs and systemd credential model are present.
  • During implementation, update server, client, deployment contract, and docs in the same unit of work so remote_access_requested is no longer documented as a heartbeat response field.
  • Before completion, rerun the affected docs search for remote_access_requested, remote_access_token, and FRPS_AUTH_TOKEN and 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_domain approval 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, and atom-<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_cmd intent 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/json surface.
  • 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_domain using 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/:id and 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.yml
  • deploy/compose/caddy/Caddyfile
  • deploy/compose/frps/frps.toml
  • packages/server/lib/nixstasis_web/controllers/tls_controller.ex
  • packages/server/lib/nixstasis_web/channels/terminal_channel.ex
  • packages/server/lib/nixstasis/devices/ssh_client.ex
  • packages/client/internal/frp/manager.go
  • packages/client/cmd/nixstasis/register.go
  • packages/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 .run file 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.json manifest with version, arch, sha256 per file, file modes, and build timestamp.
  • Consume frpc from packages/frp via the shared acquisition path, not a separate download.
  • Extend client release CI so .run files are produced and verified alongside existing archives, .deb, and .rpm packages.
  • Extend verify_artifacts.sh to validate .run archive contents and manifest integrity.
  • Constraints:
  • Do not embed frpc in the Go client binary.
  • FRPS_SERVER_ADDR remains a runtime env var, not baked into the archive.
  • Systemd units must use PrivateTmp=true.
  • build/root-dir stays as the GoReleaser staging source.
  • packages/frp remains the shared source of truth for FRP version and checksums.
  • Non-goals:
  • Replacing .deb or .rpm packaging for distros that support them.
  • Interactive TUI installer or configuration wizard.
  • Automatic service enablement or start on install.
  • Uninstall support.
  • Success criteria:
  • A .run file for each release architecture is published to GitHub Releases.
  • Running the .run file on a clean Linux system installs all required files to their FHS paths.
  • Existing /etc/nixstasis/config.yaml is preserved on upgrade unless the installer is explicitly forced to replace it; client-owned frpc.toml is updated on every upgrade from /usr/share/nixstasis/frpc.toml.
  • artifacts.json in the archive matches the installed bundle contents by sha256.
  • verify_artifacts.sh catches content or manifest drift in CI.
  • Risks and tradeoffs:
  • makeself adds 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.sh
  • packages/client/scripts/fetch_frpc.sh
  • .github/workflows/release_client.yml
  • packages/client/scripts/release/verify_artifacts.sh
  • packages/client/build/root-dir/
  • prod.env
  • Suggested validation:
  • CI step that builds the .run archive 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_requested in the device heartbeat response contract with remote_access_token.
  • Include remote_access_token only 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 nixstasis service receive the same FRPS_AUTH_TOKEN as the frps service.
  • Make the Go client start FRPC when remote_access_token is non-empty and use that value as FRPS_AUTH_TOKEN for frpc template expansion.
  • Make the Go client stop FRPC when remote_access_token is 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.toml continues to use {{ .Envs.FRPS_AUTH_TOKEN }} and frpc-native environment expansion.
  • The client continues launching FRPC through the nixstasis-frpc transient 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_TOKEN as consumed by both frps and nixstasis.
  • 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_TOKEN is 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.ex
  • packages/server/test/nixstasis_web/controllers/heartbeat_controller_test.exs
  • packages/client/internal/transport/client.go
  • packages/client/cmd/nixstasis/poll.go
  • packages/client/cmd/nixstasis/poll_test.go
  • packages/client/internal/frp/manager.go
  • deploy/compose/docker-compose.yml
  • deploy/compose/scripts/check_runtime_contract.sh
  • docs/src/client-server-interface.md
  • docs/src/modules/deployment-compose.md
  • Suggested validation:
  • Server controller tests for remote_access_token omitted when remote access is false and present when true with FRPS_AUTH_TOKEN configured.
  • 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/v1 and /e2e and 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.ex
  • packages/server/lib/nixstasis_web/controllers/device_controller.ex
  • packages/server/lib/nixstasis_web/controllers/heartbeat_controller.ex
  • packages/server/lib/nixstasis_web/controllers/device_command_controller.ex
  • packages/server/lib/nixstasis_web/controllers/e2e_run_controller.ex
  • packages/server/lib/nixstasis_web/controllers/e2e_run_result_controller.ex
  • packages/server/lib/nixstasis_web/controllers/builder_schema_controller.ex
  • packages/server/lib/nixstasis_web/controllers/builder_config_validation_controller.ex
  • packages/server/lib/nixstasis/domain.ex
  • packages/server/priv/static/openapi.yaml
  • docs/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 /e2e contract if E2E routes are moved or wrapped by Ash.
  • Run mdbook build docs and 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/Caddyfile
  • packages/server/lib/nixstasis_web/router.ex
  • packages/server/lib/nixstasis_web/controllers/
  • packages/server/lib/nixstasis_web/live/
  • docs/src/modules/edge-caddy.md
  • docs/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 docs and 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/compose as 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.yml
  • deploy/compose/scripts/check_runtime_contract.sh
  • deploy/compose/README.md
  • docs/src/modules/deployment-compose.md
  • docs/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 docs and 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_domain decisions.
  • 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/json examples from bespoke /api/v1 and /e2e controller 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 docs succeeds 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.md
  • docs/src/reference/openapi/
  • packages/server/priv/static/openapi.yaml
  • packages/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 docs and, 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

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.yaml documents the generated Ash JSON:API surface under /api/json.
  • The bespoke Phoenix controller APIs under /api/v1 and /e2e are 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.md and the Phoenix router/controller modules.
  • Do not duplicate Ash JSON:API resources here unless a bespoke /api/v1 or /e2e controller 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

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.md
  • docs/src/features/index.md
  • AGENTS.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:

  1. Use /plan-features to create or inspect the planned feature roadmap.
  2. Use /start-feature when creating a feature.
  3. Use /review-feature-spec before major implementation work.
  4. Use /implement-feature to execute the feature tasks.
  5. Use /close-feature before opening or finalizing the pull request.
  6. Use /project-alignment-review, /project-alignment-execute, and /project-alignment-land when 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.

RefCommitTimestampReport
maincbed9312026-05-23T15:40:16ZOpen report
main0142b852026-05-23T15:01:33ZOpen report
main0b46fa92026-05-23T14:49:20ZOpen report
v0.0.37cc1bb02026-05-17T14:07:47ZOpen report
main7cc1bb02026-05-17T14:03:49ZOpen report
v0.0.31dd6e9a2026-05-17T13:57:30ZOpen report
main1dd6e9a2026-05-17T13:53:42ZOpen report
v0.0.371ff6252026-05-17T13:23:13ZOpen report
main71ff6252026-05-17T13:18:06ZOpen report
v0.0.3b0ca7bb2026-05-17T13:07:13ZOpen report
mainb0ca7bb2026-05-17T13:03:20ZOpen report

Open the full E2E report index.

Client-Server Interface

Communication Methods

  • Go client to Phoenix server:
    • HTTP JSON requests under /api/v1.
  • 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:*.
  • Caddy to Phoenix:
    • Reverse proxy to nixstasis:4000.
    • HTTP ask endpoint for on-demand TLS approval.
  • E2E client to Phoenix:
    • HTTP JSON requests under /e2e.
  • 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).

ScopeDefault LimitWindow
Heartbeat (POST .../heartbeat)30 requests60 seconds
Other API requests120 requests60 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.ex
  • packages/server/lib/nixstasis_web/rate_limiter_store.ex

Go Client to Phoenix Endpoint Mapping

Go MethodHTTP EndpointServer HandlerPurpose
RegisterDevicePOST /api/v1/devices/registerDeviceController.register/2Register device and receive UUID
PollPOST /api/v1/devices/:id/heartbeat?api_key=...HeartbeatController.create/2Submit telemetry and receive remote-access/command directives
SendCommandResultsPOST /api/v1/devices/:id/command_results?api_key=...DeviceCommandController.command_results/2Acknowledge command execution results
FetchCommandPayloadGET /api/v1/devices/:id/command_payloads/:ref?api_key=...DeviceCommandController.command_payload/2Fetch deferred command payload

Traceable references:

  • packages/client/internal/transport/client.go:84-212
  • packages/server/lib/nixstasis_web/router.ex:58-65
  • packages/server/lib/nixstasis_web/controllers/device_controller.ex:31-37
  • packages/server/lib/nixstasis_web/controllers/heartbeat_controller.ex:7-27
  • packages/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-120
  • docs/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-187
  • docs/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-200
  • packages/server/lib/nixstasis_web/controllers/device_command_controller.ex:6-19
  • docs/src/features/go-client-rewrite/design.md

Command Payload

Response shape:

{
  "content_type": "...",
  "name": "...",
  "data": "..."
}

Traceable references:

  • packages/client/internal/transport/client.go:202-212
  • packages/server/lib/nixstasis_web/controllers/device_command_controller.ex:28-38
  • docs/src/features/go-client-rewrite/design.md

E2E API Mapping

EndpointHandlerPurpose
GET /e2e/suitesE2ERunController.suites/2List configured suites
GET /e2e/runsE2ERunController.index/2List runs
POST /e2e/runsE2ERunController.create/2Create or reuse run
GET /e2e/runs/:idE2ERunController.show/2Fetch run
POST /e2e/runs/:id/cancelE2ERunController.cancel/2Cancel run
GET /e2e/runs/:id/resultsE2ERunResultController.index/2List results
POST /e2e/runs/:id/resultsE2ERunResultController.create/2Submit results
GET /e2e/runs/:id/results/:journey_id/logE2ERunResultController.log/2Fetch 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-79
  • packages/server/lib/nixstasis_web/controllers/e2e_run_controller.ex:7-93
  • README.md:96-116

Authentication and Session Handling

  • Browser routes use Phoenix browser pipeline:
    • fetch_session
    • fetch_live_flash
    • protect_from_forgery
    • put_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.
  • Runtime device heartbeat, command-result, and command-payload requests require the registration-issued device token as an api_key query 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-20
  • deploy/compose/caddy/Caddyfile:25-38
  • packages/server/lib/nixstasis_web/channels/user_socket.ex:37-64
  • packages/server/lib/nixstasis_web/channels/terminal_channel.ex:20-50
  • packages/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_key return HTTP 401 with code missing_api_key.
  • Runtime device API requests with an invalid api_key return HTTP 401 with code invalid_api_key.
  • Heartbeat rejects unapproved devices with HTTP 403 and code device_not_approved.
  • Command results without a results list return HTTP 400.
  • Command-result processing errors return HTTP 422.
  • Command payload lookup returns HTTP 404 for missing payloads.
  • TLS domain denial returns HTTP 401 and {"error":"The host is not permitted"}.
  • E2E create returns typed error codes in an error object.

Traceable references:

  • packages/client/internal/transport/client.go:37-82
  • packages/server/lib/nixstasis_web/controllers/heartbeat_controller.ex:16-25
  • packages/server/lib/nixstasis_web/controllers/device_command_controller.ex:15-37
  • packages/server/lib/nixstasis_web/controllers/tls_controller.ex:21-27
  • packages/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.v1 according to README.
  • Repository tooling currently installs Go 1.26.2 through mise.toml; the client module target is go 1.26 in go.mod.

Traceable references:

  • docs/src/features/go-client-rewrite/design.md
  • packages/server/lib/nixstasis_web/controllers/e2e_run_controller.ex:7-25
  • README.md:123-135
  • packages/client/go.mod:1-13