Upstream docs: https://element-hq.github.io/synapse/latest
Everything not listed in this document should behave the same as upstream Synapse. If a feature, setting, or behavior is not mentioned here, the upstream documentation is accurate and fully applicable.
Synapse is the battle-tested, reference implementation of the Matrix protocol -- a next-generation, federated, full-featured, encrypted, independent messaging system.
- Image and Container Runtime
- Volume and Data Layout
- Installation and First-Run Flow
- Configuration Management
- Network Access and Interfaces
- Actions (StartOS UI)
- Backups and Restore
- Health Checks
- Dependencies
- Limitations and Differences
- What Is Unchanged from Upstream
- Contributing
- Quick Reference for AI Consumers
| Property | Value |
|---|---|
| Synapse image | ghcr.io/element-hq/synapse (upstream pre-built image) |
| Nginx image | nginx Alpine (upstream unmodified) |
| PostgreSQL image | postgres Alpine (database sidecar) |
| Architectures | x86_64, aarch64 |
Synapse runs behind an Nginx reverse proxy. Nginx handles client requests on port 80, proxies Matrix API traffic to Synapse on port 8008, and serves the Synapse Admin dashboard on port 8080.
| Volume | Mount Point | Purpose |
|---|---|---|
main |
/data |
Synapse config, media, keys, appservices, StartOS state |
db |
/var/lib/postgresql |
PostgreSQL data |
Key files on the main volume:
homeserver.yaml-- primary Synapse configuration (managed by StartOS)homeserver.signing.key-- server signing key (auto-generated by Synapse)homeserver.log.config-- logging configurationmedia_store/-- uploaded media filesstore.json-- StartOS metadata (admin state, postgres password, SMTP selection)appservices/*.yaml-- registered bridge/appservice configurations
| Step | Upstream | StartOS |
|---|---|---|
| Generate config | python -m synapse.app.homeserver --generate-config |
Automatic via setupOnInit |
| Set server name | Edit homeserver.yaml |
"Set Server Address/URL" action (critical task) |
| Create admin user | register_new_matrix_user CLI |
"Set Admin Password" action (critical task) -- generates a password; the actual user is registered when the service first starts |
Key difference: On first install, StartOS generates the initial Synapse config automatically. You must run the "Set Server Address/URL" action (created as a critical task) to choose your permanent domain before starting. Completing it surfaces "Set Admin Password" as a second critical task; running that generates and shows the credentials. Starting the service then registers the admin user with the chosen password.
Warning: The server address/URL is permanent and cannot be changed after the first start.
| Setting | Upstream Method | StartOS Method |
|---|---|---|
server_name |
homeserver.yaml |
"Set Server Address/URL" action (one-time) |
enable_registration |
homeserver.yaml |
"Config" action |
| Federation | homeserver.yaml listeners |
"Config" action (enable/disable + domain whitelist) |
max_upload_size |
homeserver.yaml |
"Config" action (1-2000 MB) |
| SMTP/email | homeserver.yaml |
"Configure SMTP" action (disabled/system/custom) |
| Admin password | register_new_matrix_user |
"Set Admin Password" action |
| Appservices | Manual YAML files | Register/List/Delete Appservice actions |
Configuration NOT exposed on StartOS:
log_config-- fixed logging configurationdatabase-- always PostgreSQL via sidecar containertrusted_key_servers-- defaults tomatrix.orgreport_stats-- always disabled- Listener bind addresses and ports
| Interface | Port | Protocol | Type | Purpose |
|---|---|---|---|---|
| Homeserver | 80 (nginx) | HTTP | API | Matrix client and federation API |
| Admin Dashboard | 8080 (nginx) | HTTP | UI | Synapse Admin web interface |
Internally, Synapse listens on port 8008. Nginx proxies traffic from port 80, handles .well-known/matrix/server responses, and enforces max_upload_size on Matrix API routes.
| Property | Value |
|---|---|
| ID | set-server-name |
| Visibility | Hidden after first start |
| Availability | Only when stopped |
| Purpose | Choose permanent server domain (clearnet or Tor) |
Presents available hostnames from the homeserver interface. Sets server_name and public_baseurl in homeserver.yaml. Cannot be changed after first start.
| Property | Value |
|---|---|
| ID | set-admin-password |
| Visibility | Enabled |
| Availability | Any status |
| Purpose | Set or reset the admin account password |
Generates a random 22-character password, stores it in store.json as pendingAdminPassword, displays it, and calls sdk.restart. Applying the password needs both the Synapse image (for hash_password) and a running PostgreSQL, so the work happens at startup, not in the action: an apply-admin-password oneshot in the daemon chain consumes the pending password on the next start — if no users exist in PostgreSQL it runs register_new_matrix_user to create the admin; otherwise it hashes the password with hash_password and UPDATEs the first-registered user's password_hash. The pending field is cleared on success.
The restart is what makes the new password take effect immediately. pendingAdminPassword is read in main with .once(), not .const(), because the oneshot clears it — a watch on that field would restart main on its own write. On a stopped service sdk.restart is a no-op and the password is applied at the next start, which is why the action allows any status.
| Property | Value |
|---|---|
| ID | config |
| Visibility | Enabled |
| Availability | Any status |
| Purpose | Configure registration, federation, upload limits |
Settings:
| Setting | Default | Description |
|---|---|---|
| Registration | Disabled | Allow public account creation |
| Federation | Disabled | Enable/disable with optional domain whitelist |
| Max Upload Size | 50 MB | File upload limit (1-2000 MB) |
All three write homeserver.yaml, which main holds a .const() watch on, so the service restarts itself to pick them up.
| Property | Value |
|---|---|
| ID | manage-smtp |
| Visibility | Enabled |
| Availability | Any status |
| Purpose | Email notifications (disabled/system/custom) |
The standard three-mode SMTP flow (see the packaging guide's Set Up SMTP / Email recipe). The action only writes the selection to store.json; main watches that field with .const() and renders the credentials into homeserver.yaml's email block, so changing SMTP restarts the service. When the selection is system, main also watches sdk.getSystemSmtp with .const(), so a later change to StartOS's own SMTP settings restarts Synapse with the new credentials.
Selecting Disabled writes email: null, which Synapse reads as an absent key (EmailConfig.read_config). main renders the email block before it const-reads homeserver.yaml, so the write is not a write-after-const.
System SMTP is snapshotted at action time. If StartOS's own SMTP settings change later, re-run this action to pick them up. (Several packages instead resolve getSystemSmtp in main with a .const() watch, which follows host changes live — that isn't available here without reintroducing the write-after-const problem above.)
| Property | Value |
|---|---|
| ID | register-appservice |
| Visibility | Enabled |
| Availability | Any status |
| Purpose | Register a Matrix bridge with the homeserver |
Accepts appservice credentials (ID, tokens, URL, namespace regex) and writes the registration YAML. Typically triggered automatically by bridge services via the exported ensureAppserviceRegistration API.
| Property | Value |
|---|---|
| ID | list-appservices |
| Visibility | Enabled |
| Availability | Any status |
| Purpose | View all registered bridges |
| Property | Value |
|---|---|
| ID | delete-appservice |
| Visibility | Enabled |
| Availability | Any status |
| Purpose | Remove a registered bridge |
Included in backup:
mainvolume -- Synapse config, media, keys, appservice registrations, StartOS statedbvolume -- PostgreSQL database (backed up viapg_dump, restored viapg_restore)
Restore behavior:
- All data, users, rooms, and settings are restored
- Server name, keys, and admin credentials remain the same
| Check | Method | Grace Period | Display |
|---|---|---|---|
| Database | pg_isready |
Default | "Database" |
| Homeserver | HTTP GET http://localhost:8008/health |
15 seconds | "Homeserver" |
| Nginx | Port listening on 80 | Default | Hidden |
| Admin Dashboard | HTTP GET http://localhost:8080 |
Default | "Admin Dashboard" |
None.
- Server name is permanent -- once set and started, the server address/URL cannot be changed
- Admin username fixed -- always
admin; only password can be changed - No workers -- runs as a single monolith process (no worker-based scaling)
- Fixed logging -- log configuration is not user-configurable (INFO level, 100 MB rotation)
- No direct
homeserver.yamlediting -- configuration is managed through StartOS actions - Tor federation limitations --
.onionservers can only federate with other.onionservers
- Full Matrix protocol compliance (client-server and server-server APIs)
- End-to-end encryption support
- Federation (when enabled)
- Room creation, membership, and permissions
- Media uploads and downloads
- Push notifications
- Account data and device management
- Presence and typing indicators
- All Matrix client compatibility (Element, FluffyChat, etc.)
Build and development workflow follow the StartOS packaging guide: https://docs.start9.com/packaging. Keep README.md, instructions.md, and AGENTS.md in sync with any change to user-visible behavior or package structure.
package_id: synapse
images:
synapse: dockerTag (ghcr.io/element-hq/synapse)
nginx: nginx (Alpine)
postgres: postgres (Alpine)
architectures: [x86_64, aarch64]
volumes:
main: /data
db: /var/lib/postgresql
ports:
homeserver: 80 (nginx proxy to synapse:8008)
admin: 8080 (synapse-admin dashboard)
dependencies: none
database: PostgreSQL (sidecar, localhost-only, password auth)
startos_managed_config:
- server_name (one-time, permanent)
- enable_registration
- federation (listeners + domain whitelist)
- max_upload_size
- smtp
actions:
- set-server-name (enabled/hidden, only-stopped)
- set-admin-password (enabled, any)
- config (enabled, any)
- manage-smtp (enabled, any)
- register-appservice (enabled, any)
- list-appservices (enabled, any)
- delete-appservice (enabled, any)
health_checks:
- pg_isready (postgres)
- http_get: /health (port 8008, 15s grace)
- port_listening: 80
- http_get: / (port 8080)
backup_method: pg_dump + main volume
public_api:
- ensureAppserviceRegistration (for bridge services)