NuFiDocs

SSO and reverse proxy

One sign-in across the app, the console and the admin panel, and the proxy configuration that makes the cookie travel.

The app issues the session: a JWT in a cookie set by the chat. The console reads that cookie and verifies it with the same JWT_REFRESH_SECRET, so a member who signed in to the chat is already signed in to the console. Whether the cookie reaches the console depends on where the two are hosted.

The admin panel is different. It keeps its own session and asks the app's API to authenticate, so it needs no cookie sharing, only a route to the app. It is also not part of the compose stack: run it from apps/admin-panel (its own image and compose file) or on Railway.

Hosting options

LayoutCookie shared?Effort
Same origin (nufi.example.com/, nufi.example.com/console/)yes, automaticallypath routing in the proxy
Subdomains (chat.example.com, console.example.com)yes, with two variables on the chatproxy plus the variables
Different registrable domainsnonot supported

Same origin is the least work. Subdomains are what NuFi's own hosting uses (chat.nufi.me, console.nufi.me).

Caddy, same origin

nufi.example.com {
  handle_path /console/* {
    reverse_proxy console:3000
  }
  handle {
    reverse_proxy librechat:3080
  }
}

TLS from Let's Encrypt is automatic.

Caddy, subdomains

chat.example.com {
  reverse_proxy librechat:3080
}

console.example.com {
  reverse_proxy console:3000
}

admin.example.com {
  reverse_proxy admin-panel:3000
}

And on the chat, so that the cookie it sets is valid for every subdomain:

COOKIE_DOMAIN=.example.com
COOKIE_SAMESITE=lax

The wrapper stack in deploy/railway passes both from .env. The compose stack in deploy/platform does not have them; add the two lines to the librechat service's environment: block. lax is required: strict stops the browser sending the cookie on the cross-subdomain navigation from the chat to the console.

The proxy must forward Set-Cookie unchanged. Caddy does. Traefik does too; only add a header-rewriting middleware if you had one that strips Domain.

On every request the console reads refreshToken, verifies it with JWT_REFRESH_SECRET (HS256), and takes the user id from the payload as the identity for keys, spend and traces. A missing or invalid cookie is a 401 and a redirect to /unauthorized, which links back to the chat's sign-in (VITE_LIBRECHAT_URL, inlined at build time).

For the handoff into NuFi Studio and NuFi Works the id is not enough, and the console asks the chat for the member's email and role over CHAT_BASE_URL. That call and its variables are on Single sign-on for the agent apps.

Gotchas

  • SameSite=Strict blocks the cross-subdomain cookie. Set lax with COOKIE_DOMAIN.
  • The leading dot matters. Domain=.example.com covers every subdomain.
  • HTTPS on every host. The cookie is Secure; a plain-HTTP console never receives it. Mixed schemes fail the same way.
  • One JWT_REFRESH_SECRET. The console must have the chat's value, not one of its own.

Cost

Caddy (Apache-2.0) or Traefik (MIT), one container, certificates from Let's Encrypt. Nothing to license.

See also