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 apps | Mobile / browser apps | |
|---|---|---|
| Credential | Secret organization API key (X-TenancyEngine-Api-Key) | PKCE-based OIDC (authorization code + PKCE, no client secret) |
| Where it lives | Your backend's environment / secret manager | Never a static secret -- the OIDC flow issues short-lived, per-user tokens |
| What it authenticates | Your app, acting on behalf of your organization | An individual end user signing in |
| SDK family | TenancyEngine.Sdk (.NET), @tenancy-engine/sdk (TypeScript), the Go/Python/Java/PHP/Ruby SDKs | TenancyEngine.Sdk.Maui, TenancyEngineAuth (iOS), the Android/Flutter/React Native auth SDKs, @tenancyengine/browser-sdk |
| Can create tenants, run AI extraction, send SMS, grant AI credits | Yes -- this is exactly what the secret key is for | No -- 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=S256on every authorization request (RequireProofKeyForCodeExchange()inSaaSRuntime.Api/Program.cs). - System-browser sign-in, never an embedded web view --
ASWebAuthenticationSessionon 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'sSecureStorage,flutter_secure_storage,react-native-keychain. Never plainUserDefaults/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 mobileorYour 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.xmlintent-filter / manifest placeholder, iOSInfo.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.apiis the generic platform resource scope fromPlatformDbSeed;offline_accessis 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:
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.
Related
- SDK reference -- install commands and quickstarts for every SDK
- Getting started -- server-side app setup flow
- API keys -- organization API keys (server-side apps only)