diff --git a/packages/app-framework/src/frameworks/solid/providers/StoreProvider.tsx b/packages/app-framework/src/frameworks/solid/providers/StoreProvider.tsx
index 3ef42caf..23960550 100644
--- a/packages/app-framework/src/frameworks/solid/providers/StoreProvider.tsx
+++ b/packages/app-framework/src/frameworks/solid/providers/StoreProvider.tsx
@@ -2,6 +2,7 @@ import {
AdamStoreProvider,
AiStoreProvider,
AppStoreProvider,
+ PresenceStoreProvider,
RouteStoreProvider,
SpaceStoreProvider,
TemplateStoreProvider,
@@ -17,7 +18,12 @@ export default function StoreProvider(props: ParentProps) {
- {props.children}
+
+ {/* Innermost: presence follows the current perspective and the route, so it needs
+ AdamStore and RouteStore above it. App-lifetime, not view-lifetime — it must
+ outlive navigation rather than being torn down with a view. */}
+ {props.children}
+
diff --git a/packages/app-framework/src/frameworks/solid/providers/TemplateProvider.tsx b/packages/app-framework/src/frameworks/solid/providers/TemplateProvider.tsx
index 21905c68..b0726597 100644
--- a/packages/app-framework/src/frameworks/solid/providers/TemplateProvider.tsx
+++ b/packages/app-framework/src/frameworks/solid/providers/TemplateProvider.tsx
@@ -8,6 +8,7 @@ import {
useAdamStore,
useAiStore,
useAppStore,
+ usePresenceStore,
useRouteStore,
useSpaceStore,
useTemplateStore,
@@ -34,6 +35,7 @@ export default function TemplateProvider() {
const themeStore = useThemeStore();
const templateStore = useTemplateStore();
const routeStore = useRouteStore();
+ const presenceStore = usePresenceStore();
// Set CSS custom property on :root so position:fixed elements (e.g. CesiumGlobe canvas)
// can consume the sidebar width without hard-coding it.
@@ -76,6 +78,7 @@ export default function TemplateProvider() {
themeStore,
templateStore,
routeStore,
+ presenceStore,
consoleStore,
model: modelStore,
// Host wiring, not backend adaptation — any backend would wire these the same way, so they stay
diff --git a/packages/app-framework/src/frameworks/solid/stores/PresenceStore.tsx b/packages/app-framework/src/frameworks/solid/stores/PresenceStore.tsx
new file mode 100644
index 00000000..23647e49
--- /dev/null
+++ b/packages/app-framework/src/frameworks/solid/stores/PresenceStore.tsx
@@ -0,0 +1,244 @@
+/**
+ * PresenceStore — live presence for the current space.
+ *
+ * The Solid binding over the neutral presence core in `@we/schema-shared`. It owns three things the
+ * core deliberately does not: **when** to start and stop (following the current perspective), **what**
+ * to publish as this agent's focus (following the route), and the **join** from a bare `agentId` to a
+ * displayable profile.
+ *
+ * ## Lifecycle: app-lifetime, not view-lifetime
+ *
+ * Mounted alongside the other stores in `StoreProvider`, not inside a view. Flux creates a signalling
+ * service per community view and tears it down on unmount, so leaving the view loses every peer with
+ * no path back except remounting. Here the store outlives navigation and simply re-scopes when the
+ * perspective changes.
+ *
+ * ## Scope: current space only
+ *
+ * Presence for *every* joined space would let the sidebar show live occupancy everywhere, but the
+ * traffic is (spaces × members ÷ heartbeat) inbound signals per second — uncomfortable at modest
+ * numbers, since each crosses the executor's GraphQL boundary and lands in reactive state on the main
+ * thread. Current-space-only is the conservative default; widening it is a deliberate later decision
+ * (and realistically wants a backend that reports presence server-side).
+ *
+ * ## Publishing vs subscribing
+ *
+ * Asymmetric, and easy to conflate. This agent has exactly one location, so it **publishes** once per
+ * heartbeat to the space it is in. It **subscribes** only to that same space. Our own dot needs no
+ * transport at all — it is `routeStore.currentPath`, read locally.
+ *
+ * ## What it never does
+ *
+ * Fetch profiles. Presence carries `agentId` only; profiles come from `adamStore.agents()`, the cache
+ * `$identities` and the `$agent` block already use. Flux's presence map *is* its profile cache, so it
+ * re-hydrates every peer profile on every heartbeat — an N-peer `Promise.all` every five seconds.
+ */
+import { createAd4mEphemeralPort } from '@shared/ad4mEphemeralAdapter';
+import type { AgentProfileSummary } from '@shared/agentHelpers';
+import { createTabCoordinator } from '@shared/tabCoordinator';
+import { useAdamStore } from '@solid/stores/AdamStore';
+import { useRouteStore } from '@solid/stores/RouteStore';
+import type { Activity, Focus, FocusDepth, Peer, PresenceSource, PresenceTone } from '@we/schema-shared';
+import {
+ applyFocusDepth,
+ callRosters,
+ createHeartbeatPresence,
+ peersInDataset,
+ peerTone,
+ sortByPresence,
+} from '@we/schema-shared';
+import {
+ type Accessor,
+ createContext,
+ createEffect,
+ createMemo,
+ createSignal,
+ onCleanup,
+ type ParentProps,
+ useContext,
+} from 'solid-js';
+
+/**
+ * A peer joined with whatever profile the agent cache holds, plus its derived appearance.
+ *
+ * `did` mirrors `agentId`, and `tone` is flattened onto the peer so a template can reach it with a
+ * plain `$item.tone` in a `$map` — no nested path resolution needed.
+ */
+export type PresentAgent = Peer & Partial & { did: string; tone: PresenceTone };
+
+export interface PresenceStore {
+ /** Every peer we know of in the current space, liveness-derived, offline included. */
+ peers: Accessor;
+ /** Peers in the current space who are not offline — the "who's here" list. */
+ online: Accessor;
+ /** Peers at this agent's exact route path. */
+ onlineHere: Accessor;
+ /** Concurrent calls in this space, keyed by call id. */
+ calls: Accessor