Secure multi-tenancy in Lino

Multi-tenancy allows a single application to serve multiple customers, companies, or organizations while keeping data and permissions isolated. In Lino, Tenant is treated as part of the application flow: the feature generates the tenancy foundation, resolves the Tenant by subdomain, host, or slug through ITenantContext, and exposes the user authenticated by the JWT through IUserContext.

The JWT also carries the TenantId for which the user was authenticated. Lino compares that Tenant with the Tenant resolved for the request, so this page focuses on what you need to decide after that foundation exists: entity modeling, permissions by organization, and correct use of the generated abstractions.

Adding Tenant support

Use the Tenant feature when the system is SaaS or when data from different customers must coexist in the same application without leaking between contexts.

lino feature tenant add

The assistant generates the foundation for Tenant registration, user association, current Tenant resolution, propagation through the application context, and authentication integration. The ITenantContext represents the Tenant resolved by subdomain, host, or slug, while the IUserContext represents the user authenticated by the JWT, including the TenantId for which the user was authenticated.

After generation, review which entities truly belong to a Tenant and which are global. This review is part of product modeling: multi-tenancy is not just creating a Tenants table, but deciding which rules, screens, permissions, data, and operations depend on the current Tenant validated by Lino.

  • Tenant-scoped: orders, a customer's products, company settings, CRM customers, or any data that belongs to a specific organization.
  • Global: the platform's public catalog, plans, internal operation resources, administrative settings, or data used before choosing a Tenant.
  • Hybrid: global data with per-Tenant customization, which requires explicit modeling to avoid incorrect duplication or rule leakage.

In a modular monolith, the tenancy module is usually the owner of the original Tenant entity. Other modules, such as catalog or CRM, should not access that entity directly. When they need to know Tenant data, they can keep shadow entities with the minimum required fields, fed by integration events such as Tenant creation or update.

Tenant context

With the Tenant feature, Lino centralizes resolution of the current Tenant and exposes that value through ITenantContext. This context represents the Tenant resolved from the request subdomain, host, or slug, including information such as TenantId and slug when the Tenant was found.

In authenticated flows, IUserContext reads the user from the JWT. That JWT also contains the TenantId for which the user was authenticated. Lino compares the Tenant from IUserContext with the Tenant from ITenantContext, preventing business rules from trusting divergent contexts.

In generated features or features added later, always prefer these abstractions instead of rereading claims manually or receiving an arbitrary TenantId in each handler. This keeps commands, queries, pages, and integrations using the same source of truth and preserves the security mechanisms already implemented by Lino.

Background Jobs, event consumers, and integrations also need context. A job that processes tenant-scoped products, customers, or users must explicitly receive which Tenant is being processed, because there is no active HTTP request from which ITenantContext and IUserContext can be inferred automatically.

Tenant-scoped entities

Tenant-scoped entities carry the Tenant identifier and participate in query filters. With this, the common flow does not need to manually remember to apply TenantId in every query; the feature should use the validated context and the filters generated by Lino.

  • Include Tenant in aggregates that belong to a specific organization.
  • Configure indexes considering Tenant and frequently queried fields.
  • Use ITenantContext and IUserContext in commands and queries instead of receiving Tenant as an isolated authority.
  • In imports and jobs, explicitly define which Tenant is being processed.

A tenant-scoped entity belongs to one Tenant. If an entity seems to belong to multiple Tenants, treat that as a modeling decision: maybe it is global, maybe there should be a copy per Tenant, or maybe an intermediate entity is missing to represent the relationship. A product, customer, or user can also have different meaning in different modules; in those cases, a shadow entity with a few fields may be more correct than sharing the original entity.

Also distinguish reference from ownership. A product in the catalog can reference the Tenant, but that does not mean the catalog controls the Tenant lifecycle. The tenancy module remains the owner of creation, activation, domain, branding, and isolation mode; the catalog keeps only the data required to filter and validate its own flow.

  • Indexes: combine TenantId with fields used in search, sorting, and per-Tenant uniqueness.
  • Commands: apply the operation in the current validated Tenant when the entity is tenant-scoped.
  • Queries: keep pagination, filters, and counters in the same context validated by Lino.
  • Events: include enough context for consumers to update projections without querying the producer inappropriately.

Data isolation

Isolation can be done by column, schema, or separate database. The choice depends on cost, volume, contractual requirements, and operational capacity.

StrategyAdvantagesWhen to use
TenantId columnSimpler and cheaperA good initial choice for many SaaS products, combined with filters and Lino context.
Schema per TenantIntermediate isolationUseful when there are greater separation requirements without reaching database per Tenant.
Database per TenantStrong isolationRecommended when contract, volume, or operation justify higher cost and automation.

For most early-stage SaaS products, a column per Tenant with global filters and centralized context is a pragmatic starting point. Lino helps with that foundation; your review should confirm that tenant-scoped aggregates carry the correct Tenant and that new queries continue using the current Tenant validated by the generated abstractions.

In modular services, Tenant isolation and module isolation are different concerns. The Catalog module can have a products table with TenantId; the CRM module can have customers with TenantId; and the Tenancy module remains the owner of the original Tenant registration. The presence of the same identifier does not authorize direct access between modules.

Roles and permissions per Tenant

In SaaS, a user can be an administrator in one Tenant and an operator in another. Lino generates the foundation for working with the user-Tenant link, and feature permissions must be evaluated inside the active context validated by the framework.

Administrative permissions can also exist outside a Tenant. A platform operation screen may be accessible only in the system context, while pages such as products and customers may require an active Tenant. This separation makes clear when an action is global and when it depends on the Tenant resolved in ITenantContext and the user authenticated in IUserContext.

  • Model roles per Tenant when permissions vary by organization.
  • Use the current context in authorization checks for pages, commands, and queries.
  • Keep claims compact, carrying in the token only what needs to participate in the authenticated flow.
  • Revoke or update tokens when critical permissions change.
ContextExampleHow to handle
SystemPlatform administration, plans, Tenants, and global settings.Run outside the scope of a specific Tenant.
TenantProducts, customers, orders, users, and roles of that Tenant.Use the current Tenant validated by Lino.
BothSome identity or management functions that exist both in the system and in the Tenant.Declare in the use case which context is being used.

JWT and Tenant security

When the Tenant feature is active, Lino uses the JWT as the source of the authenticated user context. The IUserContext reads the user's claims and the TenantId from the token, without turning the JWT into a copy of the database.

  • Keep issuer, audience, signature, and expiration configured correctly.
  • Use the Tenant validated by Lino instead of accepting a manual switch based only on a header, route, or screen payload.
  • Avoid overly large claims; bloated tokens make operation and renewal harder.
  • Record access logs with user, Tenant, and operation when the flow requires auditing.

In applications with subdomains, such as acme.dev.localhost and globex.dev.localhost, the ITenantContext represents the Tenant resolved for the request. The IUserContext represents the authenticated user and the Tenant present in the JWT. Lino compares these two contexts to prevent a token issued for one Tenant from being used in another Tenant context.

Authorization also remains contextual. Permission to view products, for example, applies within the Tenant where it was granted and for which the user was authenticated. When creating new features, use the available permissions and contexts instead of creating shortcuts that receive a loose TenantId and ignore the authenticated flow.

An unhandled error has occurred. Reload πŸ—™