Building a Multi-Tenant LTI® Tool
The short answer
The moment your tool serves a second institution, several things that worked stop being correct. The core rule: a tenant is identified by issuer and client ID together, never by issuer alone, and no identifier from a launch is unique across platforms.
Most multi-tenancy bugs in LTI® tools are one of those two assumptions surviving from the single-customer version.
Keying a tenant correctly
A launch identifies its origin with an issuer naming the platform, a client ID naming your registration, and a deployment ID naming the installation.
Look up registrations on issuer + client ID. Issuer alone is not enough,
because several institutions can present the same one — Canvas commonly sends
https://canvas.instructure.com regardless of the institution's own domain, so a
tool serving twenty Canvas customers sees one issuer twenty times.
And on Blackboard, the deployment ID becomes load-bearing: its multi-tenant model lets several institutions share a registration entirely, arriving with the same issuer and client ID. There, deployment ID is the only thing distinguishing them. Store it from the start — the Blackboard article has the detail.
User identity is scoped, not global
The sub claim identifies a user within its issuer. It is not globally
unique, and two platforms can send the same value for different people.
So the stored identity is the pair — issuer plus sub — and a unique constraint
on sub alone is a bug that surfaces as one institution's user being handed
another's account.
The same applies to course and resource link identifiers. All of them are scoped to their platform — 1EdTech's guidance on selecting a user ID is worth reading before you design the schema.
And do not fall back to email. Addresses are reused and changed, many platforms send none, and the same address at two institutions is not necessarily one person. The SSO article covers account linking properly.
What breaks at customer two
The list, in the order people hit it:
Configuration that was global. Anything stored once — a platform key, an endpoint, a setting — needs a tenant on it.
Cached platform keys. Key sets cached without a tenant will serve one institution's key for another's launch. Verification fails in a way that looks like a signature problem.
Nonce storage. Nonces should be scoped per platform, or one platform can consume another's — a correctness problem and a subtle denial of service.
Access tokens. Service tokens are issued per platform and per scope. A shared cache hands the wrong token to the wrong LMS, which the LMS rejects.
Onboarding assumptions. The first customer was set up by hand. The twentieth needs it to be a process, which is the argument for dynamic registration.
Isolate the keys
Not required by the specification, worth doing anyway: a key pair per registration rather than one for the whole tool.
The reason is blast radius. With a single key, a compromise affects every institution you serve at once. With a key per registration, it affects one.
The cost is a key set with several entries rather than one, which is what the
kid in a token header is for.
Where LTIAAS fits
Multi-tenancy is the architecture rather than a feature. Registrations are stored per account, platform lookups are keyed on issuer and client ID together, nonces are scoped per platform, and access tokens are cached per platform and scope.
Each registered platform gets its own key pair, published in your key set
under a kid identifying that registration — so a key is scoped to one
institution rather than to your whole tool.
Launch data arrives with the platform identified, so your application stores the pair without deriving it:
const { platform, user } = await axios.get('https://your.ltiaas.com/api/idtoken', {
headers: { Authorization: `LTIK-AUTH-V2 ${API_KEY}:${ltik}` }
}).then(r => r.data)
// key your records on platform + user.id, never user.id alone
What remains yours is your own data model: making sure a tenant identifier reaches every table, every cache and every query, which no service can do on your behalf.
Common questions
How do I identify which institution a launch came from?
By the issuer and client ID together, not the issuer alone. Several institutions can present the same issuer, so a lookup keyed only on it can hand one tenant another's configuration.
Do I need separate keys per platform?
Not required, but it limits blast radius. A key pair per registration means a compromise affects one institution rather than every one you serve.
What breaks when the second institution arrives?
Anything that assumed one registration. Global configuration, user identifiers treated as unique, cached keys stored without a tenant, and any lookup keyed on issuer alone.
Are user identifiers unique across platforms?
No. The sub claim is unique within an issuer only. Two institutions can legitimately send the same value for different people, so identity must be stored as the pair.
Next
- Every LTI® 1.3 configuration field explained
- Is LTI® single sign-on? — account linking across tenants
- The LTI® 1.3 security model
