Scopes and Lifecycle
The IDK uses a hierarchical scope system to manage component lifecycles, enable multi-tenant isolation, and provide proper dependency injection. Understanding this scope model is essential for correctly integrating the IDK into your application.
Scope Hierarchy
The IDK defines four scope levels, each building upon the previous:
Each scope level has an associated component that provides access to services available at that level. Services are instantiated within their appropriate scope and share the lifecycle of that scope.
Application Scope
The application scope represents the top-level singleton scope for your entire application. There is typically one application scope instance per running application or service. The application component is created once at startup and lives for the duration of the application.
Services at this level include global configuration, application-wide logging, and the context manager that creates lower-level scopes.
User Context Scope
The user context scope represents a specific tenant and principal combination. A principal is either a user (natural person) or a system account. This scope enables multi-tenant applications to isolate data and configuration between different users or organizations.
For mobile applications that don't require multi-tenancy, you can use the anonymous context which provides a default tenant and principal. The user context scope persists across multiple sessions and holds tenant-specific configuration values.
Session Scope
The session scope represents an active working session. What constitutes a session is defined by your application. In a mobile app, a session might span from user login to logout. In a REST API, each request might be a separate session.
Most IDK services are obtained from the session scope, including the KeyManagerService for cryptographic operations, the MdocEngagementManager for credential presentation, and HTTP clients configured with the appropriate credentials.
Creating Components
Components are created by traversing down the scope hierarchy. You start with the application component and use its managers to create user context and session instances.
- Android/kotlin
- iOS/Swift
// 1. Create the application component (singleton)
val appComponent = IdkAppComponent.init(
application = applicationContext,
appId = "my-app",
profile = "production",
version = "1.0.0"
)
// 2. Create a user context (for a specific tenant/principal)
val userContext = appComponent.userContextManager.createOrGetFromInputs(
tenantInput = DefaultTenantInputString(tenant = "acme-corp"),
principalInput = DefaultPrincipalInputString(principal = "user@example.com"),
makeActive = true
)
// Or use the anonymous context if multi-tenancy is not needed
// val userContext = appComponent.userContextManager.getAnonymous(makeActive = true)
// 3. Create a session within that user context
val session = userContext.sessionContextManager.createOrGetFromId(
sessionId = "session-123",
makeActive = true
)
// 4. Access services from the session component
val keyManager = session.component.keyManagerService
val engagementManager = session.component.engagementManager
// 1. Create the application component (singleton)
let appComponent = IdkAppComponent.companion.doInit(
application: UIApplication.shared,
appId: "my-app",
profile: "production",
version: "1.0.0"
)
// 2. Create a user context (for a specific tenant/principal)
let userContext = appComponent.userContextManager.createOrGetFromInputs(
tenantInput: DefaultTenantInputString(tenant: "acme-corp"),
principalInput: DefaultPrincipalInputString(principal: "user@example.com"),
makeActive: true
)
// Or use the anonymous context if multi-tenancy is not needed
// let userContext = appComponent.userContextManager.getAnonymous(makeActive: true)
// 3. Create a session within that user context
let session = userContext.sessionContextManager.createOrGetFromId(
sessionId: "session-123",
makeActive: true
)
// 4. Access services from the session component
let keyManager = session.component.keyManagerService
let engagementManager = session.component.engagementManager
Scope Lifecycle
Understanding when scopes are created and destroyed helps manage resources correctly.
Application Lifecycle
The application component should be created once when your application starts. On Android, this is typically in your Application class. On iOS, this is typically in your AppDelegate or during initial app setup.
class MyApplication : Application() {
lateinit var idkApp: IdkAppComponent
private set
override fun onCreate() {
super.onCreate()
idkApp = IdkAppComponent.init(
application = this,
appId = "my-app",
profile = BuildConfig.BUILD_TYPE,
version = BuildConfig.VERSION_NAME
)
}
}
User Context Lifecycle
User contexts can be created and retrieved by ID. If a context with the given tenant and principal already exists, it's returned rather than creating a new one. User contexts are typically long-lived and persist across sessions.
// Creating or retrieving a user context
val context = appComponent.userContextManager.createOrGetFromInputs(
tenantInput = DefaultTenantInputString(tenant = "tenant-id"),
principalInput = DefaultPrincipalInputString(principal = "user-id"),
makeActive = true
)
// Later, you can retrieve an active context
val activeContext = appComponent.userContextManager.getActive()
Session Lifecycle
Sessions are created within a user context and should be destroyed when no longer needed. Destroying a session cancels any outstanding asynchronous operations and releases resources.
// Create a session
val session = userContext.sessionContextManager.createOrGetFromId(
sessionId = "session-123",
makeActive = true
)
// Use the session...
// Destroy when done (optional but recommended)
userContext.sessionContextManager.destroy(sessionId = "session-123")
Service Access Patterns
Services are typically accessed through the session component. The component ensures that all dependencies are properly injected and scoped.
- Android/kotlin
- iOS/Swift
// Direct property access
val keyManager = session.component.keyManagerService
// Generic service access by type
val oauth2Client: OAuth2Client = session.getService()
// Generic service access by ID
val customService = session.getService<MyService>(MyService.SERVICE_ID)
// Direct property access
let keyManager = session.component.keyManagerService
// Generic service access
let oauth2Client = session.getService(id: OAuth2Client.companion.SERVICE_ID) as! OAuth2Client
Configuration by Scope
Configuration values can be set at different scope levels, with more specific scopes overriding less specific ones. This enables patterns like:
- Application-wide default settings
- Tenant-specific overrides for API endpoints
- Principal-specific credentials
- Session-specific temporary values
See the Configuration guide for details on how to configure values at each scope level.
Best Practices
When working with the scope system, keep these practices in mind:
The application component should be created once and stored in a location accessible throughout your app, such as the Application class on Android or a singleton on iOS.
For mobile applications without multi-tenancy requirements, use the anonymous context. This simplifies your code while still providing proper scope isolation.
Create sessions with meaningful IDs that help with debugging and logging. The session ID appears in logs and helps trace operations across the system.
Destroy sessions when they're no longer needed, especially in server applications where sessions represent individual requests. This ensures resources are released promptly.
Avoid holding references to components or services outside their scope's lifecycle. If a session is destroyed, services obtained from that session should no longer be used.