AACWorkflow Docs

Sign-in and signup configuration

Configure email + verification code sign-in, Google OAuth, signup allowlists, and local test codes.

AACWorkflow 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; for token usage and lifecycle details, see Authentication and 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:

  1. Create a Resend account and verify your domain

  2. Create an API key

  3. Set the environment variables:

    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:

ModePortAuthTLS
Anonymous internal relay25none — submission is trusted by IP / subnetnone on the wire (internal segment only)
Authenticated submission587SMTP_USERNAME + SMTP_PASSWORDSTARTTLS, upgraded automatically
Implicit TLS (SMTPS)465optional (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:

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:

SMTP_HOST=smtp.internal.example.com
SMTP_PORT=587
SMTP_USERNAME=aacworkflow
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:

SMTP_HOST=smtp.qiye.aliyun.com
SMTP_PORT=465                  # implicit TLS auto-enabled on 465
SMTP_USERNAME=aacworkflow@yourdomain.com
SMTP_PASSWORD=...
SMTP_TLS=implicit              # optional on 465; required on a non-standard SMTPS port
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

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 AACWORKFLOW_DEV_VERIFICATION_CODE to a 6-digit value such as 888888, and keep APP_ENV non-production:

APP_ENV=development
AACWORKFLOW_DEV_VERIFICATION_CODE=888888

This shortcut is ignored when APP_ENV=production.

Production deployments should leave AACWORKFLOW_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

  2. Set the Authorized redirect URIs to your AACWorkflow frontend address plus /auth/callback, for example:

    https://aacworkflow.com/auth/callback
  3. Once you have the client ID and client secret, set three environment variables:

    GOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.com
    GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxx
    GOOGLE_REDIRECT_URI=https://aacworkflow.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.

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.

Restricting who can sign up

Three environment variables combine by priority:

Rendering diagram…

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.

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 outrightALLOW_SIGNUP=true does not override that.

To actually "allow everyone," leave all three variables empty (or keep ALLOW_SIGNUP=true).

Typical configurations:

GoalConfiguration
Internal only, employees of company.ioALLOWED_EMAIL_DOMAINS=company.io
Internal + a few external collaboratorsALLOWED_EMAIL_DOMAINS=company.io + collaborator addresses added to ALLOWED_EMAILS
Disable self-serve signup entirely, invite-onlyALLOW_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 AACWorkflow 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.

Next