---
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. It requires [Resend](https://resend.com/) as the email provider:

1. Create a Resend 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

**What happens if you don't set `RESEND_API_KEY`**: 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 Resend 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
