← Changelog

The Second User Could Never Have Logged In — On Any of the Four CRMs

A stakeholder at the-water-foundation couldn't log in or create an account. The invitation had been sent, accepted by the server, and written to a log file instead of an inbox. Twenty's email driver defaults to a setting that silently discards mail, and our own runbook called SMTP optional.

· Michael Staton · with Claude Code on Claude Opus 5

Why Care?

One stakeholder at the-water-foundation was up and running. The second one tried and could neither log in nor create an account.

Nothing was broken. /healthz returned 200, the deployment was green, the invitation had been created and accepted by the server. It was just written to the worker's stdout instead of being sent:

2026-08-10T10:57:44Z  Michael has invited you to join a workspace called
                      the-water-foundation.
                      Accept invite https://…?inviteToken=5c8b1938…

That link sat in a Railway log for four days while the person it was addressed to concluded the CRM didn't work.

What Actually Happened

Twenty's EMAIL_DRIVER defaults to LOGGER. Leaving it unset does not disable email — it renders each message into the container's stdout and drops it. No error, no bounce, no failed queue job. Everything reports success.

That would be a nuisance on its own. What made it a lockout is how Twenty gates the second user:

  • IS_MULTIWORKSPACE_ENABLED unset ⇒ no self-serve "create a workspace" path.

  • Only password in authProviders ⇒ no magic-link fallback.

  • So joining an existing workspace requires an invite token — deliverable by exactly one channel.

No email, no second user. Ever. The first admin never exercises that path, which is why four deployments passed their human gate with this latent.

The Part That Was Our Fault

The runbook listed SMTP under "optional integrations (Google/Microsoft auth, calendar/messaging sync, SMTP) stay unset until a client asks."

A client cannot ask for this. They don't know it's a setting. They experience it as "the second person can't get in," which reads as a broken product, not a missing variable. Calling it optional guaranteed that every instance we ever deployed would ship stranded — and all four did.

That line is now gone, replaced with a section explaining why it's load-bearing.

Fixed

Resend SMTP on both twenty-server and twenty-worker for the-water-foundation, CONFIG_EPOCH → 2, both deploys green. Same send-scoped key and no-reply@didi.sh sender the Outline instances have used since 2026-07-06 — matching email_sender for the client in hubs/lossless-at/src/config/clients.ts, which had declared that identity all along without anything wiring it up.

Christoph's original invite was still inside its 30-day window (INVITATION_TOKEN_EXPIRES_IN = '30d'), so it was recoverable from the log rather than needing reissue.

Gotchas

  1. The worker is what sends. EMAIL_* on the server alone looks correct in every readback and delivers nothing. Both services, every time.

  2. Var names don't match across our own stack. Outline uses SMTP_USERNAME; Twenty uses EMAIL_SMTP_USER. Copying the working Outline block verbatim fails silently.

  3. Password reset dies the same way. This was the more dangerous half — it stays invisible until a member forgets a password, at which point they're locked out permanently with no recovery path. Nobody hit it. That was luck.

  4. client-stacks/ is gitignored in full, so per-client stack.md and restore-runbook.md edits are local-only. Anything that must survive a rebuild belongs in docs/twenty/setup.md. Both were updated here; only one of them is in this commit.

Still Open

lossless, palmer-ai, and reach-edu have no EMAIL_* in their recovery .env files either. That's inferred from the recovery copies, not verified against live Railway — but the same runbook built all four, so the presumption is that three CRMs are still in this state and nobody has hit it yet because nobody has invited a second user.

Worth a sweep before someone else's stakeholder concludes the product is broken.

TWF Papermark is likely in the same shape — its .env carries an explicit # NOT SET (operator-provided, needed for login): … RESEND_API_KEY.

Also unverified: a live send. The configuration is confirmed present on both services and both deployments are healthy, but no message has actually been put through Resend yet. Config present and mail leaving the box are different claims, and only the second one is worth believing.

Files changed

  • docs/twenty/setup.md
  • client-stacks/the-water-foundation/twenty/.env (gitignored)
  • client-stacks/the-water-foundation/twenty/stack.md (gitignored)
  • client-stacks/the-water-foundation/twenty/restore-runbook.md (gitignored)