> ## Documentation Index
> Fetch the complete documentation index at: https://docs.prelude.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Session Management

> Manage access tokens, refresh sessions, and handle logout with the Prelude JavaScript SDK.

## Refresh the access token

Access tokens are short-lived. Use the `refresh` method to obtain a new one without requiring the user to log in again. The SDK handles caching and prevents concurrent refresh calls across browser tabs automatically.

<Accordion title="How is the refresh flow secured?" icon="shield">
  The refresh flow is protected by [DPoP](https://datatracker.ietf.org/doc/html/rfc9449) (Demonstration of Proof-of-Possession). The SDK generates a cryptographic key pair and signs each refresh request with a proof that binds the request to the client. This protects against:

  * **Token theft** — A stolen refresh cookie is unusable without the private key, which is bound to the browser and never transmitted
  * **Token replay** — Each DPoP proof includes a unique identifier and timestamp, preventing reuse
  * **Man-in-the-middle attacks** — The proof binds to the HTTP method and URL, so it cannot be replayed against a different endpoint
  * **Token export** — The key pair is non-extractable, meaning it cannot be copied from the browser to another device
</Accordion>

```javascript theme={null}
const { user } = await client.refresh();
// user.accessToken contains the new JWT
// user.profile contains the user profile data
```

To force the next `refresh` call to hit the backend (bypassing the local cache), invalidate the cache first:

```javascript theme={null}
await client.invalidateCache();
```

<Accordion title="Try it" icon="flask">
  In the login `App.jsx` above, add `decode` to your import (`import { ..., decode } from "@prelude.so/js-sdk"`) and replace the `article` block with:

  ```jsx theme={null}
  <article>
    <p>Logged in</p>
    <pre style={{ textAlign: "left", whiteSpace: "pre-wrap", wordBreak: "break-all" }}>
      {JSON.stringify(decode(user.accessToken).claims, null, 2)}
    </pre>
    <button onClick={async () => {
      await client.invalidateCache();
      const { user: refreshed } = await client.refresh();
      setUser(refreshed);
    }}>Refresh Token</button>
  </article>
  ```

  Each click fetches a new access token and updates the decoded token content.
</Accordion>

## Log out

When a user logs out, call `logout` to revoke the session and clear the local cache:

```javascript theme={null}
await client.logout();
```

<Accordion title="Try it" icon="flask">
  Add a logout button next to the refresh button in the authenticated view:

  ```jsx theme={null}
  <button className="secondary" onClick={async () => {
    await client.logout();
    setUser(null);
  }}>Log Out</button>
  ```

  The user is redirected back to the login form after logout.
</Accordion>

## List sessions

Retrieve all active sessions for the authenticated user:

```javascript theme={null}
const { sessions, total } = await client.listSessions({
  limit: 10,
  offset: 0,
});
```

Each session contains `id`, `device_type`, `device_model`, `os_version`, `country_code`, `created_at`, `last_seen_at`, and `expires_at`.

<Accordion title="Try it" icon="flask">
  Replace `src/App.jsx` with:

  ```jsx src/App.jsx theme={null}
  import "@picocss/pico";
  import { useState, useEffect } from "react";
  import { PrldSessionClient } from "@prelude.so/js-sdk";

  const client = new PrldSessionClient({ domain: `${import.meta.env.VITE_APP_ID}.session.prelude.dev` });

  export default function App() {
    const [loggedIn, setLoggedIn] = useState(false);
    const [sessions, setSessions] = useState([]);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
      client.refresh()
        .then(() => { setLoggedIn(true); return client.listSessions(); })
        .then(({ sessions }) => setSessions(sessions))
        .catch(() => {})
        .finally(() => setLoading(false));
    }, []);

    if (loading) return <p aria-busy="true">Loading...</p>;
    if (!loggedIn) return <p>Please log in first using one of the login examples.</p>;

    return (
      <>
        <style>{`body, #root { display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; padding: 0; }`}</style>
        <main style={{ maxWidth: "800px", width: "100%" }}>
          <h1>Sessions</h1>
          <table>
            <thead>
              <tr>
                <th>Session ID</th>
                <th>Device</th>
                <th>Country</th>
                <th>Last Seen</th>
              </tr>
            </thead>
            <tbody>
              {sessions.map((s) => (
                <tr key={s.id}>
                  <td><code>{s.id}</code></td>
                  <td>{s.device_model || s.device_type || "-"}</td>
                  <td>{s.country_code || "-"}</td>
                  <td>{new Date(s.last_seen_at).toLocaleString()}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </main>
      </>
    );
  }
  ```
</Accordion>

## Revoke sessions

Use `revokeSessions` to revoke sessions. The `target` parameter controls which sessions are revoked:

| Target      | Description                                     |
| ----------- | ----------------------------------------------- |
| `"all"`     | Revoke all sessions, including the current one. |
| `"others"`  | Revoke all sessions except the current one.     |
| `"mine"`    | Revoke the current session only.                |
| `"session"` | Revoke a specific session by ID.                |

```javascript theme={null}
// Revoke all sessions
await client.revokeSessions("all");

// Revoke all other sessions
await client.revokeSessions("others");

// Revoke the current session
await client.revokeSessions("mine");

// Revoke a specific session by ID
await client.revokeSessions("session", sessionId);
```

<Accordion title="Try it" icon="flask">
  Building on the list sessions example above, add revocation controls. Replace `src/App.jsx` with:

  ```jsx src/App.jsx theme={null}
  import "@picocss/pico";
  import { useState, useEffect } from "react";
  import { PrldSessionClient } from "@prelude.so/js-sdk";

  const client = new PrldSessionClient({ domain: `${import.meta.env.VITE_APP_ID}.session.prelude.dev` });

  export default function App() {
    const [loggedIn, setLoggedIn] = useState(false);
    const [sessions, setSessions] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    const fetchSessions = async () => {
      const { sessions } = await client.listSessions();
      setSessions(sessions);
    };

    useEffect(() => {
      client.refresh()
        .then(() => { setLoggedIn(true); return fetchSessions(); })
        .catch(() => {})
        .finally(() => setLoading(false));
    }, []);

    const handleRevoke = async (target, sessionId) => {
      setError(null);
      try {
        await client.revokeSessions(target, sessionId);
        if (target === "all" || target === "mine") {
          setLoggedIn(false);
          setSessions([]);
        } else {
          await fetchSessions();
        }
      } catch (err) {
        setError(err.message || "Failed to revoke session.");
      }
    };

    if (loading) return <p aria-busy="true">Loading...</p>;
    if (!loggedIn) return <p>Please log in first using one of the login examples.</p>;

    return (
      <>
        <style>{`body, #root { display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; padding: 0; }`}</style>
        <main style={{ maxWidth: "800px", width: "100%" }}>
          <h1>Session Revocation</h1>
          {error && <p role="alert" style={{ color: "var(--pico-color-red-500)" }}>{error}</p>}
          <div style={{ display: "flex", gap: "0.5rem", marginBottom: "1rem" }}>
            <button onClick={() => handleRevoke("others")}>Revoke Others</button>
            <button className="secondary" onClick={() => handleRevoke("mine")}>Revoke Mine</button>
            <button className="contrast" onClick={() => handleRevoke("all")}>Revoke All</button>
          </div>
          <table>
            <thead>
              <tr>
                <th>Session ID</th>
                <th>Device</th>
                <th>Country</th>
                <th>Last Seen</th>
                <th>Actions</th>
              </tr>
            </thead>
            <tbody>
              {sessions.map((s) => (
                <tr key={s.id}>
                  <td><code>{s.id}</code></td>
                  <td>{s.device_model || s.device_type || "-"}</td>
                  <td>{s.country_code || "-"}</td>
                  <td>{new Date(s.last_seen_at).toLocaleString()}</td>
                  <td>
                    <button className="outline" onClick={() => handleRevoke("session", s.id)}>
                      Revoke
                    </button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </main>
      </>
    );
  }
  ```
</Accordion>

## Verify access tokens on your backend

Your backend should verify the JWT access token on each authenticated request. Retrieve the public keys from your application's [JWKS endpoint](/session/documentation/jwks):

```
https://{app_id}.session.prelude.dev/.well-known/jwks.json
```

Use any standard JWT library to verify the token signature against these keys.
