Skip to main content

Running Entrosity Matrix

Entrosity Matrix runs next to Entrosity Hub, Axis, Edge and Sphere on the same host. It is optional per host: the compose profile matrix holds its services, and the deploy scripts start them only when deploy/.env has MATRIX_ENABLED=true (Enabling Matrix on a host). Matrix needs no port besides 443: connectors connect out to it over HTTPS and WebSocket.

PartWhere
Web app (entrosity-matrix.frontend)https://hub.entrosity.com/matrix/, served by Caddy from the web image
API (entrosity-matrix.backend, image ghcr.io/entrosity/matrix-backend)https://hub.entrosity.com/matrix/api/…; Caddy strips /matrix, so the backend serves /api/v1 (browsers), /api/connector/v1 (connectors) and /api/releases/v1 (connector installers: CI uploads, expiring download links)
DatabaseIts own PostgreSQL database (matrix), migrated by the backend
Sign-inEntrosity Hub, product matrix

Services​

deploy/docker-compose.prod.yml of entrosity-infra, profile matrix:

ServiceImageWhat it does
matrix-db-initpostgres:18-alpineCreates the matrix database (owner rmm) once.
matrix-migratematrix-backendRuns migrate up as the owner, and enables the matrix_app login with MATRIX_APP_DATABASE_PASSWORD.
matrixmatrix-backendmatrix-server serve, one replica: connectors hold a WebSocket to it, the web app an event stream, and re-enable schedules run in its job queue. API on :8080 behind Caddy; metrics on :9095 (internal). Health check: matrix-server healthcheck. Memory limit MATRIX_MEMORY (384m), GOMEMLIMIT from MATRIX_GOMEMLIMIT (300MiB).

The web service (Caddy) serves Matrix too:

PathGoes to
/matrix/The web app (from the matrix-frontend image, built into the web image); /matrix redirects to /matrix/
/matrix/api/*matrix:8080, with /matrix stripped; responses are not buffered (WebSocket, event stream)

While Matrix is not enabled on a host there is no matrix service and /matrix/api/* answers 502; the Hub does not link to Matrix until the product is enabled.

The backend binary is matrix-server:

matrix-server serve run the HTTP API and job workers (default)
matrix-server migrate [cmd] database migrations: up | down [N] | version | force N
(up also sets the matrix_app login password when
MATRIX_APP_DATABASE_PASSWORD is set)
matrix-server healthcheck exit 0 when this server is ready (/readyz), for
container health checks
matrix-server release-keygen print a new connector release signing key pair
matrix-server import-stop-internet import the stop-internet panel's room rights and history
matrix-server version print the version

On the Hub, the product matrix (roles tenant_admin, teacher, viewer) is registered by the Hub's migration 0008 disabled and in beta, and enabled by migration 0009 (Rollout). The Hub must know Matrix's token: PLATFORM_PRODUCT_TOKENS includes matrix:<token>, the same value as MATRIX_PLATFORM_TOKEN. In production both come from PLATFORM_PRODUCT_TOKEN_MATRIX in deploy/.env.

Configuration reference​

The backend is configured only through MATRIX_* environment variables (entrosity-matrix.backend/internal/config). Invalid values stop it at start with a list of every problem. The RETENTION section accepts one or two underscores: MATRIX_RETENTION_JOB_DAYS = MATRIX_RETENTION__JOB_DAYS.

Server​

VariableDefaultMeaning
MATRIX_ENVdevdev, test or prod. prod refuses the development example secrets and logs JSON.
MATRIX_PUBLIC_URLhttp://localhost:8085Matrix's public address including the /matrix prefix the proxy strips (https://hub.entrosity.com/matrix). Connectors are given <MATRIX_PUBLIC_URL>/api/connector/v1/ws, and the install commands shown with new enrollment tokens use it as SERVER_URL.
MATRIX_HTTP_ADDR:8085API listener (production: :8080).
MATRIX_METRICS_ADDR:9095Internal listener for Prometheus /metrics. Never expose it publicly; empty disables it.
MATRIX_INTERNAL_ADDR:8086Internal API for Entrosity Axis's screen wall: GET /internal/v1/screen-rooms (a tenant's rooms with their computers, or a teacher's granted rooms). Served only when MATRIX_AXIS_TOKEN is set. Never expose it publicly (compose: network only, not in Caddy).
MATRIX_AXIS_TOKENempty (off)Bearer token Axis uses there (at least 32 characters; Axis's RMM_MATRIX_AXIS_TOKEN). In compose, one MATRIX_AXIS_TOKEN in .env (openssl rand -hex 32) turns the screen wall on for both services.
MATRIX_LOG_LEVELinfodebug, info, warn or error.
MATRIX_CORS_ORIGINShttp://localhost:5178Allowed browser origins, comma separated, no wildcards or paths (production: https://hub.entrosity.com).
MATRIX_TRUSTED_PROXIESnoneCIDRs or IPs allowed to set X-Forwarded-For (production: the compose network 172.30.0.0/24). The client IP recorded with each change comes from it.
MATRIX_API_RATE_PER_SECOND, MATRIX_API_RATE_BURST20, 60Web API rate limit per client IP (a school behind one NAT address shares it).
MATRIX_ENROLL_RATE_PER_MINUTE60Connector enrollments per client IP.
MATRIX_CONNECTOR_DOWNLOAD_URLnoneWhere the connector MSI can be downloaded; shown with new enrollment tokens while no connector release is stored (Connector releases).

Database​

VariableDefaultMeaning
MATRIX_DATABASE_URLnone (required)Connection URL. Production connects as matrix_app.
MATRIX_DATABASE_ROLEmatrix_appRole 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.
MATRIX_DATABASE_MAX_CONNS20Pool size.
MATRIX_DATABASE_STATEMENT_TIMEOUT30sUpper bound for any statement.
MATRIX_APP_DATABASE_PASSWORDnoneRead by migrate up: enables the matrix_app login with this password.

Secrets​

VariableMeaning
MATRIX_JWT_SECRETRequired. At least 32 bytes, raw or base64. Signs the one-minute live-update stream tokens (POST /auth/sse-token). Users' access tokens come from the Hub.
MATRIX_JWT_SECRET_OLDThe previous secret during a rotation (still verifies).
MATRIX_CREDENTIALS_KEYRequired. Exactly 32 bytes, base64: encrypts the FortiGate API tokens in the database (AES-256-GCM; the firewall's ID is bound to its ciphertext, so a ciphertext copied to another firewall does not decrypt).
MATRIX_CREDENTIALS_KEY_OLDThe previous key during a rotation: it still decrypts; tokens stored after the rotation use the current key.

With MATRIX_ENV=prod, values containing dev-only (the development examples) are refused.

Keep the credentials key

Without MATRIX_CREDENTIALS_KEY (or with a different one) no FortiGate token can be decrypted: connectors that restart or receive a new configuration cannot sign in to their firewalls until every token has been stored again. Keep a copy of deploy/.env with the backups, apart from them.

Sign-in on Entrosity Hub​

VariableDefaultMeaning
MATRIX_PLATFORM_URLnone (required)The Hub's public origin (https://hub.entrosity.com): the issuer of product tokens and where browsers sign in.
MATRIX_PLATFORM_INTERNAL_URLnone (required)The Hub's internal API, reached directly and not through the proxy (compose: http://platform:8081): signing keys and the access snapshot.
MATRIX_PLATFORM_TOKENnone (required)Matrix's bearer token on the Hub's internal API, at least 32 characters; matrix:<token> in the Hub's PLATFORM_PRODUCT_TOKENS.
MATRIX_PLATFORM_SYNC_INTERVAL30sHow often Matrix pulls users, organizations and roles from the Hub (at least 1s).

Matrix settings​

VariableDefaultMeaning
MATRIX_REPORT_STALE_AFTER90sA firewall's rooms are stale (shown as not current, changes refused with snapshot_stale) when its last report is older than this or 2.5 × the firewall's report interval, whichever is longer, when its connector is offline, or when it does not report online. At least 15s.
MATRIX_CHANGE_LIMIT_USER10Change requests per user per minute (each room of a bulk change counts). Beyond: 429 rate_limited, recorded as refused.
MATRIX_CHANGE_LIMIT_TENANT30Change requests per tenant per minute.

Connector updates​

VariableDefaultMeaning
MATRIX_RELEASE_SIGNING_KEYnoneSigns connector releases: an Ed25519 key, base64 of the 32-byte seed or the 64-byte private key. Its public half is the RELEASE_PUBLIC_KEY connectors are built with. Empty disables uploads and self-updates (uploads answer 503 releases_disabled). An invalid value stops the server.
MATRIX_RELEASE_TOKENnoneCI's bearer token for uploading connector builds (POST /api/releases/v1/connector), at least 32 characters. Empty: every upload answers 401.
MATRIX_CONNECTOR_UPDATE_CHANNELstableWhat connectors are updated to (and what Download connector offers): stable (tagged releases only) or dev (development builds and tagged releases).

Retention windows​

VariableDefaultMeaning
MATRIX_RETENTION_JOB_DAYS90Finished connector jobs. Tenants may override it (7–730).
MATRIX_RETENTION_AUDIT_DAYS365Audit log. Tenants may override it (30–3650).

0 or unset keeps the default. The change history is not purged.

Host settings​

In production the compose file fixes most of the above (MATRIX_PUBLIC_URL=https://${PLATFORM_DOMAIN}/matrix, MATRIX_CORS_ORIGINS, the listeners, the trusted proxies, MATRIX_PLATFORM_INTERNAL_URL=http://platform:8081). deploy/.env (deploy/.env.prod.example lists them) sets the rest:

VariableMeaning
MATRIX_ENABLEDtrue runs the matrix profile. Default false.
MATRIX_APP_DATABASE_PASSWORDThe matrix_app login's password. Required when enabled.
MATRIX_JWT_SECRET, MATRIX_JWT_SECRET_OLDAs above. The first is required when enabled.
MATRIX_CREDENTIALS_KEY, MATRIX_CREDENTIALS_KEY_OLDAs above. The first is required when enabled.
PLATFORM_PRODUCT_TOKEN_MATRIXMatrix's token on the Hub: becomes MATRIX_PLATFORM_TOKEN and the matrix: entry of the Hub's PLATFORM_PRODUCT_TOKENS. Required when enabled.
MATRIX_RELEASE_SIGNING_KEY, MATRIX_RELEASE_TOKENConnector self-updates, as above. Optional: empty turns them off (Enable connector self-updates).
MATRIX_CONNECTOR_DOWNLOAD_URL, MATRIX_LOG_LEVELAs above.
MATRIX_BACKEND_IMAGEDefault ghcr.io/entrosity/matrix-backend.
MATRIX_MEMORY, MATRIX_GOMEMLIMITMemory limits: 384m, 300MiB.

Generating the secrets​

openssl rand -base64 32 # MATRIX_JWT_SECRET, MATRIX_CREDENTIALS_KEY (exactly 32 bytes)
openssl rand -hex 32 # MATRIX_APP_DATABASE_PASSWORD, PLATFORM_PRODUCT_TOKEN_MATRIX, MATRIX_RELEASE_TOKEN

The database password ends up inside a connection URL, so use a value without /, + or = (hex). The product token must be at least 32 characters. Never commit these values or paste them into tickets.

Enabling Matrix on a host​

  1. Generate the secrets and set MATRIX_APP_DATABASE_PASSWORD, MATRIX_JWT_SECRET, MATRIX_CREDENTIALS_KEY and PLATFORM_PRODUCT_TOKEN_MATRIX in deploy/.env.
  2. Set MATRIX_ENABLED=true.
  3. Deploy (the next deploy run, or deploy/scripts/upgrade.sh).

With MATRIX_ENABLED=true, deploy/scripts/lib.sh adds --profile matrix to every compose command, and refuses to run while MATRIX_APP_DATABASE_PASSWORD, MATRIX_JWT_SECRET, MATRIX_CREDENTIALS_KEY or PLATFORM_PRODUCT_TOKEN_MATRIX is empty. upgrade.sh then pulls the Matrix images and, after the Hub, runs matrix-migrate and restarts matrix (and waits until it is healthy). The Hub is recreated with matrix:<token> in its product tokens. The deploy workflow's final check also waits for /matrix/api/v1/healthz on hosts with MATRIX_ENABLED=true.

Rollout​

Matrix reaches users in this order; each step needs the previous one:

  1. Shared protocol tagged. proto/matrix (and JobProgress.Detail) live in entrosity-shared-go: tag a release containing them, and bump entrosity-matrix.backend and entrosity-matrix-connector to that tag (until then they build only with a local go.work, and CI fails).
  2. Images published. ghcr.io/entrosity/matrix-backend:main and ghcr.io/entrosity/matrix-frontend:main exist (their CI on main); the connector's MSI is built by its release workflow.
  3. Infrastructure. Push the entrosity-infra change that adds Matrix (compose profile, Caddy routes, web image, scripts, deploy workflow), then set up the host as above with MATRIX_ENABLED=true, and deploy. The Hub's migration 0008 has registered the product disabled: nobody sees it yet.
  4. Enabled, in beta. Deploy the Hub release with migration 0009, which enables the product. It stays in beta: only platform admins see and open it, and they can enable it for organizations.
  5. Released. A platform admin chooses Release to organizations (Products in beta).
Order of the deploy

The deploy workflow pulls matrix-backend:main and builds the web image with matrix-frontend:main on every run, whether the host enables Matrix or not. Push the entrosity-infra change only after both images have been published, or every deploy fails (Continuous deployment). Migration 0009 belongs in a Hub release deployed after step 3: before it, the launcher would link to a Matrix that does not answer.

Rolling out allowed sites​

Allowed sites need migration 0003_allowed_sites (applied by matrix-migrate with the new backend) and connectors that announce the sites capability. Backend and connectors can be updated in any order:

  • Old connectors with the new backend keep working as before. They get no matrix.sites.set jobs; the app says The connector of this firewall does not support allowed sites yet, a save answers 409 connector_outdated, and their reports without sites keep working.
  • New connectors with the previous backend keep working too: they report the lists, which that backend ignores, and their check shows the new warnings (sites_not_set_up:) until the groups are set up.
  • Guard files are unchanged. Site writes are off until matrix-connector guard set <id> --site-writes on. The scope does not change, so no firewall has to be accepted again.
  • Nothing happens on the FortiGate until its administrator runs the setup CLI: without the groups, Matrix writes nothing, and the new switch site_writes_enabled (off) only takes effect once the groups exist.

Removing the Entrosity services​

The Entrosity services (the locked Matrix Entrosity services group, MATRIX_SERVICE_DOMAINS, the firewalls' services_enabled and the matrix.services.sync jobs) were removed: while a room's internet is off, only the sites teachers and admins list are reachable. Migration 0004_remove_entrosity_services drops firewalls.services_enabled, firewall_snapshots.services and the one-open-sync index, deletes open matrix.services.sync jobs and cancels their pending changes; the history keeps its services entries.

  • Deploy the frontend first (or together with the backend): the new API refuses services_enabled in a firewall update (422), which the previous frontend sends when a firewall is saved. The new frontend works with the previous backend.
  • Old connectors with the new backend keep working: the apply no longer carries services_enabled / service_domains, so they keep the group empty and are never sent a sync; a leftover sync would be refused at authorization (services_removed). The services in their reports are ignored; their check may still show services_not_set_up: or services_out_of_sync: warnings, which no longer matter (update the connector).
  • New connectors with the previous backend keep working too: they ignore the service fields of the apply, never write the group, report no services (so that backend never sends a sync) and fail a matrix.services.sync as an unsupported job type. allow_service_writes in an existing guard.json is ignored; no firewall has to be accepted again.
  • Remove MATRIX_SERVICE_DOMAINS from the deployment's environment (the server no longer reads it).
  • On FortiGates set up earlier, an admin can remove the empty group (Troubleshooting → Removing the old Entrosity services group).

Connector releases​

Matrix keeps the connector's installers and rolls them out: CI uploads every build, Matrix signs it, connectors that can update themselves are offered the newest one, and tenant admins download it from Download connector (Connectors → Updates).

Enable connector self-updates​

  1. Generate the Ed25519 key pair once:

    docker compose -f docker-compose.prod.yml --env-file .env --profile matrix run --rm --no-deps matrix release-keygen
    openssl rand -hex 32 # MATRIX_RELEASE_TOKEN

    release-keygen prints MATRIX_RELEASE_SIGNING_KEY (the server's secret) and RELEASE_PUBLIC_KEY (for connector builds). Keep the signing key as secret as the other keys.

  2. In the host's deploy/.env set MATRIX_RELEASE_SIGNING_KEY and MATRIX_RELEASE_TOKEN, and optionally MATRIX_CONNECTOR_UPDATE_CHANNEL.

  3. In the entrosity-matrix-connector repository set the secrets RELEASE_PUBLIC_KEY (compiled into the connector) and MATRIX_RELEASE_TOKEN (the same token), and the variable RELEASE_PUBLISH_ENABLED=true. MATRIX_RELEASE_API (a variable) changes the target, default https://hub.entrosity.com/matrix/api/releases/v1.

  4. Redeploy. The next connector build is uploaded and offered.

  5. Connectors installed before this were built without the public key and cannot update themselves: install the new version on each of them once.

Keep the signing key

Connectors accept only releases signed with the key their build contains. With another key (a lost or replaced seed), every update fails with signature_invalid until each connector has been reinstalled by hand with a build that contains the new public key.

Publishing​

CI uploads each MSI with scripts/publish-release.sh of the connector repository:

BuildVersionChannel
Tag vX.Y.Z (release.yml)X.Y.Zstable
Tag vX.Y.Z-suffixX.Y.Z-suffixdev
Every push to main (dev-release.yml)a -dev pre-release versiondev

The upload is POST /api/releases/v1/connector?version=…&channel=…&notes=… with Authorization: Bearer $MATRIX_RELEASE_TOKEN and the raw MSI as the body (at most 64 MiB). Matrix signs its manifest (component matrix-connector: version, SHA-256 and size) and keeps the newest ten releases.

AnswerMeaning
201Stored.
401 unauthorizedWrong or missing token, or MATRIX_RELEASE_TOKEN is not set.
409 release_existsThis version is already published (the script treats it as done).
422 validationNot a semantic version, a channel other than stable or dev, or an empty or too large body.
503 releases_disabledMATRIX_RELEASE_SIGNING_KEY is not set.

Offering updates​

Every 5 minutes (releases.rollout) and when a connector says hello, Matrix offers the newest release of MATRIX_CONNECTOR_UPDATE_CHANNEL to online connectors that announce the update capability, run an older version, have no update job open and were not offered the same version within the last hour. The offer is an update_agent job with download links valid 2 hours (GET /api/connector/v1/releases/{releaseID}/msi?t=…, authorized by the link itself). What the connector does: Matrix connector → Self-update.

Background jobs​

The backend runs its jobs in PostgreSQL (River):

JobWhenWhat
realtime.sweepEvery 30 secondsMarks connectors silent for 3 minutes offline; expires overdue connector jobs and resends unacknowledged ones. An expired write job becomes Unconfirmed when the connector had taken it, Error when it never had.
matrix.staleEvery 30 secondsFinds firewalls whose rooms became stale (or current again) and notifies the open web apps (matrix.rooms).
schedule.fireAt each re-enable timeMarks the schedule firing and sends the room's enable as a system change. While the connector is offline or the firewall stale it retries (30 seconds, doubling up to 15 minutes); 24 hours after the time the schedule is failed.
schedule.sweepEvery minuteQueues overdue pending schedules again (for example after the server was down).
releases.rolloutEvery 5 minutesOffers the newest connector release (Offering updates). Does nothing without MATRIX_RELEASE_SIGNING_KEY.
retention.cleanupDailySee Retention.

Besides, every connector report (matrix.report) updates the rooms (a room missing from a successful report becomes no longer on the firewall), the address groups and allowed sites when they changed, and the firewall's status, FortiOS version and guard state, and notifies the web apps. Matrix never creates a write job again on its own: a failed or unconfirmed change stays as it is until a person acts. The only system writes are the automatic re-enables.

Retention​

A daily retention.cleanup job deletes old data in batches of 10,000 rows:

DataKept
Finished connector jobsThe tenant's override (settings.retention.job_days, 7–730), else MATRIX_RETENTION_JOB_DAYS.
Audit logThe tenant's override (settings.retention.audit_days, 30–3650), else MATRIX_RETENTION_AUDIT_DAYS.
Enrollment tokens30 days after they expired or were revoked.
Hub sessions ended, used step-up tokensOnce no longer needed.
Change history (change_events)Kept (not purged).

matrix_retention_rows_deleted_total{category} counts what was removed.

Database and migrations​

Matrix has its own database. Migrations are embedded in the binary (entrosity-matrix.backend/db/migrations) and applied by migrate up:

MigrationAdds
0001_initMatrix'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, connector_releases, and row-level security with the matrix_app role.
0002_matrixfirewalls (the configuration, the sealed token token_enc with credentials_version, the write switches and what the connector last reported), rooms, firewall_snapshots (address groups), room_grants, the append-only change_events (the history), address_operations, bulk_actions, room_schedules, rate_buckets (change limits), and on jobs the columns linking a job to its firewall, policy, room, bulk action, schedule and address operation, with at most one open room switch per policy and one open address job per firewall.
0003_allowed_sitesAllowed sites: on firewalls the switches site_writes_enabled (default off) and services_enabled (default on); on firewall_snapshots the reported lists and services group (sites, sites_hash, sites_fetched_at, services); the change kinds sites and services; at most one open matrix.sites.set per firewall and list (the list is kept in jobs.room_code: shared or the room code) and one open matrix.services.sync per firewall. The down migration drops them.
0004_remove_entrosity_servicesRemoves the Entrosity services: drops firewalls.services_enabled, firewall_snapshots.services and the index of open matrix.services.sync jobs, deletes the open ones and cancels their pending changes. The history keeps its services entries (the kind stays allowed). The down migration restores the columns (services_enabled off for existing firewalls) and the index.
  • Tenant isolation: every tenant table has a row-level security policy (FORCE); the server runs as matrix_app (NOBYPASSRLS).
  • Append-only history: a trigger refuses to change or delete a finished change_events row; only a pending row gets its result and steps.
  • FortiGate tokens are stored only encrypted (token_enc) and are never part of a job, an event or the audit log.

Backups and restore​

deploy/scripts/backup.sh dumps the matrix database whenever it exists, next to the others: backups/db/matrix-<timestamp>.dump (pg_dump -Fc, checked by reading it back), pruned with the other dumps after the retention days.

deploy/scripts/restore.sh with MATRIX_ENABLED=true stops matrix, restores the matrix-… dump of the same backup run when there is one (it recreates the database and the matrix_app role), runs matrix-migrate, and starts matrix again.

The dump holds the FortiGate tokens encrypted with MATRIX_CREDENTIALS_KEY: a restore needs the same key. The dump also holds the history (user e-mail addresses and client IPs): protect backups like the database. After a restore, rooms are brought up to date by the next connector reports; changes made after the backup are not in the restored history.

Import from the stop-internet panel​

matrix-server import-stop-internet --tenant <id> --firewall <id> --users users.csv --export export.json imports the single-school panel Matrix replaces into one tenant and firewall: teachers' allowed rooms become room rights, and its history (room switches, IP changes, new computers) becomes change history with source stop-internet. Users are matched by e-mail through users.csv (username,email) and must already exist in Matrix. It is idempotent. The step-by-step procedure, including a read-only export from the panel's SQLite database: Moving from the old panel.

Monitoring​

  • GET /healthz (liveness) and GET /readyz (readiness: the database answers; it also reports whether the copy of the Hub's data is current, stale after 5 minutes, without failing) on the API listener. Publicly: https://hub.entrosity.com/matrix/api/v1/healthz.
  • Prometheus metrics on MATRIX_METRICS_ADDR (:9095; the Prometheus job matrix in deploy/monitoring/prometheus.yml), among them matrix_ws_connections, matrix_connectors_online, matrix_firewalls_online (enabled firewalls reported online), matrix_jobs_total{type,status}, matrix_job_dispatch_seconds, matrix_http_request_duration_seconds, matrix_platform_syncs_total, matrix_platform_sync_age_seconds, matrix_sse_subscribers, matrix_retention_rows_deleted_total, matrix_river_queue_depth and matrix_db_pool_connections.
  • Logs are JSON in prod. A refused write-time authorization is logged at info (connector write refused, with the job and the reason); it is also a step of the change in the history.
  • Watch Schedules → Failed re-enables in the tenants: a failed re-enable leaves a room off.

Local development​

The development stack of entrosity-infra runs Matrix next to the Hub (Development setup):

  1. deploy/.env.example has a Matrix block (MATRIX_* on :8085, metrics on :9095, development secrets, MATRIX_PUBLIC_URL=http://localhost:5175/matrix), and the Hub's PLATFORM_PRODUCT_TOKENS includes the matrix token.
  2. Create the database once: docker compose -f deploy/docker-compose.yml exec postgres createdb -U rmm matrix.
  3. In entrosity-matrix.backend (with a local go.work using ../entrosity-shared-go until the shared tag exists): make migrate, then make dev (API on :8085).
  4. In entrosity-matrix.frontend: its dev server on :5178.
  5. Open http://localhost:5175/matrix/. The Hub's dev server proxies /matrix/api to :8085 (prefix stripped) and /matrix to :5178.
  6. For rooms, run a connector with the simulator: matrix-connector enroll --server http://localhost:5175/matrix --token <token>, then make dev in entrosity-matrix-connector (Trying Matrix with the simulator).