s-m-r-t
Open the s-m-r-t source on GitHub Switch to dark color scheme
← Foundations

Foundation 02

Choose where data and authority belong

A tenant is an organization or account boundary. It tells the app which data, members, and permissions belong together.

Tenant
Parent / childMembershipRole inheritanceProfile relationship
Organization, user access, and business relationships use different links.

Use the relationship that matches the job

Tenant hierarchy, membership, and profile relationships answer different questions. Keeping them separate makes access rules clear.

  • Parent and child tenants describe organization: company, division, branch, or network member.
  • A membership gives one user access to one tenant through one role.
  • A profile relationship describes business meaning: supplier, client, partner, or representative. It links two profiles, not two tenants.

Relate profiles, and let organizations be profiles

A profile relationship is one row from one profile to another with a named type. Organization, Person, and Bot are all Profile subtypes, so two organizations relate the same way two people do. The type is a record you create, and it is either reciprocal or directional. A relationship can also name a third profile as its context, and it can carry dated terms.

  • The framework ships inverse handlers for friend, spouse, partner, colleague, and sibling. Adding one side of those adds the other side for you.
  • Supplier, client, and representative are types your application defines. Nothing seeds them, and a type without a handler writes only the side you asked for.
  • A term dates a relationship: add one when it starts, end it when it stops, and read the active one later.
org-to-org.ts
typescript
import {
  ProfileCollection,
  ProfileRelationshipTypeCollection
} from '@happyvertical/smrt-profiles';

const types = await ProfileRelationshipTypeCollection.create({ db });
await types.getOrCreateBySlug('supplier', { name: 'Supplier', reciprocal: false });

const profiles = await ProfileCollection.create({ db });
const mill = await profiles.get('northern-mill');
const shop = await profiles.get('edmonton-shop');
if (!mill || !shop) throw new Error('missing organization');

// One directional row: the mill supplies the shop. No inverse is written.
await mill.addRelationship(shop, 'supplier');

// Read it from the shop's side.
const suppliers = await shop.getRelationships({ typeSlug: 'supplier', direction: 'to' });

What a relationship does not do

A relationship records meaning, not authority: permission resolution reads the tenant cascade, the membership role, group roles, and overrides. A relationship is not one of those inputs, so nothing widens access just because two profiles are related. Tenant-to-tenant partnership is not a shipped feature either. The Tenant record has no profile field, and a relationship row carries one tenant id, not two. An application can give each tenant an organization profile and relate those profiles, but it writes and reads that link itself.

Make inheritance a choice

A parent must allow permission cascading and the child must accept it. A role can separately allow descendant authority. Nothing widens access just because two tenants are related.

tenant-setup.ts
typescript
const network = await tenants.create({
  name: 'Northern Network',
  cascadePermissions: true
});

const chapter = await tenants.createChild(network.id, {
  name: 'Edmonton Chapter',
  inheritPermissions: true
});

Put tenant scope on the model

Required scope means every row belongs to a tenant. Optional scope allows shared rows and tenant rows. Global models remain outside tenant filtering. The request session establishes the authorized tenant context. A profile relationship uses optional scope: it takes the active tenant when there is one and stays global when there is none.