# Tenancy

Use tenancy when your app is shared by many customer companies, so that
one company never sees another's data. Each customer is a tenant: one
record of an entity you choose, such as `Company`.

People belong to a company through a membership. A membership is a
stored record, such as a `CompanyMembership`, that says which user holds
which role in which company. Its entity has a `membership:` section naming
those three fields: `scope`, the company; `member`, the user; and `role`,
such as `CompanyRole.owner` ([A membership role](a-membership-role.md)
shows one in full). Grants then name roles: an entry such as
`CompanyRole.viewer` in a record's `read:` list lets in the users who hold
that role in the record's company.

## Naming the tenant

The project's root `policy.jadpo`, the policy file at the top of the
project beside `app.jadpo`, names the tenant entity once:

```jadpo
// policy.jadpo
policy {
    tenant: Company
}
```

This file holds the rules for the whole app: the tenant, and the
[actions](actions.md) that need a permission of their own. Each entity's own
`policy:` section still lists who may create, read, change and delete its
records. Naming a tenant adds these rules to every entity:

- Every entity needs one reference to `Company`, its tenancy reference,
  such as `company_id`. A task carries its own `company_id`, not only its
  project's.
- Every grant on a tenant's records applies only within companies where
  the caller has a membership. A grant is an entry in one of an entity's
  lists, such as `read: [CompanyRole.viewer]` ([Grants](grants.md)). This
  holds for `Access.authenticated`, which lets in anyone signed in, too:
  on a tenant's records it lets in anyone signed in who belongs to the
  record's company.
- An entity every tenant shares, such as a list of countries or `User`
  itself, says `tenant: Tenancy.shared` in its `policy:`. Its records
  belong to no company, and its grants apply as they would without
  tenancy.

A request has no current tenant of its own, and the signed-in caller
carries no company. Each record's tenant is the company its tenancy
reference names, and a caller who belongs to several companies is let in
to each one's records by their membership there. A route says which
company it works in through its input, such as a `company_id` in its path
or body, and policy checks the caller's membership in the company of
each record the route reads or changes.

An entity with two references to `Company` names, in its `policy:`, the
one that decides where it belongs, such as `tenant: source_company_id`.

An entity you add belongs to a tenant unless a person accepts otherwise,
as every change that widens access needs
([Protecting policy](protecting-policy.md)).

A company's first membership founds it, in the action that creates the
company ([Founding a tenant](tenancy.md#founding-a-tenant)). Every other membership
is created by someone who belongs already, as the membership's own
`create:` says. So nobody makes themselves a member, or an owner, of
someone else's company.

## References stay inside the tenant

A reference from one of a tenant's records to another always points at a
record of the same tenant: a chore of Northwind's names one of Northwind's
projects, never Contoso's. Every create and update that sets such a
reference checks it as part of that same write, whether or not the
caller's grants would allow it. One that points into another tenant is
refused with 403 `not_permitted`. A reference to a record that doesn't
exist at all still gets that entity's own not-found failure, such as
`ProjectNotFound`.

Because a chore and its project are always in the same company, anyone
who may read a chore has their role in the project's company too. So a
query of chores may include each chore's project without showing anyone
another company's data. Here a chore belongs to a company and to one of
its projects:

```jadpo
use work/entities { Company, Project }
use work/types { CompanyRole, Label }

/// One step of a project.
entity Chore {
    id: Uuid
    company_id: Company.id {
        on_delete: OnDelete.cascade
    }
    project_id: Project.id {
        on_delete: OnDelete.cascade
    }
    title: Label

    policy: {
        create: [CompanyRole.owner]
        read: [CompanyRole.owner, CompanyRole.viewer]
    }
}
```

```jadpo
use work/entities { Chore, Company }
use work/types { ChoreView }

/// A company's chores, each with its project.
query company_chores(company_id: Company.id) -> List<ChoreView> {
    from: Chore
    where: Chore.company_id == company_id
    order: [Chore.title asc]
    limit: 100
    include: {
        project: Chore.project_id
    }
}
```

In the first test, Ann owns both companies, so only the check on the
project reference refuses her. In the second, Bob, a viewer of Northwind,
sees its chores with their projects:

```jadpo
use work/entities { Company, CompanyMembership, Member }
use work/types { ChoreCard, ChoreView, CompanyRole, ProjectCard }
use work/actions { AddChore, add_chore, add_member, found_company, register, start_project }

test "a chore can't name another company's project" {
    var ann: Member = attempt register("ann")
    var northwind: Company = found_company("Northwind", Principal.user(ann))
    var contoso: Company = found_company("Contoso", Principal.user(ann))
    var rival: ProjectCard = attempt start_project(contoso.id, "Rival")
    var response: Response = request {
        method: HttpMethod.post
        path: "/chores"
        as: Principal.user(ann)
        input: AddChore {
            company_id: northwind.id
            project_id: rival.id
            title: "Borrow"
        }
    }
    assert response.status == 403
    assert response.code == "not_permitted"
}

test "a viewer sees each chore with its project" {
    var ann: Member = attempt register("ann")
    var bob: Member = attempt register("bob")
    var northwind: Company = found_company("Northwind", Principal.user(ann))
    var viewer: CompanyMembership = attempt add_member(northwind.id, bob.id, CompanyRole.viewer)
    assert viewer.role == CompanyRole.viewer
    var launch: ProjectCard = attempt start_project(northwind.id, "Launch")
    var kickoff: ChoreCard = attempt add_chore(
        AddChore {
            company_id: northwind.id
            project_id: launch.id
            title: "Plan"
        }
    )
    var seen: Response = request {
        method: HttpMethod.get
        path: "/chores/{company_id}"
        params: {
            company_id: northwind.id
        }
        as: Principal.user(bob)
    }
    assert seen.status == 200
    assert seen.output == [
        ChoreView {
            id: kickoff.id
            title: "Plan"
            project: launch
        }
    ]
}
```

A reference straight to `Company` isn't checked this way. Either it is
the record's tenancy reference, which is what puts the record in its
company, or the entity has two references to `Company`, and the one its
`tenant:` doesn't name points at another company on purpose.

## A unique rule holds within its tenant

A `unique:` rule on a tenant's records holds within each tenant. Northwind
and Contoso may each have a project called Launch, but a second Launch in
Northwind is the failure the create maps it to under `conflict:`, here
`ProjectNameTaken`. So a conflict never tells one company what another
stores.

The compiler does this by adding the record's tenancy reference to the
rule's key, the set of fields that must be unique together, so `name`
becomes unique per `company_id`. A rule that already includes the tenancy
reference is left as it is. An entity every tenant shares, and the tenant
entity itself, keep one key across the whole application. Here `Member` is
shared, so two members can never have the same `handle`.

```jadpo
use work/entities { Company }
use work/types { CompanyRole, Label }

/// A piece of work in a company.
entity Project {
    id: Uuid
    company_id: Company.id {
        on_delete: OnDelete.cascade
    }
    name: Label {
        unique: true  // within its company
    }

    policy: {
        create: [CompanyRole.owner]
        read: [CompanyRole.owner, CompanyRole.viewer]
    }
}
```

```jadpo
use work/entities { Member }
use work/types { SeatView }
use work/actions { NewCompany, StartProject, found_company, register }

test "two companies may each have a project called Launch, but not two in one" {
    var ann: Member = attempt register("ann")
    var bob: Member = attempt register("bob")
    var northwind: SeatView = found_company(
        NewCompany {
            name: "Northwind"
        },
        Principal.user(ann)
    )
    var contoso: SeatView = found_company(
        NewCompany {
            name: "Contoso"
        },
        Principal.user(bob)
    )
    var first: Response = request {
        method: HttpMethod.post
        path: "/projects"
        as: Principal.user(ann)
        input: StartProject {
            company_id: northwind.company_id
            name: "Launch"
        }
    }
    assert first.status == 201
    var second: Response = request {
        method: HttpMethod.post
        path: "/projects"
        as: Principal.user(bob)
        input: StartProject {
            company_id: contoso.company_id
            name: "Launch"
        }
    }
    assert second.status == 201
    var refused: Response = request {
        method: HttpMethod.post
        path: "/projects"
        as: Principal.user(ann)
        input: StartProject {
            company_id: northwind.company_id
            name: "Launch"
        }
    }
    assert refused.status == 409
    assert refused.code == "project_name_taken"
}
```

Since the key holds the tenancy reference, a query that finds one record
by a unique rule
([How many records come back](named-queries.md#how-many-records-come-back))
must test the tenancy reference with `==` beside the rule's fields, such
as `Project.company_id == company_id`. It needn't when it tests a
reference to another of the tenant's records, since that record already
names the tenant.

The field a person signs in by, their subject, such as an email address,
is on an entity every tenant shares, so it is unique across the app
([The principal](the-principal.md)).

When you add tenancy to an existing app, `jadpo migrate new` drops each
unique key that didn't hold the tenancy reference and makes it again with
the tenancy column, `UNIQUE ("name", "company_id")`. The migration needs
no decision from you, since every set of rows the old key allowed, the new
one allows too.

## Founding a tenant

A new company has no members, so nobody holds a role that could let
them create its first membership. That first membership founds the
company, and it is allowed in one place only: the action that creates the
company's record. There, the membership's scope is the `id` that the
company's create gave back, its member is the caller, and nobody belongs
to the company yet.

Any other new membership needs someone who belongs already, even one that
names its own caller as the member. In a grant list, a user field such as
`member_id` lets in the user it points to. So
`create: [CompanyRole.owner, member_id]` lets an owner add anyone, and
anyone else only themselves, and only in a company they already belong
to. A stranger can't add themselves to a company whose members have all
left: that is 403 `not_permitted`. Founding a new company still makes its
founder a member:

```jadpo
use work/entities { Company, Member }
use work/types { CompanyRole }

/// A member's role in a company.
entity CompanyMembership {
    id: Uuid
    company_id: Company.id {
        on_delete: OnDelete.cascade
    }
    member_id: Member.id {
        on_delete: OnDelete.cascade
    }
    role: CompanyRole

    membership: {
        scope: company_id
        member: member_id
        role: role
    }

    policy: {
        create: [CompanyRole.owner, member_id]
        read: [CompanyRole.owner, member_id]
        delete: [CompanyRole.owner, member_id]
    }
}
```

```jadpo
use work/entities { Company, CompanyMembership }
use work/types { CompanyRole, Label, SeatView }

/// What founding a company takes.
type NewCompany = Input {
    name: Label
}

/// Starts a company with its founder as its owner.
action found_company(input: NewCompany, founder: Principal.user) -> SeatView {
    var company: Company = Company.create {
        set: {
            name: input.name
        }
    }
    var seat: CompanyMembership = CompanyMembership.create {
        set: {
            company_id: company.id
            member_id: founder.id
            role: CompanyRole.owner
        }
    }
    return SeatView {
        id: seat.id
        company_id: seat.company_id
        role: seat.role
    }
}
```

```jadpo
use work/entities { Member }
use work/types { CompanyRole, SeatView }
use work/actions { NewCompany, NewMember, found_company, leave, register }

test "nobody seats themselves in a company everyone has left" {
    var ann: Member = attempt register("ann")
    var eve: Member = attempt register("eve")
    var seat: SeatView = found_company(
        NewCompany {
            name: "Northwind"
        },
        Principal.user(ann)
    )
    attempt leave(seat.id)
    var seized: Response = request {
        method: HttpMethod.post
        path: "/companies/{company_id}/members"
        params: {
            company_id: seat.company_id
        }
        as: Principal.user(eve)
        input: NewMember {
            member_id: eve.id
            role: CompanyRole.owner
        }
    }
    assert seized.status == 403
    assert seized.code == "not_permitted"
    var founded: Response = request {
        method: HttpMethod.post
        path: "/companies"
        as: Principal.user(eve)
        input: NewCompany {
            name: "Contoso"
        }
    }
    assert founded.status == 201
}
```

## Moving a record to another tenant

Moving a record to another company is rare, and it needs an action of its
own. Only an action listed under `actions:` in root `policy.jadpo` may do
it ([Actions](actions.md) shows the list). The write checks every reference
the record keeps, and that no record of the old company refers to it. In
the new company, it needs what creating the moved record there would: the
entity's own `create:` and the caller's own membership there.

A membership moved in from another company holds no role its mover
couldn't give there. So a viewer can't move in an owner's membership they
hold elsewhere.
