* feat(email): add SMTP relay as alternative to Resend
Self-hosted deployments often run behind a corporate firewall with an
existing SMTP relay (Exchange, Postfix, sendmail) and no access to
external SaaS APIs. Resend requires a public domain, an API key, and
outbound HTTPS to api.resend.com — all unavailable in air-gapped or
private-network setups.
This adds a second email delivery path using Go's stdlib net/smtp,
activated when SMTP_HOST is set. Priority order:
1. SMTP relay (SMTP_HOST set)
2. Resend API (RESEND_API_KEY set)
3. DEV stdout (neither set)
New env vars (all optional, no breaking change):
SMTP_HOST — SMTP server hostname
SMTP_PORT — port, default 25
SMTP_USERNAME — for authenticated SMTP; empty = unauthenticated relay
SMTP_PASSWORD — used only when SMTP_USERNAME is set
SMTP_TLS_INSECURE — set to "true" to skip TLS cert verification
(for private CA / self-signed certs)
The implementation:
- Dials TCP, creates smtp.Client manually (avoids smtp.SendMail which
does not expose TLS config)
- Tries STARTTLS if advertised; uses InsecureSkipVerify only when
SMTP_TLS_INSECURE=true (opt-in, nolint:gosec annotated)
- Applies PlainAuth only when SMTP_USERNAME is non-empty
- Wraps all errors with context for easier debugging
- Reuses existing HTML templates from buildInvitationParams for
invitation emails (no template duplication)
Also updates .env.example and docker-compose.selfhost.yml with the
new variables and inline documentation.
* fix(email): add dial timeout, session deadline, RFC headers for SMTP path
Address review blockers from multica-eve and Bohan-J (PR #1877):
- net.Dial → net.DialTimeout(10s) + conn.SetDeadline(30s) so a blackholed
SMTP relay cannot hang SendVerificationCode (called synchronously from the
auth handler) or leak goroutines in the invitation path.
- Add Date, Message-ID, and proper Content-Transfer-Encoding headers.
Date is required by RFC 5322; many strict relays reject messages without it.
Message-ID aids deliverability and threading.
- MIME-encode Subject via mime.QEncoding so non-ASCII workspace/inviter names
(CJK, emoji) survive without corruption across any RFC 2047-conformant relay.
- Probe 8BITMIME after (possible) STARTTLS: use Content-Transfer-Encoding 8bit
when the relay advertises 8BITMIME, quoted-printable otherwise — safe for
all relay configurations without forcing base64 overhead.
- Update SELF_HOSTING_ADVANCED.md to document Option B (SMTP relay) alongside
the existing Resend section, including all five env vars and a note that
port 465/SMTPS is not yet supported.
* fix(email): correct has8Bit assignment order (bool is first return of Extension)
The GitHub App integration code reads these two env vars and only enables
the Connect flow when both are set. .env.example never listed them, and
docker-compose.selfhost.yml did not forward them into the backend
container, so self-hosters following the integration docs had no working
way to turn the feature on.
MUL-2107
Co-authored-by: multica-agent <github@multica.ai>
docker-compose.selfhost.yml documents these as load-bearing in .env.example
but the backend service never received them, so allowlist / signup-gating
configs were silently ignored on self-hosted deployments. Wires the three
vars through with defaults matching .env.example.
Publish stable GHCR self-host images, switch self-host deploys to official image pulls with a source-build fallback, and move self-host signup / Google OAuth config onto runtime /api/config.
Self-hosted services (postgres, backend, frontend) should restart
automatically on failure or host reboot. This is standard practice
for production docker-compose deployments.
Co-authored-by: Zhazha <zhazha@openclaw.internal>
* fix(selfhost): persist local uploads and proxy file routes
* fix(selfhost): keep local uploads across container recreation
* docs(selfhost): restore relative local upload dir example
When NEXT_PUBLIC_WS_URL is not set, the WebSocket URL defaulted to
ws://localhost:8080/ws. This broke real-time features (chat streaming,
live updates, notifications) for self-hosted deployments accessed over
LAN — the browser tried connecting to localhost on the client machine
instead of the Docker host.
Now the web app derives the WebSocket URL from window.location, routing
through the existing Next.js /ws rewrite. This works for localhost, LAN,
and custom domain setups without any extra configuration.
Also adds NEXT_PUBLIC_WS_URL as a Docker build arg for explicit override,
and documents LAN access configuration in SELF_HOSTING_ADVANCED.md.
Closes#896
NEXT_PUBLIC_* env vars must be available at Next.js build time to be
inlined into the client bundle. Without this, the Google OAuth button
never renders in self-hosted Docker deployments even when the env var
is correctly set in .env.