# Ban / pause / delete watcher

> Mount once in any authenticated layout. When the user's membership flips off active, the watcher paints a full-screen blocking modal with a 9-second countdown, then signs them out and redirects.

> **Note:** Renders nothing while membership is `active`. The modal in the preview above only appears when an admin bans / pauses / deletes the user, or when the user deactivates themselves from another tab.

## The four states

Each state renders the same card chrome with its own icon, headline, copy, and tone. The countdown is identical (9 seconds at default), so the only thing the user can do is read why and watch the dots fill in.

| State | Headline | Tone |
| --- | --- | --- |
| `banned` | Your access has been revoked. | Red. Admin action, user cannot sign back in. |
| `paused` | Your access has been paused. | Amber. Temporary admin action, contact owner to restore. |
| `deleted` | Your account was removed. | Red. Membership purged in 90 days unless restored. |
| `inactive` | You're taking a break from this app. | Amber. User-initiated deactivation, fully reversible. |

## Usage

<!-- app/(signed-in)/layout.tsx -->
```tsx
import {
  ElvixLifecycleWatcher,
  ElvixProvider,
} from "@elvix.is/sdk/react";

/**
 * Mount once in the root layout that wraps every authenticated page.
 * On ban / pause / delete / inactive, the watcher paints a full-screen
 * modal with a 9-second countdown, then signs the user out.
 */
export default function AppLayout({ children }: { children: React.ReactNode }) {
  return (
    <ElvixProvider clientId="your-app">
      <ElvixLifecycleWatcher signInUrl="/sign-in" />
      {children}
    </ElvixProvider>
  );
}
```

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `graceSec` | `number` | optional | `9` | Countdown in seconds. The modal renders one progress dot per second. |
| `signInUrl` | `string` | optional | `"/sign-in/console"` | Redirect target after the countdown finishes and the session is cleared. |
| `signOutUrl` | `string` | optional | `"/api/auth/sign-out"` | Same-origin POST that revokes the cookie. Ignored cross-origin (token is dropped locally instead). |
| `cookieName` | `string` | optional | `"elvix_token"` | Cookie cleared alongside the in-memory token. Must match what your sign-in page wrote. |
| `apiBase` | `string` | optional | none | Set when embedded cross-origin (your app on a different origin than elvix). Switches the transport from SSE to polling against `POST /api/v1/session`. |
| `applicationId` | `string` | provider-resolved | from `useElvixApp()` | App scope. Pass explicitly when mounting outside the provider tree. |
| `userId` | `string` | provider-resolved | from session | Same shape as `applicationId`. |
| `pollMs` | `number` | optional | `7000` | Polling interval when in cross-origin mode. Worst-case sign-out latency is `pollMs + graceSec * 1000` (about 16s at defaults). |
| `onSignedOut` | `(reason: "banned" \| "paused" \| "deleted" \| "session_expired") => void` | optional | reload page | Skip the default reload and own the next navigation client-side. |

> **Warning:** Mount this once per signed-in surface (root layout is canonical). Do not mount it on a public marketing layout or your sign-in page. There is no session to watch, the bootstrap call is wasted, and SSE will fail with no `userId` to subscribe with.

> **Note:** Transport auto-selects. Same-origin gets SSE for near-instant detection. Cross-origin embeds (your app on `app.example.com`, elvix on `elvix.is`) fall back to polling every `pollMs` because `EventSource` cannot carry the bearer token.

## Custom signed-out flow

Without `onSignedOut`, the watcher does a `window.location.replace(signInUrl)` after clearing the token + cookie. Pass `onSignedOut` to skip that and own the navigation: route client-side, fire a toast explaining the reason, log analytics, hand the user to a custom landing page.

<!-- components/signed-in-chrome.tsx -->
```tsx
"use client";

import { ElvixLifecycleWatcher } from "@elvix.is/sdk/react";
import { useRouter } from "next/navigation";
import { toast } from "sonner";

export function SignedInChrome({ children }: { children: React.ReactNode }) {
  const router = useRouter();
  return (
    <>
      <ElvixLifecycleWatcher
        cookieName="elvix_token"
        onSignedOut={(reason) => {
          // Token + cookie already cleared. Host owns the next navigation.
          if (reason === "banned") toast.error("Your access was revoked.");
          if (reason === "paused") toast("Your access is paused.");
          if (reason === "deleted") toast("Your membership was removed.");
          router.replace("/sign-in");
        }}
      />
      {children}
    </>
  );
}
```


## Related

- [ElvixProvider](/docs/components/elvix-provider.md): Top-level context. Fetches the brand chord, factor config, and copy from elvix; pipes session state and the event channel to every nested <Elvix*> component.
- [ElvixSignInForm](/docs/components/elvix-sign-in-form.md): The full branded sign-in surface (card, header, brand wash, every Console-enabled method, Secured-by-elvix badge). Exported as both `ElvixSignInForm` and the short alias `ElvixSignIn` — same component.
