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.
Log in a user
Submit the user’s email and password. On success, the SDK caches the access token and attaches it to subsequent protected requests.- iOS
- Android
- Flutter
- React Native
import PreludeSession
do {
let user = try await client.loginWithPassword(
LoginWithPasswordOptions(
emailAddress: "user@example.com",
password: "SecureP@ssw0rd!"
)
)
// User is now authenticated.
} catch PreludeSessionError.unauthorized {
// Invalid credentials
} catch PreludeSessionError.invalidPassword {
// Password does not meet the policy
} catch PreludeSessionError.badRequest {
// Invalid email format
} catch PreludeSessionError.rateLimited {
// Too many login attempts
}
RedactedString internally — LoginWithPasswordOptions is safe to print or dump, and the SDK never persists the plaintext.import so.prelude.android.session.*
try {
val user = client.loginWithPassword(
LoginWithPasswordOptions(
identifier = "user@example.com",
password = "SecureP@ssw0rd!",
),
)
// User is now authenticated.
} catch (e: PreludeSessionError.Unauthorized) {
// Invalid credentials
} catch (e: PreludeSessionError.InvalidPassword) {
// Password does not meet the policy
} catch (e: PreludeSessionError.BadRequest) {
// Invalid email format
} catch (e: PreludeSessionError.RateLimited) {
// Too many login attempts
}
RedactedString internally — LoginWithPasswordOptions.toString() renders the value as <redacted>, and the SDK never persists the plaintext. (JVM strings can’t be wiped from memory, so this guards against accidental leaks rather than guaranteeing the password is never observable.)loginWithPassword is a suspend function — call it from a coroutine, e.g. viewModelScope.launch { ... }.import 'package:prelude_flutter_session_sdk/prelude_flutter_session_sdk.dart';
try {
final user = await client.loginWithPassword(
LoginWithPasswordOptions(
emailAddress: 'user@example.com',
password: 'SecureP@ssw0rd!',
),
);
// User is now authenticated.
} on UnauthorizedException {
// Invalid credentials
} on InvalidPasswordException {
// Password does not meet the policy
} on BadRequestException {
// Invalid email format
} on RateLimitedException {
// Too many login attempts
}
RedactedString internally — LoginWithPasswordOptions.toString() renders the value as <redacted>, and the SDK never persists the plaintext. (Dart strings can’t be wiped from memory, so this guards against accidental leaks rather than guaranteeing the password is never observable.)If the password is already wrapped further up the call stack, use the named LoginWithPasswordOptions.redacted constructor:LoginWithPasswordOptions.redacted(
emailAddress: 'user@example.com',
password: heldRedactedPassword,
);
import {
BadRequestError,
InvalidPasswordError,
RateLimitedError,
RedactedString,
UnauthorizedError,
} from "@prelude.so/react-native-session-sdk";
try {
const user = await client.loginWithPassword({
emailAddress: "user@example.com",
password: new RedactedString("SecureP@ssw0rd!"),
});
// User is now authenticated.
} catch (e) {
if (e instanceof UnauthorizedError) {
// Invalid credentials
} else if (e instanceof InvalidPasswordError) {
// Password does not meet the policy
} else if (e instanceof BadRequestError) {
// Invalid email format
} else if (e instanceof RateLimitedError) {
// Too many login attempts
} else {
throw e;
}
}
RedactedString — toString() and toJSON() render the value as <redacted>, so console.log and Hermes inspectors can’t accidentally leak it. The SDK unwraps the secret only at the bridge boundary; it’s never persisted in JS.Try it
Try it
- iOS
- Android
- Flutter
- React Native
Replace
ContentView.swift with:ContentView.swift
import SwiftUI
import PreludeSession
private let appID = "YOUR_APP_ID"
@MainActor
final class LoginModel: ObservableObject {
let client = try! PreludeSessionClient(
endpoint: .custom("https://\(appID).session.prelude.dev")
)
@Published var user: PreludeUser?
@Published var error: String?
func login(email: String, password: String) async {
error = nil
do {
user = try await client.loginWithPassword(
LoginWithPasswordOptions(emailAddress: email, password: password)
)
} catch PreludeSessionError.unauthorized {
error = "Invalid email or password."
} catch PreludeSessionError.invalidPassword {
error = "Password doesn't meet the requirements."
} catch PreludeSessionError.badRequest {
error = "Invalid email format."
} catch PreludeSessionError.rateLimited {
error = "Too many attempts. Please try again later."
} catch {
self.error = "Something went wrong. Please try again."
}
}
}
struct ContentView: View {
@StateObject private var model = LoginModel()
@State private var email = ""
@State private var password = ""
var body: some View {
VStack(spacing: 12) {
if let user = model.user {
Text("Logged in").font(.headline)
Text(user.profile.userID ?? "—").font(.caption.monospaced())
} else {
TextField("Email", text: $email)
.keyboardType(.emailAddress)
.textInputAutocapitalization(.never)
SecureField("Password", text: $password)
if let error = model.error {
Text(error).foregroundStyle(.red).font(.caption)
}
Button("Log In") {
Task { await model.login(email: email, password: password) }
}
.buttonStyle(.borderedProminent)
}
}
.textFieldStyle(.roundedBorder)
.padding()
}
}
Replace (
MainActivity.kt with:MainActivity.kt
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.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.launch
import so.prelude.android.session.*
import java.net.URL
private const val APP_ID = "YOUR_APP_ID"
class LoginViewModel(applicationContext: android.content.Context) : ViewModel() {
val client = PreludeSessionClient(
context = applicationContext,
baseUrl = URL("https://$APP_ID.session.prelude.dev"),
)
var user by mutableStateOf<PreludeUser?>(null)
private set
var error by mutableStateOf<String?>(null)
private set
fun login(email: String, password: String) {
error = null
viewModelScope.launch {
try {
user = client.loginWithPassword(
LoginWithPasswordOptions(identifier = email, password = password),
)
} catch (e: PreludeSessionError.Unauthorized) {
error = "Invalid email or password."
} catch (e: PreludeSessionError.InvalidPassword) {
error = "Password doesn't meet the requirements."
} catch (e: PreludeSessionError.BadRequest) {
error = "Invalid email format."
} catch (e: PreludeSessionError.RateLimited) {
error = "Too many attempts. Please try again later."
} catch (e: PreludeSessionError) {
error = "Something went wrong. Please try again."
}
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface {
val ctx = applicationContext
val vm: LoginViewModel = viewModel(factory = viewModelFactory(ctx))
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
Column(Modifier.fillMaxSize().padding(24.dp)) {
if (vm.user != null) {
Text("Logged in", style = MaterialTheme.typography.headlineSmall)
Text(vm.user?.profile?.userId ?: "—", fontSize = 12.sp)
} else {
OutlinedTextField(
value = email, onValueChange = { email = it },
label = { Text("Email") },
modifier = Modifier.fillMaxWidth(),
)
Spacer(Modifier.height(8.dp))
OutlinedTextField(
value = password, onValueChange = { password = it },
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth(),
)
vm.error?.let {
Spacer(Modifier.height(8.dp))
Text(it, color = MaterialTheme.colorScheme.error)
}
Spacer(Modifier.height(16.dp))
Button(onClick = { vm.login(email, password) }) {
Text("Log In")
}
}
}
}
}
}
}
}
viewModelFactory is the small helper defined in Introduction.)Replace
lib/main.dart with:lib/main.dart
import 'package:flutter/material.dart';
import 'package:prelude_flutter_session_sdk/prelude_flutter_session_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 = PreludeSessionClient(
endpoint: Endpoint.custom('https://$appID.session.prelude.dev'),
);
final _email = TextEditingController();
final _password = TextEditingController();
PreludeUser? _user;
String? _error;
Future<void> _login() async {
setState(() => _error = null);
try {
final user = await client.loginWithPassword(
LoginWithPasswordOptions(
emailAddress: _email.text,
password: _password.text,
),
);
setState(() => _user = user);
} on UnauthorizedException {
setState(() => _error = 'Invalid email or password.');
} on InvalidPasswordException {
setState(() => _error = 'Password doesn\'t meet the requirements.');
} on BadRequestException {
setState(() => _error = 'Invalid email format.');
} on RateLimitedException {
setState(() => _error = 'Too many attempts. Please try again later.');
} on PreludeSessionException {
setState(() => _error = 'Something went wrong. Please try again.');
}
}
@override
void dispose() {
_email.dispose();
_password.dispose();
client.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Padding(
padding: const EdgeInsets.all(24),
child: Center(
child: _user != null
? Column(mainAxisSize: MainAxisSize.min, children: [
const Text('Logged in', style: TextStyle(fontSize: 20)),
const SizedBox(height: 8),
Text(_user!.profile.userID ?? '—',
style: const TextStyle(fontFamily: 'monospace')),
])
: Column(mainAxisSize: MainAxisSize.min, children: [
TextField(
controller: _email,
decoration: const InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 8),
TextField(
controller: _password,
obscureText: true,
decoration: const InputDecoration(labelText: 'Password'),
),
if (_error != null) ...[
const SizedBox(height: 8),
Text(_error!, style: const TextStyle(color: Colors.red)),
],
const SizedBox(height: 16),
FilledButton(onPressed: _login, child: const Text('Log In')),
]),
),
),
),
);
}
}
Replace
app/index.tsx with:app/index.tsx
import {
BadRequestError,
Endpoint,
InvalidPasswordError,
PreludeSessionClient,
PreludeUser,
RateLimitedError,
RedactedString,
UnauthorizedError,
} from "@prelude.so/react-native-session-sdk";
import { useEffect, useRef, useState } from "react";
import { Button, Text, TextInput, View } from "react-native";
const APP_ID = "YOUR_APP_ID";
export default function Home() {
const clientRef = useRef<PreludeSessionClient | null>(null);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [user, setUser] = useState<PreludeUser | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const c = new PreludeSessionClient({
endpoint: Endpoint.custom(`https://${APP_ID}.session.prelude.dev`),
});
clientRef.current = c;
return () => { c.dispose().catch(() => {}); };
}, []);
async function logIn() {
setError(null);
const client = clientRef.current;
if (!client) return;
try {
const u = await client.loginWithPassword({
emailAddress: email,
password: new RedactedString(password),
});
setUser(u);
} catch (e) {
if (e instanceof UnauthorizedError) setError("Invalid email or password.");
else if (e instanceof InvalidPasswordError) setError("Password doesn't meet the requirements.");
else if (e instanceof BadRequestError) setError("Invalid email format.");
else if (e instanceof RateLimitedError) setError("Too many attempts. Please try again later.");
else setError("Something went wrong. Please try again.");
}
}
if (user) {
return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<Text style={{ fontWeight: "600" }}>Logged in</Text>
<Text>{user.profile.userID ?? "—"}</Text>
</View>
);
}
return (
<View style={{ padding: 24, gap: 8 }}>
<TextInput
placeholder="Email"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
style={{ borderWidth: 1, padding: 8 }}
/>
<TextInput
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
style={{ borderWidth: 1, padding: 8 }}
/>
{error && <Text style={{ color: "red" }}>{error}</Text>}
<Button title="Log In" onPress={logIn} />
</View>
);
}
Validate a password (optional)
Your application can require passwords to meet certain rules — for example, a minimum length and a mix of uppercase, lowercase, number, or symbol characters. These rules are configured per application and enforced on both the client and the server. Before submitting a sign-up form, validate the password against the configured rules. The SDK fetches the rules from the server and checks the password locally:- iOS
- Android
- Flutter
- React Native
let results = try await client.validatePassword("SecureP@ssw0rd!")
if !results.valid {
for result in results.results where !result.valid {
print("\(result.criterion): expected \(result.expected), got \(result.actual)")
}
}
let compliancy = try await client.passwordCompliancy()
let results = PreludeSessionClient.validate(
password: "SecureP@ssw0rd!",
against: compliancy
)
generalCategory and counts code points, so passwords containing emoji or combining sequences classify consistently.val results = client.validatePassword("SecureP@ssw0rd!")
if (!results.valid) {
results.results
.filter { !it.valid }
.forEach { r ->
println("${r.criterion.wireValue}: expected ${r.expected}, got ${r.actual}")
}
}
PreludePasswordCompliancy.validate is a pure function with no I/O:val policy = client.getPasswordCompliancy()
val results = policy.validate("SecureP@ssw0rd!")
Character.getType and counts code points, so non-ASCII letters and combining sequences classify consistently with the server.final results = await client.validatePassword('SecureP@ssw0rd!');
if (!results.valid) {
for (final r in results.results.where((r) => !r.valid)) {
print('${r.criterion}: expected ${r.expected}, got ${r.actual}');
}
}
PreludeSessionClient.validate is a pure static function with no I/O:final compliancy = await client.passwordCompliancy();
final results = PreludeSessionClient.validate(
password: 'SecureP@ssw0rd!',
against: compliancy,
);
length, so emoji and astral-plane characters classify consistently with the server.const results = await client.validatePassword("SecureP@ssw0rd!");
if (!results.valid) {
for (const r of results.results.filter((r) => !r.valid)) {
console.log(`${r.criterion}: expected ${r.expected}, got ${r.actual}`);
}
}
PreludeSessionClient.validate is a pure static function with no I/O:const compliancy = await client.passwordCompliancy();
const results = PreludeSessionClient.validate("SecureP@ssw0rd!", compliancy);
Try it
Try it
- iOS
- Android
- Flutter
- React Native
Replace
ContentView.swift with:ContentView.swift
import SwiftUI
import PreludeSession
private let appID = "YOUR_APP_ID"
@MainActor
final class ValidatorModel: ObservableObject {
let client = try! PreludeSessionClient(
endpoint: .custom("https://\(appID).session.prelude.dev")
)
@Published var results: PreludePasswordCompliancyResults?
func validate(_ password: String) async {
guard !password.isEmpty else { results = nil; return }
results = try? await client.validatePassword(password)
}
}
struct ContentView: View {
@StateObject private var model = ValidatorModel()
@State private var password = ""
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Password Validator").font(.title3)
SecureField("Type a password…", text: $password)
.textFieldStyle(.roundedBorder)
.onChange(of: password) { _, new in
Task { await model.validate(new) }
}
if let results = model.results {
ForEach(results.results, id: \.criterion) { r in
HStack {
Image(systemName: r.valid ? "checkmark.circle.fill" : "xmark.circle.fill")
.foregroundStyle(r.valid ? .green : .red)
Text("\(r.criterion.rawValue): \(r.actual)/\(r.expected)")
.font(.callout.monospaced())
}
}
Text(results.valid ? "Password is valid" : "Password does not meet requirements")
.font(.headline)
.foregroundStyle(results.valid ? .green : .red)
}
}
.padding()
}
}
Replace (
MainActivity.kt with a Compose validator that re-runs on every keystroke against a single cached policy:MainActivity.kt
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.material.icons.Icons
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.PasswordVisualTransformation
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.session.*
import java.net.URL
private const val APP_ID = "YOUR_APP_ID"
class ValidatorViewModel(ctx: android.content.Context) : ViewModel() {
val client = PreludeSessionClient(
context = ctx,
baseUrl = URL("https://$APP_ID.session.prelude.dev"),
)
var policy by mutableStateOf<PreludePasswordCompliancy?>(null)
private set
var results by mutableStateOf<PreludePasswordCompliancyResults?>(null)
private set
init {
viewModelScope.launch {
policy = runCatching { client.getPasswordCompliancy() }.getOrNull()
}
}
fun onChange(password: String) {
results = if (password.isEmpty()) null else policy?.validate(password)
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface {
val vm: ValidatorViewModel = viewModel(factory = viewModelFactory(applicationContext))
var password by remember { mutableStateOf("") }
Column(Modifier.fillMaxSize().padding(24.dp)) {
Text("Password Validator", style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(8.dp))
OutlinedTextField(
value = password,
onValueChange = { password = it; vm.onChange(it) },
label = { Text("Type a password…") },
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth(),
)
Spacer(Modifier.height(12.dp))
vm.results?.let { r ->
r.results.forEach { result ->
Row {
Icon(
if (result.valid) Icons.Filled.CheckCircle else Icons.Filled.Cancel,
contentDescription = null,
tint = if (result.valid) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error,
)
Spacer(Modifier.width(8.dp))
Text("${result.criterion}: ${result.actual}/${result.expected}")
}
}
Spacer(Modifier.height(8.dp))
Text(
if (r.valid) "Password is valid"
else "Password does not meet requirements",
style = MaterialTheme.typography.titleSmall,
color = if (r.valid) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error,
)
}
}
}
}
}
}
}
viewModelFactory is the small helper defined in Introduction.)Replace
lib/main.dart with a live validator that re-runs against a single cached policy:lib/main.dart
import 'package:flutter/material.dart';
import 'package:prelude_flutter_session_sdk/prelude_flutter_session_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 = PreludeSessionClient(
endpoint: Endpoint.custom('https://$appID.session.prelude.dev'),
);
PreludePasswordCompliancy? _policy;
PreludePasswordCompliancyResults? _results;
final _password = TextEditingController();
@override
void initState() {
super.initState();
client.passwordCompliancy().then((p) => setState(() => _policy = p));
}
void _onChanged(String value) {
final policy = _policy;
if (policy == null) return;
setState(() {
_results = value.isEmpty
? null
: PreludeSessionClient.validate(password: value, against: policy);
});
}
@override
void dispose() {
_password.dispose();
client.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final results = _results;
return MaterialApp(
home: Scaffold(
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Text('Password Validator',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
TextField(
controller: _password,
obscureText: true,
onChanged: _onChanged,
decoration: const InputDecoration(labelText: 'Type a password…'),
),
const SizedBox(height: 16),
if (results != null) ...[
for (final r in results.results)
Row(children: [
Icon(
r.valid ? Icons.check_circle : Icons.cancel,
color: r.valid ? Colors.green : Colors.red,
size: 18,
),
const SizedBox(width: 8),
Text('${r.criterion.name}: ${r.actual}/${r.expected}'),
]),
const SizedBox(height: 8),
Text(
results.valid
? 'Password is valid'
: 'Password does not meet requirements',
style: TextStyle(
fontWeight: FontWeight.w600,
color: results.valid ? Colors.green : Colors.red,
),
),
],
]),
),
),
);
}
}
Replace
app/index.tsx with a live validator backed by a cached compliancy policy:app/index.tsx
import {
Endpoint,
PreludePasswordCompliancy,
PreludePasswordCompliancyResults,
PreludeSessionClient,
} from "@prelude.so/react-native-session-sdk";
import { useEffect, useRef, useState } from "react";
import { Text, TextInput, View } from "react-native";
const APP_ID = "YOUR_APP_ID";
export default function Home() {
const clientRef = useRef<PreludeSessionClient | null>(null);
const [policy, setPolicy] = useState<PreludePasswordCompliancy | null>(null);
const [results, setResults] = useState<PreludePasswordCompliancyResults | null>(null);
const [password, setPassword] = useState("");
useEffect(() => {
const c = new PreludeSessionClient({
endpoint: Endpoint.custom(`https://${APP_ID}.session.prelude.dev`),
});
clientRef.current = c;
c.passwordCompliancy().then(setPolicy).catch(() => {});
return () => { c.dispose().catch(() => {}); };
}, []);
function onChange(value: string) {
setPassword(value);
if (!policy || value.length === 0) {
setResults(null);
return;
}
setResults(PreludeSessionClient.validate(value, policy));
}
return (
<View style={{ padding: 24, gap: 8 }}>
<Text style={{ fontSize: 18, fontWeight: "600" }}>Password Validator</Text>
<TextInput
placeholder="Type a password…"
value={password}
onChangeText={onChange}
secureTextEntry
style={{ borderWidth: 1, padding: 8 }}
/>
{results?.results.map((r) => (
<Text key={r.criterion} style={{ color: r.valid ? "green" : "red" }}>
{r.criterion}: {r.actual}/{r.expected}
</Text>
))}
{results && (
<Text style={{ fontWeight: "600", color: results.valid ? "green" : "red" }}>
{results.valid ? "Password is valid" : "Password does not meet requirements"}
</Text>
)}
</View>
);
}