Start, Stop, or Check a Service
You need to start a service, check whether it runs, or stop it
cleanly. fit-rc provides a single interface for all of
them. You do not need to remember the specific command, port, and
flags for each service. This page covers one bounded task. It shows
how to manage one or more services. For the full setup with
supervision and observability, see
Service Lifecycle.
Prerequisites
- Node.js 22+
-
Services defined in
config/config.jsonunder theinitkey (see Service Lifecycle for the configuration format)
Start a service
Start all configured services in order:
npx fit-rc start
The expected output follows. Your timestamps and process IDs differ:
INFO 2026-05-04T10:00:01.123Z rc svscan 42001 MSG001 - Socket ready socket="data/svscan.sock"
INFO 2026-05-04T10:00:01.456Z rc span 42001 MSG002 - Service started
INFO 2026-05-04T10:00:01.789Z rc vector 42001 MSG003 - Service started
Start up to a specific service. This helps when you need only part of the stack:
npx fit-rc start span
This starts every service from the beginning of the configuration
array through the named service. fit-rc does not start
the services after span.
Check status
npx fit-rc status
Expected output when the services run:
INFO 2026-05-04T10:05:00.123Z rc svscan 42001 MSG001 - Running
INFO 2026-05-04T10:05:00.234Z rc span 42001 MSG002 - up pid="42010"
INFO 2026-05-04T10:05:00.345Z rc vector 42001 MSG003 - up pid="42011"
Expected output when the supervision daemon does not run:
INFO 2026-05-04T10:05:00.123Z rc svscan 42001 MSG001 - Not running
Check a single service:
npx fit-rc status span
If the named service is not in the configuration,
fit-rc exits with an error:
Error: Unknown service: nonexistent
Stop a service
Stop all services in reverse order and shut down the daemon:
npx fit-rc stop
Stop from a specific service onward and leave the earlier services up:
npx fit-rc stop vector
This stops vector and every service after it in the
configuration array, in reverse order. The services before
vector stay up. The daemon stays active.
Longrun services receive SIGTERM first. If the process
does not exit within the shutdown timeout (default 3 seconds), the
daemon sends SIGKILL to the entire process group.
Oneshot services run their down command if the
configuration defines one.
Restart a service
npx fit-rc restart span
This stops the named service and everything after it in the configuration array. It then starts that same slice again. The dependents that it stopped start again. The services before the target stay untouched. Without a name, all services restart.
Read logs
View the current log for a named service:
npx fit-rc logs span
The service name is required. Each longrun service writes output to
a dedicated directory under the configured log_dir. The
log writer rotates files at 1 MB and keeps the 10 most recent
archives.
A service that produced no output has no log file yet. If no log file exists, the command returns silently.
Tune log rotation
fit-rc pipes each longrun service's output through
fit-logger, the log writer from
@forwardimpact/libsupervise. It reads lines on stdin
and prepends an ISO 8601 timestamp. It writes to a file named
current. It rotates that file to a timestamped archive
once the file grows past a size limit. You can run
fit-logger directly to capture any command's
output, or to test rotation settings:
my-service | npx fit-logger --dir data/logs/my-service
Two options tune rotation:
| Option | Short | Default | Effect |
|---|---|---|---|
--dir |
-d |
required | Directory that fit-logger writes logs to. |
--maxFileSize |
-s |
1000000 |
Bytes before current rotates to an archive.
|
--maxFiles |
-n |
10 |
Archives to keep. fit-logger deletes the oldest.
|
my-service | npx fit-logger -d data/logs/my-service -s 1048576 -n 5
fit-logger names each archive
@YYYY-MM-DD_HH-mm-ss-SSS.s. The trailing
-SSS is the millisecond segment. So when you sort the
filenames, you get chronological order. When the count exceeds
--maxFiles, fit-logger deletes the oldest
archives on the next rotation.
Supervise processes directly
fit-rc drives a supervision daemon,
fit-svscan, over a Unix domain socket. You normally
never call the daemon yourself. fit-rc start spawns it.
But when you debug a stuck service, it helps to know its control
interface. Start the daemon with a socket path, a PID file, and a
log directory:
npx fit-svscan --socket data/svscan.sock --pid data/svscan.pid --logdir data/logs
| Option | Short | Default | Effect |
|---|---|---|---|
--socket |
-s |
required | Path to the Unix socket the daemon listens on. |
--pid |
-p |
required | Path to the PID file the daemon writes. |
--logdir |
-l |
required | Directory each supervised process logs to. |
--timeout |
-t |
3000 |
Milliseconds to wait for SIGTERM before
SIGKILL.
|
You send control commands to the socket as newline-delimited JSON
objects. Each command has a command field. The
add and remove commands also carry a
service name:
| Command | Fields | Response |
|---|---|---|
ping |
{"ok":true,"message":"pong"}
|
|
add |
name, cmd, cwd |
Starts and supervises a process. |
remove |
name |
Stops and removes a supervised process. |
status |
State, PID, and restart count per service. | |
shutdown |
Stops every service and exits the daemon. |
The daemon answers each command with a single JSON line and closes
the connection. shutdown is the exception. The daemon
exits before it replies, so a client sees the connection close with
no response line. The daemon is a pure supervisor. It knows nothing
about service order or oneshot commands. fit-rc handles
the order and the oneshot commands. So fit-rc is the
interface you use day to day.
Suppress output
All commands accept the --silent flag to suppress
informational output:
npx fit-rc start --silent
Errors still print. This helps in scripts where you want to see only failures.
Programmatic usage
The ServiceManager class gives you the same operations:
import { spawn, execSync } from "node:child_process";
import { ServiceManager, sendCommand, waitForSocket } from "@forwardimpact/librc";
import { createLogger } from "@forwardimpact/libtelemetry";
import { createDefaultRuntime } from "@forwardimpact/libutil/runtime";
const runtime = createDefaultRuntime();
const config = {
rootDir: process.cwd(),
init: {
log_dir: "data/logs",
services: [
{ name: "span", command: "npx @forwardimpact/svcspan" },
],
},
};
const logger = createLogger("rc", runtime);
// spawn/execSync are injected by the caller — there is no runtime-level
// equivalent for detached, stdio-redirected daemon spawning.
const manager = new ServiceManager(config, logger, {
runtime,
spawn,
execSync,
sendCommand,
waitForSocket,
});
await manager.start("span"); // Start up to and including "span"
await manager.status("span"); // Check one service
await manager.logs("span"); // Print log to stdout
await manager.stop("span"); // Stop from "span" onward
Each method maps directly to the CLI command. start and
stop accept an optional service name. They slice the
service list the same way as the CLI. start takes
everything up to and including the named service.
stop takes the named service and everything after it.
What's next
Manage Service Lifecycle from One Interface
Keep services running and surface problems before they escalate. One interface gives you supervision and observability.
Add Observability
Add structured, machine-readable logs and spans without a logging framework. Drop in a log line or a span and it works.