Performing Custom Authentication in Nakama with Clerk

Performing Custom Authentication in Nakama with Clerk

See how I tackled custom authentication in Nakama with Clerk—allowing users to gain full control over auth.


Sample Image
Caramel Latte from First Light Cafe in Austin, TX

Providing a more robust method of authentication.

The Gift Grab app was originally authenticated using Nakama’s built-in services. It supported authentication via device ID, email, or social providers. This approach worked, but it felt limited and required extra backend work.

I then switched to using Fluo in conjunction with Nakama—essentially getting a JWT from Fluo that Nakama used to authenticate with a custom ID. This was a much better approach, but Fluo was still somewhat limited in its capabilities. Recently, I decided to use Clerk: a complete user management and authentication service designed for web and mobile apps.

Sample Image
Clerk home page

Clerk and Nakama: a match made in authentication heaven.

The benefit of using Clerk compared to other services is how robust the system is. Users can authenticate in many different ways, all managed from a single source. I can also set up production and development environments to better test authentication flows.

I was doing a similar workflow with Fluo, but I decided to dive deeper into the Nakama docs to see if there was a more structured approach for authenticating with third-party services, which there is (read more here). This new approach adds a backend step to verify the incoming JWT from Clerk before creating the user in the Nakama database.

Sample Image
Article about custom authentication with 3rd party providers

How the new process will work

Here’s a detailed breakdown of how it works.

  1. The user is presented with Clerk’s sign-in/sign-up form (ClerkAuthentication) when the app starts. The user creates a profile by signing up (email only for now, with more options coming soon).
// login_page.dart

ClerkAuthBuilder(
	signedOutBuilder: (context, authState) =>
		const ClerkAuthentication(),
  signedInBuilder: (context, authState) =>
	  const CircularProgressIndicator(),
)
  1. The ClerkAuth session now provides an ID, which triggers a Signal update and runs an effect.
// auth_controller.dart

/// Signal to track changes to the Clerk session ID.
late final Signal<String?> _clerkSessionId;

/// Cleanup function for the reactive effect tracking authentication.
late final EffectCleanup _onClerkSessionIdChangedEffect;

/// Callback to listen for Clerk auth changes and update [_clerkSessionId].
late final Function() _clerkListener;

_clerkSessionId = signal(
	_clerkAuth.session?.id,
  options: const SignalOptions(name: 'AuthController.clerkSessionId'),
);

_clerkListener = () => _clerkSessionId.value = _clerkAuth.session?.id;

_clerkAuth.addListener(_clerkListener);

// Watch the clerk session id and process nakama authentication
// accordingly.
_onClerkSessionIdChangedEffect = effect(
	() {
		final sessionId = _clerkSessionId.value;

		if (sessionId != null) {
			// Unawaited fires the async _processAuthentication() in the background
      // without blocking the synchronous effect callback or triggering
      unawaited(_processAuthentication());
    } else {
	    isAuthenticated.value = const AsyncData(false);
    }
  },
  options: const EffectOptions(
		name: 'AuthController.onClerkSessionIdChangedEffect',
	),
);
  1. The _processAuthentication method now runs. It fetches the JWT from ClerkAuth and determines whether this is a sign-up or sign-in based on the user’s createdAt and lastSignedIn values. If it’s a sign-up, it authenticates with Nakama by creating a new user; if it’s a sign-in, it authenticates with Nakama normally.
// auth_controller.dart
  
  /// Processes authentication against Nakama using the user's Clerk session JWT token.
  /// Determines if a signup or login flow should be used based on account creation time.
  Future<void> _processAuthentication() async {
    try {
      isAuthenticated.value = const AsyncLoading();

      final token = await _clerkAuth.sessionToken();
      final user = _clerkAuth.user;

      if (user == null) {
        isAuthenticated.value = const AsyncData(false);
        return;
      }

      final isSignUp =
          user.lastSignInAt.difference(user.createdAt).abs().inSeconds < 2;

      logger.d(
        isSignUp
            ? 'New clerk user sign up - ID: ${user.id}, Username: ${user.username}'
            : 'Existing clerk user sign in - ID: ${user.id}, Username: ${user.username}',
      );

      print('getting value!');

      final session = await _client.authenticateCustom(
        id: token.jwt,
        username: user.username,
        create: isSignUp,
      );

      print('Saving session...');

      await _sessionService.saveSession(session);
      isAuthenticated.value = const AsyncData(true);
    } catch (e) {
      logger.e('Error in authentication: $e');
      isAuthenticated.value = AsyncError(e, StackTrace.current);
    }
  }

Once the isAuthenticated signal becomes true, the user proceeds into the app as normal.

Note: The clerk_flutter package currently does not support web, so you will have to use their Javascript SDK to complete a Flutter web app. There’s a Github issue already reported here.

Testing it all out.

Here’s a demonstration of the authentication flow. As you can see, a user has been created in the Production instance of my Clerk dashboard, and the same user appears in the Nakama accounts section.

Next steps with Clerk & Nakama.

Now that authentication is easier, the next phase is to expand the sign-up and sign-in options in the app. Ideally, I’d like to support:

  • Phone
  • Email links
  • Google
  • Apple

Once those are covered, I think the authentication flow will be in great shape for new users.

Thanks for reading

I hope you found this article helpful—if so, please share it!

Coffee Break: Caramel Latte

It was my first time visiting, and the caramel latte was excellent. The caramel taste was noticeably stronger than usual, which was a huge plus. It’s a bit pricey, but well worth it—I would’ve grabbed a 16oz if I had room. The shop itself is really unique, doubling as a clean, cozy mini-library.

10/10


Related posts
Coffee & Code - Nakama Sessions & Authentication

Coffee & Code - Nakama Sessions & Authentication

Read more
Coffee & Code - From Flutter Mobile to Desktop for macOS

Coffee & Code - From Flutter Mobile to Desktop for macOS

Read more
Coffee & Code - New Fluo Info Screen and Fluo Rating Screen

Coffee & Code - New Fluo Info Screen and Fluo Rating Screen

Read more
Coffee & Code - Firestore Sorting & Toast Messages

Coffee & Code - Firestore Sorting & Toast Messages

Read more