Skip to main content

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.

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.
The refresh flow is protected by DPoP (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
Most apps don’t need to call refresh explicitly: protected requests auto-refresh expired access tokens transparently via the SDK’s interceptor.
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:
try await client.invalidateSession()
You can also read the cached token without forcing a refresh:
let profile = await client.profile          // PreludeProfile?
let token   = await client.accessToken      // String?
let expires = await client.accessTokenExpiresAt  // Date?

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.
try await client.logout()

List sessions

Retrieve all active sessions for the authenticated user. Each PreludeSessionView exposes id, deviceType, deviceModel, osVersion, countryCode, createdAt, lastSeenAt, and expiresAt.
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.
Replace ContentView.swift with:
ContentView.swift
import SwiftUI
import PreludeSession

private let appID = "YOUR_APP_ID"

@MainActor
final class SessionsModel: ObservableObject {
    let client = try! PreludeSessionClient(
        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() }
    }
}

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.
TargetDescription
allRevoke every session for this user, including the current one.
othersRevoke every session except the current one.
mineRevoke 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.
try await client.revokeSessions(.all)
try await client.revokeSessions(.others)
try await client.revokeSessions(.mine)
try await client.revokeSessions(.session(id: sessionID))
Building on the list-sessions example, add revocation controls:
ContentView.swift
import SwiftUI
import PreludeSession

private let appID = "YOUR_APP_ID"

@MainActor
final class RevokeModel: ObservableObject {
    let client = try! PreludeSessionClient(
        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() }
    }
}

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:
https://{app_id}.session.prelude.dev/.well-known/jwks.json
Use any standard JWT library to verify the token signature against these keys.