Entity Auth

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, unified EntityAuthError.
  • EntityAuthNetworking — composable APIClient with automatic refresh + typed requests.
  • EntityAuthDomain — DTOs, services, and the high-level EntityAuthFacade.
  • 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 state
  • snapshotPublisher (Combine) if you bridge the subject
  • fetchGraphQL(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 RealtimeCoordinator backed by Convex subscriptions.
  • After login it automatically watches: users collection for username, memberships for organizations, and the active session document.
  • Remote revocation publishes a .sessionInvalid event 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 Keychain

Handle 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")
    }
}