Capacitor plugin for Clerk native authentication using bridge pattern to integrate Clerk iOS/Android SDKs with CocoaPods/Gradle
npm install @trainon-inc/capacitor-clerk-nativeA Capacitor plugin for native Clerk authentication on iOS and Android using the bridge pattern to seamlessly integrate Clerk's native SDKs with CocoaPods/Gradle-based Capacitor plugins.
When using Clerk authentication in Capacitor iOS apps, WebView cookie limitations cause authentication failures with the error:
```
Browser unauthenticated (dev_browser_unauthenticated)
Additionally, Clerk's iOS SDK is only available via Swift Package Manager (SPM), but Capacitor plugins use CocoaPods, creating a dependency conflict.
This plugin uses a bridge pattern to resolve the CocoaPods ↔ SPM conflict:
1. Plugin (CocoaPods): Defines a protocol interface without depending on Clerk
2. App Target (SPM): Implements the bridge using Clerk's native SDK
3. AppDelegate: Connects them at runtime
This allows your Capacitor app to use Clerk's native iOS/Android SDKs (following the official Clerk iOS Quickstart), avoiding WebView cookie issues entirely.
- ✅ Uses official Clerk iOS SDK - same as native Swift apps
- ✅ Follows Clerk's best practices for iOS integration
- ✅ No WebView limitations - native authentication flows
- ✅ CocoaPods compatible - works with Capacitor's build system
- ✅ Reusable - bridge pattern can be adapted for other native SDKs
`bash`
npm install @trainon-inc/capacitor-clerk-nativeor
pnpm add @trainon-inc/capacitor-clerk-nativeor
yarn add @trainon-inc/capacitor-clerk-native
Alternatively, install from GitHub Packages:
1. Create a .npmrc file in your project root:``
@trainon-inc:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN
2. Install the package:
`bash`
npm install @trainon-inc/capacitor-clerk-native
> Note: You'll need a GitHub Personal Access Token with read:packages permission. Create one here
Before starting, ensure you have:
- ✅ A Clerk account
- ✅ A Clerk application set up in the dashboard
- ✅ Native API enabled in Clerk Dashboard → Settings → Native Applications
> Important: You must enable the Native API in your Clerk Dashboard before proceeding. This is required for native iOS integration.
1. Go to the Native Applications page in Clerk Dashboard
2. Click "Add Application"
3. Enter your iOS app details:
- App ID Prefix: Found in your Apple Developer account or Xcode (Team ID)
- Bundle ID: Your app's bundle identifier (e.g., com.trainon.member)
4. Note down your Frontend API URL (you'll need this for associated domains)
This is required for seamless authentication flows:
1. In Xcode, select your project → Select your App target
2. Go to "Signing & Capabilities" tab
3. Click "+ Capability" → Add "Associated Domains"
4. Under Associated Domains, add: webcredentials:{YOUR_FRONTEND_API_URL}webcredentials:guiding-serval-42.clerk.accounts.dev
- Example:
- Get your Frontend API URL from the Native Applications page in Clerk Dashboard
1. Open your iOS project in Xcode
2. Select your App target
3. Go to "Package Dependencies" tab
4. Click "+" → "Add Package Dependency"
5. Enter: https://github.com/clerk/clerk-ios0.69.0
6. Select version or later
7. Link to your App target
Create a file ClerkBridgeImpl.swift in your app's directory:
`swift
import Foundation
import Clerk
import capacitor_clerk_native
@MainActor
class ClerkBridgeImpl: NSObject, ClerkBridge {
private let clerk = Clerk.shared
func signIn(withEmail email: String, password: String, completion: @escaping (String?, Error?) -> Void) {
Task { @MainActor in
do {
let signIn = try await SignIn.create(strategy: .identifier(email))
let result = try await signIn.attemptFirstFactor(strategy: .password(password: password))
if result.status == .complete {
if let user = clerk.user {
completion(user.id, nil)
} else {
completion(nil, NSError(domain: "ClerkBridge", code: -1, userInfo: [NSLocalizedDescriptionKey: "Sign in completed but no user found"]))
}
} else {
completion(nil, NSError(domain: "ClerkBridge", code: -1, userInfo: [NSLocalizedDescriptionKey: "Sign in not complete"]))
}
} catch {
completion(nil, error)
}
}
}
func signUp(withEmail email: String, password: String, completion: @escaping (String?, Error?) -> Void) {
Task { @MainActor in
do {
let signUp = try await SignUp.create(
strategy: .standard(emailAddress: email, password: password)
)
if let user = clerk.user {
completion(user.id, nil)
} else {
completion(nil, NSError(domain: "ClerkBridge", code: -1, userInfo: [NSLocalizedDescriptionKey: "Sign up completed but no user found"]))
}
} catch {
completion(nil, error)
}
}
}
func signOut(completion: @escaping (Error?) -> Void) {
Task { @MainActor in
do {
try await clerk.signOut()
completion(nil)
} catch {
completion(error)
}
}
}
func getToken(completion: @escaping (String?, Error?) -> Void) {
Task { @MainActor in
do {
if let session = clerk.session {
if let token = try await session.getToken() {
completion(token.jwt, nil)
} else {
completion(nil, NSError(domain: "ClerkBridge", code: -1, userInfo: [NSLocalizedDescriptionKey: "No token available"]))
}
} else {
completion(nil, NSError(domain: "ClerkBridge", code: -1, userInfo: [NSLocalizedDescriptionKey: "No active session"]))
}
} catch {
completion(nil, error)
}
}
}
func getUser(completion: @escaping ([String: Any]?, Error?) -> Void) {
Task { @MainActor in
if let user = clerk.user {
let userDict: [String: Any] = [
"id": user.id,
"firstName": user.firstName ?? NSNull(),
"lastName": user.lastName ?? NSNull(),
"emailAddress": user.primaryEmailAddress?.emailAddress ?? NSNull(),
"imageUrl": user.imageUrl ?? NSNull(),
"username": user.username ?? NSNull()
]
completion(userDict, nil)
} else {
completion(nil, nil)
}
}
}
func isSignedIn(completion: @escaping (Bool, Error?) -> Void) {
Task { @MainActor in
completion(clerk.user != nil, nil)
}
}
}
`
> Note: Your AppDelegate should configure Clerk when the app launches, just like in the Clerk iOS Quickstart.
`swift
import UIKit
import Capacitor
import Clerk
import capacitor_clerk_native
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
private let clerkBridge = ClerkBridgeImpl()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Configure Clerk
if let publishableKey = ProcessInfo.processInfo.environment["CLERK_PUBLISHABLE_KEY"] ?? Bundle.main.infoDictionary?["ClerkPublishableKey"] as? String {
Clerk.shared.configure(publishableKey: publishableKey)
}
// Set up the Clerk bridge for the Capacitor plugin
ClerkNativePlugin.setClerkBridge(clerkBridge)
return true
}
// ... rest of AppDelegate methods
}
`
1. In Xcode, right-click your "App" folder
2. Select "Add Files to 'App'..."
3. Select ClerkBridgeImpl.swift
4. Uncheck "Copy items if needed"
5. Check the "App" target
6. Click "Add"
> Important: Get your Publishable Key from the API Keys page in Clerk Dashboard.
Option A: Build Settings
1. Select the App target → Build Settings
2. Click "+" → "Add User-Defined Setting"
3. Name: CLERK_PUBLISHABLE_KEY
4. Value: Your Clerk publishable key
Option B: xcconfig File (Recommended)
1. Create Config.xcconfig in your App folder:``
CLERK_PUBLISHABLE_KEY = pk_test_your_clerk_key_here
2. Add it to Xcode project
3. Set it for both Debug and Release configurations in Project Info
Add your Clerk publishable key:
`xml`
`typescript
import { ClerkProvider, useAuth, useUser, useSignIn, useSignUp } from '@trainon-inc/capacitor-clerk-native';
// Wrap your app
function App() {
return (
);
}
// Use in components
function LoginPage() {
const { signIn } = useSignIn();
const handleLogin = async () => {
const result = await signIn.create({
identifier: email,
password: password
});
if (result.status === 'complete') {
// Navigate to app
}
};
}
// Get auth state
function Profile() {
const { user } = useUser();
const { getToken } = useAuth();
const token = await getToken();
}
`
``
┌─────────────────────────────────────────────────┐
│ JavaScript/React (Capacitor WebView) │
│ - Uses capacitor-clerk-native hooks │
└─────────────────┬───────────────────────────────┘
│ Capacitor Bridge
┌─────────────────▼───────────────────────────────┐
│ ClerkNativePlugin (CocoaPods Pod) │
│ - Defines ClerkBridge protocol │
│ - Receives calls from JavaScript │
│ - Delegates to bridge implementation │
└─────────────────┬───────────────────────────────┘
│ Protocol/Delegate
┌─────────────────▼───────────────────────────────┐
│ ClerkBridgeImpl (App Target, SPM) │
│ - Implements ClerkBridge protocol │
│ - Uses Clerk iOS SDK directly │
│ - Handles all Clerk authentication │
└─────────────────────────────────────────────────┘
`
┌─────────────────────────────────────────────────┐
│ JavaScript/React (Capacitor WebView) │
│ - Uses @clerk/clerk-react (web provider) │
│ - Full Clerk functionality via web SDK │
└─────────────────────────────────────────────────┘
(No native plugin needed for auth)
┌─────────────────────────────────────────────────┐
│ ClerkNativePlugin (Gradle Module) - Stub │
│ - Exists for Capacitor plugin registration │
│ - Returns "use web provider" for all methods │
│ - No native Clerk SDK dependency │
└─────────────────────────────────────────────────┘
`
- signInWithPassword(email: string, password: string) - Sign in with email/passwordsignUp(email: string, password: string)
- - Create a new accountsignOut()
- - Sign out current usergetToken()
- - Get the authentication tokengetUser()
- - Get current user dataisSignedIn()
- - Check if user is signed in
- useAuth() - Authentication state and methodsuseUser()
- - Current user datauseSignIn()
- - Sign in methodsuseSignUp()
- - Sign up methodsuseClerk()
- - Full Clerk context
#### "Browser unauthenticated" error in WebView
✅ Solved! This plugin uses native SDKs instead of WebView, eliminating this issue entirely.
#### "Native API not enabled"
- Go to Clerk Dashboard → Settings → Native Applications
- Enable the Native API toggle
- Refresh your app
#### Associated Domain not working
- Verify you added webcredentials:{YOUR_FRONTEND_API_URL} correctly
- Get the exact URL from Clerk Dashboard → Native Applications
- Clean build folder and rebuild (Shift+Cmd+K in Xcode)
#### "No such module 'Clerk'" in ClerkBridgeImpl
- Ensure Clerk iOS SDK is added to your App target (not just the project)
- Check Package Dependencies tab shows Clerk linked to your target
- Clean and rebuild
#### Plugin not found or not responding
- Run npx cap sync to sync the plugin@trainon-inc/capacitor-clerk-native
- Verify plugin is in node_modules:
- Check Podfile includes the plugin after running cap sync
#### Authentication not persisting
- Clerk handles session persistence automatically
- Verify clerk.load() is called in AppDelegate
- Check Clerk SDK is properly configured with your publishable key
- 📖 Clerk iOS Documentation
- 💬 Clerk Discord Community
- 🐛 Report Issues
Important: On Android, this plugin provides a stub implementation. Android WebViews work well with web-based authentication (unlike iOS which has cookie issues), so Android should use the web Clerk provider (@clerk/clerk-react) instead of the native plugin.
- ✅ Android WebViews handle cookies correctly - no authentication issues
- ✅ The Clerk Android SDK is still in early stages (v0.1.x) with evolving APIs
- ✅ Using @clerk/clerk-react provides a stable, well-tested experience
- ✅ Simpler setup - no native configuration required
Configure your app to use the native plugin only on iOS:
`typescript
import { Capacitor } from "@capacitor/core";
import { ClerkProvider as WebClerkProvider } from "@clerk/clerk-react";
import { ClerkProvider as NativeClerkProvider } from "@trainon-inc/capacitor-clerk-native";
// Use native Clerk only on iOS (due to WebView cookie issues)
// Android WebViews work fine with web Clerk
const isIOS = Capacitor.getPlatform() === "ios";
const ClerkProvider = isIOS ? NativeClerkProvider : WebClerkProvider;
export function App() {
const clerkProps = isIOS
? { publishableKey: "pk_test_..." }
: {
publishableKey: "pk_test_...",
signInFallbackRedirectUrl: "/home",
signUpFallbackRedirectUrl: "/home",
};
return (
);
}
`
- Gradle: 8.11.1+
- Android Gradle Plugin: 8.5.0+
- Java: 17+
- Min SDK: 23 (Android 6.0)
- Target SDK: 35 (Android 15)
#### "No matching variant" error
``
Could not resolve project :trainon-inc-capacitor-clerk-native
No matching variant of project was found. No variants exist.
Solution: Update to the latest plugin version:
`bash`
npm update @trainon-inc/capacitor-clerk-native
npx cap sync android
#### "invalid source release: 21" error
The plugin uses Java 17. Ensure your Android Studio uses JDK 17+:
- File → Project Structure → SDK Location → Gradle JDK → Select JDK 17+
#### Gradle sync fails
- Clean the project: Build → Clean Project
- Invalidate caches: File → Invalidate Caches / Restart
- Delete .gradle` folder and re-sync
Contributions are welcome! This plugin was created to solve a real problem we encountered, and we'd love to make it better.
MIT
Created by the TrainOn Team to solve CocoaPods ↔ SPM conflicts when integrating Clerk authentication in Capacitor iOS apps.
- GitHub Issues
- Clerk Documentation
- Capacitor Documentation