Guide

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

RoleResponsibility
headquartersAPI, dashboard, schedules, broadcasts, routing, and fleet inventory. It does not execute signals or beacons.
stationAdvertises local definitions and executes eligible signal runs and beacon instances.
standaloneBackwards-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