docs← Back to article

Markdown for LLMs

Deployment

The source Markdown for this article. Copy it into your assistant or download it as a text file.

Download this articlePlain text ↗
# Deployment

This page gives the one supported way to run the deadline example:
the Node server as one process on a host you control. There is no
second topology in scope, so every step below is the whole
procedure. The limits below describe what `src/server.ts` does
today; they are not recommendations.

```text
docs/build/examples/deadline-app
```

## Prerequisites

- Node.js: minimum 20 (`engines: >=20`), recommended 24 Active LTS
  for a new deployment (`node --version` tells you).
- The published packages the example pins:

```text
@arxo/law 0.3.3
@arxo/canon-bgb-fristen 0.1.5
```

- The example sources with their lockfile, so `npm ci` reproduces
  the tested tree.

## Install and start

```text
npm ci
npm start
```

`npm ci` installs the pinned tree, including the canon package with
the engine files it ships. `npm start` launches the `node:http`
server from `src/server.ts`.

## Access mode

The server binds loopback only on port 8787 unless told otherwise:

```text
bind  127.0.0.1          (HOST overrides)
port  8787               (PORT overrides)
```

`HOST=0.0.0.0` exposes the example on the network — set it
deliberately, never by default. The example has no authentication,
no TLS, and no rate limiting: beyond loopback it needs a front
proxy, and that proxy is outside this page. Captures land in
`captures/` under the app directory; `DEADLINE_APP_CAPTURES`
points them elsewhere.

## Limits

What the server enforces, and what it does not:

| Limit | Behaviour | Consequence |
|---|---|---|
| Request body | Bodies over 64 KiB are refused with `413` | Oversized input never reaches validation or the engine |
| Receiving a request | Node's default timeouts apply; the server sets none | Slow clients are cut off while sending, never while the engine computes |
| Computation time | No application deadline, no cancellation | A slow computation runs to completion, however long it takes |
| Simultaneous computations | No cap; every POST evaluates independently | One process does not mean one request at a time |
| Request log | One listen line at startup, nothing per request | The audit trail is the captures, not stdout |

The details behind each row:

- **Body size: 64 KiB.** `readBody` refuses anything larger
  (`MAX_BODY` in `src/server.ts`) with `413` and a
  `body too large` error: the server declines content over its own
  limit, which is exactly what `413 Content Too Large` means. The
  upload is paused first, the 413 is sent, and the socket is
  destroyed only after the response flushes — the client receives
  the status instead of a bare connection reset.
- **Request receipt: Node defaults, not overridden.** The server is
  created with no timeout options, so `node:http` applies its own:
  `requestTimeout` 300000 ms to receive a full request,
  `headersTimeout` 60000 ms for the headers, `keepAliveTimeout`
  5000 ms on idle keep-alive sockets, no socket inactivity timeout
  (`timeout` 0), no per-socket request cap. These bound receiving,
  not computing.
- **Computation: no application deadline, no cancellation.** Once a
  request is received, the engine call runs to completion: a slow
  computation holds the event loop's attention until the engine
  answers, however long that takes.
- **No cap on simultaneous computations.** Every POST evaluates
  independently through the shared singleton model handle.
- **No request log.** The example prints one listen line at
  startup and nothing per request; the security page says what may
  and may not go into logs if you add them.

On memory sizing this page makes no claim: sizing is not
established. Steady state is the model plus one working set per
concurrent request, and only a measured control load plus margin
can size it. The form route cannot stand in for that measurement
(see readiness below).

## Readiness

Two different questions, two different probes:

- **Is the HTTP process up?** `GET /` answers 200 with the form.
  That proves the process listens and the static file reads —
  nothing about the engine.
- **Can it compute?** POST the worked case to
  `/api/deadline/evaluate` (event `2026-03-06`, 14 days, legal
  time `2026-09-17`) and expect `COMPUTED` with `2026-03-20`.
  That proves the model opened and the full path — validate,
  facts, query, engine, read, view, capture — runs.

A supervisor healthcheck should use the second probe, or the
first plus the second: the form page passing while evaluation
fails is a real failure mode (missing canon, broken pins), and
the form alone will not catch it. That probe is not side-effect
free: every evaluate call stores a new capture file under a random
id, with no dedupe. Run the supervised instance with
`DEADLINE_APP_CAPTURES` pointed at a scratch directory and rotate
that directory, so probe traffic never mixes with kept captures
and never fills the disk silently.

## Shutdown and storage

- **Stopping drops in-flight requests.** The server installs no
  signal handler and drains nothing: SIGTERM ends the process,
  and whatever request was mid-computation never answers. Stop
  it when no computation matters, or accept the dropped request.
- **A capture write can be interrupted.** `storeCapture` writes
  the file synchronously under its final name — there is no
  write-to-temp-and-rename. A crash mid-write leaves a partial
  file; replaying it later fails (truncated JSON reads as a 500
  on the replay call). Treat that as a corrupt file and
  recompute the case; never hand-edit it back into shape.
- **Restart is otherwise stateless.** No sessions, no cache, no
  warmup beyond opening the pinned model at startup. Stop the
  process, start it with the same command and the same
  `HOST`/`PORT`, and run the control query to confirm.

## Diagnostics

| Symptom | Likely cause | What to do |
|---|---|---|
| `npm start` fails naming the Node version | Node older than 20 | install a supported LTS (24 recommended; 20 is the floor) and retry |
| startup fails opening the model | canon package missing or version drift | run `npm ci` again; confirm `@arxo/canon-bgb-fristen 0.1.5` is installed |
| `listen EADDRINUSE` on start | the port is busy | set `PORT` to a free port and retry |
| `GET /` answers but the control query fails | run path broken past the static file | confirm the model opened with `offline: true` and `via` is `local`; see the offline page |
| `413 body too large` on evaluate | form over 64 KiB | shrink the body; the example takes four short fields |
| replay disagrees with a stored capture | pins moved since the capture | do not edit the capture; replay under the stored pins or recompute as a new file |
| replay of a capture answers 500 | partial file from an interrupted write | recompute the case as a new capture |

When a failure names a version, trust the version: the pins
(`@arxo/law 0.3.3`, `@arxo/canon-bgb-fristen 0.1.5`, model
`de.bgb.fristen@0.1.0`) are the supported combination, and anything else
is untested until the versioning checklist says otherwise.

## What is not in scope

One host, one process, one port. Anything beyond that — extra
instances, shared capture stores, front proxies, graceful
shutdown, log pipelines — sits outside this page and adds its own
failure modes the example does not handle. If you outgrow one
process, keep this page as the unit that each copy runs.

## Next

- [Versioning: pins, upgrades, and old captures](/build/application/versioning/)