# Keep tabs and devices in sync: sign-out, permissions and carts

> Sign out everywhere at once, apply a revoked role right away, and keep carts and settings the same in every tab and device the user has open.

Source: https://hibira.io/use-cases/multi-tab-sync

People keep your app open in several tabs and on several devices, and each copy only learns about changes when it reloads. Someone signs out on a shared computer, but the tab on their phone still shows their account. An admin removes a teammate's edit rights, but the teammate keeps editing until they refresh. A product goes into the cart on the phone, and the laptop tab still says the cart is empty. With Hibira, your server tells every open copy of the app at once.

## What your users get

- "Sign out everywhere" that really closes every open session, right away.
- A changed role or a revoked permission that applies without a reload.
- The same cart, settings and unread counts in every tab and on every device.

## Built with

- **User authentication** (`pusher.signin()`), as in [live in-app notifications](https://hibira.io/use-cases/in-app-notifications), so each signed-in socket belongs to a user.
- **`sendToUser`**, which reaches every tab and device of that user with one call.
- **`terminateUserConnections`**, which closes every socket signed in as that user.

```text
flow: One call from your server reaches every copy of the app the user has open
== Something changes for one user
Your server|sign-out, new role, cart --`sendToUser()`--> *Hibira --WebSocket--> Every tab and device|of that user
== Sign out everywhere
Your server|sessions revoked --close all--> *Hibira --closes--> Every socket|signed in as that user
```

## Step 1: sign out everywhere

Revoke the sessions in your database first, tell every open copy, then close the sockets:

```js
app.post('/account/sign-out-everywhere', requireLogin, async (req, res) => {
  const id = String(req.user.id);
  await db.sessions.revokeAll(id);
  await pusher.sendToUser(id, 'signed-out', {});
  await pusher.terminateUserConnections(id);
  res.sendStatus(204);
});
```

The closed sockets cannot sign in again, because your user authentication endpoint no longer accepts the revoked session.

## Step 2: apply a permission change at once

When an admin changes someone's role, send a nudge. The page fetches its permissions again from your server, which stays the source of truth:

```js
await db.members.update(member.id, { role: 'viewer' });
await pusher.sendToUser(String(member.userId), 'permissions-changed', {});
```

## Step 3: keep the cart the same everywhere

After every change to the cart, send the summary the header shows:

```js
app.post('/api/cart/items', requireLogin, async (req, res) => {
  const cart = await db.carts.add(req.user.id, req.body);
  await pusher.sendToUser(String(req.user.id), 'cart', { count: cart.count, total: cart.total });
  res.json(cart);
});
```

## Step 4: react in every tab

```js
pusher.user.bind('signed-out', () => location.assign('/login'));
pusher.user.bind('permissions-changed', async () => {
  applyPermissions(await fetch('/api/me/permissions').then((r) => r.json()));
});
pusher.user.bind('cart', ({ count, total }) => cartBadge.set(count, total));
```

## Tips

- Never trust the event itself for security: it only says "check again". Your API still checks the session and the role on every request.
- `BroadcastChannel` syncs tabs of one browser, but not other devices, and it cannot hear changes made by an admin or by your server.
- Guest carts can use a private channel per cart (`private-cart-<id>`) authorized with the cart's cookie.
- Send counts and totals, not the whole cart; event size depends on your plan, see [pricing](https://hibira.io/pricing).
