Skip to content

On-Premises Server

A Consystence site server is the Site-mode build of the Consystence server — the same source tree as the cloud organisation runtime, published as a Site-baked artifact. It is the on-premises tier of the deployment model: it serves operators in the browser, holds the site’s device model and runtime state in Orleans grains, and talks down to the PLCnext edge runtime over gRPC. It is built for facilities with intermittent or no internet connectivity — a CHPP, a mine site, a water-treatment plant — where operator control must keep working whether or not the cloud is reachable.

This page covers what an on-prem site server actually is today, how to install and run it under systemd, and how it joins the cloud. For cloud-hosted deployments see Azure deployment; for the edge box see edge deployment.

The same source tree publishes as either a Cloud org manager or a Site server, but the mode is baked into the artifact at dotnet publish (-p:DeploymentMode=Site|Cloud, defaulting fail-closed to Site) and is never read from configuration — a Site binary cannot be flipped to Cloud at runtime. A site server:

  • Runs ServerMode.Site — the provisioning and org-management endpoints (/api/provision/* and the like) are guarded off and return 404. The audited configuration, API-key, and sync-key admin surfaces (/api/admin/config*, /api/admin/api-keys, /api/admin/sync-keys, /api/admin/orgs/{slug}/config*) are served locally, so site-PIN admins can manage configuration and keys while offline, and the operator-initiated restore surfaces (/api/admin/orgs/{slug}/model-restore and /api/admin/orgs/{slug}/tenant/restore) are reachable on a site server.
  • Serves one organisation. The org is reached at its subdomain ({slug}.consystence.com); a site is a path under that org (/sites/{code}), never a separate domain. See the org and site model.
  • Persists Orleans grain state to PostgreSQL 16 and serves operators over SignalR with the server-driven UI.

Single-silo Orleans is the on-prem default

Section titled “Single-silo Orleans is the on-prem default”

An on-prem site server runs a single Orleans silo. That is the supported on-prem topology today.

  • Clustering defaults to localhost membership — a single silo has no membership table to coordinate.
  • The PostgreSQL ADO.NET provider backs grain storage and reminders — the durability layer for grain state and durable scheduled callbacks. It also provides opt-in multi-silo membership (Clustering:Mode=AdoNet, with a mandatory per-silo Clustering:AdvertisedIp) alongside SignalR silo-affinity placement.
  • Multi-silo clustering is the cloud’s production serving posture, not the on-prem default. The clustering and affinity code is shared across both artifacts, but an on-prem site’s serving posture for it — shared DataProtection keys, connection affinity under sustained load, NTP/clock-skew prerequisites — is still hardening. Run single-silo on-prem until that lands.
  • A Linux x64 host (physical or VM) on the site’s IT/OT-segmented network.
  • .NET 10 SDK on a build host — only for the manual-publish fallback; the packages are self-contained.
  • PostgreSQL 16, reachable from the server host, with a dedicated database and login.
  • A TLS-terminating reverse proxy (or Kestrel TLS certificates) to serve the org subdomain over HTTPS.

The site server ships as .deb/.rpm packages (consystence-site-server) and a tarball with an install.sh installer. All three lay down the same layout: the self-contained app at /usr/lib/consystence/site-server (no .NET runtime prerequisite), the consystence-site-server.service systemd unit, the consystence system user, and a config template at /etc/consystence/site-server.env.example.

Terminal window
sudo apt install ./consystence-site-server_<version>_amd64.deb # or dnf install the .rpm,
# or sudo ./install.sh from the tarball
sudo cp /etc/consystence/site-server.env.example /etc/consystence/site-server.env
sudo chown root:consystence /etc/consystence/site-server.env
sudo chmod 0640 /etc/consystence/site-server.env
# fill in the connection strings (see Configuration below), then:
sudo systemctl enable --now consystence-site-server
journalctl -u consystence-site-server -f

If you cannot use the package, publish a self-contained, framework-independent linux-x64 build (deployment mode defaults to Site at publish):

Terminal window
dotnet publish backend/Server/Consystence.Server/Consystence.Server.csproj \
-c Release \
-r linux-x64 \
--self-contained true \
-o ./stage/site-server

This produces a native Consystence.Server apphost. Copy the output to /usr/lib/consystence/site-server on the target host (ensure the frontend static assets under wwwroot/ are present), create the consystence system user, and install the unit below — mirroring the package layout keeps hand-rolled installs supportable.

Whether installed by the package or by hand, the service runs under the shipped consystence-site-server.service unit. Keep secrets (connection strings, keys) in the EnvironmentFile, not in the unit. Two settings are load-bearing and easy to miss when hand-rolling: StateDirectory=consystence (systemd creates /var/lib/consystence, where the server persists server-state.json — the write-once install id and activation state) and DataProtection__KeyPath (the service user has no home directory to fall back to, so without a pinned key ring the site-PIN JWT signing key, cookies and antiforgery tokens reset on every restart).

# /usr/lib/systemd/system/consystence-site-server.service — as shipped in the package
[Unit]
Description=Consystence site server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=consystence
Group=consystence
WorkingDirectory=/usr/lib/consystence/site-server
ExecStart=/usr/lib/consystence/site-server/Consystence.Server
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://0.0.0.0:8080
Environment=DOTNET_gcServer=1
Environment=Server__StateFilePath=/var/lib/consystence/server-state.json
# Persistent DataProtection key ring — without it the site-PIN JWT signing key,
# cookies and antiforgery tokens reset on every restart.
Environment=DataProtection__KeyPath=/var/lib/consystence/dp-keys
# systemd creates /var/lib/consystence (state file + DP keys) owned by User= on start.
StateDirectory=consystence
# Bootstrap config (connection strings and similar allow-listed settings) as Key__Sub=value env vars.
EnvironmentFile=-/etc/consystence/site-server.env
# Allow binding :443/:80 without root when in-process TLS is enabled.
AmbientCapabilities=CAP_NET_BIND_SERVICE
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Terminal window
# /etc/consystence/site-server.env — env vars map to config with `__` as the section separator
ConnectionStrings__Postgres=Host=db.internal;Database=consystence;Username=consystence;Password=CHANGE_ME
ConnectionStrings__OrleansStorage=Host=db.internal;Database=consystence;Username=consystence;Password=CHANGE_ME

Enable and start it:

Terminal window
sudo systemctl daemon-reload
sudo systemctl enable --now consystence-site-server
sudo systemctl status consystence-site-server
journalctl -u consystence-site-server -f

The default listen address is http://0.0.0.0:8080; front it with a reverse proxy that terminates TLS for the org subdomain, or enable in-process TLS (the unit grants CAP_NET_BIND_SERVICE so Kestrel can bind :443 without root).

Environment variables (including the systemd EnvironmentFile) are restricted to a bootstrap allow-list: connection strings, the listen address and environment, Kestrel, clustering identity, the state and DataProtection paths, Server__PublicUrl / Server__StateFilePath, and the API-key pepper. Everything else comes from appsettings.json or the server’s audited configuration store (encrypted secret rows, reloaded cluster-wide) — a non-bootstrap setting supplied as an environment variable is ignored. In particular, set Server:CloudUrl in appsettings.json or the config store: a Server__CloudUrl env var is ignored and activation fails with Cloud URL not configured. Deployment mode is not a setting — the published site artifact is Site-mode regardless of configuration, and a Server:Mode / Server__Mode entry is inert. The key settings are the PostgreSQL connections and the URL of the site’s cloud cell:

{
"Server": {
"CloudUrl": "https://acme.consystence.com"
},
"ConnectionStrings": {
"Postgres": "Host=db.internal;Port=5432;Database=consystence;Username=consystence;Password=...",
"OrleansStorage": "Host=db.internal;Port=5432;Database=consystence;Username=consystence;Password=..."
}
}
  • ConnectionStrings:Postgres is the application/auth database; ConnectionStrings:OrleansStorage is the Orleans grain-storage and reminders database (they may point at the same physical database). On the collapsed single-database layout, one ConnectionStrings:ConsystenceDb replaces both. Without a resolvable Orleans connection string the silo silently falls back to in-memory grain storage — no durability across restarts.
  • Server:CloudUrl is the base URL of the org’s cloud cell ({slug}.consystence.com) — not the account console. The site posts activation to {CloudUrl}/api/provision/site-activation and syncs historian and model data to the same cell; the cell mediates with the account control plane, which serves neither surface itself.

On a fresh install the server starts in setup mode and waits for a one-time activation code issued from the account console: POST /api/setup/site-activation relays { code, installId, siteCode, siteName } to the cloud cell and applies the returned bundle, establishing the server’s identity. There is no multi-step wizard or configuration import — the Fresh Install Wizard was retired, and code-keyed activation is the sole onboarding path. Authentication is the MultiAuth selector — see offline operation below for what works without the cloud.

A site server is not enrolled as a cell. It runs its own Orleans cluster (single-silo by default) and joins the cloud as a synchronising peer:

  1. The account control plane issues a one-time activation code for the site — codes are site-bound against the tenant’s declared-site registry — and holds the org’s identity, entitlement, and elevated roles (Owner/Admin). It does not hold the plant model: devices, scenes, electrical topology, and device types — the tenant.yaml surface — are site-authored and applied to both the site server and its cloud cell.
  2. The site server redeems that code through its cloud cell: the cell forwards only { code, installId } to the account under its own enrolment pull credential, uses the submitted siteCode/siteName to create or join the site, mints the site→cloud sync key (bound to {orgSlug}/{siteCode}), and returns the activation bundle. The sync key is an output of activation, never the credential the site presents to enrol. The site thereafter synchronises upward through dedicated sync grains — historian samples always, and site-model changes once enabled (model sync is opt-in during beta: Sync:ModelSyncEnabled, default off). In the other direction, an operator can restore selected model scopes from the cloud’s custody copy back onto the site — restore is always operator-initiated and site-pulled, shows a full preview (including removals) and requires exact confirmation before anything is applied; the cloud never pushes model changes down automatically. There are no cross-cluster Orleans calls: the site cluster and the cloud cluster are hard-isolated by design and communicate only through the sync surface.
flowchart TB
  subgraph cloud["Cloud (not yet GA)"]
    acct["Account control plane<br/>account.consystence.com<br/>activation codes + tenant provisioning"]
    cell["Cloud cell<br/>{slug}.consystence.com"]
  end
  subgraph site["On-premises site server — Site-mode binary"]
    silo["Orleans silo<br/>(single-silo)"]
    pg[("PostgreSQL 16<br/>grain storage + reminders")]
    silo --- pg
  end
  cell -->|redeem code under pull credential| acct
  ops["Operators (browser)"] -->|SignalR| silo
  silo -->|gRPC| edge["PLCnext edge runtime"]
  silo <-->|"activation → sync key<br/>historian sync + opt-in model sync"| cell

This is distinct from the cell-mediated provisioning of cloud-hosted orgs — see cell topology and the provisioning API for the control-plane side.

Licences are issued by the account control plane and cell-mediated; entitlement is bound to provisioned identity, not hardware. An org carries a tier — a rung on the integrator (development) or enterprise (operating) tier ladder, with Basic as the null-tier free org — plus per-seat SKU pools. For a site that loses connectivity, the cloud issues an account-signed (ECDSA) offline licence the server verifies against the published JWKS, so the site keeps running while disconnected.

Licence enforcement is degrade-not-kill and, critically, never gates operator control writes. An expired or unreachable licence may restrict cloud-tier or administrative features; it does not stop an operator commanding equipment through a running site server. See licensing for the full model.

A site server is designed to keep operating when the cloud is unreachable:

  • Authentication falls back to the locally issued site-PIN JWT (LocallyIssuedJwt) for operators, since CIAM bearer validation against id.consystence.com needs cloud reachability. Tenant API keys (csk_ prefix) remain available. See the auth model.
  • Field data continues to flow: the edge runtime buffers store-and-forward over SQLite and replays to the site server on reconnect.
  • Licensing runs on the cached offline licence until its expiry, degrading rather than blocking control.
Terminal window
# Service is up
systemctl is-active consystence-site-server.service
# Local health probe (through the reverse proxy or Kestrel directly)
curl -s http://127.0.0.1:8080/api/setup/status

The setup/status response reports the server mode, version and activation state — confirm it shows Site mode and the expected org identity. Endpoint shapes are in the API reference.