Running Entrosity Edge
Entrosity Edge runs next to Entrosity Hub and Axis on the same host:
| Part | Where |
|---|---|
Web app (entrosity-edge.frontend) | https://hub.entrosity.com/edge/, served by Caddy from the web image |
API (entrosity-edge.backend, image ghcr.io/entrosity/edge-backend) | https://hub.entrosity.com/edge/api/…; Caddy strips /edge, so the backend serves /api/v1 (browsers) and /api/connector/v1 (connectors) |
| Database | Its own PostgreSQL database (edge), migrated by the backend |
| Sign-in | Entrosity Hub, product edge |
The backend binary is edge-server:
edge-server serve run the HTTP API and the background jobs (default)
edge-server migrate up|down [N]|version|force N
database migrations (up also enables the edge_app
login when EDGE_APP_DATABASE_PASSWORD is set)
edge-server healthcheck exit 0 when /readyz answers (container health check)
edge-server release-keygen print a new connector release signing key pair:
EDGE_RELEASE_SIGNING_KEY (server) and
RELEASE_PUBLIC_KEY (connector builds)
edge-server version print the version
serve refuses to start against a database schema it was not built for:
run migrate up first.
On the Hub, the product edge (roles tenant_admin, operator,
viewer) is registered by the Hub's migration 0003 disabled: disabled
products are hidden from the launcher and cannot be given to
organizations. It is enabled once Edge is deployed; then platform admins
turn it on per organization (Platform administration).
The Hub must know Edge's token: PLATFORM_PRODUCT_TOKENS includes
edge:<token>, the same value as EDGE_PLATFORM_TOKEN.
Configuration reference
The backend is configured only through EDGE_* environment variables
(entrosity-edge.backend/internal/config). Invalid values stop it at
start with a list of every problem. The RETENTION section accepts one or
two underscores: EDGE_RETENTION_EVENT_DAYS = EDGE_RETENTION__EVENT_DAYS.
Server
| Variable | Default | Meaning |
|---|---|---|
EDGE_ENV | dev | dev, test or prod. prod refuses the development example secrets. |
EDGE_PUBLIC_URL | http://localhost:8082 | Edge's public address including the /edge prefix the proxy strips (https://hub.entrosity.com/edge). Connectors are given <EDGE_PUBLIC_URL>/api/connector/v1/ws, and the install commands shown with new enrollment tokens use it as SERVER_URL. |
EDGE_HTTP_ADDR | :8082 | API listener. |
EDGE_METRICS_ADDR | :9092 | Internal listener for Prometheus /metrics. Never expose it publicly; empty disables it. |
EDGE_LOG_LEVEL | info | debug, info, warn or error. |
EDGE_CORS_ORIGINS | http://localhost:5176 | Allowed browser origins, comma separated, no wildcards or paths (production: https://hub.entrosity.com). |
EDGE_TRUSTED_PROXIES | none | CIDRs or IPs allowed to set X-Forwarded-For (the proxy's network). |
EDGE_API_RATE_PER_SECOND, EDGE_API_RATE_BURST | 20, 60 | Web API rate limit per client IP (an office behind one NAT address shares it). |
EDGE_ENROLL_RATE_PER_MINUTE | 60 | Connector enrollments per client IP. |
EDGE_CONNECTOR_DOWNLOAD_URL | none | Where the connector MSI can be downloaded; shown as Download the connector installer with new enrollment tokens. |
Database
| Variable | Default | Meaning |
|---|---|---|
EDGE_DATABASE_URL | none (required) | Connection URL. Production connects as edge_app. |
EDGE_DATABASE_ROLE | edge_app | Role the pool switches to after connecting (SET ROLE), so row-level security applies even when connecting as the owner. Empty when the login already is the application role and cannot switch. |
EDGE_DATABASE_MAX_CONNS | 20 | Pool size. |
EDGE_DATABASE_STATEMENT_TIMEOUT | 30s | Upper bound for any statement. |
EDGE_APP_DATABASE_PASSWORD | none | Read by migrate up: enables the edge_app login with this password. |
Secrets
| Variable | Meaning |
|---|---|
EDGE_JWT_SECRET | Required. Signs the one-minute live-update stream tokens (POST /auth/sse-token). At least 32 bytes, raw or base64 (openssl rand -base64 32). Users' access tokens come from the Hub. |
EDGE_JWT_SECRET_OLD | The previous secret during a rotation (still accepted). |
EDGE_MASTER_KEY | Optional. Base64 of exactly 32 bytes (openssl rand -base64 32). Encrypts the controller PINs and keys the connector download links. Keep it: stored PINs cannot be read without it. Without it the server starts, but PINs cannot be set (master_key_missing) and connector self-update is off. An invalid value stops the server. |
EDGE_MASTER_KEY_OLD | Previous master keys during a rotation, comma separated; they only decrypt (and still verify download links). Only with EDGE_MASTER_KEY. |
EDGE_RELEASE_SIGNING_KEY | Optional. Base64 Ed25519 private key (64 bytes) or 32-byte seed that signs connector releases, from edge-server release-keygen. Needed, with the master key, to publish and roll out connector releases. |
EDGE_RELEASE_TOKEN | Optional. CI's bearer token for publishing connector releases, at least 32 characters. Unset: the publishing endpoint answers 404. |
Sign-in on Entrosity Hub
| Variable | Default | Meaning |
|---|---|---|
EDGE_PLATFORM_URL | none (required) | The Hub's public origin (https://hub.entrosity.com): the issuer of product tokens and where browsers sign in. |
EDGE_PLATFORM_INTERNAL_URL | none (required) | The Hub's internal API, reached directly and not through the proxy (compose: http://platform:8081): signing keys and the access snapshot. |
EDGE_PLATFORM_TOKEN | none (required) | Edge's bearer token on the Hub's internal API, at least 32 characters; edge:<token> in the Hub's PLATFORM_PRODUCT_TOKENS. |
EDGE_PLATFORM_SYNC_INTERVAL | 30s | How often Edge pulls users, organizations and roles from the Hub (at least 1s). |
Retention windows
| Variable | Default | Meaning |
|---|---|---|
EDGE_RETENTION_EVENT_DAYS | 365 | Access log (events). |
EDGE_RETENTION_JOB_DAYS | 90 | Finished connector jobs. Tenants may override it (7–730). |
EDGE_RETENTION_AUDIT_DAYS | 365 | Audit log. Tenants may override it (30–3650). |
0 or unset keeps the default.
Connector releases
Edge connectors update themselves from releases stored in Edge (Connectors → Updates).
Setting it up
-
Generate the signing key pair once:
edge-server release-keygen# production:docker compose run --rm --no-deps edge release-keygenIt prints
EDGE_RELEASE_SIGNING_KEY(keep it secret) and the matchingRELEASE_PUBLIC_KEY. -
On the server set
EDGE_MASTER_KEY,EDGE_RELEASE_SIGNING_KEYandEDGE_RELEASE_TOKEN(32+ characters) (in production indeploy/.env), and restart Edge. -
In the
entrosity-edge-connectorrepository set the secretRELEASE_PUBLIC_KEY(compiled into the connector: builds without it refuse updates), the secretEDGE_RELEASE_TOKEN(the same value as the server's) and the variableEDGE_RELEASE_API, the Edge address (https://hub.entrosity.com/edge). WithoutEDGE_RELEASE_APIbuilds are not published to Edge (CI workflows). -
Upgrade the connectors installed before self-update by hand once (Connectors → Updates).
Publishing
CI publishes every connector build:
| Build | Version | Channel |
|---|---|---|
Tagged release vX.Y.Z | X.Y.Z | stable |
Pre-release vX.Y.Z-suffix | X.Y.Z-suffix | beta |
Dev build (every push to main) | 0.0.<build>-dev.<commit> | beta |
curl -X POST -H "Authorization: Bearer $EDGE_RELEASE_TOKEN" \
-H "Content-Type: application/octet-stream" --data-binary @edge-connector.msi \
"https://hub.entrosity.com/edge/api/v1/admin/connector-releases?version=1.4.0&channel=stable¬es=release%20v1.4.0"
The body is the raw MSI (at most 64 MiB). Edge stores it in its database
and signs it with EDGE_RELEASE_SIGNING_KEY.
| Answer | Meaning |
|---|---|
| 201 | New release. |
| 200 | This version is already released with the same MSI (a retry). |
409 release_exists | This version is already released with a different MSI. |
409 signing_key_missing, master_key_missing | EDGE_RELEASE_SIGNING_KEY or EDGE_MASTER_KEY is not set. |
| 401 | Wrong token. |
| 404 | EDGE_RELEASE_TOKEN is not set on the server. |
Edge keeps the newest 10 releases of each channel, plus every version a
connector still runs (the watchdog's rollback). Global admins list them
with GET /api/v1/admin/connector-releases, which also returns the
public_key connector builds must embed.
Rollout
Every connector follows an update channel, Stable by default; tenant admins switch a connector to Beta (development builds) on the Connectors page. Stable connectors get the newest stable release, beta connectors the newest of the beta and stable releases.
Edge offers a newer version when a connector connects and every 5 minutes
(releases.rollout): an update_agent job with signed download links
valid 24 hours (GET /api/connector/v1/releases/{id}/msi?exp=&sig=, no
login: the signed link is the authorization). A version is offered only to
connectors that announce edge_update (builds with self-update), when no
update job of the connector is open, and not again within an hour. The
job and the installation:
Edge connector protocol,
Edge connector → Self-update.
Retention
A daily retention.cleanup job deletes old data in batches of 10,000 rows:
| Data | Kept |
|---|---|
| Access log | EDGE_RETENTION_EVENT_DAYS, for every tenant. Events are stored in monthly partitions; a month is dropped as a whole once it is entirely older than the window, so events are kept between the window and the window plus one month. |
| Finished connector jobs | The tenant's Keep connector job history (days), else EDGE_RETENTION_JOB_DAYS (never less than 7 days). |
| Audit log | The tenant's Keep audit log (days), else EDGE_RETENTION_AUDIT_DAYS (never less than 30 days). |
| Enrollment tokens | 30 days after they expired or were revoked. |
| Hub sessions ended, used step-up tokens | An hour after they ended; once expired. |
Tenant overrides are set under Tenant settings
(Tenant settings, sites and audit log).
edge_retention_rows_deleted_total{category} counts what was removed.
Background jobs
The backend runs its jobs in PostgreSQL (River), so several replicas share them safely:
| Job | When | What |
|---|---|---|
controller.sync | After every change, per controller | Compiles the controller's configuration and sends it as an edge.config.apply job, unless it already holds it. Bursts for one controller merge. |
controller.reconcile | Every 5 minutes | Re-queues controllers whose wanted version is not applied (connector was offline, lost job, a sync stuck in Syncing for over 20 minutes). |
cardholder.validity | Every minute | Resyncs the controllers of cardholders whose validity period started or ended. |
holidays.horizon | Every hour | Resyncs the controllers that have a holiday newly within the 90 days a configuration carries (local to each controller's time zone). |
releases.rollout | Every 5 minutes | Offers a newer connector release to online connectors that update themselves (Rollout). Does nothing without EDGE_MASTER_KEY and EDGE_RELEASE_SIGNING_KEY. |
realtime.sweep | Every 30 seconds | Marks connectors silent for 3 minutes offline; expires overdue connector jobs and resends unacknowledged ones (after 60 seconds). |
events.partitions | Daily | Creates the monthly event partitions two months ahead. |
retention.cleanup | Daily | See Retention. |
Database and migrations
Edge has its own database. Migrations are embedded in the binary
(entrosity-edge.backend/db/migrations) and applied by migrate up:
| Migration | Adds |
|---|---|
0001_init | Edge's copy of the Hub's tenants, users and roles (tenants, users, tenant_memberships, Hub sync state, ended sessions, used step-up tokens), sites, the append-only audit_log, enrollment_tokens, connectors, jobs, and row-level security with the edge_app role. |
0002_access_control | controllers (with wanted/applied versions and sync status), doors, readers, cardholders, cards, schedules, holidays, access_groups with their doors and members, the monthly-partitioned events log with its partition functions, and worker_watermarks. |
0003_controller_pin | controllers.pin_enc: the controller's PIN, encrypted with EDGE_MASTER_KEY (NULL: no PIN). |
0004_connector_releases | connector_releases (connector MSIs with their channel, SHA-256, size and signature; global, without row-level security), and the connectors' update_channel, update_version and update_offered_at. |
0005_controller_enabled | controllers.enabled (default true) and the sync status disabled, which a controller has exactly while it is disabled. |
- Tenant isolation: every tenant table has a row-level security policy;
the server runs as
edge_app(NOBYPASSRLS). References between tenant tables are composite(tenant_id, id)keys, so a cross-tenant reference is impossible. - Append-only:
edge_appcannot update or deleteevents(retention drops partitions) oraudit_log(retention purges through a dedicated function). - Events whose time falls outside the monthly partitions land in
events_defaultand are moved when their month's partition is created.
Monitoring
GET /healthz(liveness) andGET /readyz(readiness: the database answers; it also reports the age of the copy of the Hub's data).- Prometheus metrics on
EDGE_METRICS_ADDR:edge_ws_connections(connected connectors),edge_jobs_total{type,status},edge_job_dispatch_seconds,edge_http_request_duration_seconds,edge_platform_syncs_total,edge_platform_sync_age_seconds,edge_sse_subscribers,edge_retention_rows_deleted_total. - Logs are JSON in
prod.