diff --git a/packages/iios-messaging-ui/src/provider.test.tsx b/packages/iios-messaging-ui/src/provider.test.tsx
new file mode 100644
index 0000000..7054c46
--- /dev/null
+++ b/packages/iios-messaging-ui/src/provider.test.tsx
@@ -0,0 +1,25 @@
+import { describe, it, expect } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { MessagingProvider, useAdapter } from './provider';
+import { MockAdapter } from './adapters/mock';
+
+function ShowActor() {
+ const adapter = useAdapter();
+ return {adapter.currentActorId()};
+}
+
+describe('MessagingProvider', () => {
+ it('supplies the injected adapter to descendants', () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText('me')).toBeDefined();
+ });
+
+ it('throws a helpful error when a hook is used outside the provider', () => {
+ // React logs the error boundary trace; that noise is expected.
+ expect(() => render()).toThrow(/useAdapter must be used within a /);
+ });
+});
diff --git a/packages/iios-messaging-ui/src/provider.tsx b/packages/iios-messaging-ui/src/provider.tsx
new file mode 100644
index 0000000..748c00f
--- /dev/null
+++ b/packages/iios-messaging-ui/src/provider.tsx
@@ -0,0 +1,22 @@
+import { createContext, useContext, type ReactNode } from 'react';
+import type { MessagingAdapter } from './adapter';
+
+const AdapterContext = createContext(null);
+
+export function MessagingProvider({
+ adapter,
+ children,
+}: {
+ adapter: MessagingAdapter;
+ children: ReactNode;
+}) {
+ return {children};
+}
+
+/** Access the host-injected adapter. Throws outside a provider — a missing provider is
+ * a wiring bug, and failing loudly beats a confusing null-deref three layers down. */
+export function useAdapter(): MessagingAdapter {
+ const adapter = useContext(AdapterContext);
+ if (!adapter) throw new Error('useAdapter must be used within a ');
+ return adapter;
+}