Running a Server#
A single iOS device can capture and store data entirely on its own — see Analyzing Your Data for the zero-server workflow. The CaptureGraph server becomes useful when a capture project outgrows one phone:
- Remote targets — the app downloads procedures from the server, and uploads captures back, so many participants contribute to one dataset.
- Live editing — procedures are plain Python files on the server; edits go live without redeploying the app.
- Scheduling — server-side interceptors decide when and where each user should capture next, using the scheduling library.
- Management panel — a web UI for managing targets, browsing captures, and editing procedures.
- Syncing — capturegraph-sync pulls captured data to your analysis machine incrementally.
Installation#
The server is a member of the monorepo's uv workspace, so the standard
uv sync installs it along with the library (see
Installation):
This provides the capturegraph-server command (run it via
uv run capturegraph-server, or activate .venv first).
Starting the server#
The server runs from a root directory: every subdirectory containing a
__capture_target__.py file becomes a hosted capture target.
On first launch the server:
- Creates a commented
config.tomlin the root. - Attempts a UPnP port-forward on your router (so phones outside your network can reach it) and prints the public URL if it succeeds.
- Prints the local URL (default port
4433) and the interactive API docs at/docs— plus a note that the management panel is disabled until you setmanagement_passwordin the freshly createdconfig.toml. - Discovers and validates targets in the background — the server answers requests immediately, and targets come online one by one as they validate.
Targets are re-scanned from disk every target_refresh seconds (default
30), so dropping a new target directory into the root requires no restart.
Pointing the iOS app at the server#
In the app, open Settings → Remote Server Settings and enter the
server's URL in Server Address — include the http:// or https://
prefix, for example http://192.168.1.20:4433 or
https://capture.example.org. The app then lists every hosted target
alongside its local procedures.
CLI reference#
| Flag | Description |
|---|---|
--root PATH |
Server root containing targets and config.toml. Must exist. Defaults to the current directory. |
--mount, -m PATH:/url |
Serve a static directory at a URL prefix (repeatable), e.g. -m ./www:/static. |
--host TEXT |
Bind address for uvicorn. |
--port INTEGER |
Internal TCP port for the API. |
--public-port INTEGER |
External port requested from the router via UPnP; defaults to --port. |
--reload |
Hot-reload for development: uvicorn restarts the server whenever a .py file under the current directory changes. Cannot be combined with --headless. |
--headless |
Detach from the terminal and keep running after it closes (POSIX only). Output goes to <root>/.server.log. |
--no-delete / --allow-delete |
Disable or enable the client delete endpoints. Unset falls through to config.toml. |
Only host, port, public_port, and no_delete can be overridden from
the CLI; mounts given on the CLI are merged with those in config.toml,
and everything else (TLS, UPnP, management settings) is configured only in
the file.
Single-instance handling#
serve refuses to start while another server is already running from the
same root: it reads <root>/.server.pid, verifies the PID is alive and
actually looks like a CaptureGraph server, and (interactively) offers to
stop the old instance first. A stale pidfile left behind by a crash is
detected and ignored. To stop a headless server:
Configuration: config.toml#
config.toml lives in the server root and is auto-created with a
commented template on first launch. Precedence is
CLI flag → config.toml → built-in default.
| Key | Default | Description |
|---|---|---|
management_password |
"" |
Top-level key. Password for the management panel; the panel and its API stay disabled while blank. |
[server] host |
"0.0.0.0" |
Bind address. |
[server] port |
4433 |
Internal TCP port. |
[server] public_port |
same as port |
External port for the UPnP mapping. |
[server] no_delete |
true |
Disable the client (app) delete endpoints; the management panel can always delete. |
[server] upnp |
true |
Attempt UPnP port forwarding on startup. |
[server] target_refresh |
30 |
Seconds between disk re-scans for new/deleted targets (0 disables). |
[server] worker_threads |
64 |
Thread pool size for filesystem work. |
[server] behind_tls_proxy |
false |
Set true behind an HTTPS reverse proxy so the management cookie is marked Secure. |
[server] ssl_certfile / ssl_keyfile |
unset | Serve TLS directly from uvicorn (prefer a reverse proxy instead). |
[management] history_limit |
100 |
Max snapshots kept per target in .history/. |
[management] validation_timeout |
20 |
Seconds before a procedure validation subprocess is killed. |
[[mounts]] path, url |
— | Repeatable static directory mounts (see below). |
A config typo never takes the server down — but it disables the panel
If config.toml fails to parse, the server logs the error and starts
with all defaults — including a blank management_password, so the
management panel goes dark even if the broken file set one. Check the
startup log after editing the config.
Static mounts#
To serve extra static content (reference imagery, a project landing page) alongside the API, mount directories either from the CLI or the config:
CLI mounts and config mounts are merged; each directory is served with
StaticFiles(html=True), so an index.html works as a directory index.
UPnP#
By default the server asks your router (via UPnP/miniupnpc) to forward
public_port to the machine it runs on and prints the resulting public
URL. Failure is non-fatal — the server still starts and remains reachable
on the LAN, or publicly if you forward the port manually. Set
upnp = false on deployments behind a proper reverse proxy.
Files the server maintains#
In the server root:
| File | Purpose |
|---|---|
config.toml |
Configuration, auto-created from a commented template. |
.session_secret |
Signing secret for management sessions (mode 0600, auto-created). Logins survive restarts because of this file. |
.server.pid |
PID of the running instance, used for single-instance handling. |
.server.log |
stdout/stderr when running with --headless. |
In each target directory:
| File | Purpose |
|---|---|
__capture_target__.py |
The target definition you write. |
.persistence.db |
Durable per-target state backing the state argument of interceptors: a SQLite store (WAL mode), read-modify-write inside a BEGIN IMMEDIATE transaction so concurrent requests serialize across worker processes. |
.history/ |
Timestamped snapshots taken before every validated save from the panel. |
procedure_nodes.json |
A node-mode target's saved graph — only for the visual node editor, which is currently deferred. |
There is no per-target manifest file: a target's data layout is its
schema, which travels inside the procedure definition (see
Targets and Interceptors) and is served from
definition.
Client API#
The unauthenticated client API under /api/v1/targets is what the iOS app
and capturegraph-sync speak. Target names are
root-relative paths, so grouped targets nest naturally
(/api/v1/targets/Plants/Tulip/...). Interactive documentation is served
at /docs.
| Endpoint | Purpose |
|---|---|
GET /api/v1/targets |
List hosted target names. |
GET /api/v1/targets/{target}/definition |
The target's serialized procedure as {schema, root, nodes} (re-executes the target module, so edits go live immediately). The embedded schema is the data layout. |
GET /api/v1/targets/{target}/directories, .../directories/{path} |
List the entries at a path — enumerates dynamic Map/Array directories (key-encoded folders and zero-padded indices alike). Sync and the iOS client walk the schema with this to discover what to download. |
GET /api/v1/targets/{target}/files/{path} |
Stream a leaf's bytes — or an interceptor's freshly computed result — with ETag/X-CG-File-Extension headers; a matching If-None-Match yields 304. |
PUT /api/v1/targets/{target}/files/{path} |
Upload one leaf as the raw request body (extension in X-CG-File-Extension); returns a present leaf state. Returns 409 for interceptor-generated paths. |
POST /api/v1/targets/{target}/files:resolve |
Batch leaf-resolve used for incremental sync: per leaf, unchanged / present / missing against the client's cached version. |
POST /api/v1/targets/{target}/files:save |
Upload many small files inline in one round trip; validated and written all-or-nothing. |
DELETE /api/v1/targets/{target}/files/{path}, .../directories/{path} |
Deletes; return 403 while no_delete is set. |
Next steps#
- Targets and Interceptors — write
__capture_target__.pyfiles and schedule captures server-side. - Management Panel — enable and build the web UI.
- Syncing Data — pull captures to your analysis machine.