Skip to content

Commit 7ed451a

Browse files
Dhawal RankaDhawal Ranka
authored andcommitted
configure managed Mesh homeserver login
1 parent d888c49 commit 7ed451a

20 files changed

Lines changed: 767 additions & 12 deletions

.github/workflows/release-beta.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ jobs:
1919
runs-on: ubuntu-latest
2020
timeout-minutes: 45
2121
env:
22-
VITE_MESH_HOMESERVER: ${{ vars.VITE_MESH_HOMESERVER }}
22+
VITE_MESH_HOMESERVER: https://matrix.mesh.dhawal.org
2323
VITE_MESH_SERVICE_NAME: ${{ vars.VITE_MESH_SERVICE_NAME }}
24+
MESH_MANAGED_HOMESERVER: https://matrix.mesh.dhawal.org
25+
MESH_MANAGED_SERVER_NAME: mesh.dhawal.org
2426
steps:
2527
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
2628

@@ -87,8 +89,10 @@ jobs:
8789
timeout-minutes: 90
8890
environment: matrix-beta
8991
env:
90-
VITE_MESH_HOMESERVER: ${{ vars.VITE_MESH_HOMESERVER }}
92+
VITE_MESH_HOMESERVER: https://matrix.mesh.dhawal.org
9193
VITE_MESH_SERVICE_NAME: ${{ vars.VITE_MESH_SERVICE_NAME }}
94+
MESH_MANAGED_HOMESERVER: https://matrix.mesh.dhawal.org
95+
MESH_MANAGED_SERVER_NAME: mesh.dhawal.org
9296
permissions:
9397
contents: write
9498
steps:

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ e2e-results/
2929
# Internal documents, prompts, chats, and design artifacts
3030
*.md
3131
*.mdx
32+
!AGENTS.md
33+
!mesh/infra/homeserver/README.md
3234
*.doc
3335
*.docx
3436
*.pages
@@ -78,6 +80,7 @@ claudereview/
7880

7981
# Generated local integration-test state
8082
**/infra/matrix-spike/runtime/
83+
**/infra/homeserver/runtime/
8184

8285
# Local configuration and secrets
8386
.env

AGENTS.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Mesh Product North Star
2+
3+
Mesh must feel like a regular consumer communication app, even though it is
4+
built on decentralized infrastructure. A normal user should be able to:
5+
6+
1. install Mesh;
7+
2. open an invitation;
8+
3. create or sign in to an account; and
9+
4. enter the community.
10+
11+
That path must not require the user to understand or configure Matrix,
12+
homeservers, federation, DNS, TLS, ports, relays, TURN, Synapse, or storage
13+
replication.
14+
15+
## Non-negotiable UX rules
16+
17+
- Ship safe, working defaults for the recommended Mesh service in release
18+
builds.
19+
- Invitation links must carry or resolve everything needed to reach the
20+
service and community.
21+
- Use plain product language such as "service", "community", and "voice";
22+
reserve protocol terminology for diagnostics and advanced settings.
23+
- Keep "sign in somewhere else" and custom homeserver support available as an
24+
advanced path so decentralization does not become lock-in.
25+
- Detect service capabilities and network conditions automatically. Do not ask
26+
users to make infrastructure decisions that Mesh can make safely.
27+
- Errors must explain what the user can do next without exposing raw protocol
28+
failures as the primary message.
29+
- Never silently consume a user's disk, bandwidth, or battery for shared
30+
storage. Contribution must be explicit, bounded, encrypted, observable, and
31+
reversible.
32+
- Treat extra setup steps in the default onboarding path as product defects
33+
unless they are required for account security or informed consent.
34+
35+
## Architecture guardrails
36+
37+
- The managed Mesh homeserver is the zero-configuration default, not the only
38+
compatible service.
39+
- Keep identity, membership, permissions, text history, encryption state, and
40+
synchronization on the Matrix-compatible control plane.
41+
- Treat peer-assisted storage as an optional encrypted data plane with durable
42+
anchors, replication targets, integrity verification, repair, quotas, and
43+
garbage collection.
44+
- Preserve standard Matrix interoperability wherever possible so users and
45+
communities can bring another compatible homeserver.
46+
- Infrastructure choices must be migration-ready: stable identity domains,
47+
backed-up signing keys, portable databases/media, and configuration separate
48+
from secrets.
49+
50+
## Product references
51+
52+
Cinny is a useful reference for its calm interface, familiar
53+
community/channel organization, and configuration-driven homeserver defaults.
54+
Mesh should learn from that simplicity while hiding more infrastructure from
55+
the recommended user path. Do not copy Cinny branding or code without an
56+
explicit compatibility and licensing review.

mesh/infra/homeserver/.env.example

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Public, non-secret service identity. The Matrix server name is permanent
2+
# once accounts exist, so do not change MESH_SERVER_NAME after launch.
3+
MESH_SERVER_NAME=mesh.dhawal.org
4+
MESH_HOMESERVER_HOST=matrix.mesh.dhawal.org
5+
MESH_RTC_HOST=rtc.mesh.dhawal.org
6+
MESH_PUBLIC_ENABLED=0
7+
8+
# Keep Synapse private on the Mac. Caddy is the only public HTTP entry point.
9+
SYNAPSE_CONTROL_BIND=127.0.0.1
10+
SYNAPSE_CACHE_FACTOR=0.25
11+
12+
POSTGRES_USER=synapse
13+
POSTGRES_DB=synapse
14+
15+
# setup.sh replaces these placeholders in the untracked .env file.
16+
POSTGRES_PASSWORD=REPLACE_WITH_RANDOM_SECRET
17+
REGISTRATION_SHARED_SECRET=REPLACE_WITH_RANDOM_SECRET
18+
MACAROON_SECRET_KEY=REPLACE_WITH_RANDOM_SECRET
19+
FORM_SECRET=REPLACE_WITH_RANDOM_SECRET

mesh/infra/homeserver/Caddyfile

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
email {$ACME_EMAIL}
3+
admin off
4+
}
5+
6+
{$MESH_SERVER_NAME} {
7+
@matrix_client path /.well-known/matrix/client
8+
header @matrix_client Content-Type "application/json"
9+
header @matrix_client Access-Control-Allow-Origin "*"
10+
respond @matrix_client `{"m.homeserver":{"base_url":"https://{$MESH_HOMESERVER_HOST}"},"org.matrix.msc4143.rtc_foci":[{"type":"livekit","livekit_service_url":"https://{$MESH_RTC_HOST}/livekit/jwt"}]}` 200
11+
12+
@matrix_server path /.well-known/matrix/server
13+
header @matrix_server Content-Type "application/json"
14+
respond @matrix_server `{"m.server":"{$MESH_HOMESERVER_HOST}:443"}` 200
15+
16+
header {
17+
Strict-Transport-Security "max-age=31536000; includeSubDomains"
18+
X-Content-Type-Options "nosniff"
19+
Referrer-Policy "no-referrer"
20+
}
21+
respond "Mesh service is online." 200
22+
}
23+
24+
{$MESH_HOMESERVER_HOST} {
25+
header {
26+
Strict-Transport-Security "max-age=31536000; includeSubDomains"
27+
X-Content-Type-Options "nosniff"
28+
Referrer-Policy "no-referrer"
29+
}
30+
reverse_proxy synapse:8008
31+
}

mesh/infra/homeserver/README.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Mesh homeserver runbook
2+
3+
This stack runs the managed Mesh Matrix service on the Mac mini:
4+
5+
- Synapse for accounts, rooms, messages, encryption metadata, and federation;
6+
- PostgreSQL for durable state;
7+
- Caddy for public HTTPS and Matrix discovery after DNS/router activation.
8+
9+
The permanent Matrix server name is `mesh.dhawal.org`. User IDs will look like
10+
`@name:mesh.dhawal.org`.
11+
12+
## Local setup
13+
14+
```sh
15+
cd mesh/infra/homeserver
16+
./setup.sh
17+
./start.sh
18+
./status.sh
19+
./backup.sh
20+
```
21+
22+
The local Synapse control port binds only to `127.0.0.1:8008`. Registration is
23+
closed by default. Create alpha accounts with `register_new_matrix_user` after
24+
the service is healthy.
25+
26+
The initial operator account is `@dhawal:mesh.dhawal.org`. Its generated
27+
password is stored in macOS Keychain and can be retrieved locally with:
28+
29+
```sh
30+
security find-generic-password \
31+
-a '@dhawal:mesh.dhawal.org' \
32+
-s 'Mesh Homeserver Admin' \
33+
-w
34+
```
35+
36+
The installed `org.mesh.homeserver` user LaunchAgent opens Docker after login.
37+
Docker's `unless-stopped` policy then restarts the Mesh services. The
38+
`org.mesh.homeserver.backup` LaunchAgent makes a local backup daily at 3:15 AM
39+
under `~/Library/Application Support/Mesh/backups`. Copy those backups to a
40+
separate disk before treating the service as durable. The scheduled local
41+
copies are retained for 14 days.
42+
43+
The `org.mesh.homeserver.ddns` LaunchAgent checks the public IPv4 address every
44+
five minutes and updates only the `mesh.dhawal.org` Cloudflare A record when it
45+
changes. Its restricted API token is stored in macOS Keychain under
46+
`Mesh Cloudflare DDNS API Token`.
47+
48+
## Public activation
49+
50+
Do not start the public profile until all of these are true:
51+
52+
1. In Cloudflare DNS, add a DNS-only `A` record named `mesh` pointing to the
53+
home connection's public IPv4 address.
54+
2. Add a DNS-only `CNAME` named `matrix.mesh` targeting `mesh.dhawal.org`.
55+
3. Reserve the Mac's Ethernet address in the router, then forward TCP 80,
56+
TCP 443, and UDP 443 to it.
57+
4. Enable the macOS firewall and allow Docker's incoming listener.
58+
5. If the public IP is not static, configure a Cloudflare dynamic-DNS updater.
59+
6. Confirm that the local stack and a backup restore have passed validation.
60+
61+
Then start the proxy:
62+
63+
```sh
64+
sed -i '' 's/MESH_PUBLIC_ENABLED=0/MESH_PUBLIC_ENABLED=1/' .env
65+
./start.sh
66+
```
67+
68+
Verify from a device that is not on the home Wi-Fi:
69+
70+
```sh
71+
curl https://mesh.dhawal.org/.well-known/matrix/client
72+
curl https://matrix.mesh.dhawal.org/_matrix/client/versions
73+
```
74+
75+
The MatrixRTC stack under `../matrixrtc` is activated separately after the
76+
homeserver is publicly healthy.
77+
78+
## Critical data
79+
80+
Never lose:
81+
82+
- the PostgreSQL database;
83+
- `runtime/synapse/mesh.dhawal.org.signing.key`;
84+
- the local media store;
85+
- the untracked `.env` operator secrets.
86+
87+
The server name and signing key identify the homeserver in federation. Backups
88+
must eventually be copied off this Mac and restore-tested.
89+
90+
If operator secrets are exposed, rotate the PostgreSQL password and Synapse
91+
runtime secrets without changing the server name or signing key:
92+
93+
```sh
94+
./rotate-runtime-secrets.sh
95+
```

mesh/infra/homeserver/backup.sh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
5+
cd "$script_dir"
6+
umask 077
7+
8+
if [ ! -f .env ]; then
9+
echo "Missing .env. Run ./setup.sh first." >&2
10+
exit 1
11+
fi
12+
13+
set -a
14+
# shellcheck disable=SC1091
15+
. ./.env
16+
set +a
17+
18+
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
19+
destination="$script_dir/runtime/backups/$timestamp"
20+
mkdir -p "$destination"
21+
22+
docker compose exec -T postgres \
23+
pg_dump --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" --format=custom \
24+
> "$destination/postgres.dump"
25+
26+
tar -czf "$destination/synapse-critical.tar.gz" \
27+
-C "$script_dir/runtime/synapse" \
28+
homeserver.yaml \
29+
"$MESH_SERVER_NAME.signing.key" \
30+
"$MESH_SERVER_NAME.log.config"
31+
32+
if [ -d "$script_dir/runtime/synapse/media_store" ]; then
33+
tar -czf "$destination/media-store.tar.gz" \
34+
-C "$script_dir/runtime/synapse" \
35+
media_store
36+
fi
37+
38+
test -s "$destination/postgres.dump"
39+
echo "Backup completed: $destination"
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#!/usr/bin/env python3
2+
"""Apply Mesh's production-safe Synapse settings to a generated config."""
3+
4+
from __future__ import annotations
5+
6+
import os
7+
import pathlib
8+
import sys
9+
10+
import yaml
11+
12+
13+
def required(name: str) -> str:
14+
value = os.environ.get(name, "").strip()
15+
if not value or value.startswith("REPLACE_"):
16+
raise SystemExit(f"{name} is missing or still contains a placeholder")
17+
return value
18+
19+
20+
def main() -> None:
21+
if len(sys.argv) != 2:
22+
raise SystemExit("usage: configure_synapse.py /data/homeserver.yaml")
23+
24+
config_path = pathlib.Path(sys.argv[1])
25+
config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
26+
27+
server_name = required("MESH_SERVER_NAME")
28+
homeserver_host = required("MESH_HOMESERVER_HOST")
29+
30+
config.update(
31+
{
32+
"server_name": server_name,
33+
"public_baseurl": f"https://{homeserver_host}/",
34+
"pid_file": "/data/homeserver.pid",
35+
"report_stats": False,
36+
"listeners": [
37+
{
38+
"port": 8008,
39+
"type": "http",
40+
"tls": False,
41+
"x_forwarded": True,
42+
"bind_addresses": ["0.0.0.0"],
43+
"resources": [
44+
{
45+
"names": ["client", "federation"],
46+
"compress": False,
47+
}
48+
],
49+
}
50+
],
51+
"database": {
52+
"name": "psycopg2",
53+
"txn_limit": 10000,
54+
"args": {
55+
"user": required("POSTGRES_USER"),
56+
"password": required("POSTGRES_PASSWORD"),
57+
"dbname": required("POSTGRES_DB"),
58+
"host": "postgres",
59+
"port": 5432,
60+
"cp_min": 1,
61+
"cp_max": 5,
62+
},
63+
},
64+
"media_store_path": "/data/media_store",
65+
"max_upload_size": "100M",
66+
"enable_media_repo": True,
67+
"media_retention": {
68+
"remote_media_lifetime": "30d",
69+
},
70+
"url_preview_enabled": False,
71+
"enable_registration": False,
72+
"enable_registration_without_verification": False,
73+
"allow_guest_access": False,
74+
"registration_shared_secret": required("REGISTRATION_SHARED_SECRET"),
75+
"macaroon_secret_key": required("MACAROON_SECRET_KEY"),
76+
"form_secret": required("FORM_SECRET"),
77+
"password_config": {
78+
"enabled": True,
79+
"localdb_enabled": True,
80+
},
81+
"allow_public_rooms_without_auth": False,
82+
"allow_public_rooms_over_federation": False,
83+
"serve_server_wellknown": False,
84+
"trusted_key_servers": [{"server_name": "matrix.org"}],
85+
"suppress_key_server_warning": True,
86+
}
87+
)
88+
89+
rendered = yaml.safe_dump(config, sort_keys=False, default_flow_style=False)
90+
temporary_path = config_path.with_suffix(".yaml.tmp")
91+
temporary_path.write_text(rendered, encoding="utf-8")
92+
temporary_path.chmod(0o600)
93+
temporary_path.replace(config_path)
94+
config_path.chmod(0o600)
95+
96+
97+
if __name__ == "__main__":
98+
main()

0 commit comments

Comments
 (0)