Skip to content

Mobile and browser apps: which credential model do I use?

TenancyEngine has two fundamentally different client-side credential models. Getting this wrong in either direction either ships a secret to every user of your app, or leaves you unable to authenticate a real end user at all.

The two models

Server-side appsMobile / browser apps
CredentialSecret organization API key (X-TenancyEngine-Api-Key)PKCE-based OIDC (authorization code + PKCE, no client secret)
Where it livesYour backend's environment / secret managerNever a static secret -- the OIDC flow issues short-lived, per-user tokens
What it authenticatesYour app, acting on behalf of your organizationAn individual end user signing in
SDK familyTenancyEngine.Sdk (.NET), @tenancy-engine/sdk (TypeScript), the Go/Python/Java/PHP/Ruby SDKsTenancyEngine.Sdk.Maui, TenancyEngineAuth (iOS), the Android/Flutter/React Native auth SDKs, @tenancyengine/browser-sdk
Can create tenants, run AI extraction, send SMS, grant AI creditsYes -- this is exactly what the secret key is forNo -- never. A mobile binary or browser bundle can always be decompiled/inspected; embedding a secret key in either hands every installer of your app your organization's credential

The rule that matters in practice: if code runs on a device you do not control (an app binary a user installs, or JavaScript that executes in a browser you do not operate), it gets a PKCE OIDC client or a narrowly-scoped publishable credential -- never your secret organization API key. If code runs on a server you control, it gets the secret key. There is no in-between tier for mobile/browser apps; asking for one is the mistake this page exists to head off.

Browser apps: PKCE OIDC, or a publishable credential -- not your secret key

A web app has two sub-cases:

  • Your own backend renders/serves the page and holds the secret key server-side -- the browser never talks to TenancyEngine directly; it calls your API, which calls TenancyEngine. This is the normal case for a server-rendered app or a SPA with its own backend-for-frontend.
  • The browser talks to TenancyEngine directly -- use @tenancyengine/browser-sdk (see the SDK reference), which only exposes genuinely anonymous public-app endpoints (/api/v1/public/apps/*) plus one narrowly-scoped publishable chat key (chk_..., safe to embed the same way a Stripe publishable key is). It has no method for anything under /api/v1/runtime/* at all -- that surface requires your secret key and stays server-side.

If your web app needs a signed-in end user (not just anonymous public-app data), that is OIDC authorization code + PKCE against https://auth.saasruntime.com, the same as the TE Console itself -- a public (no client secret) OIDC client, same registration model as the mobile SDKs below.

Mobile apps: PKCE OIDC, always

Every mobile SDK in this platform (MAUI, iOS, Android, Flutter, React Native) follows the exact same shape, because a shipped app binary can never keep a secret confidential:

  • Public OIDC client, no client secret -- registered per app (see "Registering your app's OIDC client" below).
  • PKCE (RFC 7636), mandatory -- SaaSRuntime's OpenIddict server requires code_challenge/code_challenge_method=S256 on every authorization request (RequireProofKeyForCodeExchange() in SaaSRuntime.Api/Program.cs).
  • System-browser sign-in, never an embedded web view -- ASWebAuthenticationSession on iOS, Chrome Custom Tabs on Android, flutter_appauth/react-native-app-auth (both wrapping the native AppAuth SDKs) on Flutter/React Native. This is OAuth Best Current Practice for native apps (RFC 8252 Section 8.12) -- an in-app web view cannot show the user the real address bar, can be scripted/inspected by the host app, and does not reliably share cookies/session with SaaSRuntime's cookie-based session or its social-login providers.
  • OS-backed secure token storage -- iOS Keychain, Android EncryptedSharedPreferences/ Keystore, MAUI's SecureStorage, flutter_secure_storage, react-native-keychain. Never plain UserDefaults/SharedPreferences/AsyncStorage.
  • Proactive silent refresh -- every SDK refreshes the access token ~2 minutes before it actually expires (configurable), not reactively after an API call already returned 401.

Registering your app's OIDC client

Application administrators register public mobile clients themselves in Applications > your application > Environments. Expand Mobile tester access, choose Add mobile client, then enter a client name and the app-owned sign-in and sign-out callback URIs. TenancyEngine generates the client ID; no platform code or database edit is required. You will need to supply:

  • A client name that identifies the app and platform, e.g. Your app mobile or Your app iOS.
  • A redirect URI using your app's own custom URL scheme, e.g. com.yourcompany.yourapp://auth/callback -- declare that scheme in your app's own platform config (AndroidManifest.xml intent-filter / manifest placeholder, iOS Info.plistCFBundleURLTypes, etc. -- each SDK's own README has the exact platform-config snippet).
  • A post-logout redirect URI, e.g. com.yourcompany.yourapp://.
  • Scopes: openid profile email api offline_access -- the same set every TenancyEngine client requests. api is the generic platform resource scope from PlatformDbSeed; offline_access is what lets the SDK's proactive refresh work instead of forcing a fresh login every access-token lifetime.
  • Grant types: authorization_code (with PKCE required) + refresh_token.

Seeded first-party registrations remain supported for platform bootstrap, but they are not the ISV onboarding path. There is no per-framework difference in the registration shape; a MAUI, Flutter, React Native, Swift, or Kotlin app uses the same public PKCE client model.

Enrolling testers in lab or staging

Ship one production-default binary. Do not compile editable lab or staging URLs into a hidden menu. In the same Mobile tester access section, an administrator can create an expiring, revocable enrollment invitation for a registered mobile client and share its QR code or link with a tester. The invitation contains an opaque protected token, not service credentials or user-editable endpoints. The app redeems it only through a preconfigured trusted TenancyEngine origin, validates that the returned profile matches its application, client, and callback scheme, clears environment-bound authentication and cached data, then stores the profile in OS secure storage. See Signed mobile environment enrollment for the complete startup and post-enrollment runtime flow.

SDK-by-SDK notes

.NET MAUI

TenancyEngine.Sdk.Maui.Auth is the platform-agnostic core (token store/OIDC client, signed environment enrollment, and atomic runtime reconstruction); TenancyEngine.Sdk.Maui wires it up with real MAUI adapters (SecureStorageTokenStore, MauiBrowser, and SecureStorageMobileEnvironmentProfileStore). The production profile is the default; signed tester enrollment can temporarily activate another registered environment:

csharp
using TenancyEngine.Sdk.Maui;

var authClient = TenancyEngineMaui.CreateAuthClient(new TenancyEngineAuthConfig
{
    Authority = "https://auth.saasruntime.com",
    ClientId = "yourapp-mobile",
    RedirectUri = "com.yourcompany.yourapp://auth/callback",
    PostLogoutRedirectUri = "com.yourcompany.yourapp://",
});

Neither package is published to NuGet yet.

iOS (Swift)

TenancyEngineAuth is a Swift Package (iOS 15+), source-complete with a full README covering installation, ASWebAuthenticationSession configuration, and sign-in/sign-out. Not yet published as a remote git dependency -- add it via a local .package(path:) reference.

Android (Kotlin)

com.tenancyengine.auth (package tenancyengine-android-auth) is source-complete, Coroutines-based, Chrome-Custom-Tabs-driven. Its own README explicitly warns: "do not assume a package id is live just because a build.gradle.kts exists for it" -- there is no Maven feed for it yet; consume it as a local Gradle composite build (includeBuild(...)).

Flutter

tenancyengine_auth wraps flutter_appauth + flutter_secure_storage, iOS + Android today (a desktop loopback-redirect flow is possible via flutter_appauth but untested/undocumented by this package). Not yet published to pub.dev -- use a git/path dependency.

React Native

@tenancyengine/react-native-auth wraps react-native-app-auth + react-native-keychain (both peer dependencies, so your app controls their exact native-module versions -- both need a native rebuild after install, not just a Metro reload). Not published to a registry -- use a file:/git dependency.

TenancyEngine platform documentation