All posts
    serverpod
    authentication
    dart
    flutter
    backend
    security
    explainer

    Serverpod Authentication Explained: What You Need to Know Before You Start

    Authentication in Serverpod is one of the most searched and least clearly explained parts of the framework. Here is the conceptual foundation that makes the implementation make sense — before you write a single line of code.

    ·10 min read

    Authentication is the part of Serverpod development that generates the most questions in the community forums, the most threads in the Discord server, and the most GitHub issues from developers who got everything else working but can't quite get users signing in the way they expect.

    That pattern is not accidental. Authentication in Serverpod is genuinely well-designed — it handles the complexity of user management, session tracking, and multiple sign-in methods in a way that is thoughtful and coherent. But it has a conceptual surface area that is larger than most other parts of the framework, and the documentation, while accurate, tends to cover the how before the why. Developers who try to implement authentication before understanding the underlying model end up in the position of following instructions without knowing what they're building — which works until something goes wrong, and then it doesn't work at all.

    This article is the conceptual foundation. Not a step-by-step implementation guide, but the explanation of how Serverpod's authentication model is structured, what its pieces are, why they exist, and what you need to understand before the implementation steps make complete sense.


    The Core Concept: Identity and Session

    Every authentication system is built on two concepts: identity and session. Understanding how Serverpod handles each of them is the key to understanding everything else.

    Identity is the persistent record of a user. In Serverpod, identity maps to a UserInfo object — a record in the database that persists across sessions, logins, and devices. When a user creates an account, a UserInfo row is created. When they sign in from a different device, they're authenticated against the same UserInfo. When you want to store data that belongs to a specific user, you reference their UserInfo ID. Identity is stable, persistent, and device-independent.

    Session is the temporary context of an authenticated request. Every time a client makes a request to a Serverpod endpoint, that request carries a session. The session knows whether the request is authenticated, and if it is, it knows which UserInfo that authentication corresponds to. Sessions are ephemeral — they exist for the duration of a connection and are represented by an authentication key that the client stores and presents with each request.

    The relationship between these two concepts is straightforward: a session is the runtime proof that a particular client is currently authenticated as a particular identity. The UserInfo is the permanent record. The session is the temporary credential.

    This separation is why Serverpod's authentication model handles multi-device scenarios cleanly. A user with three devices has three sessions — each device has its own authentication key — but only one identity. You can invalidate one session without affecting the others. You can list all active sessions for a user and revoke them individually. The identity stays stable while sessions come and go around it.


    The Auth Module

    Serverpod's authentication capabilities live in the serverpod_auth module — a set of packages that implement user management and sign-in flows on top of Serverpod's core framework.

    The module is not a single package. It splits across several packages that you add depending on which sign-in methods your application needs:

    • serverpod_auth_server goes in your server package and provides the server-side logic for user management and authentication handling.
    • serverpod_auth_client is generated as part of the module and provides the client stubs for authentication endpoints.
    • serverpod_auth_shared_flutter goes in your Flutter package and provides utilities and widgets for authentication UI.
    • Sign-in method packages — serverpod_auth_email_flutter, serverpod_auth_google_flutter, serverpod_auth_apple_flutter — go in your Flutter package and add the sign-in method specific UI and logic.

    The multi-package structure reflects the module's design. The server-side logic is shared across all sign-in methods. The client-side packages are specific to each sign-in method because the UI and platform-specific OAuth flows differ between them. You include what you need and leave out what you don't.


    How Sign-In Flows Work

    The mechanics of a sign-in flow in Serverpod follow a consistent pattern regardless of the sign-in method being used, and understanding that pattern makes each individual sign-in method easier to reason about.

    When a user signs in, the flow moves through three stages:

    Verification. The sign-in method verifies that the user is who they claim to be. For email and password, this means checking the password hash. For Google Sign-In, this means exchanging an OAuth token with Google's servers and getting back a verified identity claim. For Apple Sign-In, this means a similar OAuth exchange with Apple. The verification step is handled by the sign-in method's package — you don't implement the verification logic yourself.

    Identity resolution. Once the sign-in method has verified the user's identity, Serverpod checks whether a UserInfo record already exists for that identity. If the user has signed in before, their existing record is retrieved. If this is their first sign-in, a new UserInfo record is created. This step is where the link between an external identity provider — a Google account, an Apple ID — and your application's internal user record is established and maintained.

    Session creation. Once identity is resolved, Serverpod creates an authentication key — a secure token that the client will use to prove its identity on subsequent requests. This key is returned to the client and stored in local storage on the device. From this point forward, the client presents this key with each request, and the session attached to that request knows who the user is.

    The sign-out flow is the reverse of the session creation step. The authentication key is invalidated on the server, and the client removes it from local storage. The UserInfo record is unaffected — the user's identity persists even though their current session does not.


    The Session Object in Your Endpoints

    Understanding how authentication integrates with your endpoint code is the practical piece that connects the conceptual model to the implementation.

    Every endpoint method in Serverpod receives a Session object as its first parameter. This session object is the endpoint's window into the current request's authentication state. There are three things you'll regularly do with it in the context of authentication.

    Checking if the user is signed in:

    Future<void> doSomethingProtected(Session session) async {
      final userId = await session.auth.authenticatedUserId;
      if (userId == null) {
        throw AuthenticationFailedException();
      }
      // Proceed with authenticated logic
    }
    

    The authenticatedUserId returns null if the request is not authenticated and an integer user ID if it is. That null check is the fundamental authentication guard in Serverpod endpoint code. It's simple, and its simplicity is deliberate — the framework gives you the primitive, and you decide where and how to apply it.

    Getting the full user record:

    Future<UserInfo?> getCurrentUser(Session session) async {
      final userId = await session.auth.authenticatedUserId;
      if (userId == null) return null;
    
      return await UserInfo.db.findById(session, userId);
    }
    

    The authenticatedUserId gives you the ID. From there, you fetch the UserInfo from the database when you need the full user record — the email address, the name, the profile image URL, any custom fields you've added through the user info extension system.

    Requiring authentication at the endpoint level:

    Serverpod also allows you to mark entire endpoints as requiring authentication, rather than checking in each individual method. This is done by overriding the requireLogin getter:

    class ProtectedEndpoint extends Endpoint {
      @override
      bool get requireLogin => true;
    
      Future<List<Article>> getMyArticles(Session session) async {
        final userId = await session.auth.authenticatedUserId;
        // userId is guaranteed non-null here because requireLogin is true
        return await Article.db.find(
          session,
          where: (a) => a.authorId.equals(userId!),
        );
      }
    }
    

    When requireLogin is true, Serverpod rejects unauthenticated requests before they reach your method. Any call to that endpoint from an unauthenticated client receives an authentication error immediately. This is the right choice for endpoints where every method requires authentication — it removes the boilerplate of checking in each method and makes the authentication requirement visible at the class level.


    Common Points of Confusion

    The authentication questions that recur most in the Serverpod community cluster around a handful of specific misunderstandings. Naming them directly is more useful than hoping they don't come up.

    The auth module tables. When you add serverpod_auth_server to your project and run migrations, Serverpod creates several database tables for user management — serverpod_users, serverpod_email_auth, and others depending on which sign-in methods you're using. These tables are owned by the module. You don't create them, and you don't directly query most of them. Your application's user data that goes beyond what UserInfo provides belongs in your own tables, linked to the UserInfo ID.

    The difference between UserInfo and your user model. UserInfo contains the fields the auth module manages — email, name, profile image, blocked status. If your application needs additional user data — a bio, a subscription tier, a list of preferences — you create your own model with a foreign key to UserInfo. You don't extend UserInfo directly. The separation keeps the module's concerns distinct from your application's concerns.

    Why the authentication key approach rather than JWT. Serverpod uses its own authentication key system rather than stateless JWTs. This is a deliberate design choice that enables server-side session invalidation — you can revoke a specific session, invalidate all sessions for a user, or implement logout-everywhere functionality. Stateless JWTs can't support these patterns without additional infrastructure. If your application requires JWT-based authentication — for interoperability with other services or for specific deployment requirements — you can implement custom authentication on top of Serverpod's session system, but the default auth module uses authentication keys.

    Authentication in development vs production. Local development with Serverpod's auth module runs against your local Docker database. Social sign-in methods — Google, Apple — require properly configured OAuth applications even in development, which means the OAuth setup for those providers is a prerequisite before the sign-in flow will work end to end. Email sign-in can be tested locally without external configuration, which makes it a useful starting point for development before the OAuth providers are set up.


    Protecting Specific Resources

    Authentication tells you who the user is. Authorisation — deciding what that user is allowed to do — is a separate concern that Serverpod doesn't prescribe but leaves to your application logic.

    The simplest authorisation pattern is ownership-based: a user can access resources they created. This is implemented by storing the creator's userId on the resource and filtering by it in queries:

    Future<List<Article>> getMyArticles(Session session) async {
      final userId = await session.auth.authenticatedUserId;
      if (userId == null) throw AuthenticationFailedException();
    
      return await Article.db.find(
        session,
        where: (a) => a.authorId.equals(userId),
      );
    }
    

    More complex authorisation — role-based access, permission systems, subscription-gated features — follows the same pattern but with additional data on the user record or in a separate permissions table. The session gives you the user ID. Your service layer uses that ID to determine what the user is allowed to do. The endpoint enforces whatever the service decides.

    The separation between authentication (who are you?) and authorisation (what are you allowed to do?) is worth maintaining deliberately in your endpoint and service structure. Authentication logic belongs close to the session. Authorisation logic belongs in the service layer, where it can be tested in isolation and changed without touching the endpoint interface.


    Before You Start Implementing

    Authentication is one of the features where the investment in understanding the model before writing code pays back most clearly. The developers who run into the most persistent authentication issues in Serverpod are usually the ones who started implementing before they understood the session model, the relationship between UserInfo and their own data, or how the auth module's tables relate to the rest of their schema.

    The concepts in this article — identity vs session, the auth module structure, the session object in endpoints, the distinction between UserInfo and application user data — are the conceptual map that makes the implementation steps land in the right place when you encounter them.

    Authentication in Serverpod is not complicated once the model is clear. It just requires that the model be clear before the implementation begins.

    That's the sequence that makes it work. Understanding first, then building. And what you build, when you start from understanding, tends to hold together considerably better than what you build while still figuring out how the pieces fit.

    Try Dartform

    Skip the CLI. Build Serverpod backends visually.

    Free 1-week trial. Native macOS app. No App Store required.

    Download Dartform
    Dartform

    Ready to build Dart backends faster?

    Download Dartform and see the difference visual development makes. Start your 1-week free trial today.