BLAuthClient stored tokens as '{productId}_access_token' but all app
wrappers use KeychainHelper.read(key: "access_token") — the bare key.
This caused a critical mismatch: after login, BlobService/LicenseService
could not find the token, and token migration from UserDefaults was invisible
to BLAuthClient.isAuthenticated.
The Keychain service name (bundleId) already namespaces per product,
making the productId prefix redundant. Now uses bare 'access_token' and
'refresh_token' keys matching existing app conventions.
|
||
|---|---|---|
| .. | ||
| Sources | ||
| Tests | ||
| Package.swift | ||
| README.md | ||
ByteLystPlatformSDK
Shared Swift platform client for all ByteLyst iOS/watchOS/macOS apps. Eliminates code duplication across products by providing a single source of truth for platform-service integration.
What's Inside
| File | What It Does |
|---|---|
BLPlatformConfig |
Product-specific configuration (productId, baseURL, bundleId, appGroupId) |
BLPlatformClient |
Generic HTTP client with auth injection, x-request-id, timeout |
BLKeychain |
Keychain CRUD for secure token storage |
BLTelemetryClient |
Telemetry event queue + batch flush (matches @bytelyst/telemetry-client) |
BLAuthClient |
Auth operations: login, register, refresh, password ops, email verify, delete account (matches @bytelyst/auth-client) |
BLFeatureFlagClient |
Feature flag polling from platform-service /flags/poll |
BLSyncEngine |
Generic offline-first sync engine with delta pull + batch push |
BLBlobClient |
Azure Blob Storage upload via SAS tokens from platform-service |
BLKillSwitchClient |
Kill switch check from platform-service (fail-open) |
BLLicenseClient |
License key activation + status via platform-service |
BLBiometricAuth |
Face ID / Touch ID wrapper (LocalAuthentication) |
BLCrashReporter |
MetricKit crash and hang reporting with local storage |
BLAuditLogger |
Local rotating JSON audit log for debugging |
Usage
1. Add to Xcode Project
In Xcode: File → Add Package Dependencies → Add Local... → select this directory:
../learning_ai_common_plat/packages/swift-platform-sdk/
Or in Package.swift:
.package(path: "../learning_ai_common_plat/packages/swift-platform-sdk")
2. Configure at App Launch
import ByteLystPlatformSDK
// Create config — one per app
let config = BLPlatformConfig.fromInfoPlist(
productId: "peakpulse",
defaultBaseURL: "https://api.peakpulse.app",
bundleId: "com.saravana.peakpulse"
)
// Create shared HTTP client
let client = BLPlatformClient(config: config)
// Create services
let telemetry = BLTelemetryClient(config: config, client: client)
let auth = BLAuthClient(config: config, client: client)
let flags = BLFeatureFlagClient(config: config, client: client)
3. Telemetry
telemetry.start()
telemetry.trackEvent("info", module: "session", name: "session_started", metrics: ["elevation": 2450.0])
telemetry.trackScreen("live_tracking")
// On app background:
telemetry.stop()
4. Auth
// Login
let user = try await auth.login(email: "user@example.com", password: "secret")
// Restore on launch
await auth.restoreSession()
// Listen for state changes
auth.onAuthStateChanged = { state in
switch state {
case .loggedIn(let user): print("Hello, \(user.displayName)")
case .loggedOut: print("Signed out")
case .error(let msg): print("Auth error: \(msg)")
case .loading: print("Loading...")
}
}
5. Feature Flags
flags.start(userId: auth.accessToken != nil ? "user-id" : nil)
if flags.isEnabled("peakpulse.pro_charts") {
// Show Pro charts
}
6. Sync Engine
// Implement your product-specific adapter
struct PeakSessionSyncAdapter: BLSyncAdapter {
typealias SyncItem = PeakSessionDTO
func pullDelta(since: Date?, client: BLPlatformClient) async throws -> [PeakSessionDTO] {
var path = "/api/peak-sessions/sync"
if let since { path += "?since=\(ISO8601DateFormatter().string(from: since))" }
return try await client.request(path: path, responseType: [PeakSessionDTO].self)
}
func pushBatch(_ items: [BLOfflineQueueItem], client: BLPlatformClient) async throws -> BLBatchResult {
let body = try JSONEncoder().encode(["items": items.compactMap(\.payload)])
let (data, _) = try await client.rawRequest(path: "/api/peak-sessions/batch", method: "POST", body: body)
return try JSONDecoder().decode(BLBatchResult.self, from: data)
}
}
let syncEngine = BLSyncEngine(
config: config,
client: client,
adapter: PeakSessionSyncAdapter()
)
Product Apps Using This SDK
| Product | Repo | Wrappers | Status |
|---|---|---|---|
| ChronoMind | learning_ai_clock |
5 files | ✅ Migrated (Cloud/ + Diagnostics/) |
| LysnrAI | learning_voice_ai_agent |
9 files | ✅ Migrated (Auth/ + Util/) |
| MindLyst | learning_multimodal_memory_agents |
4 files | ✅ Migrated (Services/) |
| PeakPulse | learning_ai_peakpulse |
— | New — will use SDK from day one |
| NomGap | learning_ai_fastgap |
— | React Native — uses TS packages |
What This Replaces
Before this SDK, each iOS app had its own copy of platform integration code:
| ChronoMind (old) | LysnrAI (old) | MindLyst (old) | SDK (new) |
|---|---|---|---|
KeychainHelper (53 lines) |
KeychainHelper (60 lines) |
KeychainHelper (60 lines) |
BLKeychain |
CMTelemetryService (139 lines) |
TelemetryService (288 lines) |
TelemetryService (139 lines) |
BLTelemetryClient |
CMAuthService (359 lines) |
AuthService (421 lines) |
AuthService (389 lines) |
BLAuthClient |
FeatureFlagService (72 lines) |
FeatureFlagService (71 lines) |
FeatureFlagService (72 lines) |
BLFeatureFlagClient |
CrashReporter (153 lines) |
— | — | BLCrashReporter |
| — | BlobService (118 lines) |
— | BLBlobClient |
| — | KillSwitchService (48 lines) |
— | BLKillSwitchClient |
| — | LicenseService (135 lines) |
— | BLLicenseClient |
| — | BiometricAuth (65 lines) |
— | BLBiometricAuth |
| — | AuditLogger (70 lines) |
— | BLAuditLogger |
PlatformSyncManager (450 lines) |
Various sync files | — | BLSyncEngine |
Total duplicated code eliminated: ~2,600+ lines across 3 product apps.
Design Decisions
- No
@MainActor— the SDK is thread-safe via NSLock. Product apps can wrap in@MainActorat the view model layer. - No singletons — product apps own the lifecycle. Create instances at app launch, inject where needed.
- No SwiftUI dependency — pure Foundation. Works in watchOS, macOS, widgets, extensions.
- Protocol-based sync —
BLSyncAdapterlets each product define its own DTOs and endpoints while reusing the queue/timer/conflict plumbing. - Fire-and-forget telemetry — errors never surface to the user. Matches the TypeScript package behavior.
Platforms
- iOS 17+
- watchOS 10+
- macOS 14+