> ## 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 mobile SDKs.

## Refresh the access token

Access tokens are short-lived. Call `refresh` to obtain a new one without prompting the user. The SDK persists tokens in the platform's secure store and coalesces concurrent refresh callers into a single request, so the single-use refresh token is never used twice — even when several callers race on the same client.

<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 on the device — Secure Enclave on iOS, AndroidKeystore (StrongBox / TEE when available) on Android — and signs each refresh request with a proof that binds it to this client. This protects against:

  * **Token theft** — A stolen refresh token is unusable without the private key, which never leaves the device
  * **Token replay** — Each DPoP proof carries 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** — Platform-native keys are non-extractable; the keypair cannot be copied to another device
</Accordion>

Most apps don't need to call `refresh` explicitly: protected requests auto-refresh expired access tokens transparently via the SDK's interceptor.

<Tabs>
  <Tab title="iOS">
    ```swift theme={null}
    let user = try await client.refresh()
    // user.accessToken — fresh JWT
    // user.profile    — claims (userID, sessionID, extras)
    ```

    To force the next protected call to mint a new token (bypassing the local cache), invalidate it:

    ```swift theme={null}
    try await client.invalidateSession()
    ```

    You can also read the cached token without forcing a refresh:

    ```swift theme={null}
    let profile = await client.profile          // PreludeProfile?
    let token   = await client.accessToken      // String?
    let expires = await client.accessTokenExpiresAt  // Date?
    ```
  </Tab>

  <Tab title="Android">
    Every method on this page is a `suspend` function — call them from a coroutine, e.g. `viewModelScope.launch { ... }`.

    ```kotlin theme={null}
    val user = client.refresh()
    // user.accessToken — fresh JWT
    // user.profile    — claims (userId, sessionId, extras)
    ```

    To force the next protected call to mint a new token (bypassing the local cache), invalidate it:

    ```kotlin theme={null}
    client.invalidateCache()
    ```

    You can also read the cached token without forcing a refresh:

    ```kotlin theme={null}
    val profile   = client.getProfile()              // PreludeProfile?
    val token     = client.getAccessToken()          // String?
    val sessionId = client.getSessionId()            // String?
    val expires   = client.getAccessTokenExpiresAt() // java.time.Instant?
    ```
  </Tab>

  <Tab title="Flutter">
    The Dart call delegates to the native SDK on each platform.

    ```dart theme={null}
    final user = await client.refresh();
    // user.accessToken — fresh JWT
    // user.profile    — claims (userID, sessionID, extras)
    ```

    To force the next protected call to mint a new token (bypassing the local cache), invalidate it:

    ```dart theme={null}
    await client.invalidateSession();
    ```

    You can also read the cached token without forcing a refresh:

    ```dart theme={null}
    final profile   = await client.getProfile();              // PreludeProfile?
    final token     = await client.getAccessToken();          // String?
    final sessionID = await client.getSessionID();            // String?
    final expires   = await client.getAccessTokenExpiresAt(); // DateTime?
    ```
  </Tab>

  <Tab title="React Native">
    The JS call delegates to the native SDK on each platform.

    ```ts theme={null}
    const user = await client.refresh();
    // user.accessToken — fresh JWT
    // user.profile    — claims (userID, sessionID, extras)
    ```

    To force the next protected call to mint a new token (bypassing the local cache), invalidate it:

    ```ts theme={null}
    await client.invalidateSession();
    ```

    You can also read the cached token without forcing a refresh:

    ```ts theme={null}
    const profile   = await client.getProfile();              // PreludeProfile | null
    const token     = await client.getAccessToken();          // string | null
    const sessionID = await client.getSessionID();            // string | null
    const expires   = await client.getAccessTokenExpiresAt(); // Date | null
    ```
  </Tab>
</Tabs>

## Log out

Revoke the current session on the server and wipe the credentials this client owns. `logout` is idempotent and concurrent-safe: callers that race onto a single `logout` share the same in-flight task. Local state is wiped first, so even a failed server round-trip leaves the client locally signed out.

<Tabs>
  <Tab title="iOS">
    ```swift theme={null}
    try await client.logout()
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={null}
    client.logout()
    ```
  </Tab>

  <Tab title="Flutter">
    ```dart theme={null}
    await client.logout();
    ```

    When you're done with the client, call:

    ```dart theme={null}
    await client.dispose();
    ```

    `dispose` releases the native plugin's reference to this logical session. After it returns, every other method on the instance throws `StateError`.
  </Tab>

  <Tab title="React Native">
    ```ts theme={null}
    await client.logout();
    ```

    When you're done with the client, call:

    ```ts theme={null}
    await client.dispose();
    ```

    `dispose` releases the native plugin's reference to this logical session. After it returns, every other method on the instance throws `DisposedError`.
  </Tab>
</Tabs>

## List sessions

Retrieve all active sessions for the authenticated user. Each `PreludeSessionView` exposes `id`, `deviceType`, `deviceModel`, `osVersion`, `countryCode`, `createdAt`, `lastSeenAt`, and `expiresAt`.

<Tabs>
  <Tab title="iOS">
    ```swift theme={null}
    let page = try await client.listSessions(
        ListSessionsOptions(limit: 10, offset: 0)
    )

    for session in page.sessions {
        print(session.id, session.deviceModel, session.lastSeenAt)
    }
    ```

    Timestamps are kept as the server's ISO-8601 strings — pick your own `Date` parsing strategy.
  </Tab>

  <Tab title="Android">
    ```kotlin theme={null}
    val page = client.listSessions(
        PreludeListSessionsOptions(limit = 10, offset = 0),
    )

    for (session in page.sessions) {
        println("${session.id} ${session.deviceModel} ${session.lastSeenAt}")
    }
    ```

    `deviceType` is a `PreludeSessionDeviceType` enum — `UNKNOWN` is used for device types added in newer servers. Timestamps are returned as `java.time.Instant` values.
  </Tab>

  <Tab title="Flutter">
    ```dart theme={null}
    final page = await client.listSessions(
      PreludeListSessionsOptions(limit: 10, offset: 0),
    );

    for (final session in page.sessions) {
      print('${session.id} ${session.deviceModel} ${session.lastSeenAt}');
    }
    ```

    `deviceType` is a `PreludeDeviceType` enum — `unknown` is used for device types added in newer servers. Timestamps are returned as `DateTime` values in UTC.
  </Tab>

  <Tab title="React Native">
    ```ts theme={null}
    const page = await client.listSessions({ limit: 10, offset: 0 });

    for (const session of page.sessions) {
      console.log(session.id, session.deviceModel, session.lastSeenAt);
    }
    ```

    `deviceType` is a string union (`"desktop" | "mobile" | "tablet" | "unknown"`); the SDK folds unknown server values into `"unknown"`. Timestamps come back as ISO-8601 strings.
  </Tab>
</Tabs>

<Accordion title="Try it" icon="flask">
  <Tabs>
    <Tab title="iOS">
      Replace `ContentView.swift` with:

      ```swift ContentView.swift theme={null}
      import SwiftUI
      import PreludeAuth

      private let appID = "YOUR_APP_ID"

      @MainActor
      final class SessionsModel: ObservableObject {
          let client = try! PreludeAuthClient(
              endpoint: .custom("https://\(appID).session.prelude.dev")
          )

          @Published var sessions: [PreludeSessionView] = []
          @Published var loading = true
          @Published var loggedIn = false

          func load() async {
              defer { loading = false }
              do {
                  _ = try await client.refresh()
                  loggedIn = true
                  sessions = try await client.listSessions().sessions
              } catch {
                  loggedIn = false
              }
          }
      }

      struct ContentView: View {
          @StateObject private var model = SessionsModel()

          var body: some View {
              Group {
                  if model.loading {
                      ProgressView()
                  } else if !model.loggedIn {
                      Text("Please log in first.")
                  } else {
                      List(model.sessions, id: \.id) { s in
                          VStack(alignment: .leading, spacing: 2) {
                              Text(s.deviceModel.isEmpty ? s.deviceType.rawValue : s.deviceModel)
                                  .font(.headline)
                              Text("\(s.countryCode) · last seen \(s.lastSeenAt)")
                                  .font(.caption).foregroundStyle(.secondary)
                              Text(s.id).font(.caption2.monospaced()).foregroundStyle(.secondary)
                          }
                      }
                  }
              }
              .task { await model.load() }
          }
      }
      ```
    </Tab>

    <Tab title="Android">
      Replace `MainActivity.kt` with:

      ```kotlin MainActivity.kt theme={null}
      package com.example.sessiontest

      import android.os.Bundle
      import androidx.activity.ComponentActivity
      import androidx.activity.compose.setContent
      import androidx.compose.foundation.layout.*
      import androidx.compose.foundation.lazy.LazyColumn
      import androidx.compose.foundation.lazy.items
      import androidx.compose.material3.*
      import androidx.compose.runtime.*
      import androidx.compose.ui.Modifier
      import androidx.compose.ui.unit.dp
      import androidx.lifecycle.ViewModel
      import androidx.lifecycle.viewModelScope
      import androidx.lifecycle.viewmodel.compose.viewModel
      import kotlinx.coroutines.launch
      import so.prelude.android.auth.*
      import java.net.URL

      private const val APP_ID = "YOUR_APP_ID"

      class SessionsViewModel(ctx: android.content.Context) : ViewModel() {
          val client = PreludeAuthClient(
              context = ctx,
              baseUrl = URL("https://$APP_ID.session.prelude.dev"),
          )

          var sessions by mutableStateOf<List<PreludeSessionView>>(emptyList()); private set
          var loading by mutableStateOf(true); private set
          var loggedIn by mutableStateOf(false); private set

          init {
              viewModelScope.launch {
                  try {
                      client.refresh()
                      loggedIn = true
                      sessions = client.listSessions().sessions
                  } catch (_: PreludeAuthError) {
                      loggedIn = false
                  } finally {
                      loading = false
                  }
              }
          }
      }

      class MainActivity : ComponentActivity() {
          override fun onCreate(savedInstanceState: Bundle?) {
              super.onCreate(savedInstanceState)
              setContent {
                  MaterialTheme {
                      Surface {
                          val vm: SessionsViewModel = viewModel(factory = viewModelFactory(applicationContext))
                          Box(Modifier.fillMaxSize().padding(24.dp)) {
                              when {
                                  vm.loading -> CircularProgressIndicator()
                                  !vm.loggedIn -> Text("Please log in first.")
                                  else -> LazyColumn {
                                      items(vm.sessions, key = { it.id }) { s ->
                                          Column(Modifier.padding(vertical = 8.dp)) {
                                              Text(
                                                  s.deviceModel.ifEmpty { s.deviceType.wireValue },
                                                  style = MaterialTheme.typography.titleSmall,
                                              )
                                              Text("${s.countryCode} · last seen ${s.lastSeenAt}",
                                                  style = MaterialTheme.typography.bodySmall)
                                              Text(s.id, style = MaterialTheme.typography.bodySmall)
                                          }
                                      }
                                  }
                              }
                          }
                      }
                  }
              }
          }
      }
      ```

      (`viewModelFactory` is the small helper defined in [Introduction](/session/documentation/frontend-sdks/mobile/introduction#helpers).)
    </Tab>

    <Tab title="Flutter">
      Replace `lib/main.dart` with:

      ```dart lib/main.dart theme={null}
      import 'package:flutter/material.dart';
      import 'package:prelude_flutter_auth_sdk/prelude_flutter_auth_sdk.dart';

      const appID = 'YOUR_APP_ID';

      void main() => runApp(const MyApp());

      class MyApp extends StatefulWidget {
        const MyApp({super.key});
        @override
        State<MyApp> createState() => _MyAppState();
      }

      class _MyAppState extends State<MyApp> {
        late final client = PreludeAuthClient(
          endpoint: Endpoint.custom('https://$appID.session.prelude.dev'),
        );

        bool _loading = true;
        bool _loggedIn = false;
        List<PreludeSessionView> _sessions = const [];

        @override
        void initState() {
          super.initState();
          _load();
        }

        Future<void> _load() async {
          try {
            await client.refresh();
            final page = await client.listSessions();
            setState(() {
              _loggedIn = true;
              _sessions = page.sessions;
            });
          } on PreludeAuthException {
            setState(() => _loggedIn = false);
          } finally {
            setState(() => _loading = false);
          }
        }

        @override
        void dispose() {
          client.dispose();
          super.dispose();
        }

        @override
        Widget build(BuildContext context) {
          Widget body;
          if (_loading) {
            body = const Center(child: CircularProgressIndicator());
          } else if (!_loggedIn) {
            body = const Center(child: Text('Please log in first.'));
          } else {
            body = ListView.builder(
              itemCount: _sessions.length,
              itemBuilder: (_, i) {
                final s = _sessions[i];
                return ListTile(
                  title: Text(s.deviceModel.isEmpty
                      ? s.deviceType.wireValue
                      : s.deviceModel),
                  subtitle: Text('${s.countryCode} · last seen ${s.lastSeenAt}'),
                  dense: true,
                );
              },
            );
          }
          return MaterialApp(home: Scaffold(body: body));
        }
      }
      ```
    </Tab>

    <Tab title="React Native">
      Replace `app/index.tsx` with:

      ```tsx app/index.tsx theme={null}
      import {
        Endpoint,
        PreludeAuthClient,
        PreludeAuthError,
        PreludeSessionView,
      } from "@prelude.so/react-native-auth-sdk";
      import { useEffect, useRef, useState } from "react";
      import { FlatList, Text, View } from "react-native";

      const APP_ID = "YOUR_APP_ID";

      export default function Home() {
        const clientRef = useRef<PreludeAuthClient | null>(null);
        const [sessions, setSessions] = useState<PreludeSessionView[]>([]);
        const [loading, setLoading] = useState(true);
        const [loggedIn, setLoggedIn] = useState(false);

        useEffect(() => {
          const c = new PreludeAuthClient({
            endpoint: Endpoint.custom(`https://${APP_ID}.session.prelude.dev`),
          });
          clientRef.current = c;
          (async () => {
            try {
              await c.refresh();
              const page = await c.listSessions();
              setSessions(page.sessions);
              setLoggedIn(true);
            } catch {
              setLoggedIn(false);
            } finally {
              setLoading(false);
            }
          })();
          return () => { c.dispose().catch(() => {}); };
        }, []);

        if (loading) return <Text style={{ padding: 24 }}>Loading…</Text>;
        if (!loggedIn) return <Text style={{ padding: 24 }}>Please log in first.</Text>;

        return (
          <FlatList
            data={sessions}
            keyExtractor={(s) => s.id}
            renderItem={({ item: s }) => (
              <View style={{ padding: 12 }}>
                <Text style={{ fontWeight: "600" }}>
                  {s.deviceModel || s.deviceType}
                </Text>
                <Text>{s.countryCode} · last seen {s.lastSeenAt}</Text>
                <Text style={{ fontSize: 12 }}>{s.id}</Text>
              </View>
            )}
          />
        );
      }
      ```
    </Tab>
  </Tabs>
</Accordion>

## Revoke sessions

Revoke one or more sessions with `revokeSessions`. The revoke target carries which sessions to revoke in the type system — the per-session case requires an id.

| Target        | Description                                                    |
| ------------- | -------------------------------------------------------------- |
| `all`         | Revoke every session for this user, including the current one. |
| `others`      | Revoke every session except the current one.                   |
| `mine`        | Revoke the current session only.                               |
| `session(id)` | Revoke one specific session by id.                             |

When the call would terminate this client's own session (`all`, `mine`, or a `session(id)` matching the current session), the SDK also wipes the local credentials — the same wipe `logout()` performs — so a leaked refresh token can't bring the session back.

<Tabs>
  <Tab title="iOS">
    ```swift theme={null}
    try await client.revokeSessions(.all)
    try await client.revokeSessions(.others)
    try await client.revokeSessions(.mine)
    try await client.revokeSessions(.session(id: sessionID))
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={null}
    client.revokeSessions(PreludeRevokeTarget.All)
    client.revokeSessions(PreludeRevokeTarget.Others)
    client.revokeSessions(PreludeRevokeTarget.Mine)
    client.revokeSessions(PreludeRevokeTarget.Session(sessionId))
    ```
  </Tab>

  <Tab title="Flutter">
    ```dart theme={null}
    await client.revokeSessions(PreludeRevokeTarget.all);
    await client.revokeSessions(PreludeRevokeTarget.others);
    await client.revokeSessions(PreludeRevokeTarget.mine);
    await client.revokeSessions(PreludeRevokeTarget.session(sessionID));
    ```

    The `session` constructor rejects empty / whitespace strings up front.
  </Tab>

  <Tab title="React Native">
    ```ts theme={null}
    import { PreludeRevokeTarget } from "@prelude.so/react-native-auth-sdk";

    await client.revokeSessions(PreludeRevokeTarget.all);
    await client.revokeSessions(PreludeRevokeTarget.others);
    await client.revokeSessions(PreludeRevokeTarget.mine);
    await client.revokeSessions(PreludeRevokeTarget.session(sessionID));
    ```

    The `session(...)` factory rejects empty / whitespace strings up front and returns a frozen target.
  </Tab>
</Tabs>

<Accordion title="Try it" icon="flask">
  Building on the list-sessions example, add revocation controls:

  <Tabs>
    <Tab title="iOS">
      ```swift ContentView.swift theme={null}
      import SwiftUI
      import PreludeAuth

      private let appID = "YOUR_APP_ID"

      @MainActor
      final class RevokeModel: ObservableObject {
          let client = try! PreludeAuthClient(
              endpoint: .custom("https://\(appID).session.prelude.dev")
          )

          @Published var sessions: [PreludeSessionView] = []
          @Published var loggedIn = false
          @Published var error: String?

          func load() async {
              do {
                  _ = try await client.refresh()
                  loggedIn = true
                  sessions = try await client.listSessions().sessions
              } catch {
                  loggedIn = false
              }
          }

          func revoke(_ target: RevokeTarget) async {
              error = nil
              do {
                  try await client.revokeSessions(target)
                  switch target {
                  case .all, .mine:
                      loggedIn = false
                      sessions = []
                  default:
                      sessions = try await client.listSessions().sessions
                  }
              } catch {
                  self.error = error.localizedDescription
              }
          }
      }

      struct ContentView: View {
          @StateObject private var model = RevokeModel()

          var body: some View {
              VStack {
                  if let error = model.error {
                      Text(error).foregroundStyle(.red).font(.caption)
                  }
                  if !model.loggedIn {
                      Text("Please log in first.")
                  } else {
                      HStack {
                          Button("Others") { Task { await model.revoke(.others) } }
                          Button("Mine") { Task { await model.revoke(.mine) } }
                              .tint(.orange)
                          Button("All") { Task { await model.revoke(.all) } }
                              .tint(.red)
                      }
                      .buttonStyle(.bordered)
                      List(model.sessions, id: \.id) { s in
                          HStack {
                              VStack(alignment: .leading) {
                                  Text(s.deviceModel.isEmpty ? s.deviceType.rawValue : s.deviceModel)
                                  Text(s.id).font(.caption2.monospaced())
                                      .foregroundStyle(.secondary)
                              }
                              Spacer()
                              Button("Revoke") {
                                  Task { await model.revoke(.session(id: s.id)) }
                              }
                              .buttonStyle(.borderless)
                          }
                      }
                  }
              }
              .task { await model.load() }
          }
      }
      ```
    </Tab>

    <Tab title="Android">
      ```kotlin MainActivity.kt theme={null}
      package com.example.sessiontest

      import android.os.Bundle
      import androidx.activity.ComponentActivity
      import androidx.activity.compose.setContent
      import androidx.compose.foundation.layout.*
      import androidx.compose.foundation.lazy.LazyColumn
      import androidx.compose.foundation.lazy.items
      import androidx.compose.material3.*
      import androidx.compose.runtime.*
      import androidx.compose.ui.Modifier
      import androidx.compose.ui.unit.dp
      import androidx.lifecycle.ViewModel
      import androidx.lifecycle.viewModelScope
      import androidx.lifecycle.viewmodel.compose.viewModel
      import kotlinx.coroutines.launch
      import so.prelude.android.auth.*
      import java.net.URL

      private const val APP_ID = "YOUR_APP_ID"

      class RevokeViewModel(ctx: android.content.Context) : ViewModel() {
          val client = PreludeAuthClient(
              context = ctx,
              baseUrl = URL("https://$APP_ID.session.prelude.dev"),
          )

          var sessions by mutableStateOf<List<PreludeSessionView>>(emptyList()); private set
          var loggedIn by mutableStateOf(false); private set
          var error by mutableStateOf<String?>(null); private set

          init { viewModelScope.launch { load() } }

          private suspend fun load() {
              try {
                  client.refresh()
                  loggedIn = true
                  sessions = client.listSessions().sessions
              } catch (_: PreludeAuthError) {
                  loggedIn = false
              }
          }

          fun revoke(target: PreludeRevokeTarget) {
              error = null
              viewModelScope.launch {
                  try {
                      client.revokeSessions(target)
                      when (target) {
                          PreludeRevokeTarget.All, PreludeRevokeTarget.Mine -> {
                              loggedIn = false
                              sessions = emptyList()
                          }
                          else -> sessions = client.listSessions().sessions
                      }
                  } catch (e: PreludeAuthError) {
                      error = e.message
                  }
              }
          }
      }

      class MainActivity : ComponentActivity() {
          override fun onCreate(savedInstanceState: Bundle?) {
              super.onCreate(savedInstanceState)
              setContent {
                  MaterialTheme {
                      Surface {
                          val vm: RevokeViewModel = viewModel(factory = viewModelFactory(applicationContext))
                          Column(Modifier.fillMaxSize().padding(24.dp)) {
                              vm.error?.let {
                                  Text(it, color = MaterialTheme.colorScheme.error)
                                  Spacer(Modifier.height(8.dp))
                              }
                              if (!vm.loggedIn) {
                                  Text("Please log in first.")
                              } else {
                                  Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
                                      OutlinedButton(onClick = { vm.revoke(PreludeRevokeTarget.Others) }) { Text("Others") }
                                      OutlinedButton(onClick = { vm.revoke(PreludeRevokeTarget.Mine) }) { Text("Mine") }
                                      Button(onClick = { vm.revoke(PreludeRevokeTarget.All) }) { Text("All") }
                                  }
                                  Spacer(Modifier.height(16.dp))
                                  LazyColumn {
                                      items(vm.sessions, key = { it.id }) { s ->
                                          Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
                                              Column(Modifier.weight(1f)) {
                                                  Text(s.deviceModel.ifEmpty { s.deviceType.wireValue })
                                                  Text(s.id, style = MaterialTheme.typography.bodySmall)
                                              }
                                              TextButton(onClick = {
                                                  vm.revoke(PreludeRevokeTarget.Session(s.id))
                                              }) { Text("Revoke") }
                                          }
                                      }
                                  }
                              }
                          }
                      }
                  }
              }
          }
      }
      ```

      (`viewModelFactory` is the small helper defined in [Introduction](/session/documentation/frontend-sdks/mobile/introduction#helpers).)
    </Tab>

    <Tab title="Flutter">
      ```dart lib/main.dart theme={null}
      import 'package:flutter/material.dart';
      import 'package:prelude_flutter_auth_sdk/prelude_flutter_auth_sdk.dart';

      const appID = 'YOUR_APP_ID';

      void main() => runApp(const MyApp());

      class MyApp extends StatefulWidget {
        const MyApp({super.key});
        @override
        State<MyApp> createState() => _MyAppState();
      }

      class _MyAppState extends State<MyApp> {
        late final client = PreludeAuthClient(
          endpoint: Endpoint.custom('https://$appID.session.prelude.dev'),
        );

        bool _loggedIn = false;
        List<PreludeSessionView> _sessions = const [];
        String? _error;

        @override
        void initState() {
          super.initState();
          _load();
        }

        Future<void> _load() async {
          try {
            await client.refresh();
            final page = await client.listSessions();
            setState(() {
              _loggedIn = true;
              _sessions = page.sessions;
            });
          } on PreludeAuthException {
            setState(() => _loggedIn = false);
          }
        }

        Future<void> _revoke(PreludeRevokeTarget target) async {
          setState(() => _error = null);
          try {
            await client.revokeSessions(target);
            if (target == PreludeRevokeTarget.all ||
                target == PreludeRevokeTarget.mine) {
              setState(() {
                _loggedIn = false;
                _sessions = const [];
              });
            } else {
              final page = await client.listSessions();
              setState(() => _sessions = page.sessions);
            }
          } on PreludeAuthException catch (e) {
            setState(() => _error = e.message);
          }
        }

        @override
        void dispose() {
          client.dispose();
          super.dispose();
        }

        @override
        Widget build(BuildContext context) {
          return MaterialApp(
            home: Scaffold(
              body: !_loggedIn
                  ? const Center(child: Text('Please log in first.'))
                  : Column(children: [
                      if (_error != null)
                        Padding(
                          padding: const EdgeInsets.all(12),
                          child: Text(_error!,
                              style: const TextStyle(color: Colors.red)),
                        ),
                      Padding(
                        padding: const EdgeInsets.all(12),
                        child: Wrap(spacing: 8, children: [
                          OutlinedButton(
                            onPressed: () => _revoke(PreludeRevokeTarget.others),
                            child: const Text('Others'),
                          ),
                          OutlinedButton(
                            onPressed: () => _revoke(PreludeRevokeTarget.mine),
                            child: const Text('Mine'),
                          ),
                          FilledButton(
                            onPressed: () => _revoke(PreludeRevokeTarget.all),
                            child: const Text('All'),
                          ),
                        ]),
                      ),
                      Expanded(
                        child: ListView.builder(
                          itemCount: _sessions.length,
                          itemBuilder: (_, i) {
                            final s = _sessions[i];
                            return ListTile(
                              title: Text(s.deviceModel.isEmpty
                                  ? s.deviceType.wireValue
                                  : s.deviceModel),
                              subtitle: Text(s.id),
                              trailing: TextButton(
                                child: const Text('Revoke'),
                                onPressed: () => _revoke(
                                  PreludeRevokeTarget.session(s.id),
                                ),
                              ),
                            );
                          },
                        ),
                      ),
                    ]),
            ),
          );
        }
      }
      ```
    </Tab>

    <Tab title="React Native">
      Building on the list-sessions example, add revocation controls:

      ```tsx app/index.tsx theme={null}
      import {
        Endpoint,
        PreludeRevokeTarget,
        PreludeAuthClient,
        PreludeAuthError,
        PreludeSessionView,
      } from "@prelude.so/react-native-auth-sdk";
      import { useEffect, useRef, useState } from "react";
      import { Button, FlatList, Text, View } from "react-native";

      const APP_ID = "YOUR_APP_ID";

      export default function Home() {
        const clientRef = useRef<PreludeAuthClient | null>(null);
        const [sessions, setSessions] = useState<PreludeSessionView[]>([]);
        const [loggedIn, setLoggedIn] = useState(false);
        const [error, setError] = useState<string | null>(null);

        useEffect(() => {
          const c = new PreludeAuthClient({
            endpoint: Endpoint.custom(`https://${APP_ID}.session.prelude.dev`),
          });
          clientRef.current = c;
          (async () => {
            try {
              await c.refresh();
              const page = await c.listSessions();
              setSessions(page.sessions);
              setLoggedIn(true);
            } catch {
              setLoggedIn(false);
            }
          })();
          return () => { c.dispose().catch(() => {}); };
        }, []);

        async function revoke(target: PreludeRevokeTarget) {
          setError(null);
          try {
            await clientRef.current!.revokeSessions(target);
            if (target.kind === "all" || target.kind === "mine") {
              setLoggedIn(false);
              setSessions([]);
            } else {
              const page = await clientRef.current!.listSessions();
              setSessions(page.sessions);
            }
          } catch (e) {
            setError(e instanceof PreludeAuthError ? e.message : String(e));
          }
        }

        if (!loggedIn) return <Text style={{ padding: 24 }}>Please log in first.</Text>;

        return (
          <View style={{ flex: 1 }}>
            {error && <Text style={{ color: "red", padding: 12 }}>{error}</Text>}
            <View style={{ flexDirection: "row", gap: 8, padding: 12 }}>
              <Button title="Others" onPress={() => revoke(PreludeRevokeTarget.others)} />
              <Button title="Mine" onPress={() => revoke(PreludeRevokeTarget.mine)} />
              <Button title="All" onPress={() => revoke(PreludeRevokeTarget.all)} />
            </View>
            <FlatList
              data={sessions}
              keyExtractor={(s) => s.id}
              renderItem={({ item: s }) => (
                <View style={{ flexDirection: "row", padding: 12, alignItems: "center" }}>
                  <View style={{ flex: 1 }}>
                    <Text>{s.deviceModel || s.deviceType}</Text>
                    <Text style={{ fontSize: 12 }}>{s.id}</Text>
                  </View>
                  <Button title="Revoke" onPress={() => revoke(PreludeRevokeTarget.session(s.id))} />
                </View>
              )}
            />
          </View>
        );
      }
      ```
    </Tab>
  </Tabs>
</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.
