Station Networks
A Station Network scales Station across processes or machines. One logical Headquarters accepts requests, presents fleet-wide state, and reconciles schedules. Execution stations advertise capacity and definitions, then atomically claim work from the shared adapters.
Roles and request flow
| Role | Responsibility |
|---|---|
headquarters | API, dashboard, schedules, broadcasts, routing, and fleet inventory. It does not execute signals or beacons. |
station | Advertises local definitions and executes eligible signal runs and beacon instances. |
standalone | Backwards-compatible single-node mode that performs both roles. |
Headquarters enqueues a run once. Stations race to claim it in the shared queue; the adapter's atomic pending-to-running transition chooses exactly one owner. If that owner disappears, its lease expires and the run is recovered. Fencing tokens prevent the old owner from later completing the recovered attempt.
Configure Headquarters
import { defineConfig } from "station-kit";import { PostgresAdapter } from "station-adapter-postgres";import { StationNetworkPostgresAdapter } from "station-adapter-postgres/network"; const connectionString = process.env.DATABASE_URL!; export default defineConfig({ role: "headquarters", adapter: new PostgresAdapter({ connectionString }), network: { id: "production", stationId: "hq-1", name: "Production HQ", adapter: new StationNetworkPostgresAdapter({ connectionString }), }, signalsDir: "./signals", // catalog + validation; never executed here scheduleAdapter, beaconAdapter,});Configure an execution station
export default defineConfig({ role: "station", adapter: new PostgresAdapter({ connectionString }), // same queue beaconAdapter, // same beacon state network: { id: "production", stationId: process.env.STATION_ID!, name: "Kenya GPU worker", adapter: new StationNetworkPostgresAdapter({ connectionString }), labels: { region: "ke", gpu: "true" }, endpoint: "https://worker-ke.internal.example", }, signalsDir: "./signals", beaconsDir: "./beacons", runner: { maxConcurrent: 12 },});Use the matching /network export for SQLite, PostgreSQL, MySQL, or Redis. Every process must use the same durable queue and network backends. Share beacon state on nodes that coordinate beacons, and share schedule state across Headquarters replicas. The memory implementations are only for standalone mode and tests. SQLite requires a shared filesystem; use PostgreSQL, MySQL, or Redis across machines.
Capacity, placement, and draining
export const render = signal("render") .input(RenderInput) .concurrency({ station: 4, network: 20 }) .placement({ labels: { gpu: "true", region: "ke" } }) .run(async (input) => { /* ... */ }); export const gateway = beacon("gateway") .placement({ labels: { region: "ke" } }) .run(async (ctx) => { const server = await listen(); ctx.expose({ protocol: "http", port: server.port, path: "/gateway" }); ctx.ready(); await ctx.untilStopped(); });Per-station concurrency limits local process pressure. Network concurrency uses shared controller leases and is enforced across the fleet. Placement labels require an exact match. Marking a stationdraining through the Stations dashboard or v1 API stops new claims while current work finishes.
Schedules and exact times
Runtime schedules support five-field cron plus an IANA timezone. The stored nextRunAt is an absolute timestamp and occurrences advance from the prior planned time, so polling latency does not create cumulative drift. Atomic occurrence claims prevent duplicate fires across control-plane processes. As with OS cron, the timestamp is when work becomes eligible; actual handler start can be delayed by polling, queue pressure, or unavailable capacity. See Schedules.
Beacon services
A networked beacon instance is protected by a single-owner lease. Calling ctx.expose() records its station, protocol, port, and base path. Headquarters proxies HTTP traffic at/api/v1/beacons/:name/instances/:id/proxy/*. The owning station must advertise a reachable network.endpoint; private/NAT-only stations need an operator-provided tunnel endpoint. The proxy requires a trigger or admin scope, removes the caller's authorization and cookie headers before forwarding, and does not proxy WebSocket upgrades. Protect direct station endpoints and do not treat the injected x-station-* headers as proof of identity on a publicly reachable service.
Production checklist
- Give every process a stable, unique
stationIdand the samenetwork.id. - Keep the lease duration above normal database, network, and event-loop jitter.
- Drain a station before maintenance; wait for active work before stopping it.
- Test at least two workers against the production backend and assert single ownership, placement, both concurrency levels, schedule deduplication, and expired-lease recovery.
- Measure the production workload. A local SQLite benchmark is useful for regression detection, not fleet sizing.