Paste the one-time bootstrap token from the server logs
(or /data/bootstrap-token), then choose an admin password.
Data encryption key
One key decrypts this installation's message bodies. It is generated here, stored on this server outside the database's own volume, and printed on a sheet you keep — because a key that exists only on the server dies with the server.
Enter an existing key is for a reinstall onto a new server with the old database — that is what the printed sheet is for. Postpone leaves message bodies stored as plain text, which is a legitimate choice; the Settings page keeps saying so.
copied
The key is no longer on this page. Read the two groups off the sheet you just made.
Sign in
Gateways
–
healthy / total
Active connections
–
client sessions
GEO zones
–
configured
TLS
–
Host load
–
1 min avg
Fleet
Every host and what is wrong with it. Counts of things that exist answer a question nobody has at 3am; this answers the one they do.
Host
Address
Role
Versions
Status
Reported
Note
Gateways
ID
GEO
Group
Address
Load
Status
Click a gateway for load & connection history.
Create GEO
Server provisioning (gateways, messaging, Postgres) now lives on the Servers & deploys tab.
Gateway groups
Egress classes for gateways. Mark those that reach the corporate network as corporate; then simply don't add them to guest/partner profiles' allowed gateways. A corporate group with a served whitelist below becomes a DMZ — its gateways forward ONLY these internal destinations and drop the rest (ADR-0006). Leave blank for an open corporate egress.
Group
Type
Description
GEOs
Code
Name
Country
Group
Gateways
IPv6 marks a gateway whose host can egress IPv6. Clients capture IPv6 into the tunnel either way — leaving it outside was a leak — and drop it unless a gateway they selected is marked here, so a v6-only destination fails at once instead of stalling.
ID
GEO
Group
Address
Load
IPv6
Transports
Status
Actions
Rule lists
ID
Kind
Entries
Ver
New list
Profile
name
on
action
match by
value
→ GEO
Preview routing
Version history
Version
Default
Rules
Groups / roles → profile
Maps a user role/group to a routing profile and the gateway groups it may use. Corporate egress is separated simply by NOT ticking corporate gateway groups for guest/partner.
Group / role
Priority
Profile
Allowed gateway groups
Installed servers
What is actually running, one row per host, grouped by role — click a role to fold it. Hosts install their own software and update themselves; Settings changes what a host should be and reaches it on its next poll, and the ⋯ menu holds its log, the configuration it wrote, and decommissioning.
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 livekit 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
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, mediation and egress. That rule is its whole boundary, so a host
with no ufw and no firewalld is refused rather than left reachable from the network
livekit ×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 egress
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), egress 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) livekit node A → 6) livekit 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 disk until S3 exists) ·
redis 1 vCPU / 1 GB (it holds room routing, not data) ·
each livekit 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 ·
egress 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): livekit 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, mediation, egress — 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
The host joins by running one
command on itself (ADR-0025) — no SSH key here, no inbound reachability needed, so a machine
in a private network works the same as a public one. The invitation is single-use and expires
in an hour; the host exchanges it for its own credential and then pulls what its role needs.
SSH key (only needed for "run it for me")
Used once and forgotten — it is never stored. Leave empty to use the coordinator's own key.
The token is not stored — only its
hash — so this command cannot be shown again. Generate another if it is lost.
Provisioning log
One row per host, newest attempt first; a +N marks how many earlier attempts that host had, whose history is in this row’s log. Hosts that pull their state show Settings instead of a redeploy, because there is nothing to push to them.
Role
ID
Host
GEO
Version
Status
Agent seen
Active sessions
User
Device
Gateway
Expires
CPU load
–
Memory
–
Disk
–
Uptime
–
Host logs
Asks a host for the last lines of one of the units it installed. Pulled on request rather than streamed, so an idle fleet costs nothing; the host caps the size and removes secrets before sending. The answer arrives on its next poll — usually within a minute.
pick a host and a unit
Coordinator logs
Client sessions
Per-client monitoring — SW version, live per-location quality (traffic light), error count and online status. Aggregated from client telemetry + active sessions.
Remotely turn on verbose xray logging and log streaming for a specific device or employee — no app update needed. Takes effect on the client's next connect. Use the device id / username shown in Client diagnostics above.
Global (all clients): ·
Audit log
Time
Actor
Action
Detail
Users
Username
Name
Email
Source
Roles
Groups
Invited by
Expires
Status
Actions
Bots
Non-human identities (ADR-0014/0021), authenticating to messaging with a scoped bot token. A service bot belongs to the tenant. A personal bot belongs to one user and is one of two things: an assistant — a session of that person, which reads what they read and writes in their name — or a worker, which acts under its own name and sees only the conversations it was added to. The owner is shown beside every personal bot, which is what makes free naming safe. Bots hold no admin role — a bot cannot sign in, so a role would be enforced nowhere; scopes and the conversation allowlist are what grant one anything. Rotating a token keeps the owner, the entity and the fence. Whether an assistant may act for its owner at all is a tenant-wide switch, in Settings → Acting on behalf of a user; with it off the tenant has no assistants, only workers.
Username
Name
Kind
Grants
Webhook
Status
Activity
Last seen
Created
Actions
Per user — who has an agent
Only personal bots are counted: a service bot belongs to the tenant, not to whoever created it. “Can act as them” is the row to read first — those are credentials that post in that person’s name.
User
Can act as them
Workers
Total
Stopped
Rooms & devices
Bookable resources (ADR-0036 phase 5, ADR-0038 §1): a meeting room or a shared device is an account that never signs in, and it owns a calendar the same way a person does — which is what makes it bookable and what a free/busy grid reads. A panel is the screen on its wall: it authenticates to the calendar service with a credential of its own, polls that room's schedule, and holds no session — so a corridor full of panels keeps working through a coordinator outage. Issue one per screen and give it the wall's name: the label is what rotation replaces, so re-issuing under the same label kills the credential that screen was using. The address is shown once — only a hash is stored, and a lost panel is re-issued, never looked up.
Name
Login
Kind
Seats
Location
Panels
Last call
Actions
Automations
Server-side rules (ADR-0014): a trigger (bot mention / keyword / schedule / webhook) runs linear steps; a step with an 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
Everything non-humans did — bot mutations, automation runs, approvals — merged from the coordinator and messaging audit trails.
Time
Actor
Action
Detail
Access recovery — pending callbacks
A guest ordered a recovery call from the app. Until the voice bot (mediation server) is deployed, call the number yourself and dictate the code — it expires in 10 minutes and allows one password reset.
Guest
Phone
Code
Expires
Roles
Role
Permissions
Directory
Single sign-on (OIDC)
Loading…
Groups
Directory groups grant roles to their members (user → groups → roles → permissions). Assign members here or on the user; map a group to a routing profile in Routing → Groups.
Group
Description
Roles
Actions
Numbering & routing (telephony)
Which trunk a dialled number leaves through, what is presented as the caller, and what is refused (ADR-0007 §C). Written here, stored as one versioned document, and pushed to the comms plane as a snapshot — never asked per call, because a control plane in the path of every telephone call is what service-roles.md §7 forbids.
Two 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.
One control that stops outbound telephony for the whole organisation, audited, reachable by whoever is on call. It exists for the person looking at a graph at three in the morning who has to stop the bleeding before understanding it. Saving the tables below never lifts it.
Countries
The dialling context a number is read in, and the place one decision lives: outgoing dialling exists in a country only after somebody has answered for that country in writing. Where refusing an emergency call is not permitted, we do not offer outgoing telephony there at all — so this is a deployment answer, not a per-user permission, and an unanswered country simply has no dialler.
ISO
Code
Trunk pfx
Intl pfx
Number len
Dialling
Written answer
Sites
An office, as an administrative fact about an account — never derived from the network. This product ships a VPN client, so a client's network position is a statement about a tunnel exit and not about a person; Webex says the same from the other side. The main number is what somebody with no direct number of their own presents.
Id
Name
Country
Main number
Timezone
Trunks
The peers. Digits is a property of the peer, not of the number: normalise to +E.164 once, then hand each trunk the shape it asks for. Numbers is the registry of what may be presented on this trunk, one per line as +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
Match is a regular expression over the normalised number and must be anchored — an unanchored pattern matching in the middle of a number is a bill nobody can explain, so it is refused when you save. From is which offices this rule is for, empty for any. Trunks are tried in the order written, comma separated. Caller ID is 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
A separate plane, not a rule. These are matched first, on the digits exactly as typed, before any normalisation — and they are refused, never routed. An emergency call is only useful if it reaches the service for the place the caller physically is, carrying an address a dispatcher can act on; we cannot establish that address, so a 112 dialled in one city and leaving through a trunk in another sends an ambulance to the wrong building. A half-working 112 is worse than an absent one. Emptying this table does not enable emergency calls — it lets those numbers fall through to ordinary routing, which is the one outcome this refuses.
Digits
Note
Spending limits
The attack this exists for is a revenue-share fraud: an attacker with a deal on a premium international range pushes as many minutes through your account as it will take, at night and at weekends, and the bill arrives a month later. Destinations are an allowlist by country code, never a blocklist — there are more premium ranges than anyone maintains — and an empty allowlist permits nothing. The counters live in the comms plane and are checked before any call is placed; the numbers are set here. A requested callback counts against all of this: it is an outgoing call at the organisation's expense to a number the caller chose, and a convenient option cannot be a way around a spend limit.
Test box
Type a number and an account, and see which rule matches, which trunks would be tried in which order, what would be presented as the caller, and the exact digits the first trunk would receive. Nothing is dialled. This exists because a dial plan is the one piece of configuration where trying it costs money and wakes a customer up — an administrator who has to place a real call to find out what their own rule does will not check, and the rule they did not check is the one that routes payroll's calls through the wrong country. Ask about an account rather than an office where you can: an account whose office was never set is exactly the fault this finds.
Client apps
What the Dashboard and the sign-in screen offer people as a way to install SimpleTwo. Store links go to TestFlight / Google Play; the rest are builds hosted here, in the coordinator's data volume. Both the list and the downloads are public — the person who needs a client cannot sign in from one yet.
Platform
Published
The 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
ADR-0039. One master key, kept in a file on this server outside the database's volume, and printed on a recovery sheet with a QR code. The working keys — message bodies, the blind search index, meeting transcripts — are derived from it, so the safe holds one sheet rather than one per purpose. Generating, rotating and re-printing need the 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.
Changing the key or printing the sheet again needs the encryption:keys permission, which your role does not hold.
New generation re-derives the working keys from the same master: new writes use them, old rows keep opening, and the printed sheet stays valid — this is the answer to a key that may have leaked from a messaging host. New master key prints a new sheet, and rows already sealed keep needing the old one.
Product options
VPN is the base — always on. Messaging, Conferencing and Recognition are options: switch them per tenant and choose HOW they are reached — directly over TLS (works without the VPN) or through the tunnel only. Clients hide disabled modules. Recognition (skills, endorsements, kudos) is served by messaging and follows its access mode; it ships OFF, because collecting this about people is a decision an organization makes rather than one it discovers.
Module
Enabled
Access
VPN
base
tunnel
Messaging
Conferencing
Recognition
follows Messaging
Offline depth
ADR-0020. How much history a client puts on the device at its FIRST sync, so it opens without a connection. The people list, the conversation list and the calendar list are synced whole and need no number — their size is bounded by the organization. Message history is not, so it is fetched by a stated depth: the newest N conversations, M messages each. Bigger means more works offline and a longer first sign-in; the clients cap it at 200 × 500 whatever is set here. Leave both blank and every client uses its own documented default (20 × 50) — which is a different thing from setting 0, and the storage screen on the device says which of the two it is. Applies at the next sign-in or fresh install; devices already warmed keep what they hold.
Acting on behalf of a user
ADR-0021 §2. An assistant is a virtual session of one person — it reads everything they can read and posts in their name. This switch decides whether this organization may have them at all. The messaging service checks it on every request, not only when an assistant is created, so turning it off stops the ones already running rather than waiting for their tokens to expire. Worker bots — which act under their own names and see only the conversations they were added to — are not governed by it.
Loading…
General
Camouflage each gateway under a country-plausible host (RU→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.
XTLS-Vision. Splices the inner TLS so a DPI cannot see TLS-inside-TLS — the fingerprint plain VLESS+Reality carries, and the one a path that kills established flows keys on. It is the designed answer to "the tunnel connects and passes nothing". The risk is the mirror image: the mobile (libXray) build has been measured negotiating Vision and never relaying the RESPONSE — gateway acknowledges, fetches, and sends a FIN with zero data. One switch for the whole install, because both ends must agree: gateways re-render within a heartbeat, clients pick it up on their next session. Turn it off here the moment mobile goes quiet.
Client tunnel profile
Remotely tune client transport knobs — 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)
Self-hosted LiveKit SFU for A/V calls, conferences, screen share. The coordinator mints room tokens (RBAC: calls:join / calls:host); media flows client↔SFU. Deploy the livekit and turn server roles, then enter the client URL + API key/secret here.
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)
One key, both kinds of push. This is not calls-only: the same APNs key rings incoming calls and delivers every message notification. Leave it blank and users get no message banners at all — the messaging service builds no APNs client and every notification silently becomes a no-op. Saving is enough: the key is part of the messaging host’s desired state, so its agent picks the change up on its next poll (about a minute) and restarts the service. Nothing to redeploy — a host that pulls has no redeploy.
Leave OFF for TestFlight and App Store builds — they are signed aps-environment: production, and the sandbox host rejects their tokens as BadDeviceToken. Only a Xcode-built debug install needs this.
Push notifications (Android / FCM)
The Android half of the same story: without it Android devices get notifications only while the app is open. Same rule as APNs — saving is enough, the messaging host’s agent applies it within a minute and restarts the service. Check push_android in the messaging /healthz to see it land.
Firebase console → Project settings → Service accounts → Generate new private key. This is notgoogle-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.
AI lane (model provider)
The tenant's model-provider key behind the /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.
Welcome flow
One button stands the whole thing up, idempotently (ADR-0023 Part 3): the Welcome Bot with a system badge, the welcome channel, the bot's membership in it, and the rule that greets whoever appears. Everything it creates stays an ordinary object afterwards — the bot on the Bots page, the room in the client, the rules in Automations — so an administrator edits the greeting there and re-running converges the rest around it.
The trigger is user_created, which the coordinator reports for an admin-created account and for a first SSO login. 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.
The greeting is your text, not a product string: it lives in the rule and is never translated for you. Placeholders: {{display_name}}, {{user}}, {{company}}, {{department}}, {{title}}.
Support case (@librarian)
One button stands the whole ADR-0031 case up, idempotently: the bot (system badge, @librarian), its AI token, the «SimpleTwo Support» channel and «SimpleTwo Dev» room, the gated automation and the webhook secrets. Re-running converges rooms/automation and rotates both bot tokens — the support node picks them up on its next convergence. Then: enroll a support role host (Servers), add people to «SimpleTwo Dev», grant them the dev role. Requires the AI lane above.
There may be several such servers — different jobs, different knowledge bases, each with its own bot and its own provider key (ADR-0031 §5). Each is a case: pick one to edit it, or name a new one to stand another up. A support host is bound to its case when it is enrolled.
Names are per-install; blank keeps the current ones (defaults on first run). Rooms are tracked by id once created — room titles here only matter at first creation, and renaming a room later in the client is free.
This server's OWN provider credential (ADR-0031): the intermediary the bot's webhook points at calls the model itself, with a key limited upstream. Several such servers — different jobs, different knowledge bases — never share one. Leave the key blank to keep the stored one, or leave it unset to fall back to the platform lane.
Knowledge base (ADR-0034): the bot learns the product from GitLab — code, docs and issue history — so it can answer instead of only filing bugs. Blank fields keep the current settings; the embedding model must also be in the bot's AI allowlist, and needs an OpenAI-compatible provider (without one the base still searches lexically).
Gateway DNS (VLESS / Reality egress)
Resolvers gateways use for tunneled traffic. Without this, a gateway resolves via its host /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)
Employee avatars are pulled from AD (thumbnailPhoto) after each SSO login and pushed to messaging. Guests/external accounts upload their own photo in the app.
SimpleOne (HRMS)
One connection to a SimpleOne instance, shared by every module integration; HRMS is the first. It enriches employee cards with the two managers an organization actually has — functional and organizational — which AD cannot express, since it carries a single manager. Plain directory data: nothing here is behind the recognition switch. We always call them, never the reverse (their Scripted REST executes as Guest User on a bad token).
Three things the query has to do, because nothing here can do them for you:
narrow it to real staff (on SimpleOne: active=1^company.class=internal, or contractors and closed accounts arrive too),
ask for display values on every reference — a manager without one comes back as an 18-digit record id, and the report counts those rather than writing them,
and 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).
Call recording (S3)
Hosts can record calls; the SFU egress uploads MP4 to this S3 bucket. Requires the egress + Redis server role.
TLS / Certificate
Let's Encrypt (auto)
Point these hostnames (DNS) at this coordinator — a Let's Encrypt cert is issued automatically on first connect. The mobile app discovers the coordinator via https://<host>/.well-known/simpletwo.json.
How auto-discovery works & setup
The mobile app asks users only for email + password and finds this coordinator from the email domain. This coordinator: —. 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 add s2.<yourdomain> to Tenant domains above. The coordinator serves the discovery document and auto-issues a Let's Encrypt certificate for that name on first connect. Nothing else to host.
DNS TXT — add _simpletwo.<yourdomain> TXT = simpletwo=https://<coordinator> (used only if 1–2 fail; may be blocked on some networks).
Example for 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.