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
| Layout | Cookie shared? | Effort |
|---|---|---|
Same origin (nufi.example.com/, nufi.example.com/console/) | yes, automatically | path routing in the proxy |
Subdomains (chat.example.com, console.example.com) | yes, with two variables on the chat | proxy plus the variables |
| Different registrable domains | no | not 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=laxThe 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.
What the console does with the cookie
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=Strictblocks the cross-subdomain cookie. SetlaxwithCOOKIE_DOMAIN.- The leading dot matters.
Domain=.example.comcovers 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
- Cloudflare tunnel when the host has no public address.
- Single sign-on for the agent apps for the identity the console issues to Studio and Works.