Multi-platform login with guards
One backend serving an admin console, a tenant portal, and a mobile app — with origin-aware login gating.
A single Nest Auth backend often serves several frontends:
- Admin console — internal staff (super-admins, support, ops). Tight permissions. Hosted at
admin.example.com. - Tenant portal — your SaaS app. Tenant owners, members, billing admins. Hosted at
app.example.com(or*.example.com). - Mobile app / public site — customers. Hosted at
example.com, or signed in viax-platform: mobile.
Each portal has its own role bundle. A user with the member role on PORTAL should be able to sign in to the portal — but not to the admin console, even with the same email and password. This recipe shows the canonical pattern: configure multiple roleGuards, detect the requesting origin, and enforce that the user has at least one role on the right guard.
1. Define your guards
Turn the guard names into a TypeScript enum so you don't typo them anywhere:
2. Wire NestAuthModule.forRootAsync
The full configuration — register all three guards, enable platform access (lock it to the admin origin), and add the beforeSignup / onLogin hooks that tie everything together.
Register it on the module:
3. Seed roles per guard
Roles live in nest_auth_roles with both a name and a guard. Same name on different guards = different rows. See the seeding-roles-and-permissions recipe for an idempotent seeder.
4. Frontend — pass the guard / origin
Web frontends don't need to do anything — the backend reads the Origin / Referer header. Mobile apps do:
Or, for explicit control, send guard in the login body:
The validateGuardConsistency helper rejects the request if the body's guard and the resolved request guard disagree — so a malicious portal page can't try to log a user in as admin by setting guard: 'ADMIN' in the body.
5. Granting platform access (super-admins)
Cross-tenant staff get NestAuthPlatformAccess, not NestAuthUserAccess. You'll typically grant this from a setup script or an admin-only endpoint:
platformAccess.validate(request) ensures the staff session only works against the admin origin — even if a leaked staff token lands on the portal subdomain, it's discarded.
6. The result
| User | Logs into admin.example.com | Logs into app.example.com | Logs into mobile (FRONTEND) |
|---|---|---|---|
Member of tenant Acme (PORTAL/member) | ❌ rejected | ✅ accepted | ❌ rejected |
Customer (FRONTEND/customer) | ❌ rejected | ❌ rejected | ✅ accepted |
Support staff (PlatformAccess: SUPPORT) | ✅ accepted | ❌ rejected (validate() returns false) | ❌ rejected |
One backend, three login experiences, zero token-leakage between portals.
Related
- RBAC — guards, roles, permissions.
- User Access & Platform Access.
- Multi-tenancy.
- Seeding roles & permissions.
- Hooks reference —
registrationHooks.beforeSignup,loginHooks.onLogin.