Ga naar hoofdinhoud

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-Organisation header.
  • An app that already declares an organisation field 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 stamps X-OpenRegister-Organisation: <uuid> (buildHeaders({ organisationUuid })).
  • Every createCrudStore() action does the same.
  • CnIndexPage watches its :active-organisation prop and forwards changes into the bound store via setActiveTenantOrganisation(uuid) so the next fetchCollection() hits the correct tenant.
  • CnFormDialog and CnAdvancedFormDialog auto-fill the organisation field on create when the schema declares one and the form data is empty.
  • CnTenantBadge renders the active organisation in the top-bar slot of CnAppRoot.

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.