Private beta — the @z0devs packages are available to design partners while we harden for general availability.Try the live demoRequest access

defineSharedStore

Schema fields, the returned store object, feature-injected registries and the plugin-facing types.

defineSharedStore is the host's half of the contract. One call produces the context plugins bind to, the per-remote namespacing, the version the manifest is gated against, and the Provider that PluginRouteLoader mounts for you.

src/store/sharedStore.tsx
import { defineSharedStore } from "@z0devs/mf-shell-kit";
import { NavRegistry, SettingsRegistry, ComponentRegistry } from "@z0devs/mf-registry";
import { createEventsEmitter } from "@z0devs/mf-context";
import { useAuth, useGlobalStore, contextualApiFetch } from "../app";
 
const events = createEventsEmitter<{
  onAuthChange: { user: User | null };
  onOrgChange: { orgId: string };
}>();
 
export const sharedStore = defineSharedStore({
  values: { useAuth, useGlobalStore, contextualApiFetch },
  registries: {
    nav: NavRegistry.api,
    settings: SettingsRegistry.api,
    components: ComponentRegistry.api,
  },
  features: { router: true, onboarding: true },
  events,
});
 
export default sharedStore.useShellSharedStore;   // ← the federated expose
export { sharedStore };                           // ← host-side access

Schema#

FieldRequiredTypeNotes
valuesRecord<string, any>What plugins may read. Keys are yours to name. Any key matching ^use[A-Z] is treated as a React hook by useSharedValue; everything else is handed back by reference.
registriesRecord<string, RegistryApi<any>>Pass the built-in's .api, not the module. Auto-namespaced per remote in useInitProps.
features{ router?: boolean; onboarding?: boolean }Library-owned registries. See below.
eventsEventsEmitter<TEvents>From createEventsEmitter<T>(). The library never emits — it is a typed bus for your own lifecycle events.
apiVersionstringDefaults to SHELL_API_VERSION from @z0devs/mf-core.

Leave `apiVersion` alone unless you're pinning

The default is what the plugin scaffold targets. Hard-coding a different value blocks every plugin built from the template — see Versioning.

features — registries the library wires for you#

Both are off by default. Enabling one injects its registry into registries, typed, without you importing its internals.

FlagInjectsAlso does
router: trueroutes: RegistryApi<RouteItem>Nothing on its own — mount <RouterContextProvider> + <Router> from @z0devs/mf-router to render them. They read the same registry.
onboarding: trueonboardingSteps: RegistryApi<OnboardingStep>PluginRouteLoader detects the registry and mounts <OnboardingHost> above its <Outlet/>.

Do not import routeRegistryApi or onboardingStepsApi yourself — you would get a second registry the loader doesn't know about.

The returned store#

MemberAudienceType / purpose
valuesHostExactly what you passed in.
registriesHostYour registries plus any feature-injected ones. Full RegistryApi — includes host-only emit, setActiveFlags, clearRemote.
apiVersionBothResolved value: yours, or SHELL_API_VERSION.
eventsBothThe emitter you passed, or undefined.
ProviderHostMount once at the top of the tree. PluginRouteLoader does this for you.
useShellSharedStore()Plugins{ values, registries } with registries narrowed to PluginRegistryApi. This is the default export your shell/sharedStore expose must have — the bridge calls it as a hook.
useInitProps(remoteName)HostSame shape, plus every registry wrapped in withRemoteNamespace(remoteName). Fed to <InitPropsProvider>; PluginRouteLoader calls it for you.
useSharedValue(key)BothAuto-invokes use* keys, returns others by reference.

RegistryApi vs PluginRegistryApi#

The narrowing is the security boundary, not a convenience.

CapabilityHost (RegistryApi)Plugin (PluginRegistryApi)
getItem / getItems / getActiveFlags
addItem / removeItem✅ — ids auto-prefixed with the remote name
subscribe✅ — see below
setActiveFlags
emit / addListener / removeListener
clearRemote

A plugin therefore cannot flip another plugin's feature flags or wipe its contributions, even by accident.

subscribe is on the plugin side while the emitter is not, and the split is the point: being told the listed set changed is a read capability. What a remote must not have is emit (a broadcast — it could fake another plugin's events) or addListener (it could watch another remote's typed traffic). Use it through the hooks rather than by hand:

import { useRegistryItem, useRegistryItems } from "@z0devs/mf-plugin-kit";
 
const widget = useRegistryItem(registries.components, "billing_mfe:InvoiceWidget");
const navItems = useRegistryItems(registries.nav);

`useSharedValue` and the rules of hooks

Call it with the same key on every render of a component. useSharedValue("useAuth") invokes a hook; useSharedValue("contextualApiFetch") returns a function. Switching keys between renders changes the call shape and breaks the hooks invariant. For dynamic access read useShellSharedStore().values[key] and invoke it yourself.

Exported types#

TypeWhat it describes
SharedStoreSchema<TValues, TRegistries, TEvents>The argument object.
DefinedSharedStore<TValues, TRegistries, TEvents>The return value. Plugins import typeof sharedStore to type their bridge.
SharedInitProps<TValues, TRegistries>{ values, registries } with full registries — host side.
PluginInitProps<TValues, TRegistries>Same, registries narrowed to PluginRegistryApi — what a remote's init receives.
StoreFeaturesThe features flags.
WithFeatureRegistries<TRegistries, TFeatures>Your registries after feature injection.
AnyRegistryRegistryApi<any> — the constraint registries values must satisfy.

These live in @z0devs/mf-shell-kit, a host package. Plugins don't install it — they declare a structural InitProps instead, for the reasons in Build a plugin.