Fleet
| Host | Address | Role | Versions | Status | Reported | Note |
|---|
Gateways
| ID | GEO | Group | Address | Load | Status |
|---|
Click a gateway for load & connection history.
Create GEO
Gateway groups
| Group | Type | Description |
|---|
GEOs
| Code | Name | Country | Group |
|---|
Gateways
| ID | GEO | Group | Address | Load | IPv6 | Transports | Status | Actions |
|---|
Rule lists
| ID | Kind | Entries | Ver |
|---|
Profile
| name | on | action | match by | value | → GEO |
|---|
Preview routing
Version history
| Version | Default | Rules |
|---|
Groups / roles → profile
| Group / role | Priority | Profile | Allowed gateway groups |
|---|
Installed servers
| Host name | Address | GEO | Load / RAM / disk | TLS | Status | Actions |
|---|
Add a host
How to lay this out
One private network, and every rule is explicit. All the hosts share it: postgres, messaging and calendar on one side of it, redis and the sfu nodes on the other, and nothing between them but rules this control plane writes. The database accepts only the messaging and calendar hosts, each as a /32 in pg_hba; the bus accepts only the services that dial it — the SFU nodes, the SIP bridge, the recorder — as a firewall rule on its own host. Both are re-applied on every state poll, so neither is a rule somebody has to remember to keep true.
| Host | Public | Private | Why |
|---|---|---|---|
| postgres | — | yes | only the messaging and calendar hosts connect, each allowed as a /32; nothing reaches it from outside |
| messaging | yes | yes | clients talk to it, it talks to the database |
| s3 | — | yes | object storage for attachments, avatars and recordings (ADR-0043): only the messaging and recorder hosts connect, each allowed as a /32 on 9000. Optional — without it attachments live on the messaging host's own disk, which means one host, one backup and no second messaging replica. Give the host a second, EMPTY disk and name it when you add the host: the platform makes it a ZFS pool for the objects, which is what makes a snapshot the backup. The alternative to the role is an endpoint the organisation already runs, in Settings → External stores |
| calendar | yes | yes | calendars, their entries and the ways in (ADR-0038). Public because the readers that matter are not our client: Outlook refreshes an ICS subscription and a CalDAV client polls, both from outside and both by name. Beside messaging rather than inside it for the same reason — those reads must not share a process with the chat fan-out. It keeps its own database on the same postgres, with its own login, created for it automatically, and obtains its own certificate for the name you give it |
| redis | — | yes | the coordination bus; no client ever touches it, and 6379 is open only to the services that dial it — the SFU nodes, the MCU and the recorder. That rule is its whole boundary, so a host with no ufw and no firewalld is refused rather than left reachable from the network |
| sfu ×2 | yes | yes | media and signalling are public; the bus is reached privately |
| turn | yes | — | a relay talks to clients and to nothing of ours |
| gateway | yes | DMZ only for corp recorder | see ADR-0006; a geo gateway needs no private leg |
| support | — | DMZ, beside the coordinator | the support bot's daemon (ADR-0031/0034): inbound only from the messaging host (signed webhooks on :8087), recorder to messaging, to GitLab and to the model provider; no client and nothing public ever reaches it. It holds its own provider key — the bot's webhook points at it, and it calls the model itself — so there may be several such hosts, one per case: different jobs, different knowledge bases, different keys. Each is bound to its case when it is enrolled. It also keeps the bot's knowledge base: it reads the product's repository and issues from GitLab (one archive request on first sync, then only what changed) so the bot can answer instead of only filing bugs — so its GitLab token needs read_api on that project, not just issue creation |
The one rule that silently breaks calls. A private address must never be advertised to clients. “Private address” goes in the host’s private address field; the address clients use goes in node IP for an SFU. Put a private address in the second one and every call connects, carries no media and times out with nothing in any log.
Order. 1) postgres → 2) messaging → 3) calendar (then redeploy postgres once: the access rules are narrowed to those hosts’ /32 and cannot be written before the hosts exist — one redeploy covers both) → 4) redis → 5) sfu node A → 6) sfu node B → 7) turn. Calendar is optional and can wait; until a calendar host exists the clients hide their calendar screens rather than offering buttons that fail, which is the honest answer for a deployment that has not stood one up. A second SFU node without redis is refused: two nodes without the bus each keep their own room table, so two people “in the same room” on different nodes cannot see each other and nothing reports a problem.
Sizing to start. postgres 2 vCPU / 4 GB / 40 GB SSD · messaging 2 vCPU / 4 GB (attachments live on its own disk unless an object store is configured) · s3 2 vCPU / 4 GB and a second, empty disk sized from the media rather than from the people — about 1 TB per 1000 people per year of chat files, doubling if meetings are recorded routinely; it is disk-bound, and neither CPU nor network is the limit at that size · redis 1 vCPU / 1 GB (it holds room routing, not data) · each sfu 4 vCPU / 8 GB and real bandwidth — an SFU forwards every stream to every participant, so the uplink is the limit long before CPU · turn 2 vCPU / 2 GB, bandwidth-bound for the same reason · recorder 4 vCPU / 4 GB per concurrent recording plus scratch disk — the one media role limited by CPU rather than bandwidth, since it records by driving a headless Chrome. It scales with recordings, not participants, and takes its jobs from the bus, so one worker serves the whole pool: put it on an SFU host for a small install and on its own machine once recording is routine · calendar 2 vCPU / 4 GB — it holds no files, and its whole working set is rows in postgres; sized for the polling rather than for the people, since a subscribed Outlook and a CalDAV client come back on their own schedule whether anybody is looking or not · support 1 vCPU / 2 GB / 10 GB — two small Go daemons plus the knowledge base, which is held in memory (a repository the size of this one is ~6k chunks ≈ 100 MB with embeddings, and its file on disk is the same order); searches are index-backed and answer in under a millisecond, so the single core is the model provider's problem, not ours. The thinking happens provider-side, so nothing here grows with chat load the way media hosts do. Deploy it last: it needs messaging live and the Support provisioning (Settings) run, in either order.
Ports the deploy opens for you (host firewall, when ufw or firewalld is present): sfu 7880/tcp, 7881/tcp, 50000-60000/udp · turn 3478/udp+tcp and 49152-65535/udp · messaging its listen port, plus 443 when it terminates its own TLS · calendar its listen port (:8085 by default). Redis is the exception: 6379 is opened only to the addresses that dial the bus — the SFU nodes, the MCU, the recorder — never to the world, and on a host with no firewall tooling the deploy fails rather than leave a database reachable. With one private network that rule is the only boundary the bus has, so it fails whatever address the bus listens on.
Certificates. messaging obtains its own (autocert) for the
hostname you set under Settings. Calendar does the same, and for the same reason it is
allowed to: it is our own Go service, so it can run ACME itself. An SFU cannot — that is
the whole reason edge exists — and the difference is worth knowing when a role
seems to be missing a certificate button. Subscription links are built from the calendar's
name, so it must already resolve to the host before the certificate can be issued. An SFU needs TLS in front of 7880 for iOS to connect at
all, and an enrolled host no longer needs you to arrange that: the coordinator runs the
ACME order and the host serves the one challenge file, because the name points at the host
while the account key stays here. The edge front then terminates 443 and
proxies every path to 7880 — every path, since moderation arrives on the same
host and port as the client’s WebSocket, and a front that forwards only the socket breaks
mute and remove while calls still look fine.
Add a host
SSH key (only needed for "run it for me")
Provisioning log
| Role | ID | Host | GEO | Version | Status | Agent seen |
|---|
Active sessions
| User | Device | Gateway | Expires |
|---|
Host logs
Coordinator logs
Client sessions
| User | Device | Version | Quality (per location) | Errors | Last seen |
|---|
Client diagnostics
| Time | User | Lvl | Event | Message | GEO/GW | App/OS |
|---|
Extended client debug
Monitoring
Your own receiver
Audit log
| Time | Actor | Action | Detail |
|---|
Users
| Username | Number | Name | Source | Roles | Groups | Invited by | Expires | Status | Actions |
|---|
Rooms & devices
| Name | Login | Number | Kind | Seats | Location | Panels | SIP | Last call | Actions |
|---|
Access recovery — pending callbacks
| Guest | Phone | Code | Expires |
|---|
| Guest | Invited by | Waiting |
|---|
Roles
| Role | Permissions | Directory |
|---|
Single sign-on (OIDC)
Groups → roles
| Group | Kind | Roles | Actions |
|---|
Numbering & routing (telephony)
service-roles.md §7 forbids. Guide: telephonyTwo things here are the opposite of what you may expect, and both are deliberate.
Order is declared twice. Rules are read top to bottom and the first one that matches wins — the rest are never looked at. The trunks inside a rule are tried in the order written. Teams randomises the gateways inside a route and documents that primary and backup therefore cannot be expressed at all; a cost difference between two trunks is the whole reason this feature exists, so ours is ordered.
Unknown denies. An account with no office is a third state, not a default one: a rule locked to a site refuses that caller rather than treating them as head office. The fix is to give the account an office, not to loosen the rule.
Internal numbers
Countries
| ISO | Code | Trunk pfx | Intl pfx | Number len | Dialling | Written answer |
|---|
Sites
| Id | Name | Country | Main number | Timezone |
|---|
Trunks
+7495… | evidence — the carrier's assignment or the letter of authority. A carrier that catches you presenting anything else rewrites it at best and refuses at worst. Calibrated means this carrier's SIP codes have been measured: until they are, the interface shows five call states instead of nine, because a carrier that answers "nobody picked up" with 486 would make it say «Занято», and a person who reads «Занято» stops trying.| Id | Name | Site | Digits | Prefix | Numbers | Max | State |
|---|
Dial rules
ddi, main, withheld or literal:+7495…. Lock means the rule may only use trunks belonging to the caller's own office and may not fail over out of it; hatch is the named exception that lets an account with no office use it anyway. Failover is allowed only before the callee's network has told us anything about the callee — once their phone has rung, another carrier only rings it a second time.| On | Id | Match | From | Trunks | Caller ID | Lock | Hatch | Failover |
|---|
Emergency numbers
| Digits | Note |
|---|
Spending limits
Test box
MCU: mixing & keypad (SIP endpoints)
default applies to every endpoint; by_device overrides it for one, keyed by the device's id in the registry of who may call (not the room's name, and not what the endpoint says about itself). Known settings: layout (speaker|grid), floor_ms, linger_ms, mix_voices, max_height, max_fps, content, allow_pin, allow_layout, keys. Keypad defaults: *1 mute, *2 hand, *3 pin, *4 layout, *0 help, ## leave — an empty value removes a key, and a key bound to an action the device is not allowed (allow_pin, allow_layout) is not bound at all rather than pressed and refused.
Out-of-range values are clamped, not refused: the MCU applies what it can and reports what it changed in its own health, because a room that mixes slightly differently is better than a room that does not answer. A key this build does not know is named there too — the one failure a settings file must never have is doing nothing in silence.
Calls right now
Federation policy
| By this list | in force |
| By approval | a partner asks, you approve — designed, not built (it needs a pending state and an audited approve/deny, not a checkbox) |
| Open to anyone | refused until per-peer rate limits and an abuse rota exist — a flag that turns this into an open relay is a one-line change with a company-sized consequence |
Offline depth
Bots
| Username | Name | Kind | Grants | Webhook | Status | Activity | Last seen | Created | Actions |
|---|
Per user — who has an agent
| User | Can act as them | Workers | Total | Stopped |
|---|
Automations
approve gate pauses until an allowed human reacts ✅ in the chat. Every rule speaks as a bot (as) and posts only into rooms that bot is a MEMBER of — so the member list answers "who can write here", and removing the bot from a room revokes it.| Name | Speaks as | Trigger | Steps | Enabled | Actions |
|---|
Recent runs
| Automation | State | Step | Updated | Error |
|---|
Agent activity
| Time | Actor | Action | Detail |
|---|
Acting on behalf of a user
AI lane (model provider)
/ai/v1/* proxy (ADR-0030 §2). Bots never see it — each holds its own ai_… token, issued per bot in the Bots table with a model allowlist and a daily budget. Same rule as push keys: saving is enough, the messaging host's agent applies it within a minute. Check ai in the messaging /healthz. Guide: the AI lane/v1. Welcome flow
user_created, which the coordinator reports for an admin-created account and for a first SSO login. It greets people only — meeting rooms and bots are accounts too, and they used to be welcomed to the company alongside them. Membership is by invitation — the bot adds people as they appear — so anyone who joined the company before this was switched on is missing until you fill the room below.{{display_name}}, {{user}}, {{company}}, {{department}}, {{title}}.Support case (@librarian)
dev role. Requires the AI lane above. Mail DNS
Rotating a DKIM key
Service addresses
| Address | What it is for | Held by |
|---|
Mail devices
| Name | Networks | Senders | Recipients | Per minute | Max size, MB | TLS |
|---|
@corp.example. The narrowest network decides which connector a machine belongs to.Quarantine
| Held at | Sender | Recipients | Subject | Held because | Message size |
|---|
Group members outside
| Group | Member | Bounces | Last bounce | State |
|---|
Mailboxes
| Address | Display name | Kind | Responsible person | Mailbox size | Mailbox state | Legal hold |
|---|
New shared mailbox
Sending as a group
| Group | Address | May write to it | May send as it |
|---|
Message trace
| Time | Event | Where it happened | Sender | Recipient | Next hop | Server reply | Details of the event |
|---|
Mailbox access audit
| Time | Who acted | Mailbox | Action | Client and method | Details of the access |
|---|
Disclaimer
Leavers' mailboxes
DMARC reports sent
| Day | Domain | Messages counted | Records | Sent to | Not sent because |
|---|
Client apps
| Platform | Published |
|---|
mac-v* and android-v* pipelines POST the build they just signed to <public-url>/v1/clients/<platform> with this token. It authorises that one thing and nothing else — not an admin session, which is what a pipeline holding admin credentials would be. Rotating stops the pipelines publishing until the CI variable is updated.Encryption at rest
encryption:keys permission and leave a line in the audit log; reading a body in the clear needs messages:export. Neither is in any built-in role.encryption:keys permission, and your role does not hold it — no built-in role except owner does. Ask an owner, or add that permission to your role in Users → Roles.Product options
name@another-domain and is the one switch that lets messages leave this organization — it ships OFF, and needs the policy below. Greyed rows are designed and not built; they are listed so that what the product will compose from is visible, not to be switched on today.| Module | Enabled | Access |
|---|---|---|
| VPN | base | tunnel |
| Messaging | ||
| Conferencing | ||
| Skills | follows Messaging | |
| Federation | server to server, over public HTTPS | |
| Work | SimpleOne tasks on the device, offline — designed, not built | |
General
yandex.ru, NL→a .nl site). Verify on the gateway with xray tls ping <host> — pick one showing Post-Quantum: false (X25519). Per-gateway blank = inherit global. Applies after the gateway's next Update.External Postgres and Redis
postgres, redis and s3 roles install. Leave a field empty and that store keeps coming from its role — this is a switch, not a migration: nothing is copied, so point a service at a new database only when it is empty or already holds its data. Guide: storageClient tunnel profile
fingerprint, block_quic, dns (mode/servers), fragment, mtu, mux, kill_switch (true/false — enforce blocking all traffic when the tunnel is down; omit to let users decide), reconnect (true/false — enforce auto-reconnect; omit for user choice) — without an app rebuild. JSON is overlaid global → group → user → device (most specific key wins); the client applies keys it understands and ignores the rest. Applies on the client's next connect.Calls & conferences (SFU)
The client URL is not a setting: each deployed SFU carries its own, and the coordinator hands a client the one that serves its room. A single field could only ever name one node, and it was the one place a wrong address could be typed — this one held the Redis host for a while, which the panel reported and clients were sent to anyway. For an SFU inside the perimeter, the address lives on the NODE and its subnet must be served by a gateway group, or a phone has no route to it.
Push notifications (APNs)
aps-environment: production, and the sandbox host rejects their tokens as BadDeviceToken. Only a Xcode-built debug install needs this.Push notifications (Android / FCM)
push_android in the messaging /healthz to see it land. google-services.json: that one is the client config, it belongs in the Android build (CI variable ANDROID_GOOGLE_SERVICES_JSON), and it is rejected here.Identity
Break-glass account
Gateway DNS (VLESS / Reality recorder)
/etc/resolv.conf (often a broken VPS stub) — sites don't load though the tunnel is up. Applies on each gateway's next config pull; redeploy to apply now. Directory photos (AD / LDAP)
source: ldap and an ldap_name — nothing else is read — and its manager, resolved to an account here. Standing (the disabled bit, accountExpires) is counted on every pass and applied only with the switch below: it is the one part that can lock a person out. SimpleOne (HRMS)
manager. Plain directory data: nothing here is behind the Skills switch. We always call them, never the reverse (their Scripted REST executes as Guest User on a bad token). /rest/v1/table/employee, which answers {"status":"OK","data":[…]} and takes Basic or Bearer. A path shaped like /v1/api/… is a Scripted REST action, which exists only if somebody built one on your instance.sysparm_query=active=1^company.class=internal, or contractors and closed accounts arrive too),
ask for display values (sysparm_display_value=1) — a manager without one comes back as an 18-digit record id, and the report counts those rather than writing them,
walk the reference where the field you want belongs to another record (immediate_unit_id.manager is the manager of the person’s own unit, and timezone_id.name is the zone name a calendar can load rather than the “(GMT+03:00) …” label),
and raise the page size (sysparm_limit=1000) — the default is twenty. Pages after the first are read automatically; pin sysparm_page yourself only to look at one.work_schedule on the employee record is a reference to a schedule, so what arrives is its name; the hours live in that schedule's elements, in a shape (night shifts, holidays, two levels of inclusion) that days/start/end cannot hold. So state the translation here once. Days are ISO weekdays, 1 = Monday. Run the preview first: it lists every schedule name your instance actually answers with, and how many people are on each. A name you leave unmapped is reported, not guessed at — those people keep the hours they chose. A name you map takes the field over: their calendar shows it as coming from HR and refuses edits, on every client.Domains
| Domain | Sign-in | Holds |
|---|
TLS / Certificate
Let's Encrypt (auto)
https://<host>/.well-known/simpletwo.json.Password autofill on iOS and macOS
/.well-known/apple-app-site-association, and this coordinator already serves it on every name that reaches it — so every domain listed above is covered with nothing to host.
The other half is in the APP, and it is baked at build time: the client must claim each of these domains. If autofill offers nothing on one of them, that is the half that is missing — not this list. The lines the app build needs:
webcredentials:<each domain above>Apple fetches the file through its own CDN and caches both success and failure for up to a day, so a freshly added domain is not offered immediately.
sudo swcutil developer-mode -e 1 on a Mac makes it fetch directly and is the way to check before the cache turns over.
How auto-discovery works & setup
—. It tries, in order:
- Subdomain (recommended) — create a DNS record
s2.<yourdomain>pointing here (CNAME to this coordinator's host, or an A record to its IP), then adds2.<yourdomain>to Tenant domains above, with Let's Encrypt enabled. The coordinator serves the discovery document and issues a certificate for that name on first connect. Nothing else to host. - Root file — publish
https://<yourdomain>/.well-known/simpletwo.jsonwith{ "coordinator_url": "…", "name": "…", "allow_insecure": false }. - DNS TXT — add
_simpletwo.<yourdomain>TXT =simpletwo=https://<coordinator>(used only if 1–2 fail; may be blocked on some networks).
user@itglobal.com: DNS s2.itglobal.com → this coordinator, add s2.itglobal.com to Tenant domains. The app then discovers it via https://s2.itglobal.com/.well-known/simpletwo.json. Verify anytime by opening that URL in a browser.