Reading tenant data with BridgeService
Section titled “Reading tenant data with BridgeService”BridgeService gives a request handler one place to read everything Bridge knows about the current
request’s tenant: its subscription, entitlements, branding, and user — without hand-rolling REST calls
to the Bridge API.
Two things to know:
- It reads on demand and caches. Each tenant’s data is fetched over REST and cached briefly. There are no push updates on the server — to react to a change (e.g. a plan upgrade), use Bridge webhooks.
- It’s per request. Every request carries a different tenant. You pass the incoming user’s JWT and get back a scope bound to that user’s tenant.
BridgeService is provided and exported automatically by BridgeModule.forRoot() /
forRootAsync() — no extra wiring. Just inject it.
import { Controller, Get, Headers, ForbiddenException } from '@nestjs/common';import { BridgeService } from '@nebulr-group/bridge-nestjs';
@Controller('reports')export class ReportsController { constructor(private readonly bridge: BridgeService) {}
@Get('export') async export(@Headers('authorization') auth: string) { const tenant = this.bridge.fromJwt(auth.replace(/^Bearer\s+/i, ''));
if (!(await tenant.entitlements.can('pdf-export'))) { throw new ForbiddenException('Your plan does not include PDF export'); }
return this.buildExport(); }}bridge.fromJwt(userJwt)
Section titled “bridge.fromJwt(userJwt)”fromJwt takes the raw user JWT (strip the Bearer prefix) and returns a tenant scope. The JWT is
forwarded to the Bridge API on the data fetch; the API derives the tenant from the token and returns the
matching data. Requests for the same user are deduped onto a single round-trip.
bridge.tenant(tenantId)— for accessing an arbitrary tenant from cron/admin code — is not yet available and throws a clear error if called. Usebridge.fromJwt(userJwt)from a request handler.
What you can read
Section titled “What you can read”The first access to any field triggers one fetch that returns subscription + entitlements + branding + user together. The result is cached (default 30s); concurrent callers share the in-flight fetch. Every field below resolves lazily off that single fetch.
interface SessionSnapshotData { app: { branding: BrandingSnapshot }; tenant: { id: string; name: string; subscription: SubscriptionSnapshot; entitlements: Record<string, boolean>; }; user: UserSnapshot;}tenant.subscription → Promise<SubscriptionSnapshot>
Section titled “tenant.subscription → Promise<SubscriptionSnapshot>”interface SubscriptionSnapshot { plan: { slug: string; name: string }; status: string; // e.g. 'active', 'trialing', 'canceled' endsAt?: string; gateEngaged?: boolean; // true when the plan gate is currently blocking the tenant}
const sub = await tenant.subscription;if (sub.plan.slug === 'free') { /* ... */ }tenant.entitlements
Section titled “tenant.entitlements”The common path is .can(key):
if (await tenant.entitlements.can('seats:10')) { /* ... */ }| Method | Behavior |
|---|---|
can(key): Promise<boolean> | Loads the data if needed, then answers. The usual call. |
snapshot(): Promise<Record<string, boolean>> | The full entitlements map; fetches on first call. |
canSync(key, cached): boolean | Synchronous check against an already-loaded map — pass the result of a prior snapshot(). Use when checking many keys in a hot path. |
// Many checks without re-awaiting each time:const ents = await tenant.entitlements.snapshot();const canExport = tenant.entitlements.canSync('pdf-export', ents);const canBulk = tenant.entitlements.canSync('bulk-import', ents);tenant.branding → Promise<BrandingSnapshot>
Section titled “tenant.branding → Promise<BrandingSnapshot>”interface BrandingSnapshot { logo: string; name: string; primaryButtonBgColor?: string; textColor?: string; bgColor?: string; fontFamily?: string;}Useful for server-rendered emails or PDFs that should carry the tenant’s branding.
tenant.user → Promise<UserSnapshot>
Section titled “tenant.user → Promise<UserSnapshot>”interface UserSnapshot { id: string; email?: string; role: string; tenantId: string;}tenant.invalidate()
Section titled “tenant.invalidate()”Force the next access to re-fetch — call this right after a change that affects the data (e.g. you just upgraded the plan and want the fresh subscription):
await upgradePlan(tenantId, 'pro');tenant.invalidate();const fresh = await tenant.subscription; // re-fetchedGating features by subscription
Section titled “Gating features by subscription”Reading the subscription and checking entitlements is how you enforce paid features server-side — there is no checkout or paywall in a backend plugin. Purchase and upgrade flows live in your frontend and in the Bridge API (webhooks drive the subscription lifecycle). Two ways to enforce:
Declarative — gate a route by plan in the central rules (see Configuration):
BridgeModule.forRoot({ appId, guard: { global: true, rules: [ { path: '/reports/*', privilege: 'TENANT_READ', plans: ['pro', 'enterprise'] }, ], },});Programmatic — gate inside a handler with an entitlement check:
if (!(await this.bridge.fromJwt(jwt).entitlements.can('feature-key'))) { throw new ForbiddenException();}Caching notes
Section titled “Caching notes”- Default cache lifetime is 30s. The same cache is injectable directly via
BRIDGE_PULL_CACHEfor other REST data you want to dedupe — see the README’s “Read modes — channel vs pull” section. - To react to a billing change (a plan upgrade, a cancellation), use Bridge webhooks rather than polling.
See also
Section titled “See also”- Configuration —
plansroute rules - Feature Flags — flag-based gating (distinct from entitlements)
- Multi-Tenancy — tenant context fundamentals