useTenantContext
Multi-tenancy hooks for Conduction apps consuming the OpenRegister MultiTenancyTrait. One shared activeOrganisationUuid flows top-to-bottom via Vue provide/inject; every outbound request, every <CnFormDialog> instance, and every store action picks up the active tenant automatically.
Spec: openspec/changes/multi-tenancy-context.
When to use
- An app where users belong to multiple organisations and the active organisation must scope CRUD reads/writes server-side via the
X-OpenRegister-Organisationheader. - An app that already declares an
organisationfield on its schemas and wants the form dialogs to auto-stamp the active tenant.
If your deployment is single-tenant you can skip this entirely — CnAppRoot mounts the provider unconditionally, but it stays inactive until something calls setActiveTenant().
Wiring at the app entry
CnAppRoot already calls provideTenantContext() for you. If you want to seed the initial tenant from server-rendered state pass :initial-organisation-uuid and :initial-organisation:
<template>
<CnAppRoot
:manifest="manifest"
:initial-organisation-uuid="bootstrap.activeOrgUuid"
:initial-organisation="bootstrap.activeOrg" />
</template>
<script>
import bootstrap from '@nextcloud/initial-state'
import manifest from './manifest.json'
export default {
data() {
return { manifest, bootstrap: bootstrap('myapp', 'tenancy') }
},
}
</script>
If you bootstrap your shell without CnAppRoot, mount the provider yourself:
// main.js
import { provideTenantContext } from '@conduction/nextcloud-vue'
const App = {
setup() {
provideTenantContext(seededUuid, seededOrg)
},
// ...
}
Reading the context
Composition API
import { useTenantContext } from '@conduction/nextcloud-vue'
export default {
setup() {
const { activeOrganisationUuid, activeOrganisation, setActiveTenant, onTenantSwitch } = useTenantContext()
// React to switches across the whole app
const off = onTenantSwitch(({ uuid }) => {
console.log('tenant switched to', uuid)
})
return { activeOrganisationUuid, activeOrganisation, setActiveTenant, off }
},
}
Options API
Use tenantContextMixin for the same surface:
import { tenantContextMixin } from '@conduction/nextcloud-vue'
export default {
mixins: [tenantContextMixin],
created() {
this.onTenantSwitch(({ uuid }) => {
this.refreshList(uuid)
})
},
methods: {
switch(orgUuid) {
this.setActiveTenant(orgUuid)
},
},
}
What the provider stamps
When a tenant is active:
- Every
useObjectStore()/createObjectStore()fetch stampsX-OpenRegister-Organisation: <uuid>(buildHeaders({ organisationUuid })). - Every
createCrudStore()action does the same. CnIndexPagewatches its:active-organisationprop and forwards changes into the bound store viasetActiveTenantOrganisation(uuid)so the nextfetchCollection()hits the correct tenant.CnFormDialogandCnAdvancedFormDialogauto-fill theorganisationfield on create when the schema declares one and the form data is empty.CnTenantBadgerenders the active organisation in the top-bar slot ofCnAppRoot.
Server-side validation
When strict_organisation_header_validation is opt-in on OR, mismatched X-OpenRegister-Organisation headers are rejected with a structured warning header X-OpenRegister-Organisation-Mismatch: 1. See the companion multi-tenancy-header-validation change in the OpenRegister openspec for the server contract.
Single-tenant fallback
useTenantContext() outside a provider returns a noop fallback: activeOrganisationUuid.value is null, setActiveTenant() logs a console warning and writes to a local stand-alone context (so consumer code does not crash). The header stamping silently no-ops; the schema auto-fill silently no-ops. Single-tenant apps can leave the code paths in place without behavioural drift.