Everything not listed in this document should behave the same as upstream SimpleX Chat. If a feature, setting, or behavior is not mentioned here, the upstream documentation is accurate and fully applicable — see the Documentation section of
instructions.mdfor links.
This package runs a SimpleX Chat client as a service and puts a WebSocket in front of it, so another program can hold a SimpleX identity and send and receive messages and files on your behalf. It is infrastructure for a bot, not a chat client for a person.
- Upstream repo: https://github.com/simplex-chat/simplex-chat
- Wrapper repo: https://github.com/Start9-Community/simplex-websocket-bridge-startos
- Image and Container Runtime
- Volume and Data Layout
- File Models
- Dependencies
- Network Access and Interfaces
- Installation and First-Run Flow
- Actions
- Tasks
- Health Checks
- Backups and Restore
- Limitations and Differences
- Quick Reference for AI Consumers
One image, consumed as published.
| Property | Value |
|---|---|
| Image | lundog/simplex-websocket-bridge |
| Architectures | x86_64, aarch64 |
| Command | The image's own entrypoint |
| Subcontainer | Purpose |
|---|---|
simplex-sub |
The only daemon — the one to attach to |
The image ships SimpleX Chat unmodified, which is why the package declares two licences: the packaging is MIT, the bundled application is AGPL, and what is distributed is the aggregate.
The image tag carries two version numbers. The leading part is the SimpleX version; the trailing revision is a rebuild of the same SimpleX version, and it moves independently. Bump it deliberately.
One volume, and the layout under it is a published contract.
| Volume | Mount Point | Purpose |
|---|---|---|
main |
/data |
The SimpleX profile and file exchange |
| Path | Written by | Holds |
|---|---|---|
.simplex/ |
SimpleX | The profile database — the identity |
.simplex/files |
SimpleX | Files received from contacts |
.simplex/tmp |
SimpleX | In-flight downloads |
.simplex/outbound |
Other packages | Files staged to be sent |
store.json |
Init and an action | The API keys |
All four directories are siblings on one filesystem on purpose. SimpleX completes a download by renaming out of its temp directory into its files directory, and a rename across filesystems fails — so splitting them across mounts would break receiving files rather than merely rearranging them. Their paths are pinned by environment rather than left to the image's defaults, so the layout is a contract and not an accident.
The outbound directory is world-writable with the sticky bit, like /tmp. A consuming package stages files there as its own user, which this package cannot know in advance and which two consumers need not share — so the directory is widened rather than chowned to a guess, and the sticky bit keeps each consumer able to delete only what it staged.
Two models, with a clean split of ownership.
| File | Format | Modelled | Written by |
|---|---|---|---|
store.json |
JSON | Yes — FileHelper.json |
Init and the action |
clientSettings.json |
JSON | Yes | The action |
The store holds the bearer tokens that gate the WebSocket, each with a label. The settings file holds everything about the client: the display name and profile, whether contact requests are auto-accepted, business mode, a welcome message, the relay selection, and how long received files are kept.
Every field carries a default, so a partial or older file parses into a complete object rather than failing — and the settings file may legitimately not exist yet, since the configuration action is meant to be run before the first start.
The ownership choice covers the profile, and only the profile — the display name, full name, picture, peer type, auto-accept, business mode and welcome message:
- Managed — StartOS owns those fields and reconciles them over the WebSocket once the socket answers.
- Hands-off — your application owns them, and StartOS leaves them alone.
Choosing hands-off is what stops this package and your program fighting over the same profile.
Message relays are StartOS's in both modes. They are applied over the WebSocket on every start and whenever the selection changes — never as container environment. An env --server is INSERTed into a table unique on host and port rather than on fingerprint, so a relay already stored under an older fingerprint aborts startup outright, which is exactly what reinstalling a SimpleX Server produces.
One, optional, and declared only while it is selected.
| Dependency | Required | Kind | Why |
|---|---|---|---|
| SimpleX Server | No — only for local relays | running |
Relaying your own traffic |
Relays can be SimpleX's public ones, your own SimpleX Server on this box, or a custom list. Only the middle option adds a dependency, and it is declared reactively — switching relay modes adds or drops it.
An unreachable local server never falls back to the public presets. The selection is left unapplied, the websocket health check reports unhealthy so dependents do not start against it, and the service watches the server's address and applies it once it resolves. Being quietly moved onto someone else's relays is not an acceptable failure mode for this.
One interface, and it is authenticated.
| Interface | Id | Type | Port | Description |
|---|---|---|---|---|
| Websocket | ws |
api | 5225 | The API for driving SimpleX programmatically |
Bearer authentication is applied at the StartOS reverse proxy, not by the application: an outside client must send a token from the store or receive a 401 before it ever reaches the container. The token set is read reactively, so adding or revoking a key takes effect without a restart.
Same-box packages bypass that gate, because they dial the container's bridge address directly and that path does not traverse the proxy. So a dependent service needs no token, and a token is only for something outside the server.
Anyone with a token can act as your SimpleX identity — read messages, send messages, and send files. Treat a token as the identity itself.
Install seeds one API key, so the gate is active from the first start and there is a working token to copy. It is seeded only at install and never re-seeded, which is what makes deleting every key a durable way to lock outside access.
The configuration action is meant to be run before the first start. Doing so means the profile is created with the name, picture and relays you want, rather than created with defaults and then edited.
The first start creates the SimpleX profile. The package then waits for the socket to actually answer before syncing — the daemon ordering gates launch, not readiness, so syncing immediately would race the socket's bind. If it fails the service keeps running rather than the failure taking down a working bridge — but a relay selection left unapplied turns the websocket check red, so it is not a silent failure.
Relays land in that same sync, and the websocket health check waits for it — so a dependent package never connects before the selection is in place. A client dialing the interface from outside with a bearer token is not gated by health, so set relays before the first start if you drive the bridge that way.
Six actions, in two groups.
Everything about the client: who owns the profile, the display and full name, the profile picture, whether it presents as a bot or a person, auto-accept, business mode, the welcome message, the relay selection, and received-file retention.
- Runnable at any status, and intended to be run before the first start.
- Cost: relays and profile apply live; only changing file retention restarts the service.
- The ownership choice is the important field — see File Models.
Manages the bearer tokens that gate outside access.
- What it changes: the token list, which the interface picks up reactively.
- Deleting every key locks out all outside access while leaving same-box dependents working.
Produces a one-time invitation link for exactly one new contact.
- Requires the service to be running.
- Each run produces a fresh link; they are not interchangeable and not reusable.
Shows the client's long-lived address, the one you can publish.
- Requires the service to be running.
Replaces the long-lived address with a new one — useful after changing relays, so the address is hosted on the new servers.
- Requires the service to be running.
- Existing contacts are unaffected. Only the old link stops working, so update anywhere you published it.
Deletes the identity, all chats, and all contacts.
- Only when the service is stopped. A fresh identity is created on the next start.
- Irreversible, and it breaks every existing connection: anyone holding your link can no longer reach this client.
- It carries a consequence specific to bots, and it is not hypothetical. SimpleX reuses low, sequential contact ids after a reset, so a brand-new contact can take a former contact's id — and a consuming application that keys state by id would treat them as the old contact, inheriting session history, pairing approval, and allow-list membership. Purge the consumer's state for this channel before letting anyone reconnect.
None. This package raises no tasks, so the service is never held on a prompt and its ordinary controls are always available.
Two checks, and the duplication is deliberate.
| Check | Displayed as | Method |
|---|---|---|
simplex |
— internal | Port 5225 is listening |
websocket |
"Websocket" | The relay selection has been applied, and port 5225 is listening |
The daemon's own check is hidden; the standalone one is shown. The standalone check exists because it has a stable id that dependent packages can require in their dependency declaration — a daemon's own check is not a contract in the same way. It also requires the sync-settings one-shot, which is what holds dependents back until the selected relays are actually in effect.
Beyond the relay selection, neither says anything about SimpleX itself: a failed message or a contact who never connects both show a green check.
The main volume is copied wholesale — sdk.setupBackups(['main']). That is the profile database, the received files, whatever is staged in the outbound directory, and the API keys.
The backup is the identity. Restoring it reproduces the same SimpleX client with the same contacts and the same address — which is what makes it worth having, and what makes it as sensitive as the messages themselves.
Do not run a restored copy alongside the original. Two clients claiming one SimpleX identity is not a supported configuration.
- This is a bot bridge, not a chat client. There is no interface for a human to read messages in.
- A bearer token is the identity. Anyone holding one can send and read as your client.
- Same-box packages need no token, because they bypass the proxy over the bridge.
- Managed and hands-off modes are mutually exclusive; running your own application against a managed profile means two writers. The choice covers the profile only — relays are StartOS's either way.
- An unreachable local relay is never replaced by a public one; the bridge reports unhealthy until the server resolves.
- Reset Client is irreversible and can strand a consumer's contact state — see the action.
- The four file directories must stay siblings on one filesystem, or receiving files breaks.
- The backup reproduces the identity, so never restore two copies.
package_id: simplex-websocket-bridge
image: lundog/simplex-websocket-bridge # tag is <simplex-version>-<image-revision>
architectures:
- x86_64
- aarch64
subcontainers:
- simplex-sub
volumes:
main: /data # .simplex/{,files,tmp,outbound} plus store.json — siblings on one fs
file_models:
- store.json # apiKeys: [{ label, token }]
- clientSettings.json # profile, relays, retention, and who owns the client
startos_managed_env_vars: [] # computed per-start from clientSettings; see serverConfig.ts
dependencies:
- simplex # optional, kind: running, only while relay mode is `local`
interfaces:
ws: { type: api, port: 5225 } # bearer auth at the OS proxy; bridge callers bypass it
actions:
- configure-client # run before first start
- api-keys
- create-invitation # only-running
- view-address # only-running
- reset-address # only-running, Danger Zone
- reset-client # only-stopped, Danger Zone, irreversible
tasks: []
health_checks:
- simplex # internal (display: null)
- websocket # displayed; stable id for dependents to require