Server
The server side is one long-running broker and a folder of handlers. The broker reads the synced folder, decides which files are entry points, opens their ports, and — for each accepted connection — runs the access checks and then hands the ready socket to your handler. You write connection logic; the broker owns the sockets.
The filename grammar
An entry point is a top-level file whose name matches one of two patterns. Each
ends in .port.js; the token before it fixes the protocol and the port.
<name>.tcp.<port>.port.js a TCP handler on <port>
<name>.udp.<port>.port.js a UDP handler on <port>
| Token | Rule |
|---|---|
<name> | A label for you — it names the handler in logs and in
ctx.name. It has no effect on what is opened. |
tcp / udp | The transport. Decides which contract the module must satisfy (below) and how the broker treats traffic. |
<port> | An integer 1–65535. This number
is authoritative — it is exactly what the broker opens on the OS firewall. |
Two handlers may not claim the same protocol and port. On a conflict the later file is refused and logged; the first to claim the pair keeps it. Only top-level files are entry points — nested files are supporting code (see Files).
The broker model
One broker process owns all listening sockets for every declared port. It runs the accept loop, optional TLS termination, and the access checks. For a TCP handler the sequence is:
accept the broker accepts the raw connection on the port
screen it applies the access rules — source CIDR, connection caps, rate limits
hand off it passes the ready socket to your handler's handle(conn, ctx)
So a handler only ever sees traffic that already passed the policy in front of it. You never call
listen(), never run an accept loop, never set up TLS — the broker did all of that. See
the manifest for how the screening rules are declared.
The handler contract
Proposed A handler is a plain module. What it must export depends on its transport.
| Export | Transport | Called |
|---|---|---|
handle(conn, ctx) | TCP | Once per accepted connection, with a
Node socket-like conn the broker has already screened. |
message(msg, rinfo, ctx) | UDP | Once per datagram. UDP is connectionless — there is nothing to hand off — so the broker binds the socket, checks the source, and dispatches each message. |
open(ctx) | both | Optional. Runs once when the handler is loaded — set up shared structures, warm a cache. |
close(ctx) | both | Optional. Runs once when the handler is being
unloaded — release anything open acquired. |
Every call receives a ctx. Proposed its fields:
| Field | What it is |
|---|---|
ctx.name | The <name> from the filename. |
ctx.port | The port this handler serves, as a number. |
ctx.protocol | "tcp" or "udp". |
ctx.shared | The broker's in-memory shared store — get/set/delete plus publish/subscribe, visible across connections and across handlers. See Shared state. |
ctx.signal | An AbortSignal that fires when the handler is
being shut down or reloaded — use it to stop loops and abort in-flight work. |
The full contract, with edge cases, is in the Handlers reference.
Concurrency
The module is loaded once. That single instance handles many concurrent
connections — the broker calls handle again for each new socket, exactly as a normal
server accepts in a loop. Anything a connection needs to share with the others lives in
ctx.shared, not in a per-call variable.
A TCP handler
An echo server: read from the screened socket, write the same bytes back. No listen,
no TLS, no accept loop.
// echo.tcp.7000.port.js
export function handle(conn, ctx) {
conn.setEncoding("utf8");
conn.write(`hello from :${ctx.port}\n`);
conn.on("data", (chunk) => conn.write(chunk));
conn.on("error", () => conn.destroy());
}
A UDP handler
A datagram counter: bump a shared total and reply to the sender. There is no connection — you get the message, who sent it, and the shared store.
// ping.udp.9000.port.js
export async function message(msg, rinfo, ctx) {
const n = (await ctx.shared.get("count")) ?? 0;
await ctx.shared.set("count", n + 1);
ctx.reply(Buffer.from(`pong #${n + 1}\n`), rinfo);
}
For UDP the broker provides a reply bound to the listening socket, so the
handler answers without binding one itself.