# Share state across connections

> Understand the one-instance-serves-everyone model and keep per-connection and shared state straight

The single most important thing to understand about voxal is this: **one app instance serves every connected user.** When five people `ssh` into your app, they are all running inside the same sandbox, multiplexed by connection. This guide shows how to keep per-connection state separate and how to share state across everyone on purpose.

## The mental model

Your app boots once. Each SSH session becomes a connection with its own `conn.id` (a unique string). The same handlers and the same module-level variables are shared by all of those connections. So there are two kinds of state:

- **Per-connection state** belongs to one user (their cursor, their draft, their view).
- **Shared state** is visible to everyone (a chat room, a presence list, a global counter).

Mixing these up is the most common bug in a multiplayer terminal app. Below is how each layer handles them.

## Core SDK: key state by conn.id

In the core SDK, hold per-connection state in a `Map` keyed by `conn.id`, and clean it up in `onClose` so it does not leak as users come and go.

```js title="app.js"

const sessions = new Map(); // conn.id -> per-user state

createApp()
  .onConnect((conn) => {
    sessions.set(conn.id, { count: 0 });
    conn.write('your count is 0. press space to add.\r\n');
  })
  .onKey((conn, data) => {
    const state = sessions.get(conn.id);
    if (data === ' ') {
      state.count += 1;
      conn.write('your count is ' + state.count + '\r\n');
    }
  })
  .onClose((conn) => {
    sessions.delete(conn.id); // always clean up
  })
  .listen();
```

<Callout type="warn">
If you store `count` in a single module-level variable instead of a per-id map, every user shares one counter and they will stomp on each other. Use the map for anything that should be private.
</Callout>

## Sharing state across connections

To make state shared, hoist it out of the per-id map into a plain module-level variable. To keep everyone in sync, hold the set of live connections and write to each one when the shared state changes.

```js title="app.js"

const conns = new Set();
let total = 0; // shared across everyone

function broadcast(line) {
  for (const c of conns) c.write(line);
}

createApp()
  .onConnect((conn) => {
    conns.add(conn);
    conn.write('shared total is ' + total + '. press space.\r\n');
  })
  .onKey((conn, data) => {
    if (data === ' ') {
      total += 1;
      broadcast('total is now ' + total + '\r\n');
    }
  })
  .onClose((conn) => {
    conns.delete(conn);
  })
  .listen();
```

## UI layer: component state is per-connection

The UI layer makes this split natural. `serve()` mounts a **separate React tree for every connection**, so any `useState` lives with that one user. Module-level variables, declared outside the component, are still shared by everyone.

```jsx title="app.jsx"

// Shared: lives outside the component, one copy for all connections.
let visitors = 0;

function App({ id }) {
  // Per-connection: each user gets their own clicks.
  const [clicks, setClicks] = useState(0);

  useInput((input) => {
    if (input === ' ') setClicks((c) => c + 1);
  });

  return (
    <Box flexDirection="column" padding={1}>
      <Text>your clicks: <Text bold>{clicks}</Text></Text>
      <Text dimColor>connection {String(id).slice(0, 4)}</Text>
    </Box>
  );
}

serve((conn) => {
  visitors += 1; // shared counter bumps for every new connection
  return <App id={conn.id} />;
});
```

`clicks` is private to each user because it is component state in their own tree. `visitors` is shared because it is a module-level variable. For a live shared view (a chat room where one person's message appears on everyone's screen), keep the shared data at module level and trigger a re-render on the other connections, for example by holding their state setters and calling them when the shared data changes.

<Callout>
Module-level state resets when the app is dropped to zero (scale-to-zero) and re-boots on the next connection. Treat it as in-memory and ephemeral, not durable storage.
</Callout>

## Next steps

<Cards>
  <Card title="Lifecycle" href="/sdk/lifecycle" description="onConnect, onClose, and when handlers fire" />
  <Card title="Connection" href="/sdk/connection" description="conn.id, write, and per-session data" />
  <Card title="Build a chat app" href="/guides/chat-app" description="Put shared state to work in a real TUI" />
</Cards>

---

Source: https://docs.voxal.sh/guides/sharing-state
