Components
ElvixLifecycleWatcher

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.

Your access has been revoked.

An administrator banned your account for this app. You'll be signed out and won't be able to sign back in. Contact the owner if you think this was a mistake.

Signing you out in 6s

Static snapshot at 6 seconds. The real watcher mounts invisibly and only renders this modal when the user's membership flips off "active".

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.

StateHeadlineTone
bannedYour access has been revoked.Red. Admin action, user cannot sign back in.
pausedYour access has been paused.Amber. Temporary admin action, contact owner to restore.
deletedYour account was removed.Red. Membership purged in 90 days unless restored.
inactiveYou're taking a break from this app.Amber. User-initiated deactivation, fully reversible.

Usage

app/(signed-in)/layout.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

PropTypeRequiredDefaultDescription
graceSecnumberoptional9Countdown in seconds. The modal renders one progress dot per second.
signInUrlstringoptional"/sign-in/console"Redirect target after the countdown finishes and the session is cleared.
signOutUrlstringoptional"/api/auth/sign-out"Same-origin POST that revokes the cookie. Ignored cross-origin (token is dropped locally instead).
cookieNamestringoptional"elvix_token"Cookie cleared alongside the in-memory token. Must match what your sign-in page wrote.
apiBasestringoptionalnoneSet when embedded cross-origin (your app on a different origin than elvix). Switches the transport from SSE to polling against POST /api/v1/session.
applicationIdstringprovider-resolvedfrom useElvixApp()App scope. Pass explicitly when mounting outside the provider tree.
userIdstringprovider-resolvedfrom sessionSame shape as applicationId.
pollMsnumberoptional7000Polling 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") => voidoptionalreload pageSkip the default reload and own the next navigation client-side.

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.

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
"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