mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 04:56:20 +02:00
* fix(email): wire SMTP_EHLO_NAME through self-host config + docs Follow-up to #3679, which added SMTP_EHLO_NAME in code but never exposed it to operators. - docker-compose.selfhost.yml: pass SMTP_EHLO_NAME through to the backend container. The compose env block is an explicit allowlist, so without this the override set in .env was silently dropped and never reached the process — making the escape hatch unusable on the docker path. - Document the var alongside its SMTP_* siblings: .env.example, SELF_HOSTING_ADVANCED.md, environment-variables.mdx, auth-setup.mdx, and self-host-quickstart.mdx (the last two with a strict-relay example). - email.go: log when os.Hostname() fails instead of silently falling back to net/smtp's lazy "localhost" — the exact greeting strict relays reject. - Add TestNewEmailService_EHLOName covering the env override, trimming, and the hostname fallback. MUL-2984 Co-authored-by: multica-agent <github@multica.ai> * fix(email): gate EHLO resolution to SMTP mode + sync docs to zh/ja/ko Addresses review nits on this PR: - email.go: resolve smtpEHLOName only when SMTP_HOST is set, so the Resend / DEV-stdout paths never call os.Hostname() or emit its failure log. The EHLO name is only ever used on the SMTP send path. - docs: add SMTP_EHLO_NAME to the zh/ja/ko variants of environment-variables, self-host-quickstart, and auth-setup, in sync with the English docs updated earlier in this PR. Note: the ja/ko self-host-quickstart and auth-setup pages were already missing the port-465 implicit-TLS example (pre-existing i18n drift from an earlier SMTP_TLS change, unrelated to this PR); the new EHLO block is inserted at the correct logical anchor regardless. A full ja/ko re-sync is left as a separate follow-up. MUL-2984 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
187 lines
10 KiB
Plaintext
187 lines
10 KiB
Plaintext
---
|
|
title: Sign-in and signup configuration
|
|
description: Configure email + verification code sign-in, Google OAuth, signup allowlists, and local test codes.
|
|
---
|
|
|
|
import { Callout } from "fumadocs-ui/components/callout";
|
|
import { Mermaid } from "@/components/mermaid";
|
|
|
|
Multica supports two sign-in methods: **email + verification code** (default) and **Google OAuth** (optional). On successful sign-in, the server issues a JWT cookie with a 30-day lifetime. This page covers how to configure each method, how to restrict who can sign up, and the single biggest trap for self-hosted deployments.
|
|
|
|
For the list of environment variables referenced below, see [Environment variables](/environment-variables); for token usage and lifecycle details, see [Authentication and tokens](/auth-tokens).
|
|
|
|
## How email + verification code sign-in works
|
|
|
|
The user enters an email on the sign-in page → the server sends a 6-digit code → the user enters it → the server verifies it → a JWT cookie is issued. Standard flow. Two delivery backends are supported — pick whichever fits your deployment:
|
|
|
|
### Option A: Resend (recommended for cloud / public-internet deployments)
|
|
|
|
1. Create a [Resend](https://resend.com/) account and verify your domain
|
|
2. Create an API key
|
|
3. Set the environment variables:
|
|
|
|
```bash
|
|
RESEND_API_KEY=re_xxxxxxxxxxxxxxxx
|
|
RESEND_FROM_EMAIL=noreply@yourdomain.com # must be a domain verified in Resend
|
|
```
|
|
|
|
4. Restart the server
|
|
|
|
### Option B: SMTP relay (for self-hosted / on-premise deployments)
|
|
|
|
Use this when the deployment can't reach `api.resend.com` or you already have an internal mail relay (Microsoft Exchange, Postfix, on-prem SendGrid, etc.). `SMTP_HOST` takes priority over `RESEND_API_KEY` when both are set — if `SMTP_HOST` is non-empty the server always goes through SMTP, even if `RESEND_API_KEY` is also configured, so verification and invite mail never leaves the internal network.
|
|
|
|
The SMTP path supports the three relay modes most on-premise mail servers (notably Microsoft Exchange's receive connectors) expose:
|
|
|
|
| Mode | Port | Auth | TLS |
|
|
|---|---|---|---|
|
|
| Anonymous internal relay | `25` | none — submission is trusted by IP / subnet | none on the wire (internal segment only) |
|
|
| Authenticated submission | `587` | `SMTP_USERNAME` + `SMTP_PASSWORD` | STARTTLS, upgraded automatically |
|
|
| Implicit TLS (SMTPS) | `465` | optional (`SMTP_USERNAME` + `SMTP_PASSWORD`) | TLS handshake on connect — auto-enabled on port `465`, or force on a non-standard port with `SMTP_TLS=implicit` |
|
|
|
|
**Anonymous Exchange relay on port 25** — the typical "internal SMTP relay" / Exchange anonymous receive connector that accepts mail from a trusted subnet without credentials:
|
|
|
|
```bash
|
|
SMTP_HOST=exchange.internal.example.com
|
|
SMTP_PORT=25
|
|
SMTP_USERNAME=
|
|
SMTP_PASSWORD=
|
|
SMTP_TLS_INSECURE=false
|
|
RESEND_FROM_EMAIL=noreply@yourdomain.com # reused as the From: header
|
|
```
|
|
|
|
**Authenticated submission on port 587** — for relays that require a service account; STARTTLS is upgraded automatically when the server advertises it:
|
|
|
|
```bash
|
|
SMTP_HOST=smtp.internal.example.com
|
|
SMTP_PORT=587
|
|
SMTP_USERNAME=multica
|
|
SMTP_PASSWORD=...
|
|
SMTP_TLS_INSECURE=false # set true only for self-signed / private CA
|
|
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
|
```
|
|
|
|
**Implicit TLS (SMTPS) on port 465** — for providers that only offer SMTPS and don't advertise STARTTLS (e.g. Aliyun / Tencent enterprise mail). Port `465` auto-enables implicit TLS; `SMTP_TLS=implicit` (aliases: `smtps`, `ssl`) forces it on a non-standard SMTPS port:
|
|
|
|
```bash
|
|
SMTP_HOST=smtp.qiye.aliyun.com
|
|
SMTP_PORT=465 # implicit TLS auto-enabled on 465
|
|
SMTP_USERNAME=multica@yourdomain.com
|
|
SMTP_PASSWORD=...
|
|
SMTP_TLS=implicit # optional on 465; required on a non-standard SMTPS port
|
|
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
|
```
|
|
|
|
**Strict public relays (e.g. Google Workspace `smtp-relay.gmail.com`)** additionally require a valid EHLO name. They reject the default `localhost` greeting from a public IP, and the relay drops the connection — which surfaces as an opaque `EOF` on a later command (`smtp auth: EOF`) rather than at the greeting. Set `SMTP_EHLO_NAME` to the FQDN the relay expects; it defaults to the machine hostname, which inside a container is usually not a valid FQDN:
|
|
|
|
```bash
|
|
SMTP_HOST=smtp-relay.gmail.com
|
|
SMTP_PORT=587
|
|
SMTP_EHLO_NAME=mail.yourdomain.com # FQDN the relay accepts; defaults to the (non-FQDN) container hostname
|
|
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
|
```
|
|
|
|
At startup the server prints which provider it picked, including the negotiated TLS mode — for example `EmailService: SMTP relay exchange.internal.example.com:25 (starttls) from=noreply@example.com` or `… smtp.qiye.aliyun.com:465 (implicit-tls) from=…` (or `Resend API` / `DEV mode`). The password is never logged. If you don't see the SMTP line after restart, `SMTP_HOST` didn't reach the process — check the container env (`docker compose -f docker-compose.selfhost.yml exec backend env | grep SMTP`).
|
|
|
|
**What happens if you set neither**: the server doesn't error, but **every email that should have been sent is written to the server's stdout only**. Handy for local development (copy the code from the logs); in production it's a black hole.
|
|
|
|
## Fixed local testing codes
|
|
|
|
<Callout type="warning">
|
|
**Do not enable a fixed verification code on a publicly reachable instance.**
|
|
|
|
The old behavior where non-production instances accepted `888888` by default has been removed. Unless you explicitly configure it, typing `888888` is treated like any other wrong code.
|
|
|
|
Local development without any email backend configured (no Resend, no SMTP) should use the generated code printed in server logs. If you need deterministic local/private automation, set `MULTICA_DEV_VERIFICATION_CODE` to a 6-digit value such as `888888`, and keep `APP_ENV` non-production:
|
|
|
|
```bash
|
|
APP_ENV=development
|
|
MULTICA_DEV_VERIFICATION_CODE=888888
|
|
```
|
|
|
|
This shortcut is ignored when `APP_ENV=production`.
|
|
</Callout>
|
|
|
|
Production deployments should leave `MULTICA_DEV_VERIFICATION_CODE` empty and set `APP_ENV=production`. If you deploy via `make selfhost` / `docker-compose.selfhost.yml`, `APP_ENV` defaults to `production`.
|
|
|
|
## Google OAuth configuration
|
|
|
|
Optional. Without it, only email + verification code is available; with it, the sign-in page gets a "Sign in with Google" button.
|
|
|
|
1. Create an OAuth 2.0 client in the [Google Cloud Console](https://console.cloud.google.com/)
|
|
2. Set the **Authorized redirect URIs** to your Multica frontend address plus `/auth/callback`, for example:
|
|
|
|
```text
|
|
https://multica.yourdomain.com/auth/callback
|
|
```
|
|
|
|
3. Once you have the client ID and client secret, set three environment variables:
|
|
|
|
```bash
|
|
GOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.com
|
|
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxx
|
|
GOOGLE_REDIRECT_URI=https://multica.yourdomain.com/auth/callback
|
|
```
|
|
|
|
4. Restart the server.
|
|
|
|
**Takes effect at runtime**: the frontend reads these settings at runtime via `/api/config` — after changing them, restart the server and the frontend picks up the new values with no rebuild or redeploy.
|
|
|
|
<Callout type="warning">
|
|
**The redirect URI must match exactly in both the Google Console and `GOOGLE_REDIRECT_URI`** — including protocol (`http` vs `https`), trailing slash, and port. Any mismatch and Google rejects the entire OAuth flow; the error shown to the user is `redirect_uri_mismatch`.
|
|
</Callout>
|
|
|
|
## Restricting who can sign up
|
|
|
|
Three environment variables combine by priority:
|
|
|
|
<Mermaid chart={`
|
|
graph TD
|
|
Start[New user first sign-in] --> A{Email in<br/>ALLOWED_EMAILS?}
|
|
A -- Yes --> Allow[Allow signup]
|
|
A -- No --> B{Domain in<br/>ALLOWED_EMAIL_DOMAINS?}
|
|
B -- Yes --> Allow
|
|
B -- No --> C{Any allowlist<br/>non-empty?}
|
|
C -- Yes --> Block[Reject]
|
|
C -- No --> D{ALLOW_SIGNUP<br/>= true?}
|
|
D -- Yes --> Allow
|
|
D -- No --> Block
|
|
`} />
|
|
|
|
**Existing users can always sign in again** — the signup allowlist only applies to **first-time signup**, not returning users.
|
|
|
|
- **`ALLOWED_EMAILS`** (highest priority) — explicit email allowlist, comma-separated. **When non-empty, only listed emails can sign up.**
|
|
- **`ALLOWED_EMAIL_DOMAINS`** — domain allowlist, comma-separated (for example `company.io,partner.com`).
|
|
- **`ALLOW_SIGNUP`** — master switch, default `true`. Set `false` to disable signup entirely.
|
|
|
|
<Callout type="warning">
|
|
**The three layers are AND semantics, not OR.** A common wrong intuition is that `ALLOWED_EMAIL_DOMAINS=company.io` + `ALLOW_SIGNUP=true` means "allow company.io plus everyone else." It does **not**. If any layer has a non-empty value, **emails not matching it are rejected outright** — `ALLOW_SIGNUP=true` does not override that.
|
|
|
|
To actually "allow everyone," leave all three variables empty (or keep `ALLOW_SIGNUP=true`).
|
|
</Callout>
|
|
|
|
**Typical configurations**:
|
|
|
|
| Goal | Configuration |
|
|
|---|---|
|
|
| Internal only, employees of `company.io` | `ALLOWED_EMAIL_DOMAINS=company.io` |
|
|
| Internal + a few external collaborators | `ALLOWED_EMAIL_DOMAINS=company.io` + collaborator addresses added to `ALLOWED_EMAILS` |
|
|
| Disable self-serve signup entirely, invite-only | `ALLOW_SIGNUP=false` |
|
|
| Open signup (not recommended for production) | All three empty |
|
|
|
|
## Can you still invite people when signup is disabled?
|
|
|
|
**Only people who already have a Multica account.** Accepting an invite doesn't check the signup allowlist — if the invitee has signed up already (for example in another workspace), clicking the invite link and signing in lets them accept.
|
|
|
|
**But people who have never signed up cannot be rescued by an invite.** Before accepting, they must sign in, and the first step of sign-in (requesting the verification code) passes through the signup allowlist check. If `ALLOW_SIGNUP=false`, or their email isn't in `ALLOWED_EMAILS` / `ALLOWED_EMAIL_DOMAINS`, they **cannot complete signup**, and therefore cannot accept the invite.
|
|
|
|
To invite an external collaborator who hasn't signed up yet: temporarily add their email to `ALLOWED_EMAILS`, wait for them to sign up and accept the invite, then remove the entry.
|
|
|
|
For how to create and use invites, see [Members and roles](/members-roles).
|
|
|
|
## Next
|
|
|
|
- [Environment variables](/environment-variables) — full definitions of every variable used on this page
|
|
- [Authentication and tokens](/auth-tokens) — JWT / PAT / daemon token categories and usage
|
|
- [Troubleshooting](/troubleshooting) — verification code not received, OAuth `redirect_uri_mismatch`, signup rejected
|