Swift SDK vNext
Modular Swift SDK mirroring the new web architecture
Configure tenant once via env (not forms)
Get your tenant ID from the dashboard and configure it in your app. Do not include workspaceTenantId in user forms.
Package Dependency: Convex Swift Fork
EntityKit depends on github.com/entityauth/convex-swift (a fork). If you're already using the official github.com/get-convex/convex-swift, remove it first to avoid package identity conflicts. Replace with https://github.com/entityauth/convex-swift.git in your dependencies.
Import EntityKit and create a facade instance that you manage (typically via a view model).
import EntityKit
let facade = EntityAuthFacade(
config: EntityAuthConfig(
environment: .production,
workspaceTenantId: "tenant_123",
clientIdentifier: "ios-app"
)
)
Architecture overview
EntityAuthCore— configuration, token/keychain storage, unifiedEntityAuthError.EntityAuthNetworking— composableAPIClientwith automatic refresh + typed requests.EntityAuthDomain— DTOs, services, and the high-levelEntityAuthFacade.EntityAuthRealtime— Convex-backed realtime coordinator powering live snapshots.
This mirrors the TypeScript SDK: networking + domain logic are decoupled from consumption, and realtime flows share a single coordinator.
Facade API (EntityAuthFacade)
let snapshot = await facade.currentSnapshot()
print(snapshot.userId)
let stream = await facade.snapshotStream()
for await value in stream {
print("active org", value.activeOrganization?.orgId ?? "none")
}
Auth
register(request:)login(request:)refreshTokens()logout()
do {
try await facade.login(request: LoginRequest(
email: "user@example.com",
password: "secure-password",
workspaceTenantId: "tenant_123"
))
} catch {
// handle error
}
Default workspace role on registration
Assign a workspace-level role during registration by providing defaultWorkspaceRole:
try await facade.register(request: RegisterRequest(
email: "first@acme.com",
password: "super-secure",
workspaceTenantId: "tenant_123",
defaultWorkspaceRole: .owner
))If omitted, the user will not be granted workspace owner/admin; organization creation will be forbidden by policy.
Organizations
let orgs = try await facade.organizations()
let active = try await facade.activeOrganization()
try await facade.createOrganization(name: "Acme", slug: "acme", ownerId: userId)
try await facade.switchOrg(orgId: "org_123")
Entities (universal)
let entities = EntitiesService(client: facade.currentDependencies.apiClient)
let list = try await entities.list(
workspaceTenantId: "YOUR_TENANT_ID",
kind: "user",
filter: ListEntitiesFilter(status: "active"),
limit: 25
)
let upserted = try await entities.upsert(
workspaceTenantId: "YOUR_TENANT_ID",
kind: "user",
properties: ["email": "a@b.com"],
metadata: nil
)
Users
Use the facade helpers for the most common flows:
try await facade.setUsername("alice")
let available = try await facade.checkUsername("alice")
Sessions
let current = try await facade.sessionService.current()
let sessions = try await facade.sessionService.list(includeRevoked: true)
Helpers
snapshotStream()→ AsyncStream of unified statesnapshotPublisher(Combine) if you bridge the subjectfetchGraphQL(query:variables:)
OpenAPI & GraphQL
struct Viewer: Decodable { let me: Me }
let query = """
query Viewer {
me { id email }
}
"""
let result: Viewer = try await facade.fetchGraphQL(query: query, variables: nil)
print(result.me.id)
Cross-Origin
Native clients use a refresh header stored in Keychain; no cookie configuration required. Ensure the server has EA_CROSS_ORIGIN_COOKIES enabled if your web apps are cross-origin.
Realtime
- The facade creates a
RealtimeCoordinatorbacked by Convex subscriptions. - After login it automatically watches:
userscollection for username, memberships for organizations, and the active session document. - Remote revocation publishes a
.sessionInvalidevent that clears tokens and drops the snapshot. - Logout tears down subscriptions and clears cached Convex clients ensured by
EntityAuthRealtime.
Best practices
Secure Token Storage
The SDK automatically stores refresh tokens in Keychain for secure persistence across app launches.
// Token storage is handled automatically
try await auth.login(request: .init(email: "user@example.com", password: "password"))
// Refresh token is now securely stored in KeychainHandle Manual Requests
Prefer the SDK request helper fetch(_:method:headers:body:authorized:), which refreshes on 401 and retries automatically. For fully custom URLSession flows, manually call refresh() when receiving 401 responses.
// Using the helper (recommended)
let data = try await auth.fetch("/api/protected", method: "GET")
// If you roll your own URLSession requests:
var request = URLRequest(url: url)
request.setValue("Bearer \(auth.currentSnapshot().accessToken ?? "")", forHTTPHeaderField: "Authorization")
let (_, response) = try await URLSession.shared.data(for: request)
if let http = response as? HTTPURLResponse, http.statusCode == 401 {
_ = try await auth.refreshTokens()
// Retry with new token...
}Monitor Authentication State
Use Combine to observe token changes, and rely on built-in realtime watchers after login.
let stream = auth.snapshotStream()
Task {
for await state in stream {
print("active org", state.activeOrganization?.orgId ?? "none")
}
}