# Serverpod framework documentation > The current stable Serverpod framework documentation as a single markdown file. Every page starts with its title and canonical URL. --- # Introduction https://docs.serverpod.dev Serverpod is an open-source full-stack framework for Flutter, built from the ground up for agentic coding. You write your entire stack in Dart, define your data models in YAML, and call your server endpoints from your Flutter app through generated, type-safe Dart code. No REST contracts to maintain, no language-switching, and no boilerplate code. :::tip If you are a Flutter developer, check out our **[Quickstart guide](https://docs.serverpod.dev/quickstart.md)** to get up and running in minutes. If you don't have Flutter installed and just want to try things out, get started with **[App Studio](https://serverpod.dev/appstudio)**. ::: [Watch the video](https://www.youtube-nocookie.com/embed/fQBlQUNxYWE) ## Build with Dart everywhere Define an endpoint on the server: ```dart class GreetingEndpoint extends Endpoint { Future hello(Session session, String name) async { return 'Hello, $name'; } } ``` Call it from your Flutter app: ```dart final greeting = await client.greeting.hello('World'); print(greeting); // Hello, World ``` Endpoint methods on the server become typed methods in your app. Serverpod handles serialization, transport, and the code generation in between. ## Who Serverpod is for Serverpod is built for Flutter teams that want a Dart-only stack: solo developers, app agencies, startups, and small teams shipping production apps without juggling languages. If your app uses Flutter, your team already knows Dart. If you'd rather ship features than struggle with infrastructure, Serverpod is a good fit. ## What's included Serverpod covers all common backend needs out of the box: - **Code generation:** Server endpoints and the app stay in sync automatically. Define a method on the server, call it from the app as if it were local Dart code. - **Fast dev loop:** See your changes as you save. `serverpod start` regenerates code and hot reloads your server and app, and the `M` key creates and applies a database migration. - **Type-safe ORM:** Query Postgres or SQLite with Dart. Filters, relations, and joins are checked at compile time. Schema changes ship as versioned migrations. - **Client-side database:** Use the same models in your Flutter app through a generated local database, including migrations. - **Authentication:** Built-in sign-in with Google, Apple, GitHub, Microsoft, Firebase, email/password, passkeys, and more. Anonymous and custom providers supported. - **Real-time streams:** Stream Dart objects to your app over WebSockets without writing connection-management code. - **Deployment:** Deploy to Serverpod Cloud, or self-host with Docker, Terraform, or your own infrastructure. No vendor lock-in. - **Logging and monitoring:** Inspect requests, exceptions, and slow queries with [Serverpod Insights](https://docs.serverpod.dev/tools/insights.md), the desktop app that ships with Serverpod. - **File uploads:** Store in Amazon S3, Google Cloud Storage, or your database. - **Task scheduling:** Schedule a method to run later with type-safe future calls. They persist across server restarts. - **Caching:** Cache primitives and serializable models in-memory or in Redis. - **Web server:** Serve REST APIs, webhooks, and web pages with the built-in [web server](https://docs.serverpod.dev/concepts/web-server/overview.md), based on [Relic](https://docs.dartrelic.dev/). --- # Installation https://docs.serverpod.dev/installation ### Set up with an AI agent If you use a coding agent such as Claude Code, Cursor, or Codex, you can configure your development environment with it. Give it this prompt, and the agent will install only what is missing and verify the results. ```text title="Agent setup prompt" Set up this machine for Serverpod development. Check what is already installed first and only install what is missing. Do not reinstall or upgrade anything without asking me. 1. Flutter: run `flutter --version`. If Flutter is missing, install the latest stable release for this OS following https://docs.flutter.dev/get-started/install. Use the Dart SDK bundled with Flutter, not a separate one. 2. Serverpod CLI: run `serverpod version`. If it is missing, run `dart install serverpod_cli` (if it was previously installed with `dart pub global activate`, run `dart pub global deactivate serverpod_cli` first). If the install fails because the installed Flutter is too old, stop and ask me whether to upgrade Flutter. 3. Make sure `flutter`, `dart`, and `serverpod` are on the PATH for new terminal sessions, including non-interactive shells. 4. Verify by running `flutter doctor`, `dart --version`, and `serverpod version`. Do not install the platform toolchains flutter doctor reports as missing (Android Studio, Xcode, Chrome) unless I ask. Finish with a summary in exactly this format, one item per line and no other commentary. Leave out a section if it would be empty. Already installed - Installed - Optional, not set up - () Use these descriptions for the optional items: Android toolchain (Android apps), Xcode (iOS and macOS apps), Chrome (web apps), Linux toolchain (Linux desktop apps), Visual Studio (Windows desktop apps). After the list, ask if I want any of them installed. Then ask me: "Do you want me to create a new Serverpod project?" and show the command to do it manually: `serverpod create `. ``` ### Prerequisites Serverpod is tested on Mac, Windows, and Linux. Before you can install Serverpod, you need to have **[Flutter](https://flutter.dev/docs/get-started/install)** installed. Serverpod 4 requires Flutter 3.44.4 or later, which includes Dart 3.12.2. :::info Check your Flutter installation by running the following command in your terminal: ```bash $ flutter doctor ``` ::: New projects use an embedded PostgreSQL that Serverpod downloads and runs for you, so no database setup is needed. If you connect to your own PostgreSQL instead, it must be version 16 or later. ### Install Serverpod Serverpod is installed using the Dart package manager. To install Serverpod, run the following command in your terminal: ```txt $ dart install serverpod_cli ``` This command will install the Serverpod command-line interface (CLI) globally on your machine. You can verify the installation by running: ```bash $ serverpod ``` If everything is correctly configured, the help for the `serverpod` command is now displayed. :::warning If a previous version of Serverpod is already installed with `dart pub global activate`, deactivate it before installing or upgrading: ```txt $ dart pub global deactivate serverpod_cli ``` ::: ### Install the VS Code extension (recommended) The Serverpod VS Code extension makes it easy to work with your Serverpod projects. It provides real-time diagnostics and syntax highlighting for model files in your project. ![Serverpod extension](https://docs.serverpod.dev/img/syntax-highlighting.png) You can **[install the extension](https://marketplace.visualstudio.com/items?itemName=serverpod.serverpod)** from the VS Code Marketplace or search for *Serverpod* from inside VS Code. ### Install Serverpod Insights (optional) **[Serverpod Insights](https://docs.serverpod.dev/tools/insights.md)** is a companion app bundled with Serverpod. It allows you to access your server's logs and health metrics. Insights is available for Mac and Windows, but we will be adding support for Linux in the future. ![Serverpod Insights](https://docs.serverpod.dev/img/serverpod-insights.webp) --- # Quickstart https://docs.serverpod.dev/quickstart Build and deploy a full-stack Flutter and Serverpod app, hosted on the web, in 10–15 minutes. We have tested the default setup with Antigravity, Cursor, and Claude Code, but most agentic editors will work. ## Prerequisites Serverpod is tested on Mac, Windows, and Linux. Before you can install Serverpod, you need to have **[Flutter](https://flutter.dev/docs/get-started/install)** installed. Serverpod 4 requires Flutter 3.44.4 or later, which includes Dart 3.12.2. :::info Check your Flutter installation by running the following command in your terminal: ```bash $ flutter doctor ``` ::: New projects use an embedded PostgreSQL that Serverpod downloads and runs for you, so no database setup is needed. ## Install Serverpod Serverpod is installed using the Dart package manager. To install Serverpod, run the following command in your terminal: ```txt $ dart install serverpod_cli ``` Verify the installation by running: ```bash $ serverpod ``` :::warning If a previous version of Serverpod is already installed with `dart pub global activate`, deactivate it before installing or upgrading: ```txt $ dart pub global deactivate serverpod_cli ``` ::: ## Create the project Use Serverpod's `quickstart` command to create a new project. ```bash $ serverpod quickstart my_project ``` The command opens an interactive screen where you select the editor or agent you plan to use. Navigate with your mouse or use the arrow keys to move around, press `Space` to select your editor, then press `Enter` to create the project. Next, open your newly created project with your favorite AI powered editor. Open the *root* folder of the full project, for example, `my_project`, which includes the server, client, and Flutter app. :::important If you are using Cursor, you will need to **enable the Serverpod and Dart MCP servers**: open **Customize** in the sidebar, click **MCPs**, and turn on both servers. ::: ## Start the server and the app Start the server and the Flutter app by opening up a terminal window and running the [`serverpod start`](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md) command: ```bash $ serverpod start ``` After the server has started and your app has finished building, the app will open in a new browser window. Try it out by entering your name and clicking the send button. ## Build your app Instruct your AI agent to build your app. Here are a few simple prompts that you can try: **Job board** > *Build a professional job marketplace where employers post openings and manage applications, while job seekers browse listings. Use an editorial-inspired design with bold typography.* **CRM** > *Build a modern CRM for a B2B sales team. Users can manage companies, contacts, and deals. Use a sleek dark theme with vibrant accent colors, compact layouts, and data-rich dashboards.* **Social network** > *Build a modern social network where users share posts, follow each other, comment, and react. Give it a bold consumer-app style with colorful gradients, large avatars, and playful animations.* *** The agent will start building your app. It will manage your database and perform a hot reload when the app is complete. :::tip If your app uses authentication, the verification code will be displayed as an alert in Serverpod's command line interface. Press the `C` key to copy the code. ::: :::tip If the agent fails to reload the app, you can always hit the `R` key to force a restart of the server and the app. ::: ## Deploy the app to the cloud Deploy your web app and backend to **[Serverpod Cloud](https://docs.serverpod.dev/cloud.md)**, a fully managed platform built by the Serverpod team. Your first project includes a one-month free trial, with no credit card required. Create your Cloud account and create a project on the **[Serverpod Cloud Console](https://console.serverpod.dev/)**. Enable the database when you create the project if your server uses one. Then launch your app: ```bash $ serverpod cloud launch ``` This links your project to Cloud and deploys your backend along with the web build of your app. After the first launch, you redeploy any changes by running `serverpod cloud launch` again. :::tip In the **[Serverpod Cloud Console](https://console.serverpod.dev/)**, open your project and use the **Logs**, **Sessions**, and **Metrics** tabs to see what your server is doing. ::: ## Get help We welcome feedback on agent skills, MCP servers, and the `serverpod start` workflow. If you run into issues, reach out on **[Discord](https://serverpod.dev/discord)** or file an issue on **[GitHub](https://github.com/serverpod/serverpod)**. Pull requests are welcome. Happy building with Serverpod 4. --- # How Serverpod works https://docs.serverpod.dev/how-it-works With Serverpod, you write type-safe Dart on both your Flutter app and the backend. Edit a file and hit save. Serverpod regenerates the code that connects your server and Flutter app. It then hot reloads both while they run. To change the database schema, you press one key to create and apply a migration. No manual rebuilds, no Docker to set up, and no API code to write by hand. Serverpod is a full backend. It manages your database, authentication, file uploads, caching, real-time communication, scheduling, and logging. You can focus on building features instead of wiring together separate services. A project is a single workspace of Dart packages, and a code generator keeps the types shared between your server and app in sync. If you rename a field or use an incorrect type, they will show up as compile-time errors rather than a surprise when you run the app. ## Create your project A Serverpod project starts with the `serverpod create` command, which walks you through a few choices that shape what it generates: - **Project type:** A server with a Flutter app (the default), a server only, or a reusable [module](https://docs.serverpod.dev/concepts/server-fundamentals/modules.md) shared across servers. - **Database and caching:** Add a Postgres database and Redis (for pub/sub and caching). - **Authentication:** Built-in email and social sign-ins. - **Web server:** Optionally serve web pages and your Flutter web app alongside your API. - **Code editors and AI agents (optional):** Pick any of Antigravity, Claude, Codex, Cursor, OpenCode, and VS Code. Serverpod installs skills and MCP servers for each one you select. The result is one workspace. For the default project type, it has three Dart packages: ```text my_project/ ├── my_project_server/ # Your backend code. ├── my_project_client/ # Generated client code used by the app. └── my_project_flutter/ # Your Flutter app. ``` Server-only projects and modules have the same layout without the `_flutter` package. In the `_server` package you add your endpoints and data models. Serverpod's code generator generates code on the server and in the client package. You get a type-safe Dart API for your app, along with the serialization and database code on the server. You never write serialization, HTTP calls, or API contracts. For what each package holds in detail, see [Your Serverpod project](https://docs.serverpod.dev/concepts/server-fundamentals/your-serverpod-project.md). ## Run your project You run the whole project with one command: ```bash $ serverpod start ``` This starts your server, launches your Flutter app, and watches your code. When you make a change, Serverpod instantly regenerates the client, hot reloads the server, and reloads your app. :::tip Start your project before you begin building. With `serverpod start` already running, every change after that, whether you make it yourself or an agent does it, is reloaded as you go. ::: ## Write an endpoint In Serverpod, endpoints are the entry points your app calls to run code on the server. You define one as a class that extends `Endpoint`, with async methods that each take a [`Session`](https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions.md) as their first argument and return a typed `Future` (or a `Stream`, for live updates): ```dart class ExampleEndpoint extends Endpoint { Future hello(Session session, String name) async { return 'Hello $name'; } } ``` The `Session` is the context for that one call, with access to the database, cache, signed-in user, and logging. Parameters and return types can be the built-in types (`bool`, `int`, `double`, `String`, `DateTime`, `Duration`, `Uri`, `UuidValue`, `BigInt`, `ByteData`), a `List`, `Map`, `Set`, or `Record` of those, or any data model you define. See [Working with endpoints](https://docs.serverpod.dev/concepts/endpoints-and-apis.md) for more information. ## Call it from your app On the app side, the generated client turns each endpoint method into what looks like a local method: ```dart final greeting = await client.example.hello('World'); ``` The client handles the request, the response, and the JSON in between. Most calls follow this request-and-response shape. For live updates, Serverpod also has [streaming methods](https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming.md) that keep a connection open so the server and app can push data to each other. ## Define your data models Models are the data classes your app and server pass back and forth, the objects your endpoints take and return. You define them in `.spy.yaml` files, each naming a class and listing its fields: ```yaml class: Company fields: name: String foundedDate: DateTime? ``` When you generate code, this becomes a `Company` Dart class shared by the server and your app, so the same type flows from your endpoints into your Flutter widgets. Fields support the same types as endpoint methods, plus enums and nested models, and can be nullable. See [Working with models](https://docs.serverpod.dev/concepts/data-and-the-database/models.md) for the details. Add a `table` key to also store the model in a database table: ```yaml class: Company table: company fields: name: String foundedDate: DateTime? ``` The generated `Company` class then gains a `db` field with type-safe methods for reading and writing rows, so you query the database in Dart instead of SQL. For example, insert a row and read it back: ```dart final company = await Company.db.insertRow(session, Company(name: 'Serverpod')); final stored = await Company.db.findById(session, company.id!); ``` These run on the same `session` your endpoint method receives. When you change a table model, press `M` in the `serverpod start` terminal to create the migration and apply it. Pending migrations also apply on startup. That database runs without setup on your part: Serverpod manages an embedded Postgres for you, with no Docker to configure. If you would rather manage Postgres yourself, you can change the configuration in the server's `config` directory. See [CRUD](https://docs.serverpod.dev/concepts/data-and-the-database/database/crud.md) for the generated query methods, [relations](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations.md) for linking tables, and [transactions](https://docs.serverpod.dev/concepts/data-and-the-database/database/transactions.md) for atomic operations. ## Build with an AI agent Serverpod is built to work with AI coding agents. You pick your agent when you create the project, and Serverpod installs skills that teach it how a Serverpod project fits together. While `serverpod start` runs, it exposes an MCP server that lets the agent work with your live project. It can write endpoints and models, manage the database, and reload the app as it goes. You describe the feature you want, and the agent builds it. Your data models and endpoint methods are the single source of truth. When the agent renames a field or changes its type, the regenerated code treats the mismatch as a compile-time error rather than a runtime bug. If you want to quickly try out Serverpod and build something with an AI agent, check out our [Quickstart](https://docs.serverpod.dev/quickstart.md) guide. ## Next steps Ready to build something for real? Follow [Build your first app](https://docs.serverpod.dev/get-started/creating-endpoints.md) to create your first endpoint and call it from Flutter. When you are ready to ship, [Deploy to Serverpod Cloud](https://docs.serverpod.dev/deployments/deploy-to-serverpod-cloud.md) takes your app to production in just a few minutes. --- # Create your first endpoint https://docs.serverpod.dev/get-started/creating-endpoints You'll build a recipe generator: a Serverpod endpoint that takes a list of ingredients, asks Google's Gemini API for a recipe, and returns it to your Flutter app. Along the way you'll see that calling your server is as simple as calling a local method. Prefer to have an AI agent build an app for you? Follow the [Quickstart](https://docs.serverpod.dev/quickstart.md) instead. This guide takes the hands-on path: you'll build the recipe app yourself, so you understand each piece. :::info The server is the right place for work you can't or shouldn't do in the Flutter app, such as calling an API secured by a secret key, accessing a database, or sending push notifications and emails. Here, it keeps your Gemini API key off the client. ::: ## Before you start - [Serverpod installed](https://docs.serverpod.dev/installation.md). Run `serverpod version` to confirm it works. - A free Gemini API key. On [Google AI Studio](https://aistudio.google.com/app/apikey), sign in with a Google account and click **Create API key**. ## Create the project Use `serverpod create` to generate a new project with a server, a client, and a Flutter app: ```bash $ serverpod create magic_recipe ``` The command is interactive. Step through the prompts, accepting the defaults. Open the project's **root** folder (`magic_recipe`) in your editor, not one of the sub-packages. This keeps the analyzer in sync when code is generated and makes it easy to move between the server and app. ### Add your Gemini API key Gemini is Google's generative AI model. Your server sends it the ingredients and gets a recipe back, and the API key authenticates those calls. Add your key to `config/passwords.yaml` in the server package. Git ignores this file, so your key stays out of version control. ```yaml # magic_recipe_server/config/passwords.yaml development: geminiApiKey: '--- Your Gemini Api Key ---' ``` Then add the Dartantic AI package to the server. It provides a single interface for talking to AI providers, including Gemini: ```bash $ cd magic_recipe_server $ dart pub add dartantic_ai ``` ## Start the app From the project's root folder, start everything with one command: ```bash $ serverpod start ``` `serverpod start` generates your code, starts the server with its built-in PostgreSQL database (no Docker required), and opens the Flutter app in your default browser. The app that opens is the default Serverpod starter: enter your name and click the send button (or press Enter), and the server responds with a greeting. Leave `serverpod start` running. It watches your project, so every time you save a file it regenerates the necessary code and hot reloads the app. You'll rely on this for the rest of the guide instead of restarting anything by hand. ## Add an endpoint Server endpoints live in `lib/src//`, like the `greetings` endpoint the template generated. Create a file at `magic_recipe_server/lib/src/recipes/recipe_endpoint.dart`: ```dart import 'package:dartantic_ai/dartantic_ai.dart'; import 'package:serverpod/serverpod.dart'; /// This is the endpoint that will be used to generate a recipe using the /// Google Gemini API. It extends the Endpoint class and implements the /// generateRecipe method. class RecipeEndpoint extends Endpoint { /// Pass in a string containing the ingredients and get a recipe back. Future generateRecipe(Session session, String ingredients) async { final geminiApiKey = session.passwords['geminiApiKey']; if (geminiApiKey == null) { throw Exception('Gemini API key not found'); } // Configure the Dartantic AI agent for Gemini before sending the prompt. final agent = Agent.forProvider( GoogleProvider(apiKey: geminiApiKey), chatModelName: 'gemini-3.5-flash-lite', ); final prompt = 'Generate a recipe using the following ingredients: $ingredients. ' 'Always put the title of the recipe in the first line, followed by the ' 'instructions. The recipe should be easy to follow and include all ' 'necessary steps.'; final response = await agent.send(prompt); final responseText = response.output; if (responseText.isEmpty) { throw Exception('No response from Gemini API'); } return responseText; } } ``` The endpoint reads your Gemini key from `session.passwords`, which Serverpod populates from the `passwords.yaml` file you edited earlier. :::info Endpoint methods take a `Session` as their first parameter and return a typed `Future` or `Stream`. You can pass and return primitive types or any [model defined in a `.spy.yaml` file](https://docs.serverpod.dev/concepts/data-and-the-database/models.md). The class name's `Endpoint` suffix is dropped on the client, so `RecipeEndpoint` is called via `client.recipe`. See [How it works](https://docs.serverpod.dev/how-it-works.md) for how that call reaches the server. ::: Save the file. Because `serverpod start` is watching, it regenerates the client bindings for `generateRecipe` automatically. You'll see it run in the terminal. ## Call it from your app Your app's UI lives in `magic_recipe_flutter/lib/screens/`, where the template already added a `GreetingsScreen`. Add a recipe screen alongside it. Create `magic_recipe_flutter/lib/screens/recipe_screen.dart`: ```dart import 'package:flutter/material.dart'; import '../client.dart'; import 'greetings_screen.dart'; class RecipeScreen extends StatefulWidget { const RecipeScreen({super.key}); @override State createState() => _RecipeScreenState(); } class _RecipeScreenState extends State { /// Holds the last result, or null if there's no result yet. String? _resultMessage; /// Holds the last error message, or null if there's no error yet. String? _errorMessage; final _textEditingController = TextEditingController(); bool _loading = false; void _callGenerateRecipe() async { try { setState(() { _errorMessage = null; _resultMessage = null; _loading = true; }); final result = await client.recipe.generateRecipe( _textEditingController.text, ); setState(() { _resultMessage = result; _loading = false; }); } catch (e) { setState(() { _errorMessage = '$e'; _loading = false; }); } } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(16), child: Column( children: [ TextField( controller: _textEditingController, decoration: const InputDecoration( hintText: 'Enter your ingredients', ), ), const SizedBox(height: 16), ElevatedButton( onPressed: _loading ? null : _callGenerateRecipe, child: _loading ? const Text('Loading...') : const Text('Generate Recipe'), ), const SizedBox(height: 16), Expanded( child: SingleChildScrollView( child: ResultDisplay( resultMessage: _resultMessage, errorMessage: _errorMessage, ), ), ), ], ), ); } } ``` `client` comes from `client.dart`, where the template already wired it to talk to your server, and `ResultDisplay` is reused from `greetings_screen.dart`. Now show the recipe screen instead of the greeting demo. In `magic_recipe_flutter/lib/main.dart`, add the import: ```dart import 'screens/recipe_screen.dart'; ``` Then, in the `MyHomePage` widget, change the body from `GreetingsScreen` to `RecipeScreen`: ```dart body: const RecipeScreen(), ``` Save. UI edits like this would normally hot reload, but adding the endpoint also changed the generated client. The app's `client` is created once by `initializeClient()` in `main()`, which only re-runs on a restart, so the app needs a hot restart to pick up the new `client.recipe` endpoint. In the `serverpod start` terminal: - Press **R** to hot restart. Then enter some ingredients and tap **Generate Recipe**. The app calls your endpoint and displays the result: ![Example Flutter App](https://docs.serverpod.dev/img/getting-started/endpoint-chrome-result.png) ## Next steps You've created an endpoint and called it from your app, passing a string back and forth. Next, you'll return structured data using a Serverpod model, with serialization handled for you. Leave `serverpod start` running; you'll keep building on the same app. --- # Create data models https://docs.serverpod.dev/get-started/models-and-data On the [previous page](https://docs.serverpod.dev/get-started/creating-endpoints.md) your endpoint returned a plain string. Here you'll define a `Recipe` model so the server returns structured, typed data instead. You define the model once in YAML, and Serverpod generates the Dart class plus all the serialization needed between server and client. Keep `serverpod start` running from the previous page. ## Define a model Serverpod models are declared in `.spy.yaml` files anywhere under the server's `lib` directory. Create `magic_recipe_server/lib/src/recipes/recipe.spy.yaml`: ```yaml ### Our AI generated Recipe class: Recipe fields: ### The author of the recipe author: String ### The recipe text text: String ### The date the recipe was created date: DateTime ### The ingredients the user passed in ingredients: String ``` Save the file. `serverpod start` regenerates the `Recipe` class for both the server and the client. :::info Fields can be primitive types, other models, or a typed `List`, `Map`, or `Set`. See [Working with models](https://docs.serverpod.dev/concepts/data-and-the-database/models.md) for the full set of options. ::: ## Return the model from your endpoint Now update `recipe_endpoint.dart` to return a `Recipe` instead of a `String`. Import the generated models at the top of the file: ```dart import '../generated/protocol.dart'; ``` Change the method's return type from `Future` to `Future`: ```dart Future generateRecipe(Session session, String ingredients) async { ``` Then replace `return responseText;` with a constructed `Recipe`: ```dart return Recipe( author: 'Gemini', text: responseText, date: DateTime.now(), ingredients: ingredients, ); ``` Save the file. The client regenerates so `generateRecipe` now returns a `Recipe`. ## Show it in your app In `recipe_screen.dart`, the call now returns a `Recipe` object, so assigning `result` to a string no longer compiles. `_resultMessage` itself stays a `String?`; you just format the `Recipe`'s fields into it. In `_callGenerateRecipe`, update the success `setState`: ```dart setState(() { _resultMessage = '${result.author} on ${result.date}:\n${result.text}'; _loading = false; }); ``` Save. Because the model and the endpoint's return type changed, the generated client changed too, so hot reload isn't enough. In the `serverpod start` terminal: - Press **R** to hot restart. Enter some ingredients and tap **Generate Recipe**. The result now shows the author and date alongside the recipe: ![Flutter Recipe App](https://docs.serverpod.dev/img/getting-started/flutter-web-ingredients.png) ## Next steps Your endpoint now returns structured data. Next, you'll store recipes in the database so they persist between sessions, and list them in the app. --- # Manage the database https://docs.serverpod.dev/get-started/working-with-the-database Right now your recipes disappear when the Flutter app reloads. Here you'll store them in the database so they persist, and list previously generated recipes in the app. Serverpod maps your model to a table and gives you a type-safe API to read and write rows, without writing any SQL. Keep `serverpod start` running from the previous page. ## Map the model to a table Add the `table` keyword to your `Recipe` model in `recipe.spy.yaml`. This maps the model to a database table called `recipes`: ```yaml ### Our AI generated Recipe class: Recipe ### The database table that stores recipes table: recipes fields: ### The author of the recipe author: String ### The recipe text text: String ### The date the recipe was created date: DateTime ### The ingredients the user passed in ingredients: String ``` Save the file. The regenerated `Recipe` class now exposes database methods through `Recipe.db`. :::info See the [Model reference](https://docs.serverpod.dev/concepts/lookups/model-reference.md) for all the keywords you can use in a table. ::: ## Create and apply the migration Changing the schema requires a [migration](https://docs.serverpod.dev/concepts/data-and-the-database/database/migrations.md): a set of SQL steps that bring the database up to date with your models. The `serverpod start` terminal has shortcuts for this, listed along the bottom. With that terminal focused: ![serverpod start tui](https://docs.serverpod.dev/img/getting-started/tui-logs.png) - Press **M** to create the migration and apply it, which creates the `recipes` table in your database. ## Save recipes to the database Now that `Recipe` is a table, you can write rows. In `recipe_endpoint.dart`, save the generated recipe before returning it. Replace the `return Recipe(...)` you added earlier with: ```dart final recipe = Recipe( author: 'Gemini', text: responseText, date: DateTime.now(), ingredients: ingredients, ); return Recipe.db.insertRow(session, recipe); ``` The `insertRow` method returns the saved row with its `id` populated by the database. ## List past recipes Add a second method to the endpoint that returns every saved recipe, newest first: ```dart /// Returns all recipes saved in the database, most recent first. Future> getRecipes(Session session) async { return Recipe.db.find(session, orderBy: (t) => t.date.desc()); } ``` :::info Both `insertRow` and `find` are Serverpod's typed database methods. See [CRUD](https://docs.serverpod.dev/concepts/data-and-the-database/database/crud.md) for the full set of operations. ::: ## Show the saved recipes in your app Update `recipe_screen.dart` to load past recipes when it opens and list them next to the generator. Replace the file with: ```dart import 'package:flutter/material.dart'; import 'package:magic_recipe_client/magic_recipe_client.dart'; import '../client.dart'; import 'greetings_screen.dart'; class RecipeScreen extends StatefulWidget { const RecipeScreen({super.key}); @override State createState() => _RecipeScreenState(); } class _RecipeScreenState extends State { /// The recipe currently shown, or null if there's none yet. Recipe? _recipe; /// Recipes loaded from the database, most recent first. List _recipeHistory = []; /// The last error message, or null if there's no error. String? _errorMessage; final _textEditingController = TextEditingController(); bool _loading = false; @override void initState() { super.initState(); client.recipe.getRecipes().then((recipes) { setState(() => _recipeHistory = recipes); }); } void _callGenerateRecipe() async { try { setState(() { _errorMessage = null; _recipe = null; _loading = true; }); final result = await client.recipe.generateRecipe( _textEditingController.text, ); setState(() { _recipe = result; _recipeHistory = [result, ..._recipeHistory]; _loading = false; }); } catch (e) { setState(() { _errorMessage = '$e'; _loading = false; }); } } @override Widget build(BuildContext context) { return Row( children: [ Expanded( child: ListView.builder( itemCount: _recipeHistory.length, itemBuilder: (context, index) { final recipe = _recipeHistory[index]; return ListTile( title: Text(recipe.text.split('\n').first), subtitle: Text('${recipe.author} - ${recipe.date}'), onTap: () => setState(() => _recipe = recipe), ); }, ), ), Expanded( flex: 3, child: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ TextField( controller: _textEditingController, decoration: const InputDecoration( hintText: 'Enter your ingredients', ), ), const SizedBox(height: 16), ElevatedButton( onPressed: _loading ? null : _callGenerateRecipe, child: _loading ? const Text('Loading...') : const Text('Generate Recipe'), ), const SizedBox(height: 16), Expanded( child: SingleChildScrollView( child: ResultDisplay( resultMessage: _recipe != null ? '${_recipe!.author} on ${_recipe!.date}:\n${_recipe!.text}' : null, errorMessage: _errorMessage, ), ), ), ], ), ), ), ], ); } } ``` The new `import 'package:magic_recipe_client/magic_recipe_client.dart';` line brings in the `Recipe` class Serverpod generated from your model. It's the same class the server uses, so when you read `recipe.author`, `recipe.text`, or `recipe.date` in the app, the field names and types are guaranteed to match the server. You added a new endpoint method (`getRecipes`), so the generated client changed. In the `serverpod start` terminal: - Press **R** to hot restart. Generate a few recipes, then reload the page. They're still there, loaded from the database: ![Final result](https://docs.serverpod.dev/img/getting-started/final-result.png) ## Next steps Your app now persists data. With the recipe generator working end to end, the last step is to put it online: next, you'll deploy it to Serverpod Cloud. --- # Deploy your app https://docs.serverpod.dev/get-started/deployment Your recipe app runs locally. The last step is to put it online. The recommended path is [Serverpod Cloud](https://docs.serverpod.dev/cloud.md), which hosts your server and database with zero configuration. ## Deploy to Serverpod Cloud From your project's root folder, run: ```bash $ serverpod cloud launch ``` The command walks you through these steps: 1. If the Serverpod Cloud CLI isn't installed yet, `serverpod cloud` installs it first. 2. If you aren't signed in, your browser opens so you can sign in. 3. For a new project, the Cloud Console opens so you can create it. Keep the database enabled there, because the recipe app stores its recipes in it. Cloud provisions a managed Postgres database, separate from the embedded one `serverpod start` runs locally. 4. You choose which custom passwords from `config/passwords.yaml` to copy to Cloud. That file stays on your machine and is never deployed. Select `geminiApiKey` so the deployed server can call Gemini. It isn't selected by default, because it sits in the `development` section. 5. The command deploys your server along with the web build of your app. The first upload includes your Flutter web build and can exceed the default timeout on a slower connection. If the upload times out, retry with a higher limit, for example, `serverpod cloud launch --timeout 600s`. If you didn't select `geminiApiKey`, set the key as a secret. Then redeploy so the server picks it up: ```bash $ serverpod cloud password set geminiApiKey "your-gemini-api-key" $ serverpod cloud deploy ``` Whenever you make changes later, redeploy with: ```bash $ serverpod cloud deploy ``` See the [Serverpod Cloud documentation](https://docs.serverpod.dev/cloud.md) for the full walkthrough, including custom domains, logs, and your free trial. ## Other deployment options Prefer to host the server yourself? See [Custom hosting](https://docs.serverpod.dev/deployments/custom-hosting/choosing-a-strategy.md) for running on a server cluster, a serverless platform, or your own machine. ## What you've built You've built and deployed a full-stack app with Flutter and Serverpod: - A custom endpoint that calls an external API from the server. - A type-safe data model shared between the server and the Flutter app. - Persistent storage with the database. - A Flutter app that talks to your server through the generated client. We're excited to see what you'll build next. If you need help, join the [Discord community](https://serverpod.dev/discord) or ask in our [community on GitHub](https://github.com/serverpod/serverpod/discussions). To go deeper into any topic, browse the [Concepts](https://docs.serverpod.dev/concepts/endpoints-and-apis.md) section. --- # Serverpod Academy https://docs.serverpod.dev/serverpod-academy [Serverpod Academy](https://academy.serverpod.dev) is a free video course on the Serverpod framework. Get Started gets you to a running server; the Academy is the deep dive. The course is an introduction to professional backend development. You'll cover the concerns you'd hit shipping a real app: databases, security, caching, and unit testing. **Ground Training**, the 4-hour course, takes you beyond Get Started into: - Authentication - Real-time streams - File uploads - Task scheduling - Caching You finish with a working app and a certificate. **[Join the Academy](https://academy.serverpod.dev)** --- # Backend fundamentals https://docs.serverpod.dev/tutorials/fundamentals This video explains the fundamentals of backend development, how a database works, and how to get started with Serverpod. It is a great starting point for a Flutter developer new to working with backends and servers. [Watch the video](https://www.youtube-nocookie.com/embed/FwttjcKyWFk) --- # Real-time communication https://docs.serverpod.dev/tutorials/real-time-communication Have you ever found real-time communication in apps challenging? It doesn't have to be. Today, we're diving into how to build a collaborative drawing experience using Flutter and Serverpod. We'll call our app Pixorama - a fun and interactive project inspired by Reddit's r/place. Pixorama lets users draw together on a shared grid, with every pixel placed updating in real-time across all connected devices. [Watch the video](https://www.youtube-nocookie.com/embed/iCDeAvuMj8I) *This tutorial is also available as a video.* :::info Before you begin, make sure that you have [installed Serverpod](https://docs.serverpod.dev/installation.md). ::: You can try out the final app here: [https://pixorama.live](https://pixorama.live) ![Serverpod Insights](https://docs.serverpod.dev/img/tutorial/pixorama/pixorama.png) ## What is Pixorama? Pixorama is a collaborative drawing app where users can place pixels on a grid to create images together. Imagine two instances of the app running simultaneously - draw a pixel on one and watch it instantly appear on the other. This seamless synchronization happens because each time you draw a pixel, a message is sent to the server, which then broadcasts it to all connected clients. ## Understanding real-time communication In traditional REST APIs, communication with the server involves sending a request and receiving a response. However, real-time communication requires the server to push updates to clients as they happen. This is commonly achieved using web sockets, which maintain an open connection between the server and client, allowing for continuous data exchange. While web sockets can be tricky, requiring data serialization and connection management, Serverpod simplifies this process. With the release of Serverpod 2.1, a new feature called [streaming methods](https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming.md) was introduced. This feature allows us to return a stream from a server method and call it from our app. Serverpod handles the underlying web socket connection for us. Now, let's get started with building Pixorama. ## Setting up the project We begin by creating a new project with the `serverpod create` command. Pixorama keeps its image in memory, so it doesn't need a database. In the interactive setup, deselect **Database (recommended)** under **Database & caching** and keep the other defaults: ```bash serverpod create pixorama ``` If you run the command non-interactively, pass `--no-interactive --no-database --no-redis --ide none` instead. The last two flags match the interactive defaults. Without them, the command adds Redis and AI agent setup for Claude, Cursor, and VS Code. Now, let's open the project in VS Code and explore the structure. The server code resides in the `pixorama_server` package. We'll start by creating models - classes that we can serialize and pass between the client and server. Our models will be placed in the `lib/src/models` directory. ## Creating models First, we remove the template's example feature from `pixorama_server`, as we won't need it. Delete the `lib/src/greetings` directory and its test, `test/integration/greeting_endpoint_test.dart`. We'll create two new models: `ImageData` and `ImageUpdate`. Place them in the `lib/src/models` directory and call them `image_data.spy.yaml` and `image_update.spy.yaml`. ```yaml # lib/src/models/image_data.spy.yaml class: ImageData fields: pixels: ByteData width: int height: int ``` The `ImageData` model represents the entire image that will be sent to the app when it connects to the server. It stores the image's pixels as ByteData, where each byte represents a pixel. Additionally, it includes the image's width and height. ```yaml # lib/src/models/image_update.spy.yaml class: ImageUpdate fields: pixelIndex: int colorIndex: int ``` The `ImageUpdate` model captures changes to individual pixels, including the pixel's index in the byte array and its new color value. With our models defined, start the project so Serverpod generates the Dart classes for them. Run the command from the project root and leave it running: it regenerates code and hot reloads the server every time you save a file. ```bash serverpod start ``` ## Building the server Next, we'll build the server. We need to create a new endpoint. An endpoint is a connection point for the client to interact with the server. In Serverpod, you create endpoints by extending the `Endpoint` class and placing it in the `lib/src/endpoints` directory. The endpoint will manage our pixel data and handle client updates. We will start by creating a `PixoramaEndpoint` class, which we place in a file called `pixorama_endpoint.dart` in the `lib/src/endpoints` directory. ```dart // lib/src/endpoints/pixorama_endpoint.dart import 'dart:typed_data'; import 'package:serverpod/serverpod.dart'; class PixoramaEndpoint extends Endpoint { static const _imageWidth = 64; static const _imageHeight = 64; static const _numPixels = _imageWidth * _imageHeight; static const _numColorsInPalette = 16; static const _defaultPixelColor = 2; final _pixelData = Uint8List(_numPixels) ..fillRange( 0, _numPixels, _defaultPixelColor, ); } ``` We define a number of constants that define the dimensions of our image. We represent the image itself with a `Uint8List`. Each byte in the list will be a pixel in our image. ### Handling pixel updates The core functionality of Pixorama lies in how the server passes image data to clients and keeps them updated. Serverpod's built-in messaging system, Message Central, allows us to publish and subscribe to events. We create a channel named `pixel-added` to handle pixel updates in our `PixoramaEndpoint` class. ```dart // lib/src/endpoints/pixorama_endpoint.dart class PixoramaEndpoint extends Endpoint { // ... static const _channelPixelAdded = 'pixel-added'; } ``` When a user draws a pixel, the `setPixel` endpoint method is called. This method verifies the validity of the input (ensuring the color index is within the valid range and the pixel index is within bounds). If valid, it updates our pixel data and broadcasts the update to all listeners within the server via the `pixel-added` channel. ```dart // lib/src/endpoints/pixorama_endpoint.dart // Here we need to import the model files from our generated protocol. import 'package:pixorama_server/src/generated/protocol.dart'; class PixoramaEndpoint extends Endpoint { // ... static const _channelPixelAdded = 'pixel-added'; /// Sets a single pixel and notifies all connected clients about the change. Future setPixel( Session session, { required int colorIndex, required int pixelIndex, }) async { // Check that the input parameters are valid. If not, throw a // `FormatException`, which will be logged and thrown as // `ServerpodClientException` in the app. if (colorIndex < 0 || colorIndex >= _numColorsInPalette) { throw FormatException('colorIndex is out of range: $colorIndex'); } if (pixelIndex < 0 || pixelIndex >= _numPixels) { throw FormatException('pixelIndex is out of range: $pixelIndex'); } // Update our global image. _pixelData[pixelIndex] = colorIndex; // Notify all connected clients that we set a pixel, by posting a message // to the _channelPixelAdded channel. session.messages.postMessage( _channelPixelAdded, ImageUpdate( pixelIndex: pixelIndex, colorIndex: colorIndex, ), ); } } ``` Finally, we create an `imageUpdates` method, which returns a stream of updates to clients. This method first sends the full image data to the client, followed by any subsequent pixel updates. This method will listen to updates from our `pixel-added` channel and relay them to the client. By creating the stream from the message channel before sending the first update to our client, we ensure no message risks being lost between the first full update and the first individual pixel being sent. ```dart // lib/src/endpoints/pixorama_endpoint.dart class PixoramaEndpoint extends Endpoint { // ... /// Returns a stream of image updates. The first message will always be a /// `ImageData` object, which contains the full image. Sequential updates /// will be `ImageUpdate` objects, which contains a single updated pixel. Stream imageUpdates(Session session) async* { // Request a stream of updates from the pixel-added channel in // MessageCentral. var updateStream = session.messages.createStream(_channelPixelAdded); // Yield a first full image to the client. yield ImageData( pixels: _pixelData.buffer.asByteData(), width: _imageWidth, height: _imageHeight, ); // Relay all individual pixel updates from the pixel-added channel to // the client. await for (var imageUpdate in updateStream) { yield imageUpdate; } } } ``` That's all the code we need to write for the server side. With `serverpod start` running, the new endpoint is generated into the client package as soon as you save. If you're not running it, run `serverpod generate` in `pixorama_server` instead. ## Building the Flutter app With the server side complete, it's time to build the Flutter app. When we created the project, Serverpod set up a basic Flutter app for us in the `pixorama_flutter` package. Since we removed the greeting endpoint, delete `lib/screens/greetings_screen.dart`, which used it. Then add the pixels package to draw our pixel editor. Import it by running the following command in your `pixorama_flutter` directory: ```bash cd pixorama_flutter flutter pub add pixels ``` Next, let's open the `main.dart` file. The template already creates the `client` in `lib/client.dart` and initializes it in `main()`, so we keep that part. Rename the `MyHomePage` class to `PixoramaApp`, remove the demo code, and replace it with a `Scaffold` containing a `Pixorama` widget. This is our new main file: ```dart // lib/main.dart import 'package:flutter/material.dart'; import 'client.dart'; import 'src/pixorama.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await initializeClient(); runApp(const PixoramaApp()); } class PixoramaApp extends StatelessWidget { const PixoramaApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Pixorama', theme: ThemeData( primarySwatch: Colors.blue, ), home: Scaffold( body: const Pixorama(), ), ); } } ``` Now, we will create the `Pixorama` widget. This is where all the drawing magic will happen. Create a new file called `pixorama.dart` and place it in `lib/src`. Start by creating a new stateful widget called `Pixorama`. We are also importing a few of the packages and files we are going to use: ```dart // lib/src/pixorama.dart import 'package:flutter/material.dart'; import 'package:pixels/pixels.dart'; import 'package:pixorama_client/pixorama_client.dart'; import '../client.dart'; class Pixorama extends StatefulWidget { const Pixorama({super.key}); @override State createState() => _PixoramaState(); } class _PixoramaState extends State { } ``` The `Pixorama` widget draws the image using the `PixelEditor` from the `pixels` package. A `PixelImageController` manages the pixel data, and in the `initState` method, we call a `_listenToUpdates` method to connect to the server and listen for updates. Let's add the `PixelImageController` and `initState` method to our `_PixoramaState` class: ```dart // lib/src/pixorama.dart class _PixoramaState extends State { // The pixel image controller contains our image data and handles updates. // If it is null, the image has not yet been loaded from the server. PixelImageController? _imageController; @override void initState() { super.initState(); // Connect to the server and start listening to updates. _listenToUpdates(); } } ``` Next, let's implement the `_listenToUpdates` method. The `_listenToUpdates` method runs indefinitely, maintaining a connection to the server and processing updates as they arrive. It handles both `ImageData` (the full image) and `ImageUpdate` (individual pixel changes), updating the `PixelImageController` accordingly. If the connection is lost, it will wait 5 seconds before it tries to reconnect to the server. ```dart // lib/src/pixorama.dart class _PixoramaState extends State { // ... Future _listenToUpdates() async { // Indefinitely try to connect and listen to updates from the server. while (true) { try { // Get the stream of updates from the server. final imageUpdates = client.pixorama.imageUpdates(); // Listen for updates from the stream. The await for construct will // wait for a message to arrive from the server, then run through the // body of the loop. await for (final update in imageUpdates) { // Check which type of update we have received. if (update is ImageData) { // This is a complete image update, containing all pixels in the // image. Create a new PixelImageController with the pixel data. setState(() { _imageController = PixelImageController( pixels: update.pixels, palette: PixelPalette.rPlace(), width: update.width, height: update.height, ); }); } else if (update is ImageUpdate) { // Got an incremental update of the image. Just set the single // pixel. _imageController?.setPixelIndex( pixelIndex: update.pixelIndex, colorIndex: update.colorIndex, ); } } } on MethodStreamException catch (_) { // We lost the connection to the server, or failed to connect. setState(() { _imageController = null; }); } // Wait 5 seconds until we try to connect again. await Future.delayed(Duration(seconds: 5)); } } } ``` Worth noting is that the `MethodStreamException` is a superclass of a set of more detailed exceptions. It's often sufficient to catch all types of failures (like what we do here) that can happen when streaming data, but it's possible to detect if the stream failed because we failed to connect, if the server went down, or if the connection was lost. ### Building the Interface Finally, we need to implement the widget's `build` method, where we create the user interface for drawing pixels. We display a progress indicator if the `_imageController` is `null` (indicating no image has been received yet). Once the image is received, we use the `PixelEditor` widget to render it, and any pixel changes made by the user are sent to the server via the `setPixel` method. ```dart // lib/src/pixorama.dart class _PixoramaState extends State { // ... @override Widget build(BuildContext context) { return Center( child: _imageController == null ? const CircularProgressIndicator() : PixelEditor( controller: _imageController!, onSetPixel: (details) { // When a user clicks a pixel we will get a callback from the // PixelImageController, with information about the changed // pixel. When that happens we call the setPixels method on // the server. client.pixorama.setPixel( pixelIndex: details.tapDetails.index, colorIndex: details.colorIndex, ); }, ), ); } } ``` ## Running Pixorama If you kept `serverpod start` running, the server and the app have already picked up your changes. Otherwise, start them from the project root: ```bash serverpod start ``` `serverpod start` runs the server and opens the Flutter app, and hot reloads both as you save changes. :::info To pick the device yourself, run `serverpod start --no-flutter`, then change to the `pixorama_flutter` directory and run `flutter run -d chrome`. Running `flutter run` next to the app `serverpod start` opened also works: each instance is another drawing client. ::: You can also start a second instance of the app to see real-time updates reflected across both instances. ## Conclusion This project was a brief introduction to building real-time apps with Flutter and Serverpod. With less than a page of code on the server side, we created a collaborative drawing app that's both fun and functional. You can find the full Pixorama code on GitHub here: [https://github.com/serverpod/pixorama](https://github.com/serverpod/pixorama) Happy coding! --- # Integrating AI & RAG https://docs.serverpod.dev/tutorials/ai-and-rag Are you curious about how best to include AI in your Flutter app? In this talk, we will explore how you can use Serverpod to stream content generated by Gemini to your Flutter app in real-time. We will also dive into calling the Gemini APIs from Dart and linking them to your custom knowledge database using retrieval-augmented generation (RAG) and a vector database. We build a practical example where you can chat with the Flutter and Serverpod documentation, pulling in data from official docs, issues, and discussions on GitHub. [Watch the video](https://www.youtube-nocookie.com/embed/NJ9H1kmwhuE) --- # Support & community https://docs.serverpod.dev/support Welcome to the Serverpod community! We offer several ways to get help, stay informed, and connect with other developers. If you get stuck, you can get support through our GitHub community. The authors of Serverpod are checking in pretty much every day and helping out as much as we can. **[Public discussion board](https://github.com/serverpod/serverpod/discussions)** ## Reporting issues Serverpod is a work in progress, and there may be issues that we aren't aware of. If you run into something that doesn't seem to work the way you expect it to, please file an issue so that we can get it fixed as soon as possible. **[Serverpod issue tracker](https://github.com/serverpod/serverpod/issues)** ## Stay up-to-date We set up a mailing list to keep you up-to-date with any news and updates around Serverpod. We send an email about once a month or when something significant happens. **[Join the email list](https://serverpod.news)** ## Community updates Stay connected with our video content! We regularly publish videos on YouTube covering: - Announcements of major new features - Step-by-step tutorials - Tips and best practices - Community highlights **[Follow our YouTube channel](https://www.youtube.com/@serverpod)** --- # Your Serverpod project https://docs.serverpod.dev/concepts/server-fundamentals/your-serverpod-project A Serverpod project is one workspace holding three Dart packages: your server, a generated client, and your Flutter app. You write data models and endpoints on the server; Serverpod generates everything between them, so your app calls the server through typed Dart methods instead of hand-written networking code. Knowing what lives where makes every other page in this section easier to follow. Running `serverpod create myproject` generates one workspace with three packages: ```text myproject/ ├── myproject_server/ # Your server: endpoints, models, config, migrations ├── myproject_client/ # Generated client: how your app calls the server └── myproject_flutter/ # Your Flutter app ``` That is the whole mental model: you work in the server and the app, and Serverpod maintains the client between them. Everything else in the workspace supports those three, and the sections below introduce each piece as you need it. Expand the full workspace tree. ```text myproject/ ├── pubspec.yaml # Workspace root: one `dart pub get` resolves all packages ├── AGENTS.md # Instructions for AI agents working in the project ├── .github/ # CI workflows: analyze, format, and test ├── .vscode/ # Attach-to-server debug configs and a serverpod start task, plus mcp.json if you picked VS Code ├── .mcp.json, .claude/ # If you picked Claude: MCP config and skills ├── .cursor/ # If you picked Cursor: MCP config and skills ├── .codex/ # If you picked Codex: MCP config ├── opencode.json, .opencode/ # If you picked OpenCode: MCP config and skills ├── .agents/ # If you picked VS Code, Codex, or Antigravity: skills and Antigravity's MCP config ├── myproject_server/ # Your server │ ├── bin/main.dart # Entry point, calls run() in lib/server.dart │ ├── lib/server.dart # The run() function: creates and starts the server │ ├── lib/src/greetings/ # Example feature: a model file and an endpoint │ ├── lib/src/auth/ # Endpoints for the scaffolded email sign-in │ ├── lib/src/web/ # Routes and widgets for the web server │ ├── lib/src/generated/ # Generated server code (do not edit) │ ├── config/ # Run-mode configs, passwords, generator.yaml │ ├── migrations/ # Database migrations │ ├── web/ # Static files and pages the web server serves │ ├── test/integration/ # Endpoint tests and generated test tools │ └── docker-compose.yaml # Container alternative for the database, plus the test database ├── myproject_client/ # Generated client package │ └── lib/src/protocol/ # Generated calls and models (do not edit) └── myproject_flutter/ # Your Flutter app ├── lib/main.dart # App entry point: initializes the client, then runs the app ├── lib/client.dart # Creates the global Client that talks to the server ├── lib/screens/ # Scaffolded sign-in and greetings screens ├── lib/driver.dart # Entry point serverpod start uses to launch the app └── assets/config.json # The server URL the app reads at startup ``` The exact set depends on your create-time choices: the auth endpoints and sign-in screen come with authentication, the web pieces with the web server option, and the MCP configs and skills with the editors you picked. The `AGENTS.md` file is always created. ## How the packages relate The Flutter app depends on the client package, and the client is how the app reaches the server: it holds a typed method for every endpoint you write. Neither the server nor the client depends on the other; instead, the server's `config/generator.yaml` points at the client package (`client_package_path`), and code generation writes the client's contents directly. The workspace is a Dart pub workspace: each package declares `resolution: workspace`, and a single `dart pub get` at the project root resolves all three. ## Where your code lives, and what is generated You write two kinds of source files on the server, and both can live anywhere under the server's `lib/`: - **Model files** (`.spy.yaml`) define your serializable classes and database tables. The template keeps them next to the code that uses them, such as `lib/src/greetings/greeting.spy.yaml`. See [Working with models](https://docs.serverpod.dev/concepts/data-and-the-database/models.md). - **Endpoints** are Dart classes extending `Endpoint`, such as `lib/src/greetings/greeting_endpoint.dart`. Each public method becomes a callable client method: `GreetingEndpoint.hello` is called as `client.greeting.hello(...)`. See [Working with endpoints](https://docs.serverpod.dev/concepts/endpoints-and-apis.md). The scaffolded project follows the same pattern beyond the greeting example: `lib/src/auth/` holds the endpoints behind the default email sign-in, and `lib/src/web/` holds the routes the web server serves. From those, `serverpod generate` produces the bridge between server and app: - On the server, `lib/src/generated/` gets the endpoint dispatch code, the protocol description, the pre-configured `Serverpod` class the server starts from, and a Dart class per model. - In the client package, `lib/src/protocol/` gets the `Client` class with its typed endpoint methods, plus the same model classes for the app's side. - In `test/integration/test_tools/`, the generated [test tools](https://docs.serverpod.dev/concepts/testing/get-started.md). Generated code is overwritten on every run, so never edit it; the scaffolded analyzer config excludes it for that reason. While [`serverpod start`](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md) runs, generation happens automatically when you save; outside a session, run `serverpod generate` yourself. ## The server entry point The server starts in `bin/main.dart`, which only calls the `run` function in `lib/server.dart`. That function creates the server from the generated `Serverpod` class, registers everything that is not an endpoint, and starts it: ```dart void run(List args) async { // The generated Serverpod class. final pod = Serverpod(args); // Registrations go here: authentication services and web routes // in the scaffolded project, and anything else you add. // Start the server. await pod.start(); } ``` Endpoints need no registration: the generated `Serverpod` class already carries them all. Web routes, by contrast, are registered imperatively on `pod.webServer`, which is why the scaffolded `run()` contains route setup. When `pod.start()` runs, the server connects to the database, applies pending migrations when started with `--apply-migrations`, connects to Redis if enabled, brings up its servers, and starts background work such as [future calls](https://docs.serverpod.dev/concepts/scheduling/overview.md) and [health checks](https://docs.serverpod.dev/concepts/operations/health-checks.md). ## The three servers One Serverpod instance runs up to three servers, each on its own port in development: - **The API server (8080)** handles incoming endpoint calls from your app. This is the server your generated client talks to. - **The Insights server (8081)** is a separate, secret-protected server used by Serverpod's tooling. See [Insights](https://docs.serverpod.dev/tools/insights.md). - **The web server (8082)** serves web content: HTML routes, static files, and your built Flutter web app. See [Web server](https://docs.serverpod.dev/concepts/web-server/overview.md). The scaffolded development config also reserves port 8090 for the database and 8091 for [Redis](https://docs.serverpod.dev/concepts/operations/redis.md). ## The config and migrations directories The server's `config/` directory holds one YAML file per [run mode](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md#run-modes) (`development`, `test`, `staging`, `production`), the `passwords.yaml` secrets file scaffolded with generated per-environment secrets, and `generator.yaml`, which configures code generation. See [Configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md) for all of them. The `migrations/` directory starts with one migration that creates your initial schema, and grows as your models change. See [Migrations](https://docs.serverpod.dev/concepts/data-and-the-database/database/migrations.md). After the first server start, a gitignored `.serverpod/` directory also appears in the server package: it holds the [embedded database's](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md#database-backends) data files, one subdirectory per run mode, created by the server on demand. ## Editor and agent files The workspace ships ready for IDE debugging and AI agents: `.vscode/` contains attach configurations that connect the debugger to a running `serverpod start` session, and `AGENTS.md` instructs AI agents on how to work in the project. Each editor you pick at create time gets its own MCP config, which registers Serverpod's [MCP server](https://docs.serverpod.dev/concepts/cli/commands/mcp-server.md), and a skills folder: | Editor | MCP config | Skills | | ----------- | ---------------------------------- | ------------------- | | Claude | `.mcp.json` | `.claude/skills/` | | Cursor | `.cursor/mcp.json` | `.cursor/skills/` | | VS Code | `.vscode/mcp.json` | `.agents/skills/` | | Codex | `.codex/config.toml` | `.agents/skills/` | | OpenCode | `opencode.json` | `.opencode/skills/` | | Antigravity | `.agents/plugins/serverpod-local/` | `.agents/skills/` | The setup screen preselects no editors, so pick the ones you use. With `--no-interactive`, `serverpod create` configures Claude, Cursor, and VS Code unless you pass `--ide`. The workspace `.gitignore` excludes these files for every editor except OpenCode, so Git picks up `opencode.json` and `.opencode/` unless you add them to `.gitignore`. ## Related - [How it works](https://docs.serverpod.dev/how-it-works.md): the guided tour of building with Serverpod. - [Running your server](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md): the development loop around this project. - [Configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md): every file in `config/` in depth. - [Working with endpoints](https://docs.serverpod.dev/concepts/endpoints-and-apis.md): writing the API the client calls. - [Working with models](https://docs.serverpod.dev/concepts/data-and-the-database/models.md): defining your data. --- # Running your server https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server As you build, one command runs your server, database, and app together. When you save a file, it regenerates code and hot reloads your running server and app. The `serverpod start` command generates the latest code first, then runs everything inside a single interactive terminal. Run it from your project's root folder or one of its package folders: ```bash serverpod start ``` The interactive terminal shows a tab for the server and for each running app, with the most common actions along the bottom. Shortcuts are unshifted key presses: **M** means typing a lowercase `m`, while **Shift+M** produces the capital and triggers the variant. Press **H** for the full list of shortcuts, and **Q** to quit the session. If a session is already running for the project, a second `serverpod start` tells you so and exits. While the session runs, it also exposes an MCP endpoint that AI agents can use to drive it. See the [`serverpod mcp-server` reference](https://docs.serverpod.dev/concepts/cli/commands/mcp-server.md). New projects use an embedded PostgreSQL database that the server manages for you, so there is nothing else to start. See [Database backends](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md#database-backends) for how it is configured and how to use an external database instead. Use `serverpod start` for local development only. To run your server in production, deploy it instead. See [Deploy to Serverpod Cloud](https://docs.serverpod.dev/deployments/deploy-to-serverpod-cloud.md) or [Custom hosting](https://docs.serverpod.dev/deployments/custom-hosting/choosing-a-strategy.md). ## Save a file to hot reload By default, `serverpod start` watches your project. What happens when you save depends on the file: - **Server code:** hot reloads, and restarts the server if the reload fails. - **Models:** regenerates code and hot reloads. The database does not change until you [apply a migration](#manage-migrations-from-the-terminal). - **Flutter app code:** hot reloads the running apps. - **Static web files:** refreshes the browser. - **Dependencies:** loads them in place. The server restarts only for native assets. Flutter apps hot restart for pure Dart packages and relaunch for native ones. - **Flutter assets and fonts:** relaunches the app. - **Configuration:** not watched, so press **R** after editing `config/`. The **R** key adapts to the situation. In watch mode, where saves already hot reload, **R** performs a hot restart: use it when a change cannot be hot reloaded, such as code that only runs at startup. If the project has errors instead, the session stays open and tells you what failed; the server boots automatically once a save fixes the errors, or press **R** to retry the build and start. If the session keeps reporting compilation errors you already fixed, stop it, run [`serverpod clean`](https://docs.serverpod.dev/concepts/cli/commands/clean.md) to delete the cached server build, and start it again. To start without watching, pass `--no-watch`. Nothing reloads on save; there, **R** hot reloads on demand, and **Shift+R** restarts the server. ## Manage migrations from the terminal Every time `serverpod start` boots the server, including restarts, it applies any pending migrations. While it runs, you handle further schema changes from the terminal without leaving the session: - **M** creates a migration from your current model changes and applies it. - **P** creates a repair migration to reconcile a database that has drifted from your migrations, and applies it. This shortcut is not shown in the bottom bar; press **H** to see it listed. - **A** applies pending migrations. Use it to retry after a failed apply. Hold **Shift** with **M** or **P** to force the migration past Serverpod's warning that information may be destroyed, so force deliberately. **Shift+M** still creates nothing when your models have no changes. For an empty migration, run [`serverpod create-migration --empty`](https://docs.serverpod.dev/concepts/cli/commands/create-migration.md). **Shift+P** creates a repair migration even when the database has not drifted. For how migrations work, see [Migrations](https://docs.serverpod.dev/concepts/data-and-the-database/database/migrations.md). ## Choose a run mode By default, `serverpod start` runs in the `development` run mode. To start in another mode, forward a `--mode` argument to the server after `--`: ```bash serverpod start -- --mode staging ``` The run mode selects which configuration and passwords the server loads. See [Run modes](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md#run-modes) for what each mode reads. Flutter apps only launch in the `development` run mode. ## Choose which Flutter apps start The `serverpod: flutter_apps:` block in the server's `pubspec.yaml` lists the Flutter apps that `serverpod start` can run. Each one has its own entry, keyed by an id you choose. New projects with a Flutter app include this block: ```yaml serverpod: flutter_apps: my_project: path: ../my_project_flutter displayName: "My project app" auto_launch: true target: lib/driver.dart ``` Each entry accepts these keys: | Key | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `path` | Required. The Flutter package folder, relative to the server package. | | `displayName` | The name on the app's tab. Defaults to the app id. | | `auto_launch` | Set to `true` to launch the app when the session starts. Defaults to `false`. | | `device` | The `flutter run -d` device, for example `chrome` or `macos`. Without it, the app runs on Flutter's web server and opens in your browser. | | Any other key | Passed to `flutter run` as a flag. For example, `target: lib/driver.dart` becomes `--target=lib/driver.dart`, and `release: true` becomes `--release`. | Apps without `auto_launch: true` stay stopped when the session begins. To run one, press **Ctrl+R** in the `serverpod start` terminal and pick it from the app launch panel. Without a `flutter_apps` block, `serverpod start` launches the `_flutter` package beside your server, when that package exists. In watch mode, adding or removing an app takes effect when you save. A running app keeps its old settings until you restart it from the same panel. ## Run the server on its own By default, `serverpod start` also runs the Flutter apps [set to launch automatically](#choose-which-flutter-apps-start). To start only the server, disable that: ```bash serverpod start --no-flutter ``` You can still launch an app on demand: press **Ctrl+R** in the terminal to open the app launch panel. If your project has a Docker Compose file (`docker-compose.yaml`, `compose.yaml`, or another standard name), `serverpod start` brings it up automatically when your database is a Postgres on `localhost` with no `dataPath`, and stops the services it started when the session ends. When that does not apply, for example an embedded or remote database with [Redis](https://docs.serverpod.dev/concepts/operations/redis.md) in Compose, pass `--docker` to start the stack anyway, or `--no-docker` to keep it off. To run without the interactive terminal, pass `--no-tui`. When the output is not a terminal, for example in CI, `serverpod start` falls back to plain output on its own. ## Run the server directly Outside `serverpod start`, the server is a plain Dart program you can run yourself from the server package directory: ```bash dart run bin/main.dart --apply-migrations ``` The server accepts arguments that control how it starts: `--mode` selects the run mode, `--role` selects the [server role](#choose-a-server-role), and `--apply-migrations` applies pending migrations during startup. Without `--apply-migrations`, the server still checks the database schema at startup. On a mismatch, it exits with code 1 in the `development` run mode and logs a warning in other run modes. See the [run options reference](https://docs.serverpod.dev/concepts/lookups/configuration-reference.md#run-options) for the full list, and the [Deploy](https://docs.serverpod.dev/deployments/deploy-to-serverpod-cloud.md) section for running in production. ## Choose a server role In production, the `--role` argument controls which parts of the server run: - **`monolith`** (default) runs everything: the API, Insights, and web servers, plus [future calls](https://docs.serverpod.dev/concepts/scheduling/overview.md) and [health checks](https://docs.serverpod.dev/concepts/operations/health-checks.md). - **`serverless`** serves requests only. Future calls and [health metric collection](https://docs.serverpod.dev/concepts/operations/health-checks.md#health-metrics) are disabled, which fits platforms that start and stop instances on demand. The health probes still answer. - **`maintenance`** starts no servers. It performs one-shot work and exits, which fits CI jobs and scheduled maintenance tasks. With `--apply-migrations` or `--apply-repair-migration`, it applies the migrations and does nothing else. Without those flags, it runs any due future calls and one round of health checks. If a migration fails, or the database doesn't match the latest migration afterward, the process exits with code 1 in every run mode. ## Run code on shutdown Register a shutdown task to do cleanup work when the server stops, such as flushing state or releasing an external resource. Tasks run after the server stops accepting requests but before the database and Redis connections close, so they can still use them. :::warning Shutdown tasks are an experimental API and can change in a breaking way in any minor release. ::: Register them on the `Serverpod` instance in `lib/server.dart`, where you create it: ```dart pod.experimental.shutdownTasks.addTask(#taskIdentifier, () async { // Your shutdown logic here. }); ``` Each task is registered under an identifier you choose, used in log messages and to remove the task again. Any object works; `#taskIdentifier` above is a Dart symbol, which is a convenient way to write a constant name. ```dart pod.experimental.shutdownTasks.removeTask(#taskIdentifier); ``` Registering two tasks under the same identifier throws a `StateError`. All tasks run concurrently, and the server waits for them all before shutting down. A task that throws does not stop the shutdown, but the error is logged and the process exits with a non-zero code, which a host reading exit status will treat as a failed shutdown. ## Related - [`serverpod start` reference](https://docs.serverpod.dev/concepts/cli/commands/start.md): every command-line option. - [Configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md): the run modes and files the server loads on start. - [Migrations](https://docs.serverpod.dev/concepts/data-and-the-database/database/migrations.md): how schema changes reach the database. - [Deploy to Serverpod Cloud](https://docs.serverpod.dev/deployments/deploy-to-serverpod-cloud.md): run your server in production instead of locally. --- # Configuration https://docs.serverpod.dev/concepts/server-fundamentals/configuration Configuration decides what your server listens on, which database and Redis (an in-memory store Serverpod can use for pub/sub and caching) it connects to, and how it behaves in each run mode, so the same code runs locally, in staging, and in production without edits. Serverpod reads configuration from three sources: the `config/.yaml` files, environment variables, and a `ServerpodConfig` Dart object passed to the `Serverpod` constructor. Environment variables override matching keys in the YAML files, and a Dart object replaces both. With no configuration at all, Serverpod falls back to built-in defaults, so you can start with nothing and add configuration as you need it. ## How configuration works The YAML files and environment variables combine, and the Dart configuration object replaces both: - **YAML files** (`config/development.yaml`, `config/staging.yaml`, `config/production.yaml`, `config/test.yaml`): the baseline configuration, one file per run mode. - **Environment variables**: override matching YAML values, useful for per-deployment settings and secrets. - **Dart configuration object**: a `ServerpodConfig` passed to the `Serverpod` constructor. When you pass one, the server ignores `config/.yaml` and the `SERVERPOD_*` configuration variables. Command-line options such as `--mode` still override its values. To adjust the loaded configuration instead of replacing it, pass a `configOverride` function to the `Serverpod` constructor. It receives the configuration from the sources above, and returns the one the server uses. For every available option, its environment variable, config-file key, and default, see the [Configuration reference](https://docs.serverpod.dev/concepts/lookups/configuration-reference.md). ### Defaults If no YAML config files exist, no environment variables are configured, and no Dart config is supplied, the server runs with this built-in default: the API server on port 8080 at `localhost`. ```dart ServerpodConfig( apiServer: ServerConfig( port: 8080, publicHost: 'localhost', publicPort: 8080, publicScheme: 'http', ), ); ``` ## Run modes The server always runs in one of four run modes: `development` (the default), `test`, `staging`, or `production`. The run mode is selected with the `--mode` argument when the server starts, and it decides what the server loads: - The matching `config/.yaml` file. - The matching section of the [passwords file](#manage-secrets), merged with its `shared` section. The `test` mode is used by the [test tools](https://docs.serverpod.dev/concepts/testing/get-started.md), which start the server against `config/test.yaml`. The other modes separate your local setup from your deployed environments. See [Running your server](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md#choose-a-run-mode) for how to pass `--mode` through `serverpod start`. Run modes configure the server side. Your Flutter app picks the matching server address per environment on its own; see [Point the client at each environment](https://docs.serverpod.dev/concepts/endpoints-and-apis.md#point-the-client-at-each-environment). ## Configure with YAML files The `config` directory at the root of the server project holds one file per run mode, created with your project. The server loads the file named after the run mode it starts in: `config/development.yaml` is used when running in the `development` run mode. This is the scaffolded development file for a project named `myproject`, with comments and optional keys removed: ```yaml title="config/development.yaml" apiServer: port: 8080 publicHost: localhost publicPort: 8080 publicScheme: http insightsServer: port: 8081 publicHost: localhost publicPort: 8081 publicScheme: http webServer: port: 8082 publicHost: localhost publicPort: 8082 publicScheme: http database: host: localhost port: 8090 name: myproject user: postgres dataPath: .serverpod/development/pgdata redis: enabled: false host: localhost port: 8091 maxRequestSize: 524288 sessionLogs: persistentEnabled: true consoleEnabled: true consoleLogFormat: text ``` The three server blocks configure Serverpod's three servers: the API server your app talks to, the [Insights](https://docs.serverpod.dev/tools/insights.md) server used by Serverpod's tooling, and the [web server](https://docs.serverpod.dev/concepts/web-server/overview.md) for serving web content. Redis is optional and ships disabled; see [Redis](https://docs.serverpod.dev/concepts/operations/redis.md) for how to enable it and connect a client. The `sessionLogs` block controls how session logs are stored and printed; see [Logging](https://docs.serverpod.dev/concepts/operations/logging.md). Keys not in the scaffolded file cover future calls, websocket ping intervals, and more; see the [Configuration reference](https://docs.serverpod.dev/concepts/lookups/configuration-reference.md) for the full list with defaults. ### Database backends Serverpod supports both PostgreSQL and SQLite as database backends. A server that does not use a database leaves the `database` section out of every run mode file. Its models then can't use `table`, and `serverpod create-migration` won't create migrations. :::warning All run modes must use the same database backend, and either all of them or none of them configure a database: `serverpod generate` fails otherwise. This also keeps your development environment consistent with production. ::: #### PostgreSQL New projects run an embedded PostgreSQL in the `development` and `test` run modes. When `dataPath` is set, the server boots and manages a PostgreSQL in that directory before connecting, so there is no separate database to install or start. The embedded database is for local development and testing only; in production, connect to an external PostgreSQL by omitting `dataPath`: ```yaml database: host: localhost port: 8090 name: database_name user: postgres maxConnectionCount: 10 ``` The external PostgreSQL must be version 16 or later. Set the database password in `passwords.yaml` (`database`) or through `SERVERPOD_PASSWORD_database`. #### SQLite ```yaml database: filePath: server.db ``` No database password is required when using SQLite. SQLite doesn't support persistent session logs. `persistentEnabled` still defaults to `true`, so the server warns at startup until you set it to `false`. Console logging is on by default only in `development`. To see session logs in other run modes, set `consoleEnabled`. See [Configure logging](https://docs.serverpod.dev/concepts/operations/logging.md#configure-logging). ## Configure in Dart To configure Serverpod in Dart, pass an instance of the `ServerpodConfig` class to the `Serverpod` constructor, normally inside the `run` function in your `server.dart` file. When you configure this way, `apiServer` is the one field you must provide. ```dart Serverpod( args, config: ServerpodConfig( apiServer: ServerConfig( port: 8080, publicHost: 'localhost', publicPort: 8080, publicScheme: 'http', ), insightsServer: ServerConfig( port: 8081, publicHost: 'localhost', publicPort: 8081, publicScheme: 'http', ), webServer: ServerConfig( port: 8082, publicHost: 'localhost', publicPort: 8082, publicScheme: 'http', ), ), ); ``` ## Manage secrets Secrets are declared in the `config/passwords.yaml` file. Serverpod's API reads them with `getPassword`, so this page uses *secret* and *password* interchangeably. The file has a `shared` section, applied in all run modes, and one section per run mode: ```yaml title="config/passwords.yaml" shared: stripeApiKey: 'sk_test_123...' development: database: 'development_password' redis: 'development_password' serviceSecret: 'development_service_secret' twilioApiKey: 'dev_twilio_key' production: database: 'production_password' redis: 'production_password' serviceSecret: 'production_service_secret' twilioApiKey: 'prod_twilio_key' ``` You can also provide any secret through [environment variables](#via-environment-variables). ### Built-in secrets The following table shows the built-in secrets that Serverpod uses for its core functionality. These can be configured either through environment variables or by adding the corresponding key in a run-mode or `shared` section of the passwords file. They are separate from any custom secrets you define. | Environment variable | Passwords file | Default | Description | | ---------------------------------- | -------------- | ------- | ------------------------------------------------------------------------ | | SERVERPOD\_PASSWORD\_database | database | - | The password for the database. | | SERVERPOD\_PASSWORD\_serviceSecret | serviceSecret | - | The token used to connect with Insights. Must be at least 20 characters. | | SERVERPOD\_PASSWORD\_redis | redis | - | The password for the Redis server. | Each built-in secret also has a dedicated environment variable: `SERVERPOD_DATABASE_PASSWORD`, `SERVERPOD_SERVICE_SECRET`, and `SERVERPOD_REDIS_PASSWORD`. All environment variables override the passwords file, and the `SERVERPOD_PASSWORD_*` form wins when both are set. ### Secrets for first-party packages For secrets related to first-party Serverpod packages, see their respective documentation: - **Cloud storage**: see [File uploads](https://docs.serverpod.dev/concepts/endpoints-and-apis/file-uploads.md) for Google Cloud Storage, AWS S3, and Cloudflare R2 secrets, and [Custom cloud storage](https://docs.serverpod.dev/concepts/endpoints-and-apis/custom-cloud-storage.md) for local disk. - **Authentication**: see [Storing Secrets](https://docs.serverpod.dev/concepts/authentication/setup.md#storing-secrets) on the Authentication setup page. ### Custom secrets Add your own keys to the passwords file the same way, under `shared` or a run-mode section, as `stripeApiKey` and `twilioApiKey` are in the example above. #### Via environment variables You can also define custom secrets using environment variables with the `SERVERPOD_PASSWORD_` prefix. For example, `SERVERPOD_PASSWORD_myApiKey` becomes available as `myApiKey`: the prefix is stripped. These variables override same-named entries in the passwords file and, like the `shared` section, apply in all run modes. ```bash export SERVERPOD_PASSWORD_stripeApiKey=sk_test_123... ``` ### Access secrets in code Secrets are only available on the server. They are never sent to or accessible from your Flutter app. Inside an endpoint, read a secret from the [`Session`](https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions.md) through the `passwords` map: ```dart Future processPayment(Session session, PaymentData data) async { final stripeApiKey = session.passwords['stripeApiKey']; // Use the API key to make requests to Stripe. } ``` The same value is available from `session.serverpod.getPassword('stripeApiKey')`. Outside of a request, for example during startup in your server's `run` function, read it from the `Serverpod` instance with `getPassword`: ```dart // `pod` is the Serverpod instance created in run(). final stripeApiKey = pod.getPassword('stripeApiKey'); ``` This works for built-in and custom secrets alike, whether they come from the passwords file or an environment variable. ### Secrets in production A new project's `.gitignore` excludes `config/passwords.yaml` and credential files such as `config/firebase_service_account_key.json`, so secrets are not committed by default. Keep production secrets out of source control. In production, set secrets through `SERVERPOD_PASSWORD_*` environment variables, or your host's secret manager, rather than a checked-in passwords file. ### Passwords on Serverpod Cloud On [Serverpod Cloud](https://docs.serverpod.dev/cloud.md), you set the values that `getPassword` reads from the command line, with `serverpod cloud password set`, instead of editing a passwords file. See [Passwords, secrets, and environment variables](https://docs.serverpod.dev/cloud/concepts/passwords-secrets-env-vars.md) for the full reference. ## Configure code generation Serverpod uses a `generator.yaml` file to configure code generation. Place this file in the `config` directory of your server project. For every `generator.yaml` option, its type and default, see the [Configuration reference](https://docs.serverpod.dev/concepts/lookups/configuration-reference.md#code-generation). The sections below explain the options you set most often. ### Package types The `type` field determines how Serverpod treats your package: - **server**: A standard Serverpod application (default). - **module**: A reusable module that can be imported by other Serverpod projects. - **internal**: Used only by Serverpod's own framework packages; you will not use this. For modules, you can also specify a `nickname`: ```yaml type: module nickname: auth ``` ### Client package path By default, Serverpod expects the client package to be located at `../[project_name]_client`. You can customize this: ```yaml client_package_path: ../my_custom_client ``` ### Test tools generation Fresh projects include this line in `generator.yaml`, which generates the integration test tools: ```yaml server_test_tools_path: test/integration/test_tools ``` Delete the line to stop generating test tools. See the [testing documentation](https://docs.serverpod.dev/concepts/testing/get-started.md) for how the test tools are used. ### Module dependencies Declare module dependencies and optionally assign nicknames for easier reference: ```yaml modules: serverpod_auth_core: nickname: auth my_custom_module: nickname: custom ``` This allows you to reference module classes as `module:auth:AuthUser` in your model files. See the [modules documentation](https://docs.serverpod.dev/concepts/server-fundamentals/modules.md) for more information. ### Shared packages Shared packages let you define models that can be imported by both server and client code. They depend only on `serverpod_serialization` and are safe to use across your full stack. Configure them in `generator.yaml`: ```yaml shared_packages: - ../my_shared_package - ../another_shared_package ``` Models and the protocol file are generated in each shared package's own directory when you run `serverpod generate` from your server project. See the [shared packages documentation](https://docs.serverpod.dev/concepts/data-and-the-database/models/shared-packages.md) for setup, usage, and restrictions. ### Custom serializable classes Register custom classes for use in your models: ```yaml extraClasses: - package:my_shared_package/my_shared_package.dart:CustomClass - package:my_shared_package/my_shared_package.dart:AnotherCustomClass ``` See the [serialization documentation](https://docs.serverpod.dev/concepts/data-and-the-database/models/custom-serialization.md) for implementing custom serializable classes. ### Experimental features Experimental features are off by default and opted into per project with the `experimental_features` key. The current one is `databaseSync`, which enables [`database: sync`](https://docs.serverpod.dev/concepts/data-and-the-database/database/client-side-database.md#sync-tables-with-the-server) on table models; `all` enables every available feature: ```yaml experimental_features: databaseSync: true ``` The `--experimental-features databaseSync` command line flag does the same for a single run. The `experimental_features` key only affects code generation. Experimental runtime APIs are on `pod.experimental` instead: `shutdownTasks` to [run code on shutdown](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md#run-code-on-shutdown), and `submitDiagnosticEvent` for [exception monitoring](https://docs.serverpod.dev/concepts/operations/exception-monitoring.md). :::warning Experimental features may change or be removed in future versions. ::: ## Related - [Configuration reference](https://docs.serverpod.dev/concepts/lookups/configuration-reference.md): every option with its environment variable, config key, and default. - [Running your server](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md): how the server starts and how `--mode` is passed. - [Database connection](https://docs.serverpod.dev/concepts/data-and-the-database/database/connection.md): connecting to your database in depth. - [Redis](https://docs.serverpod.dev/concepts/operations/redis.md): enabling Redis, the password, and connecting a GUI. - [Configure HTTP calls](https://docs.serverpod.dev/concepts/endpoints-and-apis/configure-http-calls.md): the API server's response headers and CORS defaults. --- # Modules https://docs.serverpod.dev/concepts/server-fundamentals/modules Modules let you drop ready-made features into your server, such as authentication, or package your own server and app code for reuse across projects and on pub.dev. A module is similar to a Dart package but bundles server code, generated client code, and Flutter widgets, along with its own endpoints and database tables. Each module has its own namespace for endpoints and methods to minimize the risk of conflicts. The `serverpod_auth_core` and `serverpod_auth_idp` modules, both maintained by the Serverpod team, are examples: they add complete user authentication to your project. ## Add a module to your project A module ships as one package per part of your project, so you add it to each `pubspec.yaml` that uses it. ### Add the server package To add the `serverpod_auth_idp` module, add `serverpod_auth_idp_server` to your server's `pubspec.yaml`: ```yaml title="myproject_server/pubspec.yaml" dependencies: serverpod_auth_idp_server: 4.0.0 ``` :::info Use the same version as your Serverpod packages, such as `4.0.0`. All official packages share one version number, so matching versions work together. Pin the exact version rather than a caret range, so the packages stay in sync with the CLI. ::: In your `config/generator.yaml`, you can optionally list the module and give it a `nickname`, which sets how you [reference the module's models](#reference-a-module-in-your-models). Without an entry, the module's own declared nickname is used; for the official modules that is the full package name. ```yaml modules: serverpod_auth_idp: nickname: idp ``` See [Configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md#module-dependencies) for the full set of `generator.yaml` module options. Then fetch dependencies; one `dart pub get` anywhere in the workspace resolves every package: ```bash dart pub get ``` Start the server, which regenerates your code so the module's endpoints and models become part of your server and client: ```bash serverpod start ``` The module adds tables to your database, so create and apply a migration: in the `serverpod start` terminal, press **M**. ### Add the client package In your client's `pubspec.yaml`, add the module's generated client package: ```yaml title="myproject_client/pubspec.yaml" dependencies: serverpod_auth_idp_client: 4.0.0 ``` ### Add the Flutter package In your Flutter app's `pubspec.yaml`, add the module's Flutter package: ```yaml title="myproject_flutter/pubspec.yaml" dependencies: serverpod_auth_idp_flutter: 4.0.0 ``` ## Reference a module in your models You can reference a module's serializable classes from the model files in your project by adding the `module:` prefix, followed by the module's nickname. A module has exactly one effective nickname: the one you set in your `generator.yaml`, or, if you set none, the module's own declared nickname. For the official modules, the declared nickname is the full package name. With `nickname: auth` set for `serverpod_auth_core` in your `generator.yaml`, reference its `AuthUser` class as: ```yaml class: MyClass fields: userInfo: module:auth:AuthUser ``` With no `generator.yaml` entry, use the module's declared nickname instead: ```yaml class: MyClass fields: userInfo: module:serverpod_auth_core:AuthUser ``` Setting your own nickname replaces the declared one, so only one of these forms works at a time. See [Working with models](https://docs.serverpod.dev/concepts/data-and-the-database/models.md) for how model files work. ## Create your own module With the `serverpod create` command, you can create your own module for code that is shared between projects or that you want to publish to pub.dev. To create a module instead of a server project, pass `module` to the `--template` flag: ```bash serverpod create --template module my_module ``` The command creates a server and a client Dart package. If you also want to ship Flutter code, create a package for it with Flutter: ```bash flutter create --template package my_module_flutter ``` In the Flutter package, import the client libraries that `serverpod create` generated. If your module needs database tables, prefix their names with the module name to avoid conflicts with the projects that use it. Serverpod's own tables are prefixed with `serverpod_`. ## Related - [Configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md#module-dependencies): every `generator.yaml` module option. - [Authentication](https://docs.serverpod.dev/concepts/authentication/get-started.md): the auth modules in practice. - [`serverpod create` reference](https://docs.serverpod.dev/concepts/cli/commands/create.md): all create templates and flags. --- # Working with endpoints https://docs.serverpod.dev/concepts/endpoints-and-apis Endpoints are how your app talks to your server: you write a method on the server, and Serverpod generates a typed Dart method your app calls directly, with no REST layer, JSON parsing, or API contract to maintain. This page covers defining endpoints, calling them from your app, and pointing the app at each environment. The rest of this section builds on it, page by page: [sessions](https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions.md), [error handling](https://docs.serverpod.dev/concepts/endpoints-and-apis/error-handling-and-exceptions.md), [streaming](https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming.md), [server events](https://docs.serverpod.dev/concepts/endpoints-and-apis/server-events.md), and [file uploads](https://docs.serverpod.dev/concepts/endpoints-and-apis/file-uploads.md). See [Related](#related) for the full map. For the client code to be generated: - Place the endpoint file anywhere under the `lib` directory of your server. - Create a class that extends `Endpoint`. - Define methods that return a typed `Future` (or a `Stream`, see [Streaming](https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming.md)) and take a `Session` object as their first argument. The [`Session`](https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions.md) object holds information about the call being made and provides access to the database. ## Create an endpoint ```dart import 'package:serverpod/serverpod.dart'; class ExampleEndpoint extends Endpoint { Future hello(Session session, String name) async { return 'Hello $name'; } } ``` The above code creates an endpoint called `example` (the Endpoint suffix is removed) with the single `hello` method. While [`serverpod start`](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md) is running, the client-side code is generated automatically when you save. Outside a start session, run `serverpod generate` (or `serverpod generate --watch`) in the server directory. ## Call an endpoint from your app Your app calls the method through the generated client: ```dart var result = await client.example.hello('World'); ``` The scaffolded Flutter app already creates that client in `lib/client.dart`, connected to your development server, and `main()` initializes it before running the app: ```dart // lib/client.dart final serverUrl = getServerUrl(); late final Client client; Future initializeClient() async { client = Client(await serverUrl) ..connectivityMonitor = FlutterConnectivityMonitor(); } // lib/main.dart void main() async { WidgetsFlutterBinding.ensureInitialized(); await initializeClient(); runApp(const MyApp()); } ``` The `getServerUrl()` helper from `serverpod_flutter` picks the server address from the first of these that is set: 1. The `SERVER_URL` compile-time define, for [choosing the URL per build](#point-the-client-at-each-environment). 2. The `apiUrl` value in `assets/config.json`. 3. The default, `http://localhost:8080/`, where localhost adapts to the platform, so an Android emulator reaches your machine through `10.0.2.2` without any changes. The `connectivityMonitor` lets the client observe network changes. Projects created with authentication also attach an `authSessionManager` here. On a physical device, localhost is the device itself, so the app needs your machine's network address instead: 1. Find your machine's LAN IP, with `ifconfig` on Linux/macOS or `ipconfig` on Windows. 2. Set it as the server URL, either as `apiUrl` in `assets/config.json` or with `--dart-define=SERVER_URL=http://192.168.1.20:8080/`. Also set `publicHost` in the development config to the same IP, so URLs the server hands out, such as public file links, point somewhere the device can reach; see [Configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md). To enable browser credentials for CORS or use platform-native HTTP clients, see [Configure HTTP calls](https://docs.serverpod.dev/concepts/endpoints-and-apis/configure-http-calls.md). ## Point the client at each environment The resolved server URL is the server the app connects to, so it changes between development, staging, and production. Keep the scaffolded client setup and choose the URL per build with `--dart-define`: ```bash flutter run --dart-define=SERVER_URL=https://staging.example.com/ ``` Without the define, the app falls back to `assets/config.json` and then the local server. For a Flutter web app served by your Serverpod web server, the server provides `config.json` at runtime with its own API address, so the deployed web app finds the right server without a rebuild. In production, use your deployed server's public URL. If you deploy to [Serverpod Cloud](https://docs.serverpod.dev/cloud.md), that is the URL of your deployed app. ## Restrict access to your endpoints Endpoints are open to any client by default. To require a signed-in user, override `requireLogin` and to require specific scopes, override `requiredScopes`: ```dart class PrivateEndpoint extends Endpoint { @override bool get requireLogin => true; Future onlyForSignedInUsers(Session session) async { return 'Hello ${session.authenticated?.userIdentifier}'; } } ``` If a call fails the check, the app receives a typed error; see [error handling](https://docs.serverpod.dev/concepts/endpoints-and-apis/error-handling-and-exceptions.md). For how users sign in and how scopes work, see [Authentication](https://docs.serverpod.dev/concepts/authentication/basics.md). ## Pass and return data Parameters and return values can be of type `bool`, `int`, `double`, `String`, `UuidValue`, `Duration`, `DateTime`, `ByteData`, `Uri`, `BigInt`, [`dynamic`](https://docs.serverpod.dev/concepts/data-and-the-database/models/dynamic-fields.md), [vector and geography types](https://docs.serverpod.dev/concepts/data-and-the-database/database/vector-and-geography-fields.md), or generated serializable objects ([data models](https://docs.serverpod.dev/concepts/data-and-the-database/models.md)). You can also use `List`, `Map`, `Record`, and `Set`, strictly typed with the types above. Null safety is supported, and return types follow the same rules as parameters. A `DateTime` is always converted to UTC when passed. Parameters are sent by name in the request between app and server, so renaming an endpoint method's parameters breaks older app builds. See [backward compatibility](https://docs.serverpod.dev/concepts/endpoints-and-apis/backward-compatibility.md) before changing a published API. Binary data over a method call is capped by the request size limit, 512 KiB by default (an oversized call fails with an HTTP 413 error). The limit is the `maxRequestSize` key in the [Configuration reference](https://docs.serverpod.dev/concepts/lookups/configuration-reference.md). For transferring files, use the [file upload](https://docs.serverpod.dev/concepts/endpoints-and-apis/file-uploads.md) interface instead. ## Exclude an endpoint from generation If you want the code generator to ignore an endpoint definition, annotate either the entire class or individual methods with `@doNotGenerate`. This is useful for keeping a definition in your codebase without generating server or client bindings for it. ### Exclude an entire `Endpoint` class ```dart import 'package:serverpod/serverpod.dart'; @doNotGenerate class ExampleEndpoint extends Endpoint { Future hello(Session session, String name) async { return 'Hello $name'; } } ``` The above code generates no server or client bindings for the example endpoint. ### Exclude individual `Endpoint` methods ```dart import 'package:serverpod/serverpod.dart'; class ExampleEndpoint extends Endpoint { Future hello(Session session, String name) async { return 'Hello $name'; } @doNotGenerate Future goodbye(Session session, String name) async { return 'Bye $name'; } } ``` In this case the `ExampleEndpoint` only exposes the `hello` method, and the `goodbye` method is not accessible externally. For how the annotation interacts with inheritance, see [Endpoint inheritance](https://docs.serverpod.dev/concepts/endpoints-and-apis/endpoint-inheritance.md). ## Test your endpoints The generated test tools call your endpoints the same way production code does, against a test database. See [Get started with testing](https://docs.serverpod.dev/concepts/testing/get-started.md). ## Related - [Sessions](https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions.md): the object every endpoint method receives. - [Error handling and exceptions](https://docs.serverpod.dev/concepts/endpoints-and-apis/error-handling-and-exceptions.md): typed errors between server and app. - [Streaming](https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming.md): push live data to your app. - [File uploads](https://docs.serverpod.dev/concepts/endpoints-and-apis/file-uploads.md): direct-to-storage uploads. - [Custom cloud storage](https://docs.serverpod.dev/concepts/endpoints-and-apis/custom-cloud-storage.md): local disk, NAS, and custom backends. - [Endpoint inheritance](https://docs.serverpod.dev/concepts/endpoints-and-apis/endpoint-inheritance.md): share behavior across endpoints and reshape module endpoints. - [Server events](https://docs.serverpod.dev/concepts/endpoints-and-apis/server-events.md): publish and subscribe to messages across sessions and servers. - [Endpoint middleware](https://docs.serverpod.dev/concepts/endpoints-and-apis/endpoint-middleware.md): wrap every API request for logging or rate limiting. - [Configure HTTP calls](https://docs.serverpod.dev/concepts/endpoints-and-apis/configure-http-calls.md): CORS credentials and native HTTP stacks. - [Working with models](https://docs.serverpod.dev/concepts/data-and-the-database/models.md): define the data your endpoints exchange. --- # Sessions https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions Every endpoint method receives a `Session` object. It is the entry point for the database, cache, file storage, and messaging system. Serverpod creates and closes sessions automatically for endpoint calls. For background work, you create one yourself, most easily with [`withSession`](#create-a-session-for-background-work). :::note A Serverpod Session should not be confused with the concept of "web sessions" or "user sessions" which persist over multiple API calls. See the [Authentication documentation](https://docs.serverpod.dev/concepts/authentication/setup.md) for managing persistent authentication. ::: ## Session types Serverpod creates a session for every unit of work it runs, and the type reflects what triggered it. You will see these names in your [logs](https://docs.serverpod.dev/concepts/operations/logging.md) and in Insights when tracing a call: | Type | Created for | Lifetime | Typical uses | | ----------------------- | ---------------------------------------------------------------------------------------- | --------------- | --------------------------- | | **MethodCallSession** | `Future` [endpoint methods](https://docs.serverpod.dev/concepts/endpoints-and-apis.md) | Single request | API calls, CRUD operations | | **WebCallSession** | [Web server](https://docs.serverpod.dev/concepts/web-server/overview.md) routes | Single request | Web pages, form submissions | | **MethodStreamSession** | [Streaming methods](https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming.md) | Stream duration | Real-time updates, chat | | **FutureCallSession** | [Scheduled tasks](https://docs.serverpod.dev/concepts/scheduling/overview.md) | Task execution | Email sending, batch jobs | | **InternalSession** | [Manual creation](#create-a-session-for-background-work) | Until closed | Background work, migrations | You rarely choose a type yourself: your endpoint methods receive the right one, and manual creation always produces an `InternalSession`. ```dart class ExampleEndpoint extends Endpoint { Future hello(Session session, String name) async { // A MethodCallSession was created for this call, and it closes // automatically when the method returns. return 'Hello $name'; } } ``` ## What a session provides | Member | What it is | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `db` | Database access. See the [CRUD docs](https://docs.serverpod.dev/concepts/data-and-the-database/database/crud.md). | | `caches` | Local and distributed caching. See [caching](https://docs.serverpod.dev/concepts/endpoints-and-apis/caching.md). | | `storage` | File storage. See [file uploads](https://docs.serverpod.dev/concepts/endpoints-and-apis/file-uploads.md). | | `messages` | Server events for real-time communication. See [server events](https://docs.serverpod.dev/concepts/endpoints-and-apis/server-events.md). | | `passwords` | Secrets from config and environment. See [configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md). | | `authenticated` | The current user's authentication info. See [authentication](https://docs.serverpod.dev/concepts/authentication/basics.md). | | `log(...)` | Write a log entry tied to this session. See [logging](#logging). | | `addWillCloseListener(...)` | Register a cleanup callback that runs before the session closes. | | `serverpod` | The running Serverpod instance, for example for [future calls](https://docs.serverpod.dev/concepts/scheduling/overview.md). | ## Session lifecycle Serverpod creates the session when the request or task starts and closes it when the work completes. Closing finalizes the session's log and releases its resources. The exception is `InternalSession`: you [create it yourself](#create-a-session-for-background-work), so you close it yourself. ### Run cleanup when a session closes Register callbacks that execute just before a session closes with `addWillCloseListener`. This is useful for releasing resources or performing final operations: ```dart Future processData(Session session) async { var tempFile = File('/tmp/processing_data.tmp'); session.addWillCloseListener((session) async { if (await tempFile.exists()) { await tempFile.delete(); session.log('Cleaned up temporary file'); } }); await tempFile.writeAsString('processing...'); // The session closes automatically after the method returns, // and the cleanup callback runs. } ``` Cleanup callbacks run in the order they were registered and fire for all session types, including manual sessions when you call `session.close()`. ## Create a session for background work Inside an endpoint method, use the session you were given. It is already managed for you. When code runs outside a call, such as startup work in `run()` or a background job, create a session with `withSession`, which builds an `InternalSession`, runs your callback, and closes the session afterwards: ```dart await pod.withSession((session) async { await cleanupOldRecords(session); await updateStatistics(session); }); ``` If the callback throws, the session is closed with the error and stack trace attached, so they reach the logs, and the error is rethrown. If you need to manage the lifetime yourself, create the session manually and close it when done. An unclosed session is never finalized and its resources are not released, so pair `createSession` with a `finally`: ```dart var session = await Serverpod.instance.createSession(); try { var users = await User.db.find(session); // Process users. } finally { await session.close(); } ``` ## Run work after the call returns The session that an endpoint method receives closes when the method returns, so do not capture it in timers or delayed callbacks: ```dart Future processUser(Session session, int userId) async { Timer(Duration(seconds: 5), () async { // Do not do this: this session closed when processUser returned, // and this callback runs outside any managed lifetime. await updateLastSeen(session, userId); }); } ``` Schedule the work as a [future call](https://docs.serverpod.dev/concepts/scheduling/future-calls.md) instead. Future calls survive server restarts and receive their own session when they run: ```dart Future processUser(Session session, int userId) async { await session.serverpod.futureCalls .callWithDelay(const Duration(seconds: 5)) .userMaintenance .updateLastSeen(userId); } ``` For one-off work that can run immediately and does not need to survive a restart, `withSession` inside the callback is the lighter alternative. ## Logging Use `session.log` to write log entries tied to the session. Entries are persisted according to your [session log configuration](https://docs.serverpod.dev/concepts/operations/logging.md), and closing the session finalizes its log. Sessions for [streaming methods](https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming.md) are the exception: they write entries continuously while the stream is open, so a long-lived stream appears in your logs before it ends. This is on by default in the runtime log settings. See [Logging](https://docs.serverpod.dev/concepts/operations/logging.md) for storage, retention, and console output. ```dart session.log('Processing started'); session.log('Something looks off', level: LogLevel.warning); ``` ## Test with sessions The test tools provide a `sessionBuilder` for calling endpoints in tests and simulating session state such as authentication. See [Get started with testing](https://docs.serverpod.dev/concepts/testing/get-started.md) and [Writing tests](https://docs.serverpod.dev/concepts/testing/writing-tests.md) for the patterns. ## Related - [Working with endpoints](https://docs.serverpod.dev/concepts/endpoints-and-apis.md): the methods that receive sessions. - [Scheduling](https://docs.serverpod.dev/concepts/scheduling/overview.md): future calls, the managed way to run delayed work. - [Logging](https://docs.serverpod.dev/concepts/operations/logging.md): where session log entries go. - [Authentication](https://docs.serverpod.dev/concepts/authentication/basics.md): what `session.authenticated` holds. --- # Error handling and exceptions https://docs.serverpod.dev/concepts/endpoints-and-apis/error-handling-and-exceptions Errors on the server reach your app as typed Dart exceptions. You define an exception once, throw it on the server, and catch it by type in your Flutter app. Failures carry structured data instead of strings. If you throw a normal exception that isn't caught by your code, it will be treated as an internal server error. The exception will be logged together with its stack trace, and a 500 HTTP status (internal server error) will be sent to the client. On the client side, this throws a `ServerpodClientException` with status code 500 (specifically `ServerpodClientInternalServerError`). The error message and stack trace stay on the server. They are stored with the session logs in `serverpod_session_log`, not in `serverpod_log`. See [logging](https://docs.serverpod.dev/concepts/operations/logging.md) for the difference. The Serverpod Insights app shows failed and slow calls. ## Serializable exceptions Serverpod allows adding data to an exception you throw on the server and extracting that data in the client. You define them in the same model files as any serializable model (see [Working with models](https://docs.serverpod.dev/concepts/data-and-the-database/models.md) for how model files work). The only difference is that you use the keyword `exception` instead of `class`. ```yaml exception: MyException fields: message: String errorType: MyEnum ``` Once the code is generated, you can throw that exception when processing a call to the server. ```dart class ExampleEndpoint extends Endpoint { Future doThingy(Session session) { // ... do stuff ... if (failure) { throw MyException( message: 'Failed to do thingy', errorType: MyEnum.thingyError, ); } } } ``` In your app, catch the exception as you would catch any exception. ```dart try { await client.example.doThingy(); } on MyException catch(e) { print(e.message); } catch(e) { print('Something else went wrong.'); } ``` ### Exception hierarchies Related failures often share fields and the same handling in the app. Use [`extends`](https://docs.serverpod.dev/concepts/data-and-the-database/models/inheritance-and-polymorphism.md) to put them in a hierarchy, and mark the root `sealed` when you know every concrete case and want the app to handle all of them. Define each exception in its own model file, starting with the sealed base: ```yaml title="api_exception.spy.yaml" exception: ApiException sealed: true fields: message: String ``` Then extend it with the concrete failures the endpoint can throw: ```yaml title="not_found_exception.spy.yaml" exception: NotFoundException extends: ApiException fields: resource: String ``` ```yaml title="validation_exception.spy.yaml" exception: ValidationException extends: ApiException fields: field: String ``` Save with `serverpod start` running (or run `serverpod generate`), then throw the concrete exceptions from an endpoint: ```dart class UserEndpoint extends Endpoint { Future updateName( Session session, { required int userId, required String name, }) async { if (name.trim().isEmpty) { throw ValidationException( message: 'Enter a name.', field: 'name', ); } var user = await User.db.findById(session, userId); if (user == null) { throw NotFoundException( message: 'The user was not found.', resource: 'user', ); } await User.db.updateRow(session, user.copyWith(name: name)); } } ``` On the client, catch the base type and use an exhaustive switch over its concrete subtypes: ```dart try { await client.user.updateName(userId: userId, name: name); } on ApiException catch (error) { var message = switch (error) { NotFoundException(:final resource) => '$resource was not found.', ValidationException(:final field) => 'Check the $field field.', }; showError(message); } ``` Because the base is sealed, the analyzer requires the switch to cover every subtype. Adding another concrete exception then becomes a deliberate change in the app instead of a case that quietly falls through to a general catch. A hierarchy can live in a [shared package](https://docs.serverpod.dev/concepts/data-and-the-database/models/shared-packages.md) when several Serverpod projects need the same exception types. Every subtype of a sealed exception has to stay in the package that declares the base, so a consuming project cannot add a case of its own. Exceptions and regular model classes cannot be mixed in the same hierarchy. An exception only extends another exception, and a class only extends another class. Anything else fails validation when you generate the code. ### Custom serializable exception classes If you already have a Dart exception class in a package shared by the server and client, the class must implement `SerializableException` for Serverpod to send it to the client. Otherwise, Serverpod treats the exception as an internal server error. ```dart import 'package:serverpod_serialization/serverpod_serialization.dart'; class UserFacingException implements SerializableException { final String message; final String? code; UserFacingException({ required this.message, this.code, }); factory UserFacingException.fromJson(Map json) { return UserFacingException( message: json['message'] as String, code: json['code'] as String?, ); } @override Map toJson() { return { 'message': message, 'code': code, }; } @override String toString() => message; } ``` The class must also be known to Serverpod's generated serialization manager. Register it in the server project's `config/generator.yaml`, then run `serverpod generate`: ```yaml extraClasses: - package:my_project_shared/my_project_shared.dart:UserFacingException ``` The `SerializableException` interface marks the exception as safe to serialize to the client. See [Custom serializable classes](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md#custom-serializable-classes) for how the `extraClasses` entry registers the type for code generation. ### Default values in exceptions Serverpod allows you to specify default values for fields in exceptions, similar to models: `default` sets the value when the field is omitted, and `defaultModel` sets it on the Dart side. See [default values](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#default-values) in Working with models. Since exceptions are not persisted in the database, the `defaultPersist` keyword is not supported. If both `default` and `defaultModel` are specified, `defaultModel` takes precedence. ```yaml exception: MyException fields: message: String, default="An error occurred" errorCode: int, default=1001 ``` ## Handle errors in your app A call from the client can fail in a few ways, and you usually handle each one differently: - A **serializable exception you defined** (`MyException` above): a known, app-level failure. Catch it by its type and show the user what happened. (It is sent as an HTTP 400 response with a typed payload.) - A **`ServerpodClientHttpException`**: the server answered with an error status, available as `statusCode`. Its subclasses map to HTTP status codes: `ServerpodClientBadRequest` (400), `ServerpodClientUnauthorized` (401), `ServerpodClientForbidden` (403), `ServerpodClientNotFound` (404), and `ServerpodClientInternalServerError` (500). Any other status is a `ServerpodClientUnknownHttpException`, such as the 413 returned when a call exceeds the [request size limit](https://docs.serverpod.dev/concepts/endpoints-and-apis.md#pass-and-return-data). - A **`ServerpodClientNetworkException`**: the app cannot reach the server (offline, wrong URL, or a timeout). There is no status code. Both extend the sealed `ServerpodClientException`, along with `ServerpodClientUnknownException` for failures the client cannot classify. Catching `ServerpodClientException` handles all of them at once. A call to a [streaming method](https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming.md) that the server refuses to open fails with the same `ServerpodClientHttpException` subclasses. Failures in the stream connection itself use their own exception family. See [error handling in streams](https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming.md#error-handling). Catch the specific cases first, then fall back to the general one: ```dart try { await client.example.doThingy(); } on MyException catch (e) { // A failure you defined and threw on the server. showError(e.message); } on ServerpodClientUnauthorized catch (_) { // The call requires the user to sign in. redirectToSignIn(); } on ServerpodClientNetworkException catch (_) { // Could not reach the server. showError('Cannot reach the server. Check your connection and try again.'); } on ServerpodClientHttpException catch (e) { // The server returned an error, for example a 500. showError('Something went wrong (${e.statusCode}). Please try again.'); } on ServerpodClientException catch (_) { // Anything else the client could not classify. showError('Something went wrong. Please try again.'); } ``` ## Don't leak sensitive data Only the serializable exceptions you define reach the client, and every field on them is sent as-is. An uncaught exception becomes a generic 500 with no details, so internal errors never leak on their own. The risk is what you put on the exceptions you do send. - Don't put stack traces, secrets, database IDs, or internal messages into serializable exception fields. Send only what the user should see. - Write user-facing messages, and keep the diagnostic detail in your server logs where you can look it up later. - Validate and sanitize input before acting on it, so a bad request fails cleanly instead of surfacing an internal error. ## Related - [Database exceptions](https://docs.serverpod.dev/concepts/data-and-the-database/database/exceptions.md): the typed exceptions that database operations throw on the server. --- # Streaming https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming Some features need the server to push data to the app the moment it changes: chat, multiplayer games, live dashboards. Streaming methods cover this: an endpoint method that returns or receives a Dart `Stream`, transmitted over a shared WebSocket connection that Serverpod manages for you. For a real-world example, check out [Pixorama](https://pixorama.live). It's a multi-user drawing experience that showcases Serverpod's real-time capabilities. Its complete source code is available. ## Streaming methods When an endpoint method is defined with `Stream` instead of `Future` as the return type, or includes `Stream` as a method parameter, it is recognized as a streaming method. ### Define a streaming method Streaming methods are defined by using the `Stream` type as either the return value or a parameter. Following is an example of a streaming method that echoes back any message: ```dart class ExampleEndpoint extends Endpoint { Stream echoStream(Session session, Stream stream) async* { await for (var message in stream) { yield message; } } } ``` The `Stream` can also be given a type argument, such as `Stream`. The type carries into the generated client, so the compiler checks the messages you send and receive. The streaming method above can then be called from the client like this: ```dart var inStream = StreamController(); var outStream = client.example.echoStream(inStream.stream); outStream.listen((message) { print('Received message: $message'); }); inStream.add('Hello'); inStream.add(42); // This will print // Received message: Hello // Received message: 42 ``` In the example above, the `echoStream` method reads each message from its stream parameter and sends it back on its return stream. These streams have no type argument, so they are dynamic and can carry any type that Serverpod can serialize. ### Lifecycle of a streaming method Each time the client calls a streaming method, a new [`Session`](https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions.md) is created for the call, and it closes automatically when the call is over. If the WebSocket connection is lost, all streaming methods are closed on both the server and the client. How long the call stays alive depends on the method's shape: - A method returning a `Stream` is kept alive until the client cancels its subscription or the method returns. - A method returning a `Future` (with `Stream` parameters) is kept alive until the method returns. A stream parameter closes when the client closes the stream or the server cancels its subscription to it, and all stream parameters close when the call ends. ### Authentication Authentication is integrated into streaming method calls. When a client initiates a streaming method, the server automatically authenticates the session. Authentication is validated when the stream is first established, using the authentication data stored in the `Session` object. If a user's authentication is revoked, the stream is closed and an exception is thrown. For more details on handling revoked authentication, refer to the section on [handling revoked authentication](https://docs.serverpod.dev/concepts/authentication/custom-overrides.md#handling-revoked-authentication). ### WebSocket ping interval The server sends periodic ping messages on open streaming connections to keep them alive. The interval between pings is configurable and defaults to 30 seconds. If you deploy behind a load balancer or proxy with a shorter idle timeout (for example, 15-20 seconds), you may need to lower the ping interval so connections are not closed. Set the `SERVERPOD_WEBSOCKET_PING_INTERVAL` environment variable to the desired interval in seconds, or configure `websocketPingInterval` in your config file. See the [Configuration reference](https://docs.serverpod.dev/concepts/lookups/configuration-reference.md). ### Error handling A streaming call can fail in three ways: - Errors your code raises travel over the stream as [serializable exceptions](https://docs.serverpod.dev/concepts/endpoints-and-apis/error-handling-and-exceptions.md). - When the server refuses to open the stream, the call fails with one of the [`ServerpodClientHttpException` subclasses](https://docs.serverpod.dev/concepts/endpoints-and-apis/error-handling-and-exceptions.md#handle-errors-in-your-app) that a regular call uses. For example, a caller who is not signed in gets `ServerpodClientUnauthorized`. - Failures in the connection itself throw a [`MethodStreamException` subtype](#connection-level-exceptions). If an exception is thrown on a stream, the stream is closed with an exception. If the thrown exception is serializable, it is serialized and delivered over the stream before the stream closes, in both directions: stream parameters can pass exceptions to the server, and return streams can pass exceptions to the client. Define the exception in a model file, like any [serializable exception](https://docs.serverpod.dev/concepts/endpoints-and-apis/error-handling-and-exceptions.md): ```yaml exception: CountdownException fields: message: String ``` Throw it from the streaming method, and catch it on the stream in your app: ```dart class ExampleEndpoint extends Endpoint { Stream countdown(Session session) async* { yield 3; yield 2; throw CountdownException(message: 'Countdown aborted'); } } ``` ```dart var stream = client.example.countdown(); stream.listen((number) { print('Countdown: $number'); }, onError: (error) { if (error is CountdownException) { print('Countdown failed: ${error.message}'); } }); ``` In the other direction, add the error to a stream parameter on the client, and catch it typed on the server around the `await for`: ```dart Future reportErrors(Session session, Stream values) async { try { await for (final _ in values) {} return 'completed without error'; } on CountdownException catch (e) { return 'caught: ${e.message}'; } } ``` ```dart var controller = StreamController(); var result = client.example.reportErrors(controller.stream); controller.addError(CountdownException(message: 'Error from client')); await controller.close(); print(await result); // caught: Error from client ``` ### Connection-level exceptions When the failure is in the connection rather than your code, the client throws a subtype of `MethodStreamException`: | Exception | When it is thrown | | ------------------------------------ | -------------------------------------------------------------------------- | | `WebSocketConnectException` | The WebSocket connection to the server could not be opened. | | `ConnectionAttemptTimedOutException` | Opening the connection did not complete in time. | | `WebSocketListenException` | Listening on the WebSocket failed after it was opened. | | `WebSocketClosedException` | The WebSocket closed while the stream was in use. | | `ConnectionClosedException` | The stream connection closed, for example when authentication was revoked. | | `MethodStreamIdleTimeoutException` | The stream was idle past the client's idle timeout. | Catch `MethodStreamException` to handle them as one group, or match specific subtypes when the reaction differs. ## Example: live updates for a filtered query A common real-time need is to push updates for one record or filter, for example the messages in a single chat room. Combine a streaming method with [server events](https://docs.serverpod.dev/concepts/endpoints-and-apis/server-events.md): the streaming method subscribes the client to a channel scoped to that filter, and whatever changes the data posts to the same channel. ```dart class ChatEndpoint extends Endpoint { // The client subscribes to live messages for one room. Stream watchRoom(Session session, int roomId) async* { yield* session.messages.createStream('room_$roomId'); } // Posting a message stores it and broadcasts it to everyone watching the room. Future postToRoom(Session session, int roomId, String text) async { var message = ChatMessage(roomId: roomId, text: text); await ChatMessage.db.insertRow(session, message); await session.messages.postMessage('room_$roomId', message); } } ``` On the client, listen to the returned stream and call `postToRoom` to publish: ```dart var messages = client.chat.watchRoom(roomId); messages.listen((message) { print('Room ${message.roomId}: ${message.text}'); }); await client.chat.postToRoom(roomId, 'Hello, room!'); ``` Because the channel name includes the `roomId`, each client receives updates only for the room it is watching. The same pattern works for any filter: scope the channel by the id or query you care about, and post to it whenever the data changes. To fan the updates out across multiple server instances, enable Redis (see [Message scope](https://docs.serverpod.dev/concepts/endpoints-and-apis/server-events.md#message-scope)). --- # Server events https://docs.serverpod.dev/concepts/endpoints-and-apis/server-events Serverpod has a built-in event system: post a message to a named channel, and every listener on that channel receives it, even on other servers in a cluster. It is how one user's action reaches everyone else, like the chat-room updates in [streaming's live example](https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming.md#example-live-updates-for-a-filtered-query). You access it through `session.messages`. ## Send messages Send a message with the `postMessage` method. The message is published to the specified channel and must be a [data model](https://docs.serverpod.dev/concepts/data-and-the-database/models.md). ```dart var message = UserUpdate(); // Model that represents changes to user data. await session.messages.postMessage('user_updates', message); ``` In the example above, the message is published on the `user_updates` channel. Any subscriber to this channel in the server will receive the message. ### Message scope Serverpod uses Redis to pass messages between servers. A message is sent to every server connected to the same Redis instance when [Redis is enabled](https://docs.serverpod.dev/concepts/operations/redis.md), and stays on the current server when it is not. Set the `scope` parameter to choose the delivery explicitly. | Scope | Delivery | | --------------------- | ----------------------------------------------------------------------- | | `MessageScope.auto` | Cluster-wide if Redis is enabled, otherwise local. This is the default. | | `MessageScope.global` | Cluster-wide. Throws a `StateError` if Redis is not enabled. | | `MessageScope.local` | Only to subscribers on the current server. | The default already goes cluster-wide when Redis is enabled. Pass `MessageScope.local` to keep a message on the current server: ```dart var message = UserUpdate(); // Model that represents changes to user data. await session.messages.postMessage( 'user_updates', message, scope: MessageScope.local, ); ``` ## Receive messages Serverpod provides two ways to handle incoming messages: by creating a stream that subscribes to a channel or by adding a listener to a channel. ### Create a stream To create a stream that subscribes to a channel, use the `createStream` method. The stream emits a value whenever a message is posted to the channel. The typed variant, `createStream(...)`, emits an error if a posted message does not match the type. ```dart var stream = session.messages.createStream('user_updates'); stream.listen((message) { print('Received message: $message'); }); ``` In the above example, a stream is created that listens to the `user_updates` channel and processes incoming messages. #### Stream lifecycle The stream is automatically closed when the session is closed. To close the stream manually, cancel the stream subscription. ```dart var stream = session.messages.createStream('user_updates'); var subscription = stream.listen((message) { print('Received message: $message'); }); subscription.cancel(); ``` ### Add a listener To add a listener to a channel, use the `addListener` method. The listener will be called whenever a message is posted to the channel. ```dart session.messages.addListener('user_updates', (message) { print('Received message: $message'); }); ``` In the above example, the listener will be called whenever a message is posted to the `user_updates` channel. Listeners receive every message that reaches this server, whatever its scope. #### Listener lifecycle The listener is automatically removed when the session is closed. To manually remove a listener, use the `removeListener` method. ```dart var myListenerCallback = (message) { print('Received message: $message'); }; // Register the listener session.messages.addListener('user_updates', myListenerCallback); // Remove the listener session.messages.removeListener('user_updates', myListenerCallback); ``` In the above example, the listener is first added and then removed from the `user_updates` channel. ## Broadcast revoked authentication The messaging interface also carries authentication revocation events: `session.messages.authenticationRevoked(userIdentifier, message)` notifies every server that a user's authentication changed, falling back to local delivery when Redis is disabled. For when and how to call it, see [handling revoked authentication](https://docs.serverpod.dev/concepts/authentication/custom-overrides.md#handling-revoked-authentication). --- # Caching https://docs.serverpod.dev/concepts/endpoints-and-apis/caching Some work is expensive to repeat: a query that joins several tables, a result assembled from many rows, or a value fetched from another service. Caching stores the result under a key so the next request can read it back instead of doing the work again. Serverpod's caches are reached from the [`Session`](https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions.md) object, alongside the database and messaging. Anything you cache must be serializable: the primitives (`int`, `String`, `bool`, `double`, `DateTime`, `Duration`, `ByteData`, `UuidValue`, `Uri`, `BigInt`), collections of them, and your own [models](https://docs.serverpod.dev/concepts/data-and-the-database/models.md). Use a model rather than a hand-written class: one with a `toJson` stores fine and throws on read, one without fails immediately with `JsonUnsupportedObjectError`. ## The caches | Cache | Where it lives | Use it for | | -------------------------- | ---------------------------------------- | -------------------------------------------------------------- | | `session.caches.local` | Memory of the server handling this call | The default choice for cached values. | | `session.caches.localPrio` | Memory of the same server, kept separate | Values you want to keep hot, held apart from ordinary entries. | | `session.caches.global` | Redis, shared by every server instance | Values that must be the same on every instance. | There is a fourth cache, `session.caches.query`, which Serverpod manages for cacheable database queries. It is not a place to put your own objects. Each local cache holds at most 10,000 entries. When a cache is full, the oldest entry is dropped to make room, based on when it was inserted rather than when it was last read, so a frequently read key can still be evicted. ## Cache an object Read a value with `get`, and write one with `put`: ```dart Future getUserData(Session session, int userId) async { var cacheKey = 'UserData-$userId'; // Try the cache first. var userData = await session.caches.local.get(cacheKey); if (userData != null) return userData; // Not cached, so load it and store it for five minutes. userData = await UserData.db.findById(session, userId); if (userData != null) { await session.caches.local.put( cacheKey, userData, lifetime: const Duration(minutes: 5), ); } return userData; } ``` The `lifetime` sets how long the entry stays valid. After it elapses, the next read misses and returns `null`. Omit `lifetime` and the entry never expires on its own, so it stays until you invalidate it or it is evicted to make room. Cache `DateTime` values as UTC, since that is how they come back: ```dart await session.caches.local.put('lastUpdate', DateTime.now().toUtc()); var cached = await session.caches.local.get('lastUpdate'); ``` Collections work the same way. Give `get` the full type, including what the collection holds: ```dart await session.caches.local.put('users', [alice, bob]); var users = await session.caches.local.get>('users'); ``` Serverpod generates the code to read a collection type back only when your project uses it, such as in an endpoint signature or a model field. Caching a shape that appears nowhere else in your project throws on read. ### Load on a miss The `CacheMissHandler` class folds "read it, and compute it if it is missing" into one call. Serverpod stores whatever the handler returns: ```dart Future getUserData(Session session, int userId) async { return session.caches.local.get( 'UserData-$userId', CacheMissHandler( () async => UserData.db.findById(session, userId), lifetime: const Duration(minutes: 5), ), ); } ``` The call returns `null` when the handler does, and nothing is stored in that case. If several requests miss the same key at once, the local caches run the handler once and let the others wait for that result. The Redis-backed global cache runs it per request. ## Invalidate entries A cached value goes stale as soon as the underlying data changes, so remove it when you write: ```dart await UserData.db.updateRow(session, userData); await session.caches.local.invalidateKey('UserData-${userData.id}'); ``` To drop several related entries at once, put them in a group and invalidate the group: ```dart await session.caches.local.put( 'UserData-$userId', userData, group: 'user-$userId', ); // Later, remove every entry in the group. await session.caches.local.invalidateGroup('user-$userId'); ``` Use `containsKey` to check for an entry without reading it, and `clear` to empty the cache. :::warning Groups are a local-cache feature. On `session.caches.global`, passing `group:` to `put` or calling `invalidateGroup` throws `UnimplementedError`, so code that groups entries breaks when you move it to the global cache. ::: ## The global cache and Redis The global cache uses Redis, which is what makes it shared between server instances. When Redis is not configured, it falls back to an in-memory cache that belongs to a single server process. :::warning The fallback applies in every run mode, production included. It exists so you can run code that uses the global cache without a Redis server during development, not as a substitute for Redis in production. In that state each instance keeps its own copy, nothing is shared between instances, and everything is lost on restart. The server does not warn you about this at normal logging levels, and the [readiness probe](https://docs.serverpod.dev/concepts/operations/health-checks.md) still passes, so confirm your Redis configuration before running more than one instance. If Redis is configured but unreachable, the readiness probe fails, so that case does get caught. ::: Redis counts as configured when your config has a `redis` section that is enabled. A `redis` section that omits `enabled` is treated as enabled. If the section is present but its password is missing, the server fails to start rather than falling back. See [Redis](https://docs.serverpod.dev/concepts/operations/redis.md) for how to enable it, set the password, and connect a client. Serverpod does not manage the size of the global cache or decide what it evicts. Redis does, according to how you have configured it, so set a memory limit and an eviction policy there if you cache a large number of objects globally. Serverpod Cloud does not provide a managed Redis instance, so a deployed app uses the in-memory fallback unless you connect your own. See the [Redis guide](https://docs.serverpod.dev/cloud/guides/redis.md) for how to set one up. ## Send Redis commands directly Redis supports operations the cache API does not expose, such as atomic counters and sorted sets. You can borrow the connection Serverpod manages: ```dart Future incrementCounter(Session session, String counterName) async { var command = await session.serverpod.redisController?.getConnection(); if (command == null) return null; var result = await command.send_object(['INCR', 'my_app:$counterName']); return result is int ? result : null; } ``` The connection is `null` when Redis is disabled or unreachable, including when the global cache is on its in-memory fallback. Responses come back in the shape Redis defines for each command, so check the result before casting it. The returned `RedisCommand` is Serverpod's own connection: do not close it, and do not replace `Serverpod.redisController`. Namespace your keys so they cannot collide with Serverpod's own cache entries. ## Related - [Sessions](https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions.md): the object the caches hang off. - [Server events](https://docs.serverpod.dev/concepts/endpoints-and-apis/server-events.md): the other Redis-backed feature, for messaging between instances. - [Redis](https://docs.serverpod.dev/concepts/operations/redis.md): enabling Redis, the password, and connecting a GUI. --- # File uploads https://docs.serverpod.dev/concepts/endpoints-and-apis/file-uploads Let your users upload avatars, documents, or any other files. The app sends the file straight to storage instead of through your endpoint methods, which keeps large files out of your API calls. Out of the box, your server stores files in the database, which works well for development. In production, configure Google Cloud Storage, AWS S3, or Cloudflare R2 instead. To keep files on local disk or a NAS, see [Custom cloud storage](https://docs.serverpod.dev/concepts/endpoints-and-apis/custom-cloud-storage.md). ## Upload a file A `public` and a `private` file storage are set up by default. You can replace these or add more configurations for other file storages. ### Server-side code There are a few steps required to upload a file. First, you must create an upload description on the server and pass it to your app. The upload description grants access to the app to upload the file. If you want to grant access to any file, you can add the following code to one of your endpoints. However, in most cases, you may want to restrict which files can be uploaded. ```dart Future getUploadDescription(Session session, String path) async { return await session.storage.createUploadDescription( storageId: 'public', path: path, ); } ``` The `createUploadDescription` method also accepts an optional `UploadOptions` object to control the upload: - **`UploadOptions.expirationDuration`**: How long the upload URL is valid. Defaults to 10 minutes. - **`UploadOptions.maxFileSize`**: Maximum allowed file size in bytes. Defaults to 10 MiB. - **`UploadOptions.contentLength`**: The exact file size in bytes. When provided, the storage provider validates the upload size against `maxFileSize`. - **`UploadOptions.preventOverwrite`**: When `true`, the upload will fail if a file already exists at the given path. Defaults to `false`. - **`UploadOptions.metadata`**: HTTP metadata and custom key-value data to store with the file. Upload metadata is represented by `FileMetadata`. It supports `contentType`, `cacheControl`, `contentDisposition`, `contentEncoding`, and a `custom` map. Serverpod includes these values in the signed upload request so the storage provider can attach them to the file. ```dart Future getRestrictedUploadDescription( Session session, String path, int fileSize, ) async { return await session.storage.createUploadDescription( storageId: 'public', path: path, options: UploadOptions( maxFileSize: 50 * 1024 * 1024, // 50 MB contentLength: fileSize, preventOverwrite: true, metadata: const FileMetadata( contentType: 'image/jpeg', cacheControl: 'public, max-age=3600', custom: {'source': 'profile-image'}, ), ), ); } ``` An option may not be supported by every storage provider. Serverpod throws an exception when an unsupported or invalid option is used. After the file is uploaded, verify that the upload completed. With a third-party service such as S3 or Google Cloud Storage, this is the only way to know it was not canceled. ```dart Future verifyUpload(Session session, String path) async { return await session.storage.verifyUpload( storageId: 'public', path: path, ); } ``` ### Client-side code To upload a file from the app side, first request the upload description. Next, upload the file, from either a `Stream` or a `ByteData` object. When uploading from a `Stream`, pass the file length if you know it: without a length, a multipart upload buffers the whole file in memory. The uploader does not report upload progress. On failure it returns `false` with no status code and no exception, so a network error and a file that is too large look the same. Finally, verify the upload with the server. ```dart final uploadDescription = await client.myEndpoint.getUploadDescription('myfile'); final uploader = FileUploader(uploadDescription); final uploaded = await uploader.upload(myStream, myFileLength); final success = await client.myEndpoint.verifyUpload('myfile'); ``` :::info In a real-world app, you most likely want to create the file paths on your server. For your file paths to be compatible with S3, do not use a leading slash. Only use standard characters and numbers. For example: ```dart 'profile/$userId/images/avatar.png' ``` ::: ## Size limits When the app uploads through your API server (the default database storage), two size limits apply. The upload uses the smaller of the two. | Limit | Default | Set in | | --------------------------- | ---------------- | -------------------------------------------------------- | | `maxRequestSize` | 524288 (512 KiB) | `config/.yaml` or `SERVERPOD_MAX_REQUEST_SIZE` | | `UploadOptions.maxFileSize` | 10 MiB | `createUploadDescription` | A 5 MB file with default config is rejected even though `maxFileSize` is 10 MiB. Raise `maxRequestSize` in every run-mode YAML you use to at least the largest file you accept. See the [Configuration reference](https://docs.serverpod.dev/concepts/lookups/configuration-reference.md). Uploads to S3, Google Cloud Storage, and R2 go to the provider, so `maxRequestSize` does not apply to the file body. ## Access stored files You can check if a file exists or retrieve it directly from your server. Files in public storage are also accessible via URL. To check if a file exists, use the `fileExists` method. ```dart final exists = await session.storage.fileExists( storageId: 'public', path: 'my/file/path', ); ``` If the file is in a public storage, you can access it through its URL. ```dart final url = await session.storage.publicDownloadUrl( storageId: 'public', path: 'my/file/path', ); ``` To access public URLs for many files at once, use `publicDownloadUrls`. ```dart final urls = await session.storage.publicDownloadUrls( storageId: 'public', paths: const ['my/file/path', 'my/other-file/path'], ); ``` A private storage can provide temporary, signed URLs when you want an authorized user to download a specific file without making the whole storage public. First confirm that the caller is allowed to access the file. Then create a signed URL that grants temporary access to the file: ```dart final url = await session.storage.temporaryDownloadUrl( storageId: 'private', path: 'my/private/file/path', options: const TemporaryDownloadUrlOptions( expirationDuration: Duration(minutes: 5), downloadFileName: 'private.pdf', contentType: 'application/pdf', ), ); ``` Anyone who has the signed URL can download that one file until the URL expires. Return it only after your endpoint has authenticated and authorized the caller. `downloadFileName` asks the browser to download the response under that name, and `contentType` overrides the response content type. Use `statFile` to retrieve the file size, last-modified time, content headers, provider entity tag, and custom metadata: ```dart final stat = await session.storage.statFile( storageId: 'public', path: 'my/file/path', ); ``` You can also retrieve a file directly from your server: ```dart final myByteData = await session.storage.retrieveFile( storageId: 'public', path: 'my/file/path', ); ``` To store a file directly from the server, use `storeFile` with `StoreFileOptions`. You can set `preventOverwrite` to `true` to ensure the write fails if a file already exists at the given path. ```dart await session.storage.storeFile( storageId: 'public', path: 'my/file/path', byteData: myByteData, options: const StoreFileOptions( preventOverwrite: true, metadata: FileMetadata( contentType: 'application/pdf', cacheControl: 'private, max-age=300', custom: {'document-type': 'invoice'}, ), ), ); ``` `StoreFileOptions` also has an optional `expiration` timestamp. Storage providers that cannot expire individual files throw an exception when it is set. To delete a stored file, use `deleteFile` with the same `storageId` and `path`. ## Save a file reference in a model To link a stored file to your data, such as a document a user uploaded, save where the file lives rather than the file itself. Add `storageId` and `path` fields to the [model](https://docs.serverpod.dev/concepts/data-and-the-database/models.md): ```yaml title="document.spy.yaml" class: Document table: document fields: title: String storageId: String path: String ``` The storage methods above identify a file by its `storageId` and `path`. These two fields are enough to read, delete, or create a URL for the file later. - **Private storage:** Don't save a URL. Signed URLs expire, so call `temporaryDownloadUrl` each time an authorized user asks for the file. - **Public storage:** You can also save the URL from `publicDownloadUrl` in a `Uri` field, so the app gets the link along with the rest of the data. Keep `storageId` and `path` next to it, because a URL alone isn't enough to delete or replace the file. A saved URL also keeps the domain it was created with, so create new URLs from those two fields if you move the files or change their domain. ## Configure a storage provider Each storage is identified by a `storageId`. Serverpod comes with two default storages, `public` and `private`. Replace these with a cloud-backed implementation, or add additional storages with custom IDs. Calling `pod.addCloudStorage` with `public` or `private` replaces that default. Call it before `pod.start()`. For local disk or a NAS, see [Custom cloud storage](https://docs.serverpod.dev/concepts/endpoints-and-apis/custom-cloud-storage.md). In new projects, `server.dart` already swaps both defaults for `ServerpodCloudProvider`, from the `serverpod_cloud_storage` package: ```dart pod.addCloudStorage( await ServerpodCloudProvider.private( fallback: () => DatabaseCloudStorage('private'), ), ); pod.addCloudStorage( await ServerpodCloudProvider.public( fallback: () => DatabaseCloudStorage('public'), ), ); ``` To connect a storage you created in your Serverpod Cloud project under another id, use `custom` with that id. This needs `serverpod_cloud_storage` 4.0.2 or later: ```dart pod.addCloudStorage( await ServerpodCloudProvider.custom( storageId: 'exports', fallback: () => DatabaseCloudStorage('exports'), ), ); ``` If the storage does not exist in your Cloud project, the server throws a `CloudStorageException` at startup. To create one, see [Storage](https://docs.serverpod.dev/cloud/concepts/storage.md) in the Cloud docs. When Serverpod Cloud provides storage for your project, these calls connect to it. Otherwise, for example when you run the server locally, the `fallback` runs and files are stored in the database. To keep files somewhere else, replace these calls with one of the storages below. Pick the package that matches your provider. Use [serverpod\_cloud\_storage\_s3](https://pub.dev/packages/serverpod_cloud_storage_s3) for AWS S3, [serverpod\_cloud\_storage\_gcp](https://pub.dev/packages/serverpod_cloud_storage_gcp) for Google Cloud Storage, or [serverpod\_cloud\_storage\_r2](https://pub.dev/packages/serverpod_cloud_storage_r2) for Cloudflare R2. ### Configure Google Cloud Storage The default GCP path uses HMAC keys and the S3-compatible API. Create a [service account HMAC key](https://cloud.google.com/storage/docs/authentication/hmackeys), then: 1. Grant the service account the Storage Admin role. 2. Create an HMAC key for that account under **Cloud Storage** > **Settings** > **Interoperability**. 3. When you create the bucket, set **Access control** to **Fine-grained** and turn off **Prevent public access**. Add the HMAC keys to `config/passwords.yaml`, or pass them as `SERVERPOD_HMAC_ACCESS_KEY_ID` and `SERVERPOD_HMAC_SECRET_KEY`. ```yaml title="config/passwords.yaml" shared: HMACAccessKeyId: 'XXXXXXXXXXXXXX' HMACSecretKey: 'XXXXXXXXXXXXXXXXXXXXXXXXXXX' ``` Add the package and import it in `server.dart`. ```bash dart pub add serverpod_cloud_storage_gcp ``` ```dart import 'package:serverpod_cloud_storage_gcp/serverpod_cloud_storage_gcp.dart' as gcp; ``` The `bucket` parameter is the GCP bucket name. Set `publicHost` if the bucket is reachable on a custom domain behind a load balancer. Use the bucket's GCS region, for example `us-central1`. ```dart pod.addCloudStorage( gcp.GoogleCloudStorage( serverpod: pod, storageId: 'public', public: true, region: 'us-central1', bucket: 'my-bucket-name', publicHost: 'storage.myapp.com', ), ); ``` ### Use native Google Cloud Storage As an alternative to HMAC keys, use Google Cloud Storage's native JSON API with a service account. This path supports custom metadata, conditional writes with `preventOverwrite`, and signed temporary download URLs. It lives in the same `serverpod_cloud_storage_gcp` package. For a public storage, make the bucket itself publicly readable, because Serverpod doesn't make each uploaded file public. For example, turn on uniform bucket-level access and grant `allUsers` the Storage Object Viewer role. The factory constructors are asynchronous, so create the storage before starting the pod: ```dart pod.addCloudStorage( await gcp.NativeGoogleCloudStorage.create( serverpod: pod, storageId: 'public', public: true, bucket: 'my-bucket-name', publicHost: 'storage.myapp.com', ), ); ``` The `create` factory loads the service account JSON from `passwords.yaml` (key: `gcpServiceAccount`) or the environment variable `SERVERPOD_PASSWORD_gcpServiceAccount`: ```yaml title="config/passwords.yaml" shared: gcpServiceAccount: '{"type":"service_account","project_id":"...","private_key":"...",...}' ``` To pass the JSON directly instead, use `fromServiceAccountJson`: ```dart pod.addCloudStorage( await gcp.NativeGoogleCloudStorage.fromServiceAccountJson( storageId: 'public', public: true, bucket: 'my-bucket-name', serviceAccountJson: myServiceAccountJson, ), ); ``` On Google Compute Engine or Cloud Run, use Application Default Credentials instead: ```dart pod.addCloudStorage( await gcp.NativeGoogleCloudStorage.fromApplicationDefaultCredentials( storageId: 'public', public: true, bucket: 'my-bucket-name', ), ); ``` :::note When using Application Default Credentials, the service account must have the `iam.serviceAccounts.signBlob` IAM permission to generate signed URLs. ::: ### Configure AWS S3 Create an S3 bucket and an IAM user whose access is limited to that bucket. Avoid root-user access keys. Put CloudFront in front of the bucket if you want a custom domain and TLS certificate. Add the access keys to `config/passwords.yaml`, or pass them as `SERVERPOD_AWS_ACCESS_KEY_ID` and `SERVERPOD_AWS_SECRET_KEY`. ```yaml title="config/passwords.yaml" shared: AWSAccessKeyId: 'XXXXXXXXXXXXXX' AWSSecretKey: 'XXXXXXXXXXXXXXXXXXXXXXXXXXX' ``` Add the package and import it in `server.dart`. ```bash dart pub add serverpod_cloud_storage_s3 ``` ```dart import 'package:serverpod_cloud_storage_s3/serverpod_cloud_storage_s3.dart' as s3; ``` Set `publicHost` if the bucket is accessible on a custom domain through CloudFront. ```dart pod.addCloudStorage( s3.S3CloudStorage( serverpod: pod, storageId: 'public', public: true, region: 'us-west-2', bucket: 'my-bucket-name', publicHost: 'storage.myapp.com', ), ); ``` ### Configure Cloudflare R2 R2 is S3-compatible and uses presigned PUT uploads. Create [R2 API tokens](https://developers.cloudflare.com/r2/api/tokens/) in the Cloudflare dashboard, then add them to `config/passwords.yaml`, or pass them as `SERVERPOD_R2_ACCESS_KEY_ID` and `SERVERPOD_R2_SECRET_KEY`. ```yaml title="config/passwords.yaml" shared: R2AccessKeyId: 'XXXXXXXXXXXXXX' R2SecretKey: 'XXXXXXXXXXXXXXXXXXXXXXXXXXX' ``` Add the package and import it in `server.dart`. ```bash dart pub add serverpod_cloud_storage_r2 ``` ```dart import 'package:serverpod_cloud_storage_r2/serverpod_cloud_storage_r2.dart' as r2; ``` Configure the storage with your Cloudflare account ID and bucket name: ```dart pod.addCloudStorage( r2.R2CloudStorage( serverpod: pod, storageId: 'public', public: true, bucket: 'my-bucket-name', accountId: 'your-cloudflare-account-id', publicHost: 'storage.myapp.com', ), ); ``` ## Use a custom S3-compatible endpoint The S3, GCP HMAC, and R2 packages are the ones to add by hand. They sit on [serverpod\_cloud\_storage\_s3\_compat](https://pub.dev/packages/serverpod_cloud_storage_s3_compat), which you can also use directly for another S3-compatible service such as LocalStack. ```bash dart pub add serverpod_cloud_storage_s3_compat ``` ```dart import 'package:serverpod_cloud_storage_s3_compat/serverpod_cloud_storage_s3_compat.dart'; ``` Pass credentials yourself. `CustomEndpointConfig` sets the base URI. `MultipartPostUploadStrategy` matches AWS-style POST uploads, for example LocalStack. Use `PresignedPutUploadStrategy` for providers that only accept presigned PUT, such as R2. ```dart pod.addCloudStorage( S3CompatCloudStorage( storageId: 'public', public: true, region: 'us-east-1', bucket: 'my-bucket-name', accessKey: pod.getPassword('MyAccessKeyId')!, secretKey: pod.getPassword('MySecretKey')!, endpoints: CustomEndpointConfig( baseUri: Uri.http('localhost:4566', '/'), serviceName: 'LocalStack', ), uploadStrategy: MultipartPostUploadStrategy(), ), ); ``` ## Related - [Custom cloud storage](https://docs.serverpod.dev/concepts/endpoints-and-apis/custom-cloud-storage.md): local disk, NAS, and implementing `CloudStorage`. - [Configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md): passwords file and environment variables for storage keys. - [Sessions](https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions.md): the `storage` member used in the examples above. --- # Custom cloud storage https://docs.serverpod.dev/concepts/endpoints-and-apis/custom-cloud-storage Store files on a NAS, a local disk, or another object store when AWS, Google Cloud Storage, or Cloudflare R2 are not the right fit. A custom backend implements `CloudStorage` and is registered with `pod.addCloudStorage` before the server starts. Serverpod does not ship a local filesystem package yet, so this page is the workaround: subclass the default database storage and write bytes to disk. The [file uploads](https://docs.serverpod.dev/concepts/endpoints-and-apis/file-uploads.md) page covers the default database backend and the GCP, S3, and R2 packages. Use this page when those are not enough. ## When to use a custom backend | You want | Use | | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | Files on local disk or a NAS, with `FileUploader` talking to your API server | This page | | MinIO, Synology S3, LocalStack, or another S3 API | [A custom S3-compatible endpoint](https://docs.serverpod.dev/concepts/endpoints-and-apis/file-uploads.md#use-a-custom-s3-compatible-endpoint) | | AWS S3, Google Cloud Storage, or Cloudflare R2 | [Configure a storage provider](https://docs.serverpod.dev/concepts/endpoints-and-apis/file-uploads.md#configure-a-storage-provider) | Pick one upload path and stick to it. Mixing a disk backend with a signed provider URL (or the other way around) is why `storeFile` never runs and `FileUploader` returns `false`. ## How uploads reach storage The `FileUploader` never calls `CloudStorage`. It POSTs or PUTs to the URL inside the upload description. **Through your API server** (database storage, local disk, NAS): 1. An authorized endpoint calls `session.storage.createUploadDescription`. 2. Storage inserts a short-lived auth row and returns a URL on your API server: `/serverpod_cloud_storage?method=upload&storage=...&path=...&key=...`. 3. The app POSTs the raw bytes to that URL. 4. The server checks the auth row and calls `storeUnverifiedFile` (not `storeFile`). 5. An authorized endpoint calls `session.storage.verifyUpload`. **Straight to an object store** (S3, GCP, R2, MinIO): 1. The storage returns a signed URL at the provider. 2. The app uploads to that URL. The file never passes through Serverpod. 3. Call `verifyUpload` to check that the object exists. 4. The `storeFile` method is not called. Use it only for server-side writes (`session.storage.storeFile`). A class that extends `CloudStorage` and returns JSON whose `url` is a local file path never receives the upload. The client has no valid URL to POST to, and even a correct server URL would call `storeUnverifiedFile`, not `storeFile`. The built-in upload URL only accepts a `DatabaseCloudStorage`. Extend that class, not `CloudStorage`, if the app uses `FileUploader` against your API server. ## Store files on local disk Keep using `FileUploader` against the API server. Extend `DatabaseCloudStorage` so the upload URL stays registered. Keep the inherited `createUploadDescription`, `publicDownloadUrl`, and `temporaryDownloadUrl`. Those create the auth row and the `/serverpod_cloud_storage` URLs. Override every method that reads or writes bytes, including `storeUnverifiedFile` and `retrieveFileWithStat`, which the upload URL calls and which are not on `CloudStorage`. The database is still required. Upload auth and temporary download tokens live in tables; the file bytes go to disk. ```dart title="lib/src/cloud_storage/local_storage.dart" import 'dart:io' as io; import 'dart:typed_data'; import 'package:path/path.dart' as p; import 'package:serverpod/serverpod.dart'; class LocalStorage extends DatabaseCloudStorage { LocalStorage({ required String storageId, required String pathPrefix, }) : pathPrefix = p.normalize(p.absolute(pathPrefix)), super(storageId); final String pathPrefix; io.File _file(String path) { final resolved = p.normalize(p.join(pathPrefix, path)); if (!p.isWithin(pathPrefix, resolved)) { throw CloudStorageException( 'Path "$path" is outside storage "$storageId".', ); } return io.File(resolved); } Future _write( String path, ByteData byteData, StoreFileOptions options, ) async { if (options.expiration != null) { throw CloudStorageUnsupportedOperationException( storageId: storageId, operation: 'per-file expiration', ); } final file = _file(path); if (options.preventOverwrite && file.existsSync()) { throw CloudStorageFileAlreadyExistsException( storageId: storageId, path: path, ); } file.parent.createSync(recursive: true); await file.writeAsBytes( byteData.buffer.asUint8List( byteData.offsetInBytes, byteData.lengthInBytes, ), ); } @override Future storeFile({ required Session session, required String path, required ByteData byteData, StoreFileOptions options = const StoreFileOptions(), }) async { await _write(path, byteData, options); } @override Future storeUnverifiedFile({ required Session session, required String path, required ByteData byteData, StoreFileOptions options = const StoreFileOptions(), }) { return storeFile( session: session, path: path, byteData: byteData, options: options, ); } @override Future retrieveFile({ required Session session, required String path, }) async { final file = _file(path); if (!file.existsSync()) { throw CloudStorageFileNotFoundException( storageId: storageId, path: path, ); } final bytes = await file.readAsBytes(); return ByteData.sublistView(bytes); } @override Future<({ByteData file, FileStat stat})> retrieveFileWithStat({ required Session session, required String path, }) async { final bytes = await retrieveFile(session: session, path: path); return ( file: bytes, stat: await statFile(session: session, path: path), ); } @override Future statFile({ required Session session, required String path, }) async { final file = _file(path); if (!file.existsSync()) { throw CloudStorageFileNotFoundException( storageId: storageId, path: path, ); } final ioStat = file.statSync(); return FileStat( size: ioStat.size, lastModified: ioStat.modified.toUtc(), ); } @override Future fileExists({ required Session session, required String path, }) async { return _file(path).existsSync(); } @override Future deleteFile({ required Session session, required String path, }) async { final file = _file(path); if (file.existsSync()) { await file.delete(); } } @override Future verifyUpload({ required Session session, required String path, }) async { return _file(path).existsSync(); } } ``` This example does not persist `FileMetadata` (content type, cache headers, custom keys). Public downloads still get a content type from the file extension. Per-file `expiration` throws `CloudStorageUnsupportedOperationException`. A written file is available immediately, so `retrieveFile` can succeed before `verifyUpload`. The default database storage hides bytes until verify. Use an absolute directory. A `~` in `pathPrefix` is not expanded. ## Register the storage Call `pod.addCloudStorage` before `pod.start()`. The same `storageId` replaces the default. Register both `public` and `private` if you want neither to stay in the database. ```dart import 'dart:io'; import 'package:path/path.dart' as p; ``` ```dart title="lib/server.dart" pod.addCloudStorage( LocalStorage( storageId: 'public', pathPrefix: p.join(Directory.current.path, '..', 'storage', 'public'), ), ); pod.addCloudStorage( LocalStorage( storageId: 'private', pathPrefix: p.join(Directory.current.path, '..', 'storage', 'private'), ), ); ``` The public download URL is served only for `storageId` `public`. Private files need `temporaryDownloadUrl` or `retrieveFile`. See [Access stored files](https://docs.serverpod.dev/concepts/endpoints-and-apis/file-uploads.md#access-stored-files). If you replace both defaults with a class that extends `CloudStorage` rather than `DatabaseCloudStorage`, Serverpod does not register `/serverpod_cloud_storage`, and `FileUploader` has nowhere to post. ## Raise the request size limit Uploads that post to your API server are capped by two limits. The endpoint uses the smaller of the two. | Limit | Default | Set in | | --------------------------- | ---------------- | -------------------------------------------------------- | | `maxRequestSize` | 524288 (512 KiB) | `config/.yaml` or `SERVERPOD_MAX_REQUEST_SIZE` | | `UploadOptions.maxFileSize` | 10 MiB | `createUploadDescription` | A 5 MB file with default config is rejected even though `maxFileSize` is 10 MiB. Raise `maxRequestSize` in every run-mode YAML you use (development, staging, production, and test) to at least the largest file you accept: ```yaml title="config/development.yaml" maxRequestSize: 52428800 ``` Object-store uploads (S3, GCP, R2) go to the provider, so `maxRequestSize` does not apply to the file body. See [Size limits](https://docs.serverpod.dev/concepts/endpoints-and-apis/file-uploads.md#size-limits) on the file uploads page. ## Authorize the upload endpoints The upload URL is a capability token: anyone who has it can POST the file until it expires. Require a signed-in user on the endpoint (`requireLogin`), create the path on the server, and do not accept a client-supplied path that can escape into another user's prefix. See [Authentication basics](https://docs.serverpod.dev/concepts/authentication/basics.md). ```dart class FileEndpoint extends Endpoint { @override bool get requireLogin => true; Future getUploadDescription(Session session) async { final userIdentifier = session.authenticated!.userIdentifier; return await session.storage.createUploadDescription( storageId: 'public', path: 'uploads/$userIdentifier/avatar.png', ); } } ``` Use the same authorization on `verifyUpload`. After a successful verify, persist the path your server generated, not a path the client sent. The rest of the app flow matches [file uploads](https://docs.serverpod.dev/concepts/endpoints-and-apis/file-uploads.md#upload-a-file): describe, `FileUploader.upload`, then `verifyUpload`. On failure, `FileUploader` returns `false` with no status code and no exception, so a dropped connection and a file that is too large look the same. ## Implement the CloudStorage methods Throw `CloudStorageException` subclasses on failure, not generic errors. | Method | Role | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `storeFile` | Server-side write. Honor `StoreFileOptions.preventOverwrite` and `metadata`, or throw `CloudStorageUnsupportedOperationException`. | | `storeUnverifiedFile` | Client upload through `/serverpod_cloud_storage`. Not on `CloudStorage`; override it on `DatabaseCloudStorage`. | | `retrieveFile` | Return bytes, or throw `CloudStorageFileNotFoundException`. | | `retrieveFileWithStat` | Bytes plus metadata for public and temporary HTTP GET. Not on `CloudStorage`. | | `statFile` | Size and metadata. | | `fileExists` | True if the file is available. Override it on a `DatabaseCloudStorage` subclass; the parent queries the database instead of calling `statFile`. | | `deleteFile` | No-op if the file is missing. | | `publicDownloadUrl` | Public HTTP URL, or `CloudStorageUnsupportedOperationException`. | | `temporaryDownloadUrl` | Time-limited URL, or unsupported. | | `createUploadDescription` | Return `BinaryUploadDescription` or `MultipartUploadDescription`. | | `verifyUpload` | `true` if the client upload is present. | The `FileUploader` understands two JSON shapes. Extra keys such as `path`, `expiration`, or `maxFileSize` are ignored. Auth and size belong in the database auth row (API-server uploads) or in the signed URL (object-store uploads). - Binary: `{ "url", "type": "binary", "headers", optional "method", "file-name" }` - Multipart: `{ "url", "type": "multipart", "field", "file-name", "request-fields" }` ## Use an S3-compatible store If the NAS or appliance speaks S3, do not send the file through `/serverpod_cloud_storage`. Use [serverpod\_cloud\_storage\_s3\_compat](https://docs.serverpod.dev/concepts/endpoints-and-apis/file-uploads.md#use-a-custom-s3-compatible-endpoint) with `CustomEndpointConfig`, or extend `CloudStorage` and return a signed `BinaryUploadDescription` with `method: 'PUT'`, the same pattern as native Google Cloud Storage. Then `verifyUpload` checks that the object exists at the provider. `storeFile` is unused for client uploads. ## Troubleshooting ### Why does `FileUploader.upload` return `false` after a few milliseconds? The file is larger than the smaller of `maxFileSize` and `maxRequestSize`. With default config that cap is 512 KiB. The connection can reset (`Connection reset by peer`) instead of returning a 413. Raise `maxRequestSize`, then confirm `uploaded` is `true` before calling `verifyUpload`. ### Why is `storeFile` never called? Client uploads do not call it. Override `storeUnverifiedFile`. If the class extends `CloudStorage` instead of `DatabaseCloudStorage`, the built-in upload handler returns `false` and never reaches your code. ### Why are public URLs 404s after a successful upload? The `retrieveFileWithStat` method still reads the database unless you override it. Inherited `publicDownloadUrl` is fine once that method and `fileExists` read from disk. ## Related - [File uploads](https://docs.serverpod.dev/concepts/endpoints-and-apis/file-uploads.md): describe, upload, verify, and the GCP, S3, and R2 packages. - [Authentication basics](https://docs.serverpod.dev/concepts/authentication/basics.md): `requireLogin` on the upload endpoint. - [Configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md): run-mode YAML files where `maxRequestSize` is set. - [Sessions](https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions.md): the `storage` member used in the examples above. --- # Endpoint inheritance https://docs.serverpod.dev/concepts/endpoints-and-apis/endpoint-inheritance An endpoint can extend another endpoint, declared like any Dart class: `class ChildEndpoint extends ParentEndpoint`. Use inheritance to share behavior across endpoints, such as [auth requirements](https://docs.serverpod.dev/concepts/authentication/basics.md), to extend or reshape endpoints from modules, and to control what the generated client exposes. How the parent is marked decides what gets generated for it. An `abstract` parent is not exposed on the server, and a `@doNotGenerate` parent gets no client code. Your concrete subclass always gets a client, unless it opts out itself. The sections below walk through each case. ## Inheriting from an `Endpoint` class Given an existing `Endpoint` class, it is possible to extend or modify its behavior while retaining the already exposed methods. ```dart import 'package:serverpod/serverpod.dart'; class CalculatorEndpoint extends Endpoint { Future add(Session session, int a, int b) async { return a + b; } } class MyCalculatorEndpoint extends CalculatorEndpoint { Future subtract(Session session, int a, int b) async { return a - b; } } ``` The generated client code will now be able to access both `CalculatorEndpoint` and `MyCalculatorEndpoint`. Whereas the `CalculatorEndpoint` only exposes the original `add` method, `MyCalculatorEndpoint` now exposes both the inherited `add` and its own `subtract` methods. ## Inheriting from an `Endpoint` class marked `abstract` Endpoints marked as `abstract` are not added to the server. But if they are subclassed, their methods will be exposed through the subclass. ```dart import 'package:serverpod/serverpod.dart'; abstract class CalculatorEndpoint extends Endpoint { Future add(Session session, int a, int b) async { return a + b; } } class MyCalculatorEndpoint extends CalculatorEndpoint {} ``` Since `CalculatorEndpoint` is `abstract`, it will not be exposed on the server. However, an abstract client class will be generated, which will be extended by the class generated from `MyCalculatorEndpoint`. The concrete client exposes the `add` method it inherited from `CalculatorEndpoint`. See [Client-side endpoint inheritance](#client-side-endpoint-inheritance) for more details on how abstract endpoints are represented on the client. ### Extending an `abstract` `Endpoint` class In the above example, the `MyCalculatorEndpoint` only exposed the inherited `add` method. It can be further extended with custom methods like this: ```dart import 'package:serverpod/serverpod.dart'; class MyCalculatorEndpoint extends CalculatorEndpoint { Future subtract(Session session, int a, int b) async { return a - b; } } ``` In this case, it will expose both an `add` and a `subtract` method. ## Inheriting from an `Endpoint` class annotated with `@doNotGenerate` Suppose you had an `Endpoint` class marked with `@doNotGenerate` and a subclass that extends it: ```dart import 'package:serverpod/serverpod.dart'; @doNotGenerate class CalculatorEndpoint extends Endpoint { Future add(Session session, int a, int b) async { return a + b; } } class MyCalculatorEndpoint extends CalculatorEndpoint {} ``` Since `CalculatorEndpoint` is marked as `@doNotGenerate`, it will not be exposed on the server, and no client class will be generated for it. Only `MyCalculatorEndpoint` will be accessible from the client, which provides the inherited `add` method from its parent class. When a parent is marked with `@doNotGenerate`, no client class is generated for the parent. The subclass's client class extends `EndpointRef` directly, with the parent's methods inlined alongside the subclass's own methods. ## Overriding endpoint methods It is possible to override methods of the superclass. This can be useful when you want to modify the behavior of specific methods but preserve the rest. ```dart import 'package:serverpod/serverpod.dart'; abstract class GreeterBaseEndpoint extends Endpoint { Future greet(Session session, String name) async { return 'Hello $name'; } } class ExcitedGreeterEndpoint extends GreeterBaseEndpoint { @override Future greet(Session session, String name) async { return '${await super.greet(session, name)}!!!'; } } ``` Since `GreeterBaseEndpoint` is `abstract`, it will not be exposed on the server. The `ExcitedGreeterEndpoint` will expose a single `greet` method, and its implementation will augment the superclass's one by adding `!!!` to that result. This way, you can modify the behavior of endpoint methods while still sharing the implementation through calls to `super`. Be aware that the method signature has to be compatible with the base class per Dart's rules, meaning you can add optional parameters, but can not add required parameters or change the return type. ## Hiding endpoint methods with `@doNotGenerate` In case you want to hide a method that the child class declares itself (i.e. one that is not inherited from a parent class whose client is still being generated), annotate it with `@doNotGenerate` like so: ```dart import 'package:serverpod/serverpod.dart'; class AdderEndpoint extends Endpoint { Future add(Session session, int a, int b) async { return a + b; } @doNotGenerate Future subtract(Session session, int a, int b) async { throw UnimplementedError(); } } ``` The `AdderEndpoint` exposes `add`, but `subtract` is annotated with `@doNotGenerate` and is therefore excluded from the generated client. ### Hiding an inherited method Hiding a method that is *inherited* from a parent class only works if the parent class itself is fully annotated with `@doNotGenerate`, meaning no client class is generated for it at all: ```dart import 'package:serverpod/serverpod.dart'; @doNotGenerate class CalculatorEndpoint extends Endpoint { Future add(Session session, int a, int b) async { return a + b; } Future subtract(Session session, int a, int b) async { return a - b; } } class AdderEndpoint extends CalculatorEndpoint { @doNotGenerate @override Future subtract(Session session, int a, int b) async { throw UnimplementedError(); } } ``` Since `CalculatorEndpoint` is annotated with `@doNotGenerate`, it will not be exposed on the server, and no client class is generated for it. `AdderEndpoint` inlines the inherited methods directly, and since it re-declares `subtract` with `@doNotGenerate`, only `add` is exposed on the generated client. Don't worry about the exception in the `subtract` implementation. That is only added to satisfy the Dart compiler. In practice, nothing will ever call this method on `AdderEndpoint`. :::warning If the parent class is only `abstract` (and not itself annotated with `@doNotGenerate`), Serverpod still generates an abstract client class that mirrors it, and every subclass's generated client must implement all of its methods. In that case, `@doNotGenerate` **cannot** be used to hide an inherited method. Doing so removes the method from the subclass's generated client while the abstract client class still declares it, which causes a Dart compile error ("missing concrete implementation"). To hide an inherited method, the parent class must be marked `@doNotGenerate` itself, not just `abstract`. If the parent class is a normal, concrete class (neither `abstract` nor `@doNotGenerate`), it is exposed on the server and the client in its own right. Hiding the method on the child only removes it from the child's client. It remains accessible through the parent's own generated client class. ::: ## Unhiding endpoint methods annotated with `@doNotGenerate` in the super class The reverse of the previous example would be a base endpoint that has a method marked with `@doNotGenerate`, which you now want to expose on the subclass. ```dart import 'package:serverpod/serverpod.dart'; abstract class CalculatorEndpoint extends Endpoint { Future add(Session session, int a, int b) async { return a + b; } // Ignored, as this expensive computation should not be exposed by default @doNotGenerate Future addBig(Session session, BigInt a, BigInt b) async { return a + b; } } class MyCalculatorEndpoint extends CalculatorEndpoint { @override Future addBig(Session session, BigInt a, BigInt b) async { return super.addBig(session, a, b); } } ``` Since `CalculatorEndpoint` is `abstract`, it will not be exposed on the server. The generated `MyCalculatorEndpoint` client will expose both the `add` and `addBig` methods, since annotations are not inherited: overriding `addBig` without repeating `@doNotGenerate` re-exposes it. ## Building base endpoints for behavior Beyond inheriting or hiding methods, endpoint subclassing can pre-configure any other property of the `Endpoint` class. For example, you could define a base class that requires callers to be signed in, using the `requireLogin` flag from [authentication on endpoints](https://docs.serverpod.dev/concepts/authentication/basics.md): ```dart abstract class LoggedInEndpoint extends Endpoint { @override bool get requireLogin => true; } ``` And now every endpoint that extends `LoggedInEndpoint` will check that the user is logged in. Similarly, you could wrap up a specific set of required scopes in a base endpoint, and use it for the app's endpoints instead of repeating the scopes in each: ```dart abstract class AdminEndpoint extends Endpoint { @override Set get requiredScopes => {Scope.admin}; } ``` Have your custom endpoint extend `AdminEndpoint`, and the user is guaranteed to hold the appropriate [scopes](https://docs.serverpod.dev/concepts/authentication/basics.md). ## Client-side endpoint inheritance When you use endpoint inheritance on the server, Serverpod generates matching client-side classes that mirror your inheritance hierarchy. This allows you to write type-safe client code that works with abstract endpoint types. ### Abstract endpoint client generation When you define an abstract endpoint on the server, Serverpod generates an abstract client endpoint class. This is particularly useful for module developers who want to provide base functionality that users can extend. **Server-side abstract endpoint:** ```dart import 'package:serverpod/serverpod.dart'; abstract class CalculatorEndpoint extends Endpoint { Future add(Session session, int a, int b) async { return a + b; } } ``` **Generated client-side abstract class:** ```dart abstract class EndpointCalculator extends EndpointRef { EndpointCalculator(EndpointCaller caller) : super(caller); Future add(int a, int b); } ``` When you extend this abstract endpoint in your server: ```dart class MyCalculatorEndpoint extends CalculatorEndpoint { Future subtract(Session session, int a, int b) async { return a - b; } } ``` The generated client class will extend the abstract client class: ```dart class EndpointMyCalculator extends EndpointCalculator { EndpointMyCalculator(EndpointCaller caller) : super(caller); @override String get name => 'myCalculator'; @override Future add(int a, int b) => caller.callServerEndpoint( 'myCalculator', 'add', {'a': a, 'b': b}, ); Future subtract(int a, int b) => caller.callServerEndpoint( 'myCalculator', 'subtract', {'a': a, 'b': b}, ); } ``` ### Using `getEndpointOfType` for type-safe endpoint access When working with abstract endpoints, you can use the `getEndpointOfType` method to retrieve concrete endpoint instances by their type. This is especially useful when writing code that depends on abstract endpoint interfaces provided by modules. ```dart // Get an endpoint by its type var calculator = client.getEndpointOfType(); // Now you can call methods defined in the abstract base class var result = await calculator.add(5, 3); ``` The `getEndpointOfType` method will: - Return the single endpoint of the requested type if only one exists. - Throw `ServerpodClientEndpointNotFound` if no endpoint of that type is found. - Throw `ServerpodClientMultipleEndpointsFound` if multiple endpoints of that type exist. #### Disambiguating multiple endpoints If you have multiple concrete implementations of the same abstract endpoint, you can disambiguate by providing the endpoint name: ```dart // Server-side: Two implementations of the same abstract endpoint class BasicCalculatorEndpoint extends CalculatorEndpoint {} class AdvancedCalculatorEndpoint extends CalculatorEndpoint { Future multiply(Session session, int a, int b) async { return a * b; } } ``` ```dart // Client-side: Specify which implementation you want var basicCalc = client.getEndpointOfType('basicCalculator'); var advancedCalc = client.getEndpointOfType('advancedCalculator'); ``` #### Use case: Module-provided abstract endpoints This pattern is especially useful for modules. A module can provide an abstract endpoint that defines an interface, and users of the module can extend it to expose the functionality on their server: **In a module:** Declare an abstract endpoint with common methods on the server: ```dart abstract class AuthSessionEndpoint extends Endpoint { Future isAuthenticated(Session session) async { return session.isUserSignedIn; } Future logout(Session session, {required bool allSessions}) async { // Implementation... } } ``` Write client-side code that depends on the generated abstract type and uses its methods: ```dart class UserLoggedInWidget extends StatelessWidget { final Client client; UserLoggedInWidget({required this.client}); // Will throw if no concrete implementation of the endpoint exists. EndpointAuthSession get sessionEndpoint => client.getEndpointOfType(); @override Widget build(BuildContext context) { return FutureBuilder( future: sessionEndpoint.isAuthenticated(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return CircularProgressIndicator(); } else if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } else if (snapshot.hasData && snapshot.data == true) { return Text('User is logged in'); } else { return Text('User is not logged in'); } }, ); } } ``` **In the user application:** The user extends the abstract endpoint to expose it on their server. Then, any client code that depends on the abstract endpoint will work regardless of the concrete class name or location. ```dart // Extend the module's abstract endpoint to expose it class SessionEndpoint extends AuthSessionEndpoint {} ``` This approach allows module developers to provide reusable endpoint logic while giving application developers full control over which endpoints are exposed on their server. --- # Backward compatibility https://docs.serverpod.dev/concepts/endpoints-and-apis/backward-compatibility As your app evolves, features will be added or changed. Your users may still run older versions of the app, since not everyone updates and app store updates take time. Therefore it may be essential to make updates to your server compatible with older app versions. Following a simple set of rules, your server will stay compatible with older app versions: 1. **Avoid changing parameter names in endpoint methods.** In the API Serverpod generates, the parameters are passed by name. This means that changing the parameter names of the endpoint methods will break backward compatibility. 2. **Do not delete endpoint methods or change their signature.** Instead, add new methods if you must pass another set of parameters. Technically, you can add new named parameters if they are not required, but creating a new method may still feel cleaner. 3. **Avoid changing or removing fields and types in the serialized classes.** However, you are free to add new fields as long as they are nullable or have a default value. 4. **Do not rename registered future calls.** Queued invocations are stored in the database under the future call's registered name, so a rename breaks calls that were scheduled before the update. 5. **Keep future call payload models compatible.** A future call receives a single serializable object, and queued invocations were serialized with the old model version, so the model rules above apply to it too. For polymorphic models, Serverpod can also deserialize an unknown subtype as its known base class, which lets you add subtypes without breaking older clients. See [Handling unknown class names](https://docs.serverpod.dev/concepts/data-and-the-database/models/inheritance-and-polymorphism.md#handling-unknown-class-names). ## Managing breaking changes with endpoint inheritance An endpoint sub-class can be useful when you have to make a breaking change to an entire endpoint but need to keep supporting existing clients. Doing so allows you to share most of its implementation with the old endpoint. For the mechanics of endpoint inheritance and the `@doNotGenerate` annotation, see [Endpoint inheritance](https://docs.serverpod.dev/concepts/endpoints-and-apis/endpoint-inheritance.md). Imagine you had a "team" management endpoint where before a user could join if they had an e-mail address ending in the expected domain, but now it should be opened up for anyone to join if they can provide an "invite code". Additionally, the return type (serialized classes) should be updated across the entire endpoint, which would not be allowed on the existing one. Transitioning from the current to the new endpoint structure might look like this: ```dart @Deprecated('Use TeamV2Endpoint instead') class TeamEndpoint extends Endpoint { Future join(Session session) async { // … } // many more methods, like `leave`, etc. } class TeamV2Endpoint extends TeamEndpoint { @override @doNotGenerate Future join(Session session) async { throw UnimplementedError(); } Future joinWithCode(Session session, String invitationCode) async { // … } } ``` In the above example, we created a new `TeamV2` endpoint, which hides the `join` method and instead exposes a `joinWithCode` method with the added parameter and the new return type. Additionally, all the other inherited (and untouched) methods from the parent class are exposed. While we may have liked to re-use the `join` method name, Dart inheritance rules do not allow doing so. Otherwise, we would have to write the endpoint from scratch, meaning without inheritance, and re-implement all methods we would like to keep. In your client, you could then move all usages from `client.team` to `client.teamV2` and eventually (after all clients have upgraded) remove the old endpoint on the server. That means either marking the old endpoint with `@doNotGenerate` on the class or deleting it and moving the re-used method implementations you want to keep to the new V2 endpoint class. An alternative pattern to consider would be to move all the business logic for an endpoint into a helper class and then call into that from the endpoint. A V2 endpoint can then reuse the business logic through the helper class without subclassing the old endpoint. This has the added benefit of the endpoint class clearly listing all exposed methods, and you don't have to wonder what you inherit from the base class. --- # Endpoint middleware https://docs.serverpod.dev/concepts/endpoints-and-apis/endpoint-middleware Middleware wraps every request to the API server, before and after your endpoints, making it suitable for logging, caching, and rate limiting. Serverpod middleware follows the middleware interface of [Relic](https://docs.dartrelic.dev/reference/middleware), the HTTP server framework Serverpod is built on. To add middleware to web server routes instead, scoped to path prefixes, see [Web server middleware](https://docs.serverpod.dev/concepts/web-server/web-server-middleware.md). ## Add middleware to your server Add middleware to your server in the `run` function before starting the server: ```dart import 'src/generated/serverpod.dart'; void run(List args) async { final pod = Serverpod(args); // Add middleware before starting pod.server.addMiddleware(myCustomMiddleware()); await pod.start(); } ``` Middleware runs on every API-server request in the order it is added: not only endpoint method calls, but also the health check, WebSocket upgrades for [streaming](https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming.md), and file-storage requests. Keep each middleware focused and efficient, and return early for paths it does not care about. ## Create custom middleware ### Middleware signature A middleware is a function with this signature: ```dart typedef Handler = FutureOr Function(Request request); typedef Middleware = Handler Function(Handler innerHandler); ``` The return value is a `Result`, which can be a `Response`, `Hijack`, or `WebSocketUpgrade`. Check `is Response` before modifying response-specific fields. The types come with the `package:serverpod/serverpod.dart` import. No separate Relic import is needed. ### Add a header to all responses Here's a basic middleware that adds a custom header to all responses: ```dart Middleware customHeaderMiddleware() { return (Handler innerHandler) { return (Request req) async { // Inspect or modify `req` here if needed // Call the inner handler to process the request final result = await innerHandler(req); // Modify the response if (result is Response) { return result.copyWith( headers: result.headers.transform((h) { h['X-Custom-Header'] = ['my-value']; }), ); } return result; }; }; } ``` ### Rate limit requests Middleware can answer a request itself instead of calling the inner handler, which is how a rate limiter rejects excess traffic. This example allows each client IP a fixed number of requests per time window: ```dart import 'package:serverpod/serverpod.dart'; /// Limits each client IP to [maxRequests] requests per [window]. /// /// Requests over the limit receive a 429 Too Many Requests response. Middleware rateLimitMiddleware({ int maxRequests = 60, Duration window = const Duration(minutes: 1), }) { final clients = {}; return (Handler innerHandler) { return (Request req) async { final ip = req.connectionInfo.remote.address.toString(); final now = DateTime.now(); var client = clients[ip]; if (client == null || now.difference(client.windowStart) >= window) { // Start a new window for this client. client = (windowStart: now, count: 0); } client = (windowStart: client.windowStart, count: client.count + 1); clients[ip] = client; if (client.count > maxRequests) { return Response( 429, body: Body.fromString('Too many requests. Try again later.'), ); } return innerHandler(req); }; }; } ``` When adapting it, keep three things in mind: - Behind a load balancer or proxy, `req.connectionInfo` is the nearest network hop. Read the client address from a forwarding header such as `X-Forwarded-For` instead. - Middleware wraps all API-server requests, so the counter also ticks for health checks and other non-endpoint traffic. - The in-memory map grows with each new client IP and is local to one server. Production quota patterns need eviction, and state shared across servers such as a database counter. ## Handle errors in middleware Middleware can catch errors from the handlers it wraps, for example to record metrics, before rethrowing. Serverpod's own error handling wraps all user middleware, so a rethrown exception still gets the standard status mapping. There is no `Session` in middleware, so use your own logging or metrics facility here. ```dart Middleware errorHandlingMiddleware() { return (Handler innerHandler) { return (Request req) async { try { return await innerHandler(req); } catch (e) { // Record metrics or logs here. // Re-throw to let Serverpod handle it rethrow; } }; }; } ``` --- # Configure HTTP calls https://docs.serverpod.dev/concepts/endpoints-and-apis/configure-http-calls Two situations call for customizing how your app sends requests: a web app that must include cookies on cross-origin calls, and apps that want the platform-native network stacks on iOS and Android. Both are handled by the `httpClientOverride` parameter on the generated `Client`, which swaps the underlying HTTP transport. The `serverUrl` in the examples below is the resolved server address from [calling endpoints](https://docs.serverpod.dev/concepts/endpoints-and-apis.md#call-an-endpoint-from-your-app). ## Include CORS credentials on web By default, browser requests do not include cookies or HTTP authentication credentials in cross-origin requests. If your app relies on cookie-based sessions or similar mechanisms, pass a `BrowserClient` with `withCredentials` enabled: ```dart import 'package:http/browser_client.dart'; final client = Client( serverUrl, httpClientOverride: BrowserClient()..withCredentials = true, ); ``` ### Allow credentials on the server On the server, Serverpod adds CORS headers to API responses by default through `httpResponseHeaders` and `httpOptionsResponseHeaders` on the `Serverpod` constructor. The defaults allow cross-origin `POST` requests from any origin (`Access-Control-Allow-Origin: *`) and permit common request headers such as `Authorization` on preflight `OPTIONS` requests. Credential-aware requests require `Access-Control-Allow-Credentials: true` and a specific origin instead of the wildcard. Override the defaults in your `lib/server.dart` (or wherever you construct `Serverpod`): ```dart import 'src/generated/serverpod.dart'; /// The starting point of the Serverpod server. void run(List args) async { final pod = Serverpod( args, httpResponseHeaders: Headers.build((mh) { mh.accessControlAllowOrigin = AccessControlAllowOriginHeader.origin( origin: Uri.parse('http://localhost:49660'), // Your Flutter web app origin ); mh.accessControlAllowCredentials = true; mh.accessControlAllowMethods = AccessControlAllowMethodsHeader.methods( [Method.post], ); }), httpOptionsResponseHeaders: Headers.build((mh) { mh.accessControlAllowHeaders = AccessControlAllowHeadersHeader.headers([ 'Content-Type', 'Authorization', 'Accept', 'User-Agent', 'X-Requested-With', ]); }), ); // Start the server await pod.start(); } ``` Set `origin` to the exact origin of your Flutter web app (scheme, host, and port). In production, list each allowed origin explicitly. ## Use platform-native HTTP clients You can also override the default HTTP client with a platform-native HTTP client. On iOS and macOS, you can use [cupertino\_http](https://pub.dev/packages/cupertino_http) to route traffic through `NSURLSession`. On Android, you can use [cronet\_http](https://pub.dev/packages/cronet_http) to use the Cronet network stack. Add the corresponding package to your Flutter app's `pubspec.yaml` before using these clients. ```dart import 'dart:io'; import 'package:cronet_http/cronet_http.dart'; import 'package:cupertino_http/cupertino_http.dart'; import 'package:http/http.dart' as http; import 'package:my_project_client/my_project_client.dart'; void main() async { http.Client? httpClient; if (Platform.isAndroid) { final engine = CronetEngine.build( cacheMode: CacheMode.memory, cacheMaxSize: 2 * 1024 * 1024, userAgent: 'my-app'); httpClient = CronetClient.fromCronetEngine(engine, closeEngine: true); } else if (Platform.isIOS || Platform.isMacOS) { final config = URLSessionConfiguration.ephemeralSessionConfiguration() ..cache = URLCache.withCapacity(memoryCapacity: 2 * 1024 * 1024) ..httpAdditionalHeaders = {'User-Agent': 'my-app'}; httpClient = CupertinoClient.fromSessionConfiguration(config); } final client = Client( serverUrl, httpClientOverride: httpClient, ); } ``` ### Support web with conditional imports The above example does not work if your app also targets web, since `dart:io` is not available there. Put the platform-specific `http.Client` creation logic behind a conditional import instead: ```dart import 'src/http_client_stub.dart' if (dart.library.io) 'src/http_client_io.dart'; final client = Client( serverUrl, httpClientOverride: createHttpClient(), ); ``` The stub file provides a fallback for platforms without `dart:io` (such as web), where the default client is used: ```dart title="src/http_client_stub.dart" import 'package:http/http.dart' as http; http.Client? createHttpClient() => null; ``` The `dart:io` implementation wraps the platform-native client from the example above in a top-level `createHttpClient()` function: ```dart title="src/http_client_io.dart" import 'dart:io'; // Same imports as the platform-native example above. http.Client? createHttpClient() { if (Platform.isAndroid) { // ... return the CronetClient shown above } else if (Platform.isIOS || Platform.isMacOS) { // ... return the CupertinoClient shown above } return null; } ``` --- # Working with models https://docs.serverpod.dev/concepts/data-and-the-database/models A data model is a YAML definition that becomes a typed Dart class on both the server and the client, and, with a [table](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md) key, a database table as well. Models are the unit of data your endpoints pass and your database stores. You can also [define models that you only use in Flutter](#using-models-on-the-client-only). The recommended file extension is `.spy.yaml` (.spy stands for "Serverpod YAML"), with `.spy` and `.spy.yml` accepted as well. These files can be placed anywhere in your server's `lib` directory, and the extension enables syntax highlighting through the [Serverpod Extension](https://marketplace.visualstudio.com/items?itemName=serverpod.serverpod) for VS Code. Files with a plain `.yaml` or `.yml` extension are not model files. Serverpod 4 ignores them in `lib/src/models` and `lib/src/protocol` and reports an error asking you to rename them. The Serverpod CLI reads the model files when generating code and creating migrations. With `serverpod start` running, saving a model file regenerates the code. Outside a session, run `serverpod generate`. This page covers the model file format: classes, exceptions, enums, default values, and the generated Dart code. The other pages in this group build on it: - [Field types](https://docs.serverpod.dev/concepts/data-and-the-database/models/field-types.md): YAML types mapped to Dart, Postgres, SQLite, and JSON. - [Inheritance and polymorphism](https://docs.serverpod.dev/concepts/data-and-the-database/models/inheritance-and-polymorphism.md): share fields between models with `extends` and `sealed`, and use parent types in endpoints. - [Custom serialization](https://docs.serverpod.dev/concepts/data-and-the-database/models/custom-serialization.md): pass hand-written Dart classes through endpoints and models. - [Shared packages](https://docs.serverpod.dev/concepts/data-and-the-database/models/shared-packages.md): define models in a package that both the server and the app depend on. ## Class ```yaml class: Company fields: name: String foundedDate: DateTime? employees: List ``` ### Supported types The following types can be used as field types. See [Field types](https://docs.serverpod.dev/concepts/data-and-the-database/models/field-types.md) for the YAML-to-Dart, Postgres, SQLite, and JSON mapping of each type, including defaults and query operators. - **Core Dart types**: [bool](https://api.dart.dev/dart-core/bool-class.html), [int](https://api.dart.dev/dart-core/int-class.html), [double](https://api.dart.dev/dart-core/double-class.html), [String](https://api.dart.dev/dart-core/String-class.html), [Duration](https://api.dart.dev/dart-core/Duration-class.html), [DateTime](https://api.dart.dev/dart-core/DateTime-class.html), [ByteData](https://api.dart.dev/dart-typed_data/ByteData-class.html), [UuidValue](https://pub.dev/documentation/uuid/latest/uuid_value/UuidValue-class.html), [Uri](https://api.dart.dev/dart-core/Uri-class.html), and [BigInt](https://api.dart.dev/dart-core/BigInt-class.html). - **Vector and geography types**: [Vector](https://docs.serverpod.dev/concepts/data-and-the-database/database/vector-and-geography-fields.md#vector), [HalfVector](https://docs.serverpod.dev/concepts/data-and-the-database/database/vector-and-geography-fields.md#halfvector), [SparseVector](https://docs.serverpod.dev/concepts/data-and-the-database/database/vector-and-geography-fields.md#sparsevector), [Bit](https://docs.serverpod.dev/concepts/data-and-the-database/database/vector-and-geography-fields.md#bit), and the [geography types](https://docs.serverpod.dev/concepts/data-and-the-database/database/vector-and-geography-fields.md#geography-fields). - **Your own types**: other serializable [classes](#class), [exceptions](#exception), and [enums](#enum). - **Collections**: [List](https://api.dart.dev/dart-core/List-class.html)s, [Map](https://api.dart.dev/dart-core/Map-class.html)s, and [Set](https://api.dart.dev/dart-core/Set-class.html)s of the supported types, with the type arguments specified. All supported types can also be used inside [Record](https://api.dart.dev/dart-core/Record-class.html)s. - **[dynamic](https://docs.serverpod.dev/concepts/data-and-the-database/models/dynamic-fields.md)**: holds any serializable value when the type is not known at compile time. Null safety is supported: append `?` to any type to make the field nullable. Once your classes are generated, you can use them as parameters or return types to [endpoint methods](https://docs.serverpod.dev/concepts/endpoints-and-apis.md). When values are sent between the server and the client, `bool`, `int`, `double`, and `String` pass through as native JSON. Other types convert: | Type | Sent as | | ----------- | --------------------------------- | | `DateTime` | ISO 8601 string, converted to UTC | | `Duration` | Integer, in milliseconds | | `ByteData` | Base64-encoded string | | `UuidValue` | UUID string | | `Uri` | String | | `BigInt` | String | The [Field types](https://docs.serverpod.dev/concepts/data-and-the-database/models/field-types.md#serialization) catalog lists every mapping, including enums, collections, records, vectors, and geography. ### Required fields Whether a field is required follows from its nullability. Non-nullable fields are always required constructor parameters. Append `?` to the type, e.g. `String?` or `List?`, to make a field optional. The `required` keyword can only be used on nullable fields, and makes the field a required constructor parameter while keeping its type nullable. ```yaml class: Person fields: name: String # Required, cannot be null nickname: String?, required # Required, but can be set to null age: int? # Optional ``` In the example above, `name` and `nickname` are both required constructor parameters, and only `nickname` accepts null. :::tip Once your app is in users' hands, changing or removing a model's fields can break older app versions. Adding new fields is safe when they are nullable or have a default value. See [backward compatibility](https://docs.serverpod.dev/concepts/endpoints-and-apis/backward-compatibility.md). ::: ### Limiting visibility of a generated class By default, generated code for your serializable objects is available both on the server and the client. You may want to have the code on the server side only. E.g., if the serializable object is connected to a database table containing private information. To make a serializable class generated only on the server side, set the serverOnly property to true. ```yaml class: MyPrivateClass serverOnly: true fields: hiddenSecretKey: String ``` It is also possible to set a `scope` on a per-field basis. By default all fields are visible to both the server and the client. The available scopes are `all`, `serverOnly`, `none`. A field with a scope other than `all` must be nullable. :::info The `none` scope is not typically used in Serverpod apps. It is intended for the Serverpod framework itself. ::: ```yaml class: SelectivelyHiddenClass fields: hiddenSecretKey: String?, scope=serverOnly publicKey: String ``` :::info Models can be saved to and read from the database. See the [Database](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md) section. ::: ### Using models on the client only You can define models that you only use in the Flutter app, for example to hold local UI state or form data. Leave `serverOnly` unset so the class is generated for the client. There is no separate `clientOnly` flag. Place the `.spy.yaml` file anywhere in the server's `lib` directory. With `serverpod start` running, saving the file regenerates the code; otherwise run `serverpod generate`. Then import the class from the client package. ```yaml class: CartItem fields: productId: String quantity: int ``` ```dart import 'package:your_client/your_client.dart'; var item = CartItem(productId: 'sku-1', quantity: 2); ``` The class is also generated on the server, which is harmless: without a `table` key no database table is created, and nothing requires you to use the class in an endpoint. Set `immutable: true` if you want value equality for Flutter state management. See [Immutable classes](#immutable-classes). If you want the YAML and generated classes in a package that both the server and the app depend on, use a [shared package](https://docs.serverpod.dev/concepts/data-and-the-database/models/shared-packages.md). ### JSON key aliasing By default, fields are serialized to JSON using their Dart field name as the key. The `jsonKey` property allows you to specify a different key name for JSON serialization and deserialization, which is useful when integrating with external APIs that use different naming conventions. ```yaml class: User fields: displayName: String, jsonKey=display_name emailAddress: String, jsonKey=email createdAt: DateTime, jsonKey=created_at ``` This generates a class where the Dart field names remain camelCase, but the JSON representation uses the specified keys: ```dart // Dart field names var user = User( displayName: 'John Doe', emailAddress: 'john@example.com', createdAt: DateTime.parse('2024-01-15T10:30:00.000Z'), ); // Serializes to JSON with custom keys // { // "display_name": "John Doe", // "email": "john@example.com", // "created_at": "2024-01-15T10:30:00.000Z" // } ``` This is particularly helpful when: - Consuming external APIs that use snake\_case or other naming conventions - Working with legacy systems that have specific JSON field requirements - Working with databases or services that reserve key names, such as mapping `id` to `_id` for MongoDB :::info The `jsonKey` property affects JSON serialization and deserialization. It does not affect the database column name. To customize the database column name, use the [`column` property](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md#column-name-override) instead. ::: ### Immutable classes By default, generated classes in Serverpod are mutable, meaning their fields can be changed after creation. However, you can make a class immutable by setting the `immutable` property to `true`. Immutable classes are especially useful when working with state management solutions or when you need value-based equality. ```yaml class: ImmutableUser immutable: true fields: name: String email: String ``` When you mark a class as immutable: 1. **All fields become final**: Fields cannot be reassigned after the object is created 2. **Generates `operator ==`**: Provides deep equality comparison between instances 3. **Generates `hashCode`**: Ensures instances with the same values have the same hash code 4. **Compatible with `copyWith`**: You can still create modified copies of immutable objects using the `copyWith` method Example usage: ```dart var user1 = ImmutableUser(name: 'Alice', email: 'alice@example.com'); var user2 = ImmutableUser(name: 'Alice', email: 'alice@example.com'); // Equality comparison works based on values print(user1 == user2); // true // Fields are final and cannot be reassigned // user1.name = 'Bob'; // This would cause a compile error // Use copyWith to create modified copies var user3 = user1.copyWith(name: 'Bob'); print(user3.name); // Bob print(user3.email); // alice@example.com ``` ## Default values Fields can be given default values with three keywords that determine where the default applies: - **default**: sets the default value for both the model (code) and the database (persisted data). It acts as the fallback when the more specific keywords are absent. - **defaultModel**: sets the default value for the model only, overriding `default` on the code side. - **defaultPersist**: sets the default value for the database column only, overriding `default` on the persisted side. It is only meaningful on models with a [table](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md). You can use the keywords individually or in combination. :::info A database default only applies when an insert omits a value for the column. A field with `default` or `defaultModel` always has a value when the row is written, so its `defaultPersist` never fires. `defaultPersist` only comes into play when the model does not provide a value, for example on a nullable field without a model-side default. ::: ### Supported default values | Type | Allowed values | Example | | ----------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------- | | `bool` | `true` or `false` | `boolDefault: bool, default=true` | | `int` | Any integer value | `intDefault: int, default=10` | | `double` | Any double value | `doubleDefault: double, default=10.5` | | `String` | Any string value | `stringDefault: String, default='This is a string'` | | `DateTime` | `now`, or a UTC string in the format `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` | `dateTimeDefault: DateTime, default=2024-05-01T22:00:00.000Z` | | `Duration` | A duration in the format `Xd Xh Xmin Xs Xms` | `durationDefault: Duration, default=1d 2h 10min 30s 100ms` | | `UuidValue` | `random`, `random_v7`, or a UUID string such as `'550e8400-e29b-41d4-a716-446655440000'` | `uuidDefault: UuidValue, default=random` | | `Uri` | Any valid URI string | `uriDefault: Uri, default='https://serverpod.dev'` | | `BigInt` | Any integer value, as a string | `bigIntDefault: BigInt, default='1234567890'` | | Enums | Any of the enum's values | `enumDefault: ByNameEnum, default=byName1` | For `UuidValue`, `random` generates a UUID v4 (`Uuid().v4obj()` in Dart, `gen_random_uuid()` in the database), and `random_v7` generates a UUID v7 (`Uuid().v7obj()` in Dart, a generated `gen_random_uuid_v7()` function in the database). For enums, the persisted value follows the enum's serialization mode. A `byName` enum default is stored as the value's name, and a `byIndex` enum default is stored as its index, e.g. `0` for the first value. On an `int` field, `defaultPersist=serial` hands the value over to a database sequence, the same mechanism behind an `int` `id` column, though each serial column gets its own sequence. Because only the database can produce it, `serial` is rejected with `default` and `defaultModel`. On SQLite, it is only supported on the `id` column due to dialect limitations. ```yaml class: Invoice table: invoice fields: invoiceNumber: int?, defaultPersist=serial ``` :::info On an [immutable class](#immutable-classes), or a class extending one, `default` and `defaultModel` cannot use the non-constant values `now`, `random`, or `random_v7`. On a persisted model, use `defaultPersist` for those instead. ::: ### Example ```yaml class: DefaultValue table: default_value fields: ### Sets the current date and time as the default value. dateTimeDefault: DateTime, default=now ### Defaults to false in code and true in the database. boolDefault: bool, defaultModel=false, defaultPersist=true ### Sets the database-side default value for an integer field. intDefault: int?, defaultPersist=20 ### Defaults to 10.5 in code, with a separate database default. doubleDefault: double, default=10.5, defaultPersist=20.5 ``` ## Exception Model files can define exceptions that can be thrown in endpoints, by using the `exception` keyword. For a more in-depth description of how to work with exceptions, see [Error handling and exceptions](https://docs.serverpod.dev/concepts/endpoints-and-apis/error-handling-and-exceptions.md). ```yaml exception: MyException fields: message: String errorType: MyEnum ``` ## Enum The `enum` keyword defines a custom enum with serialization support. ```yaml enum: Animal values: - dog - cat - bird ``` By default the enum is serialized by name. You can also opt into index-based serialization by setting `serialized: byIndex`. ```yaml enum: Animal serialized: byIndex values: - dog - cat - bird ``` The `serialized` keyword has two valid values: `byName` (the default) and `byIndex`. When using `byName` the string literal of the enum is used; when using `byIndex` the index value (0, 1, 2, etc.) is used. :::warning Using `byIndex` is fragile: adding, removing, or reordering enum values silently changes the serialized form, which can corrupt persisted data. Only use `byIndex` when you need to maintain compatibility with existing data already serialized that way. ::: ### Handling unknown enum values A default value is used when an unknown value is deserialized. This can happen, for example, if a new enum option is added and older clients receive it from the server, or if an enum option is removed but the database still contains the old value. To configure a default value, use the `default` keyword. ```yaml enum: Animal serialized: byName default: unknown values: - unknown - dog - cat - bird ``` In the example above, if the Enum `Animal` receives an unknown option such as `"fish"` it will be deserialized to `Animal.unknown`. This is useful for maintaining backward compatibility when changing the enum values. :::warning If no default value is specified, deserialization of unknown values will throw an exception. Adding a default value prevents these exceptions, but may also hide real issues in your data. Use this feature with caution. ::: ### Enhanced enums with properties Serverpod supports enhanced enums that can have custom properties attached to each value. This is useful when you need to associate additional data with enum values, such as display names, codes, or configuration values. To define an enhanced enum, add a `properties` section that declares the property names and types, then specify property values for each enum value: ```yaml enum: HttpStatus serialized: byName properties: code: int message: String values: - ok: code: 200 message: 'OK' - notFound: code: 404 message: 'Not Found' - internalError: code: 500 message: 'Internal Server Error' ``` This generates an enhanced Dart enum with the specified properties: ```dart enum HttpStatus implements SerializableModel { ok(200, 'OK'), notFound(404, 'Not Found'), internalError(500, 'Internal Server Error'); const HttpStatus(this.code, this.message); final int code; final String message; // Serialization methods... } ``` You can then access the properties on any enum value: ```dart var status = HttpStatus.ok; print(status.code); // 200 print(status.message); // OK ``` #### Supported property types Enhanced enum properties support the following types: - `int` / `int?` - `double` / `double?` - `bool` / `bool?` - `String` / `String?` #### Default property values Properties can have default values, making them optional when defining enum values. The syntax follows the same pattern as [class field defaults](#default-values): ```yaml enum: Priority properties: level: int description: String, default='No description' values: - low: level: 1 - medium: level: 2 description: 'Medium priority' - high: level: 3 description: 'High priority - handle first' ``` In this example, `low` will use the default description `'No description'`, while `medium` and `high` have explicit descriptions. ## Adding documentation Serverpod allows you to add documentation to your serializable objects in a similar way that you would add documentation to your Dart code. Use three hashes (###) to indicate that a comment should be considered documentation. ```yaml ### Information about a company. class: Company fields: ### The name of the company. name: String ### The date the company was founded, if known. foundedDate: DateTime? ### A list of people currently employed at the company. employees: List ``` ## Generated code Serverpod generates some convenience methods on the Dart classes. ### copyWith The `copyWith` method creates a copy of an object with selective field updates and is available on all generated classes: ```dart // User has a nullable age field: age: int? var john = User(name: 'John Doe', age: 25); var jane = john.copyWith(name: 'Jane Doe'); // age stays 25 var ageless = john.copyWith(age: null); // age is explicitly set to null ``` The `copyWith` method generates a deep copy of an object, preserving all original fields unless explicitly modified. As the example shows, it distinguishes between a nullable field set to `null` and a field left unspecified. ### toJson / fromJson The `toJson` and `fromJson` methods are generated on all models to help with serialization. Serverpod manages all serialization for you out of the box and you will rarely have to use these methods yourself. See [Custom serialization](https://docs.serverpod.dev/concepts/data-and-the-database/models/custom-serialization.md) for more info. On the server, models also get a `toJsonForProtocol` method that produces the JSON sent to clients. It leaves out fields whose [scope](#limiting-visibility-of-a-generated-class) hides them from the client. ### Custom methods Sometimes you will want to add custom methods to the generated classes. The easiest way to do this is with [Dart's extension feature](https://dart.dev/language/extension-methods). ```dart extension MyExtension on MyClass { bool isCustomMethod() { return true; } } ``` ## Related - [Field types](https://docs.serverpod.dev/concepts/data-and-the-database/models/field-types.md): YAML types mapped to Dart, Postgres, SQLite, and JSON. - [Model reference](https://docs.serverpod.dev/concepts/lookups/model-reference.md): every keyword available in a model file, and whether it applies to a `class`, `exception`, or `enum`. - [Tables](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md): store a model in the database. - [Backward compatibility](https://docs.serverpod.dev/concepts/endpoints-and-apis/backward-compatibility.md): evolve models without breaking older app versions. --- # Field types https://docs.serverpod.dev/concepts/data-and-the-database/models/field-types Every model field has a YAML type that becomes a Dart type, a JSON encoding, and, on a [table](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md) model, a database column. This catalog lists the mapping, allowed defaults, query operators, and storage caveats for each type. For the model file format, see [Working with models](https://docs.serverpod.dev/concepts/data-and-the-database/models.md). ## Overview | YAML | Dart | Postgres | SQLite | JSON | Defaults | | ----------- | ----------- | ----------------------------- | ---------------------------- | ------------------------------ | -------------------------------------------- | | `bool` | `bool` | `boolean` | `INTEGER` (`0` / `1`) | `true` / `false` | `true`, `false` | | `int` | `int` | `bigint` | `INTEGER` | number | any int; `serial` only with `defaultPersist` | | `double` | `double` | `double precision` | `REAL` | number | any double | | `String` | `String` | `text` | `TEXT` | string | quoted `'...'` or `"..."` | | `DateTime` | `DateTime` | `timestamp without time zone` | `INTEGER` (UTC epoch ms) | UTC ISO-8601 string | `now`, or `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` | | `Duration` | `Duration` | `bigint` (milliseconds) | `INTEGER` (ms) | int milliseconds | `Xd Xh Xmin Xs Xms` | | `ByteData` | `ByteData` | `bytea` | `BLOB` | `decode('', 'base64')` | none | | `UuidValue` | `UuidValue` | `uuid` | `BLOB` (16 bytes, no dashes) | UUID string | `random`, `random_v7`, or a quoted UUID | | `Uri` | `Uri` | `text` | `TEXT` | `Uri.toString()` | quoted URI string | | `BigInt` | `BigInt` | `text` | `TEXT` | decimal string | any `BigInt.parse`-able value | Vector types require a dimension of at least 1, for example `Vector(1536)`. | YAML | Dart | Postgres | SQLite | JSON | Defaults | | ----------------- | -------------- | -------------- | ------ | -------------------- | -------- | | `Vector(n)` | `Vector` | `vector(n)` | `TEXT` | `List` | none | | `HalfVector(n)` | `HalfVector` | `halfvec(n)` | `TEXT` | `List` | none | | `SparseVector(n)` | `SparseVector` | `sparsevec(n)` | `TEXT` | string `{i:v,...}/n` | none | | `Bit(n)` | `Bit` | `bit(n)` | `TEXT` | bit string | none | Geography types use SRID 4326 (WGS 84), which is not configurable per column. | YAML | Dart | Postgres | SQLite | JSON | Defaults | | ----------------------------- | ----------------------------- | ------------------------------------ | ------------- | -------------------------------------------- | -------- | | `GeographyPoint` | `GeographyPoint` | `geography(Point,4326)` | `TEXT` (EWKT) | EWKT, for example `SRID=4326;POINT(lon lat)` | none | | `GeographyLineString` | `GeographyLineString` | `geography(LineString,4326)` | `TEXT` | EWKT | none | | `GeographyPolygon` | `GeographyPolygon` | `geography(Polygon,4326)` | `TEXT` | EWKT | none | | `GeographyGeometryCollection` | `GeographyGeometryCollection` | `geography(GeometryCollection,4326)` | `TEXT` | EWKT | none | Enums store as `text` or `bigint`, not json. A field default is any of the enum's values. | `serialized` | JSON | Postgres | SQLite | | ------------------ | -------------- | -------- | --------- | | `byName` (default) | string literal | `text` | `TEXT` | | `byIndex` | int index | `bigint` | `INTEGER` | Collections, records, nested models, custom classes, and `dynamic` persist as `json` or `jsonb`. See [JSON vs JSONB](#json-vs-jsonb). | YAML | JSON | Postgres default | SQLite (`json`) | SQLite (`jsonb`) | | --------------------------------- | --------------------------------- | ---------------- | --------------- | -------------------- | | `List` | JSON array | `json` | `TEXT` | `BLOB` via `jsonb()` | | `Set` | JSON array (order not guaranteed) | `json` | `TEXT` | `BLOB` via `jsonb()` | | `Map` | JSON object | `json` | `TEXT` | `BLOB` via `jsonb()` | | `Map` (`K` is not `String`) | JSON array of `{k, v}` objects | `json` | `TEXT` | `BLOB` via `jsonb()` | | `(int, String)` / `({int n})` | `{ "p": [...], "n": {...} }` | `json` | `TEXT` | `BLOB` via `jsonb()` | | nested model / custom class | object JSON | `json` | `TEXT` | `BLOB` via `jsonb()` | | `dynamic` | value plus type metadata | `json` | `TEXT` | `BLOB` via `jsonb()` | ## Nullability A trailing `?` on the type makes the field nullable. Non-nullable fields are required constructor parameters. The `required` keyword is valid only on nullable fields: it makes the constructor parameter required while keeping the type nullable. ```yaml class: Person fields: name: String nickname: String?, required age: int? ``` The `dynamic` type is already nullable. `dynamic?` and `List` are rejected. See [Required fields](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#required-fields) and [Dynamic fields](https://docs.serverpod.dev/concepts/data-and-the-database/models/dynamic-fields.md#nullability). ## Serialization Three encodings are involved when a value moves between the app, the server, and the database: 1. **Protocol JSON** (client and server): `toJson()` / `fromJson()`. - `int`, `bool`, `double`, and `String` pass through as native JSON. - Every other type converts, as listed in the overview tables. 2. **Postgres literals** used when writing SQL: UUIDs are quoted, `ByteData` is `\x` hex, vectors are `'[1,2,3]'`, geography is EWKT, and collections are JSON text. 3. **SQLite literals** used by the [client-side database](https://docs.serverpod.dev/concepts/data-and-the-database/database/client-side-database.md): `bool` is `0` / `1`, `DateTime` is epoch milliseconds, `UuidValue` is a 16-byte blob, `ByteData` is `X'hex'`, and `jsonb` values use `jsonb(...)`. Protocol JSON for `DateTime` always uses `toUtc().toIso8601String()`. Postgres stores the value as `timestamp without time zone`, so stored values are UTC. ## Primitive types ```yaml class: Sample fields: active: bool count: int price: double name: String createdAt: DateTime timeout: Duration payload: ByteData uuid: UuidValue homepage: Uri huge: BigInt ``` ### bool ```yaml fields: published: bool, default=false ``` SQLite stores `false` as `0` and `true` as `1`, and reads those integers back as bools. Ordering operators (`>`, `<`, `>=`, `<=`) are not available. Equality operators are: `equals`, `notEquals`, `inSet`, `notInSet`. ### int ```yaml fields: views: int, default=0 invoiceNumber: int?, defaultPersist=serial ``` An `int` column is always 64-bit Postgres `bigint`, not `integer`. Serial IDs use `bigserial`. The `serial` default is valid only with `defaultPersist`, never with `default` or `defaultModel`. On SQLite, `serial` is supported only on the `id` column. The `Duration` type also uses Postgres `bigint`. The column type is the same; the meaning is not. A `Duration` value is milliseconds. ### double ```yaml fields: rating: double, default=0.0 ``` On SQLite, `NaN` is stored as `NULL`, and `Infinity` is stored as `1e999`. ### String ```yaml fields: title: String, default='Untitled' ``` Strings are unbounded `text` (Postgres) or `TEXT` (SQLite). There is no `varchar` length. String columns support `like`, `notLike`, `ilike`, and `notIlike` in addition to ordering and equality. On SQLite, `like` and `ilike` are both case-insensitive for ASCII characters. See [Filter](https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering.md#like). ### DateTime ```yaml fields: createdAt: DateTime, default=now publishedAt: DateTime?, default=2024-05-01T22:00:00.000Z ``` Values are always converted to UTC. The Postgres default for `now` is `CURRENT_TIMESTAMP`. SQLite stores UTC epoch milliseconds as `INTEGER`. On an [immutable class](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#immutable-classes), `now` is not valid with `default` or `defaultModel`. A table model can set `defaultPersist=now` instead. ### Duration ```yaml fields: timeout: Duration, default=1d 2h 10min 30s 100ms ``` Stored as milliseconds in a `bigint` (Postgres) or `INTEGER` (SQLite) column, the same SQL type as `int`. JSON is that millisecond count as an integer. ### ByteData ```yaml fields: blob: ByteData ``` No YAML default. Postgres inserts use `\x` hex for `bytea`. SQLite uses a `BLOB` with `X'hex'` literals. On the wire, the protocol JSON is a Postgres-style string: `decode('', 'base64')`. ### UuidValue ```yaml fields: publicId: UuidValue, default=random orderedId: UuidValue, default=random_v7 fixedId: UuidValue, default='550e8400-e29b-41d4-a716-446655440000' ``` The `random` default generates a UUID v4 (`Uuid().v4obj()` in Dart, `gen_random_uuid()` in Postgres). The `random_v7` default generates a UUID v7 (`Uuid().v7obj()` in Dart, `gen_random_uuid_v7()` in Postgres). SQLite stores the 16-byte value as a `BLOB` with no dashes. On an [immutable class](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#immutable-classes), `random` and `random_v7` are not valid with `default` or `defaultModel`. A table model can set `defaultPersist` instead. ### Uri ```yaml fields: homepage: Uri, default='https://serverpod.dev' ``` A URI default must be a quoted string. Stored as `text` / `TEXT`. Query operators are equality only (`equals`, `notEquals`), not ordered comparison. ### BigInt ```yaml fields: huge: BigInt, default='1234567890' ``` Stored as `text` / `TEXT`, not a numeric column. The JSON value is a decimal string. Any value `BigInt.parse` accepts is a valid default. Query operators are equality only (`equals`, `notEquals`). ## Enums ```yaml enum: Animal serialized: byName default: unknown values: - unknown - dog - cat ``` The `serialized` keyword has two values. `byName` is the default and stores the string literal as Postgres `text` / SQLite `TEXT`. `byIndex` stores the index as Postgres `bigint` / SQLite `INTEGER`. Changing the order of a `byIndex` enum changes the stored integers and can corrupt existing data. The default `byName` mode is stable when values are added or reordered. A field default is any of the enum's values. The enum's own `default` is the fallback when an unknown value is deserialized, not a column default. See [Handling unknown enum values](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#handling-unknown-enum-values). Enhanced enum [properties](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#enhanced-enums-with-properties) support `int`, `double`, `bool`, `String`, and their nullable forms. Those properties exist only on the generated Dart enum; they are not extra columns. ## Collections and records Type arguments are required: `List`, `Map`, `Set`. Untyped `List` or `Map` is rejected. ```yaml class: Packed fields: tags: List uniqueTags: Set counts: Map labeled: Map pair: (int, String) named: ({int count}) ``` - `List` is a JSON array. - `Set` is also a JSON array. Order is not guaranteed after a round trip. - `Map` is a JSON object. - `Map` where `K` is not `String` is a JSON array of `{k, v}` objects, not a JSON object, because JSON object keys must be strings. - Records serialize positional fields under `"p"` and named fields under `"n"`. A record with only positional fields omits `"n"`; a record with only named fields omits `"p"`. A single-element record needs a trailing comma: `(int,)`. ```json { "pair": { "p": [1, "hello"] }, "named": { "n": { "count": 42 } }, "labeled": [{ "k": 1, "v": "a" }, { "k": 2, "v": "b" }] } ``` No YAML defaults for `List`, `Map`, `Set`, or records. ## Nested models A nested model (or custom class) in a JSON column is a copy stored on that row. Updating it does not update other rows that hold a similar object. A [`relation`](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations.md) field stores a foreign key instead, so multiple rows can point at the same object. ```yaml class: Company table: company fields: address: Address billingAddress: Address?, relation ``` The `address` field is stored as json on `company`. The `billingAddress` field is a foreign key to an `address` row. See [Tables](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md#data-representation). ## JSON vs JSONB Serializable types (`List`, `Map`, `Set`, records, nested models, custom classes, and `dynamic`) default to Postgres `json` (SQLite `TEXT`). Setting `serializationDataType=jsonb` on the field, the class, or `serialize_as_jsonb_by_default: true` in `config/generator.yaml` stores them as: - Postgres: `jsonb`, which can use a [GIN index](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md#gin-indexes) - SQLite: `BLOB` via SQLite's `jsonb()` function The `serializationDataType` keyword is not valid on primitives (`String`, `int`, `bool`, and the other core types). A GIN index requires every indexed field to be jsonb. See [Storing serializable fields as JSONB](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md#storing-serializable-fields-as-jsonb). ## Vector types ```yaml class: Document table: document fields: embedding: Vector(1536) ``` Dimension is required: `Vector(1536)`, `HalfVector(1536)`, `SparseVector(10000)`, `Bit(256)`. The dimension must be at least 1. SQLite has no native vector type. Values are stored as text, vector query operators are not supported, and HNSW / IVFFLAT indexes are omitted. On Postgres, the default index type is `hnsw`. `Vector`, `HalfVector`, and `Bit` also allow `ivfflat`. `SparseVector` allows only `hnsw`. Distance functions are `l2`, `innerProduct`, `cosine`, and `l1`. `Bit` also has Hamming and Jaccard. Vector fields require the pgvector extension. See [Vector and geography fields](https://docs.serverpod.dev/concepts/data-and-the-database/database/vector-and-geography-fields.md) for usage, Dart APIs, and setup. ## Geography types ```yaml class: Store table: store fields: location: GeographyPoint ``` Coordinates in EWKT are longitude then latitude: `SRID=4326;POINT(lon lat)`. No YAML defaults. Indexes are `gist` (default) or `spgist` only. Query operators are `intersects`, `distanceWithin`, `distance`, `contains`, and `within` (Postgres `ST_*` / `ST_GeogFromText`). Geography fields require PostGIS. On SQLite they are opaque EWKT strings: CRUD round-trips, spatial operators throw. See [Vector and geography fields](https://docs.serverpod.dev/concepts/data-and-the-database/database/vector-and-geography-fields.md#geography-fields) and [Upgrading to PostGIS support](https://docs.serverpod.dev/upgrading/upgrade-to-postgis.md). ## Dynamic and custom classes A `dynamic` field holds any serializable value when the type is not known at compile time. Serverpod includes type metadata in the JSON so the value round-trips. That wrapper is an implementation detail. See [Dynamic fields](https://docs.serverpod.dev/concepts/data-and-the-database/models/dynamic-fields.md). A hand-written Dart class can be a field type after it is registered in `config/generator.yaml`. It persists as json / jsonb like a nested model. See [Custom serialization](https://docs.serverpod.dev/concepts/data-and-the-database/models/custom-serialization.md). ## ID types If `id` is omitted on a table model, it is `int?` with `defaultPersist=serial`. | Type | YAML | Persist default | Model default | Postgres | SQLite | | ----------- | --------------------------------------- | --------------------- | --------------------- | ----------------------- | ----------------------------- | | `int` | `id: int?, defaultPersist=serial` | `serial` | not allowed | `bigserial PRIMARY KEY` | `INTEGER PRIMARY KEY` (ROWID) | | `UuidValue` | `id: UuidValue?, defaultPersist=random` | `random`, `random_v7` | `random`, `random_v7` | `uuid` | `BLOB` | An `int` id must be nullable. `serial` is `defaultPersist` only, never `defaultModel`. SQLite allows autoincrement only on the `id` column. Choosing between `int` and `UuidValue`, and generating the id before insert with `defaultModel=random`, is covered in [Choosing an ID strategy](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md#choosing-an-id-strategy). ## Defaults Allowed values by type are in the [overview](#overview) and in each type section. The `default`, `defaultModel`, and `defaultPersist` keywords are explained in [Default values](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#default-values). These types cannot have a YAML default: - `ByteData` - Vector types (`Vector`, `HalfVector`, `SparseVector`, `Bit`) - Geography types - `List`, `Map`, `Set` - Records - Nested models and custom classes - `dynamic` On an immutable class, `now`, `random`, and `random_v7` are not valid with `default` or `defaultModel`. Those values belong on `defaultPersist` instead. ## Indexes Fields marked `!persist` cannot be indexed. | Fields | Default index | Allowed | | ------------------------------- | ------------- | ------------------------------------------------ | | Scalars | `btree` | `btree`, `hash`, `gin`, `gist`, `spgist`, `brin` | | jsonb serializable | `gin` | `gin` requires every indexed field to be jsonb | | `Vector` / `HalfVector` / `Bit` | `hnsw` | `hnsw`, `ivfflat` | | `SparseVector` | `hnsw` | `hnsw` only | | Geography | `gist` | `gist`, `spgist` | Index types other than `btree` are Postgres-only. On SQLite they are skipped when a migration is created. See [Indexing](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md). ## Query operators Table columns expose the operators below in [filters](https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering.md). | Types | Operators | | --------------------------------------------------------------------- | -------------------------------------------------------------------- | | `int`, `double`, `String`, `DateTime`, `Duration`, `UuidValue`, enums | `equals`, `notEquals`, `>`, `<`, `>=`, `<=`, `inSet`, `notInSet` | | `int`, `double`, `DateTime`, `Duration` | `between`, `notBetween` | | `String` | `like`, `notLike`, `ilike`, `notIlike` | | `bool` | `equals`, `notEquals`, `inSet`, `notInSet` | | `Uri`, `BigInt` | `equals`, `notEquals` | | `Vector`, `HalfVector`, `SparseVector` | `distanceL2`, `distanceInnerProduct`, `distanceCosine`, `distanceL1` | | `Bit` | `distanceHamming`, `distanceJaccard` | | Geography | `intersects`, `distanceWithin`, `distance`, `contains`, `within` | Vector and geography operators run on Postgres. They are not supported on SQLite. ## Postgres vs SQLite The [client-side database](https://docs.serverpod.dev/concepts/data-and-the-database/database/client-side-database.md) is SQLite. These mappings differ from Postgres: | Concern | Postgres | SQLite | | ------------------- | --------------------------------------------- | -------------------------------------- | | `bool` | `boolean` | `INTEGER` `0` / `1` | | `int` | `bigint` | `INTEGER` | | `DateTime` | `timestamp without time zone` | UTC epoch milliseconds | | `UuidValue` | `uuid` | 16-byte `BLOB` | | `ByteData` | `bytea` (`\x` hex) | `BLOB` (`X'hex'`) | | `json` collections | `json` | `TEXT` | | `jsonb` collections | `jsonb` | `BLOB` via `jsonb()` | | Vectors | pgvector types, HNSW / IVFFLAT | `TEXT`, no vector indexes or operators | | Geography | PostGIS geography, spatial operators | EWKT `TEXT`, no spatial operators | | `serial` | any `int` column with `defaultPersist=serial` | `id` column only | | `like` / `ilike` | `like` is case-sensitive | both are case-insensitive for ASCII | | Non-`btree` indexes | created as declared | omitted, with a warning | The `Duration` and `int` types share a 64-bit integer column on both dialects. The `BigInt` type is text on both, not a numeric type. ## Related - [Working with models](https://docs.serverpod.dev/concepts/data-and-the-database/models.md): model file format, required fields, and default keywords. - [Tables](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md): table models, ID types, and json vs jsonb. - [Vector and geography fields](https://docs.serverpod.dev/concepts/data-and-the-database/database/vector-and-geography-fields.md): pgvector and PostGIS usage. - [Dynamic fields](https://docs.serverpod.dev/concepts/data-and-the-database/models/dynamic-fields.md): values whose type varies at runtime. - [Custom serialization](https://docs.serverpod.dev/concepts/data-and-the-database/models/custom-serialization.md): hand-written Dart classes as field types. - [Indexing](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md): index types, GIN, HNSW, and geography indexes. - [Filter](https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering.md): how to write the operators above. - [Client-side database](https://docs.serverpod.dev/concepts/data-and-the-database/database/client-side-database.md): SQLite on the device. - [Model reference](https://docs.serverpod.dev/concepts/lookups/model-reference.md): every keyword in a model file. --- # Inheritance and polymorphism https://docs.serverpod.dev/concepts/data-and-the-database/models/inheritance-and-polymorphism Model inheritance lets you define fields once in a parent class and share them across child classes with `extends`, or model a closed set of variants with `sealed`. Polymorphism builds on this: endpoints can take and return the parent type, and the actual subtype survives the round trip between client and server. When old and new app versions run side by side, unknown subtypes can fall back to their base class. See [Handling unknown class names](#handling-unknown-class-names) and the [backward compatibility](https://docs.serverpod.dev/concepts/endpoints-and-apis/backward-compatibility.md) rules. ## Inheritance Serverpod models support inheritance, which allows you to define class hierarchies that share fields between parent and child classes. Common fields, such as audit timestamps, are defined once in a parent class, and every child gets them. Generated classes will maintain the same type hierarchy as the model files. ### Extending a class To inherit from a class, use the `extends` keyword in your model files, as shown below: ```yaml class: ParentClass fields: name: String ``` ```yaml class: ChildClass extends: ParentClass fields: age: int ``` This will generate a class with both `name` and `age` fields. In the generated class, inherited fields come first. To change that for a field, mark it with the `tail` keyword. See [Ordering inherited fields](#ordering-inherited-fields). #### Inheritance on table models Inheritance can also be used with table models. However, a class cannot define a `table` if another class above it in its inheritance chain already has one. The table can be placed at any level in the chain (top, middle or bottom). Separate branches of a hierarchy can each define their own table, so a parent without a table can have several child classes that each have one, as in the [polymorphism example](#polymorphism) below. :::info This is a current limitation due to the parent class implementing the `table` getter and other table-related fields, so classes that `extends` the parent cannot override such properties with different types. ::: When a class in the hierarchy has a table, all inherited fields are stored as columns in that table. The `id` field is automatically added to table classes and inherited by child classes. You can customize the [`id` type](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md#choosing-an-id-strategy) in a parent class, and children will inherit it. A common use case for inheritance on table models is to have a base class that defines a custom `id` type, audit fields and other common properties that must be present on several table models. Below is an example: ```yaml class: BaseClass fields: id: UuidValue?, defaultPersist=random_v7 createdAt: DateTime, default=now updatedAt: DateTime, default=now ``` ```yaml class: ChildClass extends: BaseClass table: child_table fields: name: String indexes: created_at_index: fields: createdAt # Index on inherited field ``` As the example shows, indexes can be defined on inherited fields in a child class that has a table. Relations also work normally with inherited table classes. #### Restrictions - If a parent class is marked as `serverOnly`, all child classes must also be marked as `serverOnly`. A non-serverOnly class cannot extend a serverOnly class, but a serverOnly child can extend a non-serverOnly parent. - You can only extend classes from your own project or from [shared packages](https://docs.serverpod.dev/concepts/data-and-the-database/models/shared-packages.md), not from modules. - Child classes cannot redefine fields that exist in parent classes. To use a base model that is shared between server and client and extend it on the server with a table, see [Shared packages](https://docs.serverpod.dev/concepts/data-and-the-database/models/shared-packages.md). ### Ordering inherited fields Inherited fields are ordered from the root class down to the child class. Since a base class is declared first, its audit fields end up in front of the fields that describe the child. Mark a field with the `tail` keyword to push it behind every normal field in the hierarchy instead. Add `tail` after the field type and any other keywords: ```yaml class: BaseEntity fields: ownerId: UuidValue createdAt: DateTime, default=now, tail updatedAt: DateTime, default=now, tail ``` Tail ordering applies across the whole hierarchy. Normal fields keep their root-to-child order, and the tail fields follow, starting from the most specific child and working back toward the root parent: ```yaml class: Document extends: BaseEntity fields: authorName: String archivedAt: DateTime?, tail ``` ```yaml class: Article extends: Document fields: title: String publishedAt: DateTime?, tail ``` The generated `Article` class orders its fields like this: 1. `ownerId`, a normal field from the root class. 2. `authorName`, a normal field from the parent class. 3. `title`, a normal field from the child class. 4. `publishedAt`, a tail field from the child class. 5. `archivedAt`, a tail field from the parent class. 6. `createdAt` and `updatedAt`, tail fields from the root class, in the order they are declared. The same order applies to the generated constructor parameters and the serialized output. For table models it also determines the generated table definition, where the primary `id` column always comes first. The `tail` keyword cannot be used on `id`. Adding or removing `tail` only affects generated code. It does not rename a database column, and it does not produce a migration that reorders the columns of an existing table. ### Sealed classes In addition to the `extends` keyword, you can also use the `sealed` keyword to create sealed class hierarchies. The compiler then knows all subclasses, so a `switch` over the model is exhaustive and every possible case must be handled. :::info If a class is sealed, it cannot have a table property. This is because a sealed class is abstract and cannot be instantiated, so it cannot represent a table row. ::: ```yaml class: ParentClass sealed: true fields: name: String ``` ```yaml class: ChildClass extends: ParentClass fields: age: int ``` This generates classes equivalent to: ```dart sealed class ParentClass { String name; } class ChildClass extends ParentClass { int age; } ``` ## Polymorphism Serverpod supports polymorphism for models that use inheritance. When you define a class hierarchy you can use parent types as parameters and return types in your endpoints, and Serverpod will automatically serialize and deserialize the correct subtype based on the runtime type. Below is an example of a polymorphic model hierarchy. The `EmailNotification` and `SMSNotification` classes extend the `Notification` sealed class. Each notification type has its own table and specific fields for delivery. Note that it is not possible to define relations to the `Notification` class, since it does not have a table. ```yaml class: Notification sealed: true fields: title: String message: String createdAt: DateTime, default=now sentAt: DateTime? ``` ```yaml class: EmailNotification extends: Notification table: email_notification fields: recipientEmail: String subject: String ``` ```yaml class: SMSNotification extends: Notification table: sms_notification fields: phoneNumber: String provider: String? ``` ### Using polymorphic types in endpoints Polymorphic types can be used as parameters and return types in endpoint methods and streaming endpoints. The runtime type is preserved through serialization and deserialization, including inside Lists, Maps, Sets, Records, and nullable contexts: ```dart class NotificationEndpoint extends Endpoint { Future sendNotification( Session session, { required Notification notification, }) async { final sentNotification = switch (notification) { EmailNotification email => await _sendEmail(session, email), SMSNotification sms => await _sendSMS(session, sms), }; return sentNotification.copyWith(sentAt: DateTime.now()); } /// Save to database and send email Future _sendEmail( Session session, EmailNotification notification, ) async { final saved = await EmailNotification.db.insertRow(session, notification); // ... email sending logic return saved; } /// Save to database and send SMS Future _sendSMS( Session session, SMSNotification notification, ) async { final saved = await SMSNotification.db.insertRow(session, notification); // ... SMS sending logic return saved; } } ``` ### Handling unknown class names When deserializing polymorphic types, Serverpod uses the class name encoded in the serialized data to determine which concrete subtype to instantiate. However, there are situations where the class name in the incoming data may not correspond to any known class on the server or client: - An older client is sending data with a class that no longer exists on the server. - An older client is receiving data from a class that was recently added on the server. - A newer client is sending data with a class that hasn't been deployed to the server yet. If the missing class is a subclass of a known class, Serverpod will try to deserialize the model as the known class. This makes it safe to replace base classes with subclasses on endpoints without breaking [backward compatibility](https://docs.serverpod.dev/concepts/endpoints-and-apis/backward-compatibility.md). :::info This will only work for non-streaming endpoints. Streaming endpoints will always throw an exception if the class name is not known. ::: #### Example scenario Consider a notification system where you initially had the following type: ```yaml # Notification class that was not originally inherited. class: Notification fields: title: String message: String ``` If you later add another notification type to the server, older clients will deserialize it as the base `Notification` class instead of throwing an exception. ```yaml # New class that is not yet available on some older clients. class: EmailNotification extends: Notification fields: recipientEmail: String ``` :::warning Note that this behavior does not apply if the base class is a `sealed` class, since it is not possible to instantiate a `sealed` class. In this case, an exception will be thrown. ::: --- # Dynamic fields https://docs.serverpod.dev/concepts/data-and-the-database/models/dynamic-fields Use `dynamic` fields when a model property needs to hold different value types at runtime, for example, plugin configuration, user-defined form responses, or JSON payloads from external APIs. Serverpod serializes the value with its type information so it round-trips correctly between the client, server, and database. ## Define a dynamic field Add `dynamic` as the field type in your model file: ```yaml class: FormResponse table: form_response fields: formId: int ### The submitted value may be a string, number, bool, list, or map. value: dynamic ``` Save the file with `serverpod start` running (or run `serverpod generate`) to update the generated Dart classes. ### Nullability The `dynamic` type is already nullable, so it is written without a `?`. Writing `dynamic?` or `List` is rejected by code generation, since the `?` mark is redundant. ## Supported values A `dynamic` field can hold any value that Serverpod can serialize, including: - Primitives such as `bool`, `int`, `double`, and `String`. - Other supported core types such as `DateTime`, `Duration`, and `UuidValue`. - Generated serializable models from your project, modules, and shared packages. - Collections (`List`, `Map`, and `Set`), including mixed-type contents. - Nested combinations of these values. Below is an example with several dynamic and collection fields: ```yaml class: DynamicExample table: dynamic_example fields: payload: dynamic jsonbPayload: dynamic, serializationDataType=jsonb payloadList: List payloadMap: Map payloadSet: Set payloadMapWithDynamicKeys: Map ``` In Dart, assign values directly. In this example, `SimpleData` is a generated model: ```dart var object = DynamicExample( payload: 42, jsonbPayload: 10.23, payloadList: [1, 'b', SimpleData(num: 7)], payloadMap: {'a': 1, 'b': 2, 'c': SimpleData(num: 3)}, payloadSet: {1, 2, 3, 'd'}, payloadMapWithDynamicKeys: {'a': 1, 2: SimpleData(num: 1)}, ); ``` Values round-trip through `toJson` and `fromJson`, endpoint calls, and database operations. Serverpod includes runtime type metadata in the serialized value. Treat that representation as an implementation detail rather than constructing or editing it directly. Dynamic fields also work across project, module, and shared-package model boundaries. A dynamic field on a module or shared model can contain a generated model from the consuming project. Run `serverpod generate` after adding or changing the involved models so each generated protocol has the required type registrations. ## Use dynamic values in endpoints Endpoint parameters and return values can use `dynamic` directly: ```dart class UtilityEndpoint extends Endpoint { Future echo(Session session, dynamic value) async { return value; } } ``` The generated client preserves the runtime type when it sends and receives supported values. Collections (`List`, `Map`, and `Set`) with `dynamic` type arguments are also supported. ## Store dynamic fields in the database When the model has a `table` keyword, `dynamic` fields are stored in a `json` column by default. To store the value as `jsonb` instead, set `serializationDataType=jsonb` on the field. This format supports [GIN indexing](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md#gin-indexes): ```yaml class: Product table: product fields: name: String metadata: dynamic, serializationDataType=jsonb ``` See [Storing serializable fields as JSONB](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md#storing-serializable-fields-as-jsonb) for class-level and project-level settings. After adding or changing `dynamic` fields on a table model, run `serverpod create-migration` and apply the migration. ## Copy models with dynamic fields The generated [copyWith](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#copywith) method keeps its standard behavior on dynamic fields: omitting the argument preserves the current value, and passing `null` explicitly clears the field: ```dart var cleared = object.copyWith(payload: null); ``` ## Limits - **No compile-time type checks.** Cast values in Dart when you know the expected type (`value as String`, `value is SimpleData`). - **No database schema validation.** The database stores the serialized JSON; invalid shapes are only caught at runtime in Dart. - **Prefer typed models when the schema is stable.** `dynamic` trades type safety for flexibility. ## Related - [Working with models](https://docs.serverpod.dev/concepts/data-and-the-database/models.md): model file syntax and supported types. - [Field types](https://docs.serverpod.dev/concepts/data-and-the-database/models/field-types.md): YAML types mapped to Dart, Postgres, SQLite, and JSON. - [Working with endpoints](https://docs.serverpod.dev/concepts/endpoints-and-apis.md): endpoint parameters and return values. - [Database tables](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md): how model fields map to columns, including JSONB storage. - [Custom serialization](https://docs.serverpod.dev/concepts/data-and-the-database/models/custom-serialization.md): registering custom serializable classes. --- # Custom serialization https://docs.serverpod.dev/concepts/data-and-the-database/models/custom-serialization Custom serialization lets you pass your own hand-written Dart classes, such as value types from your codebase or classes from a third-party package, through endpoints and models. Serverpod can serialize any class that follows three rules: 1. The class must have a method called `toJson()` which returns a JSON serialization of the object. ```dart Map toJson() { return { 'name': 'John Doe', }; } ``` 2. There must be a constructor or factory called `fromJson()`, which takes a JSON serialization as parameters. ```dart factory ClassName.fromJson( Map json, ) { return ClassName( name: json['name'] as String, ); } ``` 3. The class must be declared in the `config/generator.yaml` file in the server project. The path needs to be accessible from both the server package and the client package. ```yaml extraClasses: - package:my_project_shared/my_project_shared.dart:ClassName ``` ## Using a custom class as a model field If your custom class will be used as a field inside a generated `.spy.yaml` model, also implement a `copyWith()` method. The parent model's generated `copyWith` calls `.copyWith()` on each field to produce a deep copy. `copyWith()` is not needed when the class is only used as an endpoint parameter or return type. ```dart ClassName copyWith({ String? name, }) { return ClassName( name: name ?? this.name, ); } ``` :::tip In the framework, `copyWith()` is implemented as a deep copy to ensure immutability. We recommend following this approach when implementing it for custom classes to avoid unintentional side effects caused by shared mutable references. ::: When a custom class is used as a field on a table model, it is stored as a `json` or `jsonb` column. See [Storing serializable fields as JSONB](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md#storing-serializable-fields-as-jsonb). ## Where custom classes live Custom classes must live where both the server and the client package can import them, typically a [shared package](https://docs.serverpod.dev/concepts/data-and-the-database/models/shared-packages.md). See that page for creating one and wiring it into the server and client `pubspec.yaml` files. The same package can carry both generated models and hand-written custom classes. A plain Dart package works too, since `extraClasses` puts no Serverpod dependency requirements on the class. A minimal custom class looks like this: ```dart class ClassName { String name; ClassName(this.name); Map toJson() { return { 'name': name, }; } factory ClassName.fromJson( Map jsonSerialization, ) { return ClassName( jsonSerialization['name'], ); } } ``` After declaring the class in `extraClasses`, save with `serverpod start` running, or run `serverpod generate`. The class can now be used in your endpoints with the full serialization and deserialization management that comes with Serverpod. In your server project, you can create an endpoint returning your custom object. ```dart import 'package:my_project_shared/my_project_shared.dart'; import 'package:serverpod/serverpod.dart'; class ExampleEndpoint extends Endpoint { Future getMyCustomClass(Session session) async { return ClassName( 'John Doe', ); } } ``` ## Custom class with Freezed Serverpod also has support for using custom classes created with the [Freezed](https://pub.dev/packages/freezed) package. ```dart import 'package:freezed_annotation/freezed_annotation.dart'; part 'freezed_custom_class.freezed.dart'; part 'freezed_custom_class.g.dart'; @freezed abstract class FreezedCustomClass with _$FreezedCustomClass { const factory FreezedCustomClass({ required String firstName, required String lastName, required int age, }) = _FreezedCustomClass; factory FreezedCustomClass.fromJson( Map json, ) => _$FreezedCustomClassFromJson(json); } ``` In the `config/generator.yaml`, you declare the package and the class: ```yaml extraClasses: - package:my_shared_package/my_shared_package.dart:FreezedCustomClass ``` ## Custom class with ProtocolSerialization If you need certain fields to be omitted when transmitting to the client-side, your server-side custom class should implement the `ProtocolSerialization` interface. This requires adding a method named `toJsonForProtocol()`. Serverpod will then use this method to serialize your object for protocol communication. If the class does not implement `ProtocolSerialization`, Serverpod defaults to using the `toJson()` method. ### Implementation example Here’s how you can implement it: ```dart class CustomClass implements ProtocolSerialization { final String? value; final String? serverSideValue; CustomClass({this.value, this.serverSideValue}); // Serializes fields specifically for protocol communication Map toJsonForProtocol() { return { "value": value, }; } // Serializes all fields, including those intended only for server-side use Map toJson() { return { "value": value, "serverSideValue": serverSideValue, }; } } ``` Client-side classes never need `toJsonForProtocol()`. Only the server uses it, to leave out fields that should not reach the client. The client always serializes with `toJson()`. --- # Shared packages https://docs.serverpod.dev/concepts/data-and-the-database/models/shared-packages Shared packages let you define models and logic that can be safely imported in both server and client code. They contain the models and the protocol file, depend exclusively on the `serverpod_serialization` package, and have no server-only dependencies. This makes them ideal for data structures that need to be used across your full stack, for example DTOs, API request/response shapes, or domain models that flow between Flutter and your Serverpod backend with their custom logic. Models and the protocol file are generated in the shared package's own directory when your server project's code generation runs. The shared package is tied to the project through the `shared_packages` field in `config/generator.yaml`. ## Setup ### Create the shared package Create a new Dart package, for example with `dart create -t package my_shared_package`, and give it a minimal `pubspec.yaml`: ```yaml name: my_shared_package description: Models shared between server and client version: 1.0.0 publish_to: none environment: sdk: ^3.12.2 dependencies: serverpod_serialization: SERVERPOD_VERSION ``` :::info Use the same Serverpod version as your project. Replace `SERVERPOD_VERSION` with your Serverpod version (for example, `4.0.0`). ::: ### Add model files Place your `.spy.yaml` model files anywhere under the package's `lib` directory: ```text my_shared_package/ ├── lib/ │ ├── my_shared_package.dart │ └── src/ │ └── models/ │ └── shared_model.spy.yaml └── pubspec.yaml ``` Example model: ```yaml # lib/src/models/shared_model.spy.yaml class: SharedModel fields: id: UuidValue, default=random name: String description: String? createdAt: DateTime, default=now ``` ### Configure the server project Add the shared package to your server's `config/generator.yaml`: ```yaml shared_packages: - ../my_shared_package ``` Paths are relative to the server project directory. You can list multiple shared packages. ### Generate the code With `serverpod start` running, saving the model files generates the code. Outside a session, run `serverpod generate` from your server directory. This generates the Dart classes and protocol in the shared package's `lib/src/generated/` directory. After generation, a typical shared package looks like: ```text my_shared_package/ ├── lib/ │ ├── my_shared_package.dart │ └── src/ │ ├── generated/ │ │ ├── protocol.dart │ │ └── models/ │ │ └── shared_model.dart │ └── models/ │ └── shared_model.spy.yaml └── pubspec.yaml ``` Then, add the export for the `protocol.dart` file to the shared package's `lib/my_shared_package.dart` file to make the classes available in the shared package: ```dart export 'src/generated/protocol.dart'; ``` ### Add the dependency to the server and client Add the shared package to both your server and client (or Flutter app) `pubspec.yaml`: ```yaml # In my_project_server/pubspec.yaml and my_project_client/pubspec.yaml dependencies: my_shared_package: path: ../my_shared_package ``` You are now ready to use the shared package in your server and client code. ## Using shared models The shared package is nothing more than a regular Dart package that contains the models and the protocol file. ### Importing the package You can import it in your server and client code just like any other package and use the generated classes: ```dart import 'package:my_shared_package/my_shared_package.dart'; // Use in endpoints, Flutter widgets, etc. final profile = UserProfile( displayName: 'Alice', avatarUrl: 'https://example.com/avatar.png', ); ``` ### Table models A shared model can declare a `table`, as long as it also sets [`database: all`](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md#choosing-where-a-table-lives) (or the experimental `database: sync`). A shared package is used from both the server and the client, so its tables have to be declared for both. ```yaml class: SharedRecord table: shared_record database: all fields: name: String ``` ### Extending shared models You can also define a base model in a shared package and extend it on the server, which keeps the table definition out of the shared package. **In the shared package** (`lib/src/shared/vehicle.spy.yaml`): ```yaml class: Vehicle fields: id: UuidValue, default=random brand: String model: String ``` **On the server** (`lib/src/models/car.spy.yaml`): ```yaml class: Car extends: Vehicle table: cars fields: year: int ``` The server model extends the shared `Vehicle` and adds a `table` for database persistence plus any additional fields. You can also add server-only fields with `scope=serverOnly` in the server subclass. Note that the `Car` class will be available in the server and client packages as normal, unless it is defined as `serverOnly`. ### Referencing shared models in server models Shared model names are available in the same namespace when the shared package is configured. Reference them directly in `extends`, `fields`, and other model definitions: ```yaml class: MyModel fields: sharedModel: SharedModel sharedModels: List ``` ## Shared packages in a module A [module](https://docs.serverpod.dev/concepts/server-fundamentals/modules.md) declares its shared packages the same way, in its own `config/generator.yaml`: ```yaml shared_packages: - ../my_module_shared ``` Projects that install the module reach those models through `my_module_server` and `my_module_client`, so they need no dependency on the shared package itself. ## Restrictions Shared models support most Serverpod model features, with these exceptions: | Restriction | Reason | | -------------------------------------------- | ----------------------------------------------------------------------------------------------- | | A `table` requires `database: all` or `sync` | The package is used on both sides, so its tables cannot be limited to the server or the client. | | No `serverOnly` on the class | Models must be usable on both server and client. | | No `scope: serverOnly` on fields | All fields must be serializable for the client. | If you need server-only fields, define them in a server model that extends the shared model. The shared package can also contain custom serializable classes. Register them in the server's `generator.yaml` under `extraClasses` if they need to be used in protocol serialization. See [Custom serializable classes](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md#custom-serializable-classes) for details. --- # Tables https://docs.serverpod.dev/concepts/data-and-the-database/database/tables Storing a [data model](https://docs.serverpod.dev/concepts/data-and-the-database/models.md) in the database takes one line: add the `table` key to its model file, and the class gains a generated `db` field with typed methods for reading and writing rows. From there the path is short: create and apply a [migration](https://docs.serverpod.dev/concepts/data-and-the-database/database/migrations.md) (press **M** in the `serverpod start` terminal), then call the [CRUD methods](https://docs.serverpod.dev/concepts/data-and-the-database/database/crud.md) from your endpoints. The rest of this page covers what the table definition itself can do. ```yaml class: Company table: company fields: name: String ``` When the `table` keyword is added to the model, Serverpod generates new methods for [interacting](https://docs.serverpod.dev/concepts/data-and-the-database/database/crud.md) with the database. The keyword is also picked up when a migration is created, which generates the [migrations](https://docs.serverpod.dev/concepts/data-and-the-database/database/migrations.md) needed to update the database. For the full list of keywords you can use in a model file, see the [Model reference](https://docs.serverpod.dev/concepts/lookups/model-reference.md). For how each type is stored in Postgres, SQLite, and JSON, see [Field types](https://docs.serverpod.dev/concepts/data-and-the-database/models/field-types.md). :::info When you add a `table` to a serializable class, Serverpod will automatically add an `id` field of type `int?` to the class. You should not define this field yourself. The `id` is set when you interact with an object stored in the database. ::: ## Choosing where a table lives By default, a table model generates its table on the server, and the client package gets the same class as a plain model without database bindings. The `database` keyword changes this. It accepts four values: | Value | Description | | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `server` | Generates tables only on the server, and a non-table model on the client package (default). | | `client` | Generates tables only on the client, and a non-table model on the server package. | | `all` | Generates table models on both server and client. | | `sync` | Experimental. Like `all`, and additionally keeps the table synchronized between the client and the server. See [Sync tables with the server](https://docs.serverpod.dev/concepts/data-and-the-database/database/client-side-database.md#sync-tables-with-the-server). | For example, to store the table only on the device: ```yaml class: Company table: company database: client ``` A client table lives in a SQLite database on the device, which is useful for local caches and offline data. It gets its own SQLite [migrations](https://docs.serverpod.dev/concepts/data-and-the-database/database/migrations.md) in the client package. With `all`, the server and client tables are separate databases, and data does not move between them automatically. For how to use the database on the device, see [Client-side database](https://docs.serverpod.dev/concepts/data-and-the-database/database/client-side-database.md). ## Non persistent fields You can opt out of creating a column in the database for a specific field by using the `!persist` keyword. ```yaml class: Company table: company fields: name: String, !persist ``` All fields are persisted by default and have an implicit `persist` set on each field. ## Data representation A field with a primitive / core Dart type is stored as its respective column type. An `int` field is Postgres `bigint` (64-bit), not `integer`. Complex types, such as another model, a `List`, or a `Map`, are stored as a `json` column in the database by default. Fields of type [`dynamic`](https://docs.serverpod.dev/concepts/data-and-the-database/models/dynamic-fields.md) are stored the same way, and can always hold null. See [Field types](https://docs.serverpod.dev/concepts/data-and-the-database/models/field-types.md) for the full YAML-to-column mapping, including SQLite. ```yaml class: Company table: company fields: address: Address # Stored as a json column ``` This means that each row has its own copy of the nested object that needs to be updated individually. If you instead want to reference the same object from multiple different tables, you can use the `relation` keyword. This creates a database relation between two tables and always keeps the data in sync. ```yaml class: Company table: company fields: address: Address?, relation ``` For a complete guide on how to work with relations, see the [Relations](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations.md) section. ### Storing serializable fields as JSONB By default, complex types are stored as `json` in the database. You can opt into `jsonb` storage instead using the `serializationDataType` keyword. JSONB is a binary format that supports efficient querying and [GIN indexing](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md#gin-indexes) for PostgreSQL. :::info The `serializationDataType` keyword is only valid on serializable field types (nested models, Lists, Maps, Sets, records, custom classes, and `dynamic`). Primitive types like `String` and `int` have their own native database column types and are not affected by this setting. ::: You can set `serializationDataType` at three levels, each overriding the one above it: #### Field level Applies to a single field: ```yaml class: Product table: product fields: tags: List, serializationDataType=jsonb metadata: Map, serializationDataType=jsonb ``` #### Class level Applies to all serializable fields in the class. Can be overridden by field-level setting. ```yaml class: Product table: product serializationDataType: jsonb fields: tags: List # Stored as jsonb metadata: Map # Stored as jsonb name: String history: List, serializationDataType=json # Stored as json (override) ``` #### Project level Applies to all models in the project. Add this to your `config/generator.yaml`: ```yaml serialize_as_jsonb_by_default: true ``` When enabled, all serializable fields across all models default to `jsonb` unless overridden at the class or field level. #### Migrating between json and jsonb If you change the `serializationDataType` between `json` and `jsonb` at any level, the migration system will convert existing columns automatically with no data loss. ## Choosing an ID strategy Serverpod supports two id types: `int` (the default) and `UuidValue`. The right choice depends on how the id is generated and where it is exposed. An `int` id defaults to `serial`, which means the database assigns an auto-incrementing integer when the row is inserted. Serial ids are compact and sequential, which keeps indexes efficient and makes them a good fit for internal references. The trade-offs are that the id only exists after the row is saved, and the values are guessable and enumerable, so a sequential id in a public URL can leak how many records you have or let someone probe for others. A `UuidValue` id is a 128-bit random value. It is not enumerable, and it can be generated before the row is inserted, which suits ids that appear in public URLs, are created offline on the client, or are merged from several sources. UUIDs are larger than integers and random rather than sequential, so index locality is slightly worse than with serial ids. As a rule of thumb, use the default `int` serial id for internal tables, and reach for `UuidValue` when the id is public-facing, needs to exist before the row is stored, or is created across more than one system. Because a UUID can be generated on the client, never treat a client-supplied id as proof that the caller owns a record; always check that the authenticated user is allowed to use it. ## Change ID type Changing the type of the `id` field allows you to customize the identifier type for your database tables. This is done by declaring the `id` field on table models with one of the supported types. If the field is omitted, the id field will still be created with type `int` by default. The following types are supported for the `id` field: | **Type** | Default | Default Persist options | Default Model options | Description | | :------------ | :------ | :---------------------- | :-------------------- | :---------------------------------------------- | | **int** | serial | serial (optional) | - | 64-bit serial integer (`bigint` / `bigserial`). | | **UuidValue** | random | random, random\_v7 | random, random\_v7 | UUID v4 (`random`) or v7 (`random_v7`). | ### Declaring a custom ID type To declare a custom type for the `id` field in a table model file, use the following syntax: ```yaml class: UuidIdTable table: uuid_id_table fields: id: UuidValue?, defaultPersist=random ``` ```yaml class: IntIdTable table: int_id_table fields: id: int?, defaultPersist=serial # The default keyword for 'int' is optional. ``` #### Default Uuid model value For UUIDs, it is possible to configure the `defaultModel` value. This will ensure that UUIDs are generated as soon as the object is created, rather than when it is persisted to the database. This is useful for creating objects offline or using them before they are sent to the server. ```yaml class: UuidIdTable table: uuid_id_table fields: id: UuidValue, defaultModel=random ``` When using `defaultModel=random` or `defaultModel=random_v7`, the UUID will be generated when the object is created. Since an id is always assigned the `id` field can be non-nullable. To have the database assign the id on insert instead of generating it when the object is created, use `defaultPersist=random` or `defaultPersist=random_v7` without `defaultModel`. ## Column name override By default, the column name in the database is the same as the name of the field in the model. However, you can override this to use a different name by using the `column` keyword. For example, if you have a model property named `userName` you might want to store it in the database as `user_name`. To override the column name, use the `column` keyword on the field definition: ```yaml class: User table: users fields: userName: String, column=user_name ``` :::info When adding a column name override to an existing model, the next migration will contain a column rename operation to rename the column in the database. ::: Restrictions: - The `id` field cannot have a column name override. - The column name must be unique within the model. - On a relation, set `column` on the [foreign key field](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one.md#with-an-id-field), not on the object relation field. ### Relations You can use the `column` keyword to override the name of the foreign key field in a relation. ```yaml # Employee class: Employee table: employee fields: name: String departmentId: int, relation(name=department_employees, parent=department), column=fk_employee_department_id # Department class: Department table: department fields: name: String employees: List?, relation(name=department_employees) ``` Overriding the column name is also supported when the field is used in an index. Note that the index uses the name of the field, not the column name. ```yaml # Contractor class: Contractor table: contractor fields: name: String serviceIdField: int?, column=fk_contractor_service_id service: Service?, relation(field=serviceIdField) indexes: contractor_service_unique_idx: fields: serviceIdField unique: true # Service class: Service table: service fields: name: String description: String? ``` ### Inheritance Besides column override being a database-level feature, it is allowed to be set on non-table models to support [inheritance](https://docs.serverpod.dev/concepts/data-and-the-database/models/inheritance-and-polymorphism.md). If a base class has a column name override, all child classes that implement a table will inherit the column name override. This is especially useful when you have multiple database tables that share similar column names. ```yaml # Entity class: Entity fields: name: String createdAt: DateTime, default=now, column=created_at updatedAt: DateTime, default=now, column=updated_at # Employee class: Employee extends: Entity table: employee fields: departmentId: int, relation(name=department_employees, parent=department), column=fk_employee_department_id # Department class: Department extends: Entity table: department fields: employees: List?, relation(name=department_employees) ``` In the above example, the `Employee` and `Department` classes will receive the `createdAt` and `updatedAt` fields from the `Entity` class with the columns overridden to `created_at` and `updated_at`, respectively. --- # CRUD https://docs.serverpod.dev/concepts/data-and-the-database/database/crud To interact with the database you need a [`Session`](https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions.md) object as this object holds the connection to the database. All CRUD operations are accessible via the session object and the generated models. The methods can be found under the static `db` field in your generated models. For the following examples we will use this model: ```yaml class: Company table: company fields: name: String ``` :::note You can also access the database methods through the session object under the field `db`. However, this is typically only recommended if you want to do custom queries where you explicitly type out your SQL queries. ::: ## Create There are two ways to create a new row in the database. ### Inserting a single row Inserting a single row to the database is done by calling the `insertRow` method on your generated model. The method will return the entire company object with the `id` field set. ```dart var row = Company(name: 'Serverpod'); var company = await Company.db.insertRow(session, row); ``` ### Inserting several rows Inserting several rows in a batch operation is done by calling the `insert` method. This is an atomic operation, meaning no entries will be created if any entry fails to be created. ```dart var rows = [Company(name: 'Serverpod'), Company(name: 'Google')]; var companies = await Company.db.insert(session, rows); ``` ### Ignoring conflicts When inserting rows that might violate a unique or exclusion constraint, you can set `ignoreConflicts` to `true` on the `insert` method. Rows that would cause a unique or exclusion constraint violation are silently skipped, and only the non-conflicting rows are inserted. ```dart var rows = [Company(name: 'Serverpod'), Company(name: 'Google')]; var inserted = await Company.db.insert(session, rows, ignoreConflicts: true); ``` The method returns only the rows that were successfully inserted. If all rows conflict, an empty list is returned. Unlike a regular `insert`, which fails entirely if any row violates a constraint, `ignoreConflicts` allows partial inserts where only the non-conflicting rows are written. This is useful for idempotent operations where you want to insert data without failing on duplicates. Only unique and exclusion constraint violations are ignored. Other violations such as `NOT NULL`, `CHECK`, or foreign key constraints still throw an exception. :::warning When using `ignoreConflicts` with models that have [non-persistent fields](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md#non-persistent-fields), each row is inserted individually instead of in a single batch. This is necessary because the database cannot report which rows were skipped in a batch insert, making it impossible to correctly match non-persistent field values back to inserted rows. For large numbers of rows, this can cause performance issues. Consider removing non-persistent fields from the model or inserting in smaller batches. ::: ## Read There are three different read operations available. ### Finding by id You can retrieve a single row by its `id`. ```dart var company = await Company.db.findById(session, companyId); ``` This operation either returns the model or `null`. ### Finding a single row You can find a single row using an expression. ```dart var company = await Company.db.findFirstRow( session, where: (t) => t.name.equals('Serverpod'), ); ``` This operation returns the first model matching the filtering criteria or `null`. See [filter](https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering.md) and [sort](https://docs.serverpod.dev/concepts/data-and-the-database/database/sorting.md) for all filter operations. :::info If you include an `orderBy`, it will be evaluated before the list is reduced. In this case, `findFirstRow()` will return the first entry from the sorted list. ::: ### Finding multiple rows To find multiple rows, use the same principle as for finding a single row. ```dart var companies = await Company.db.find( session, where: (t) => t.id < 100, limit: 50, ); ``` This operation returns a `List` of your models matching the filtering criteria. See [filter](https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering.md) and [sort](https://docs.serverpod.dev/concepts/data-and-the-database/database/sorting.md) for all filter and sorting operations and [pagination](https://docs.serverpod.dev/concepts/data-and-the-database/database/pagination.md) for how to paginate the result. ## Update There are multiple update operations available for different use cases. ### Update a single row To update a single row, use the `updateRow` method. ```dart var company = await Company.db.findById(session, companyId); // Fetched company has its id set company.name = 'New name'; var updatedCompany = await Company.db.updateRow(session, company); ``` The object that you update must have its `id` set to a non-`null` value and the id needs to exist on a row in the database. The `updateRow` method returns the updated object. #### Update specific columns It is possible to target one or several columns that you want to mutate, meaning any other column will be left unmodified even if the dart object has introduced a change. ```dart var company = await Company.db.findById(session, companyId); company.name = 'New name'; company.address = 'Baker street'; var updatedCompany = await Company.db.updateRow(session, company, columns: (t) => [t.name]); ``` In the above example, only the company name will be updated, the address column will not be changed. ### Update several rows To batch update several rows use the `update` method. ```dart var companies = await Company.db.find(session); companies = companies.map((c) => c.copyWith(name: 'New name')).toList(); var updatedCompanies = await Company.db.update(session, companies); ``` This is an atomic operation, meaning no entries will be updated if any entry fails to be updated. The `update` method returns a `List` of the updated objects. #### Update specific columns The same syntax is available for updating specific columns on multiple rows. ```dart var companies = await Company.db.find(session); companies = companies.map((c) => c.copyWith(name: 'New name', address: 'Baker Street')).toList(); var updatedCompanies = await Company.db.update(session, companies, columns: (t) => [t.name]); ``` ### Update by ID To update a row by its ID without fetching it first, use the `updateById` method. This method allows you to specify which columns to update directly. ```dart var updatedCompany = await Company.db.updateById( session, companyId, columnValues: (t) => [t.name('New name'), t.address('New address')], ); ``` The `updateById` method updates only the specified columns for the row with the given ID. The method returns the updated row, or throws a `DatabaseUnexpectedResultException` if no row with the given ID exists. At least one column must be specified in the `columnValues` parameter, otherwise an `ArgumentError` will be thrown. See [Database exceptions](https://docs.serverpod.dev/concepts/data-and-the-database/database/exceptions.md) for the full set of database exception types and when they are thrown. You can also update columns to null values: ```dart var updatedCompany = await Company.db.updateById( session, companyId, columnValues: (t) => [t.name(null), t.address(null)], ); ``` ### Update where To update rows based on filter criteria, use the `updateWhere` method. This method allows you to update specific columns for all rows matching a where clause. ```dart var updatedCompanies = await Company.db.updateWhere( session, columnValues: (t) => [t.name('Updated name')], where: (t) => t.name.like('%Ltd'), ); ``` The `updateWhere` method updates all rows matching the where expression, modifying only the specified columns. The method returns a list of the updated rows. If no rows match the criteria, an empty list is returned. See [filter](https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering.md) for all available filtering operations. The method also supports [pagination](https://docs.serverpod.dev/concepts/data-and-the-database/database/pagination.md) and [ordering](https://docs.serverpod.dev/concepts/data-and-the-database/database/sorting.md): ```dart var updatedCompanies = await Company.db.updateWhere( session, columnValues: (t) => [t.name('Updated name'), t.address('New address')], where: (t) => t.id > 100, orderBy: (t) => t.id, // or t.id.asc() limit: 10, offset: 5, ); ``` ## Upsert An upsert inserts a row, or updates the existing row when it collides with one that is already stored. The database runs the check and the write as a single atomic operation, so no other transaction can slip in between them. The collision is decided by `conflictColumns`, which must be covered by a unique index (or be the primary key); without one, the call throws a `DatabaseQueryException`. The examples below use a `Product` model whose `sku` field has a [unique index](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md#make-fields-unique). ### Upsert a single row To insert-or-update a single row, use the `upsertRow` method: ```dart var product = await Product.db.upsertRow( session, Product(sku: 'chair-01', name: 'Office chair', price: 199.00), conflictColumns: (t) => [t.sku], ); ``` If no row with that `sku` exists, the row is inserted. If one exists, it keeps its `id`, and every persistent column except `id` and the `conflictColumns` is overwritten with the supplied values. The method returns the stored row. The `id` column can be the conflict target as well, which is useful when the same code path handles an object that may or may not already have an id: ```dart var product = await Product.db.upsertRow( session, Product(id: productId, sku: 'chair-01', name: 'Office chair', price: 199.00), conflictColumns: (t) => [t.id], ); ``` To limit which columns an update touches, pass `updateColumns`. A new row is still inserted with all of its values, and an existing row only has the selected columns updated: ```dart var product = await Product.db.upsertRow( session, Product(sku: 'chair-01', name: 'Renamed chair', price: 249.00), conflictColumns: (t) => [t.sku], updateColumns: (t) => [t.price], ); ``` To update only rows in a certain state, pass `updateWhere`. When the existing row does not match the expression, nothing is changed and `upsertRow` returns `null`: ```dart var product = await Product.db.upsertRow( session, Product(sku: 'chair-01', name: 'Office chair v2', price: 249.00), conflictColumns: (t) => [t.sku], updateWhere: (t) => t.price > 500.0, ); ``` :::info The literal `500.0` matches the `double` column type. Comparison operators check value types at runtime, so `t.price > 500` compiles but throws. See [comparison operators](https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering.md#comparison-operators). ::: ### Upsert several rows The batch `upsert` inserts and updates rows in a single atomic operation, so no rows are written if any row fails: ```dart var products = await Product.db.upsert( session, [ Product(sku: 'chair-01', name: 'Office chair v2', price: 249.00), Product(sku: 'desk-01', name: 'Standing desk', price: 599.00), ], conflictColumns: (t) => [t.sku], ); ``` The batch method takes the same `updateColumns` and `updateWhere` parameters as `upsertRow`. The result contains one row per input, in the same order. When `updateWhere` is set, conflicting rows that do not match are skipped and left out of the result, so the list can be shorter than the input. For large batches, the read-back can be skipped entirely. See [Skipping returned rows](#skipping-returned-rows). Like the other batch operations, `upsert` accepts a `transaction` parameter to join a larger [transaction](https://docs.serverpod.dev/concepts/data-and-the-database/database/transactions.md). For models with [non-persistent fields](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md#non-persistent-fields), the input values of those fields are carried over to the returned objects. They take no part in conflict detection and are never written to the database, and such batches are upserted row by row internally, which can be slow for large inputs. A single-row upsert that unexpectedly matches multiple rows throws a `DatabaseUnexpectedResultException`. See [exceptions](https://docs.serverpod.dev/concepts/data-and-the-database/database/exceptions.md). ## Delete Deleting rows from the database is done in a similar way to updating rows. However, there are three delete operations available. ### Delete a single row To delete a single row, use the `deleteRow` method. ```dart var company = await Company.db.findById(session, companyId); // Fetched company has its id set var companyDeleted = await Company.db.deleteRow(session, company); ``` The input object needs to have the `id` field set. The `deleteRow` method returns the deleted model. ### Delete several rows To batch delete several rows, use the `delete` method. This method also supports [ordering](https://docs.serverpod.dev/concepts/data-and-the-database/database/sorting.md) the returned deleted results. ```dart var companiesDeleted = await Company.db.delete( session, companies, orderBy: (t) => t.id, // or t.id.asc() ); ``` This is an atomic operation, meaning no entries will be deleted if any entry fails to be deleted. The `delete` method returns a `List` of the models deleted, ordered as specified by the orderBy. ### Delete by filter You can also do a [filtered](https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering.md) delete and delete all entries matching a `where` query, by using the `deleteWhere` method. This method also supports [ordering](https://docs.serverpod.dev/concepts/data-and-the-database/database/sorting.md) of the returned deleted results. ```dart var companiesDeleted = await Company.db.deleteWhere( session, where: (t) => t.name.like('%Ltd'), orderByList: (t) => [t.name.desc(), t.id.asc()], ); ``` The above example will delete any row where the `name` ends in *Ltd*. The `deleteWhere` method returns a `List` of the models deleted, ordered by name in descending order, followed by id in ascending order. ## Skipping returned rows The batch and filtered write methods read the affected rows back from the database and return them as a list. Pass `noReturn: true` when the write itself is all you need. ```dart await Company.db.updateWhere( session, columnValues: (t) => [t.name('Archived company')], where: (t) => t.name.like('%Ltd'), noReturn: true, ); ``` The write runs with the same filters, conflict handling, transaction, and atomicity guarantees, but the method returns an empty list instead of reading the rows back. Skipping that read saves transferring and deserializing every affected row, which is worth it for bulk imports, cleanup jobs, and any write where the generated ids, database defaults, and updated values are not used afterwards. The `noReturn` parameter is available on `insert`, `update`, `updateWhere`, `upsert`, `delete`, and `deleteWhere`. The single-row methods `insertRow`, `updateRow`, and `deleteRow` always return the affected row. `upsertRow` returns it too, or `null` when the existing row doesn't match its `updateWhere` expression. Since the result is empty, `orderBy` and `orderByList` have no visible effect on it. They still matter on filtered operations, where they decide which rows a `limit` or `offset` selects. ## Count Count is a special type of query that helps counting the number of rows in the database that matches a specific [filter](https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering.md). ```dart var count = await Company.db.count( session, where: (t) => t.name.like('s%'), ); ``` The return value is an `int` for the number of rows matching the filter. --- # Filter https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering Filters narrow which rows a query returns. You build them in the `where` callback that most database methods accept, and they are statically type-checked. The callback receives a table descriptor, by convention named `t`, which exposes each column of the table as a typed field. Filtering happens in the database, so only the matching rows are sent to the server. This page covers filtering on column values, on logical combinations, and on related rows. To include related data in the result instead of filtering by it, see [Relation queries](https://docs.serverpod.dev/concepts/data-and-the-database/database/relation-queries.md). ## Column operations Each column datatype supports the set of operations that makes sense for that type. ### Equals Compare a column to an exact value, meaning only rows that match exactly will remain in the result. ```dart await User.db.find( session, where: (t) => t.name.equals('Alice'), ); ``` In the example we fetch all users with the name Alice. Not equals is the negated version of equals. ```dart await User.db.find( session, where: (t) => t.name.notEquals('Bob') ); ``` In the example we fetch all users with a name that is not Bob. If a non-`null` value is used as an argument for the notEquals comparison, rows with a `null` value in the column will be included in the result. ### Comparison operators The `>`, `>=`, `<`, and `<=` operators compare a column to a value. They are supported for `int`, `double`, `Duration`, `DateTime`, `String`, and `UuidValue` columns. ```dart await User.db.find( session, where: (t) => t.age > 25 ); ``` In the example we fetch all users that are older than 25. :::info Comparison operators check value types at runtime. Comparing a `double` column against an `int` literal, such as `t.price > 500`, compiles but throws. Write the literal to match the column type, `t.price > 500.0`. ::: ### Between The between method takes two values and checks if the column's value is between them, inclusive of the boundaries. It is available on `int`, `double`, `Duration`, and `DateTime` columns. ```dart await User.db.find( session, where: (t) => t.age.between(18, 65) ); ``` In the example we fetch all users between 18 and 65 years old. This can also be expressed as `(t.age >= 18) & (t.age <= 65)`. The `notBetween` operation negates the condition. The boundaries are still inclusive. ```dart await User.db.find( session, where: (t) => t.age.notBetween(18, 65) ); ``` In the example we fetch all users that are not between 18 and 65 years old. This can also be expressed as `(t.age < 18) | (t.age > 65)`. ### In set In set can be used to match with several values at once. The `inSet` method makes an exact comparison, like equals, but against every value in the set. ```dart await User.db.find( session, where: (t) => t.name.inSet({'Alice', 'Bob'}) ); ``` In the example we fetch all users with a name matching either Alice or Bob. If an empty set is used as an argument for the inSet comparison, no rows will be included in the result. The `notInSet` operation negates the condition. ```dart await User.db.find( session, where: (t) => t.name.notInSet({'Alice', 'Bob'}) ); ``` In the example we fetch all users with a name not matching Alice or Bob. Rows with a `null` value in the column will be included in the result. If an empty set is used as an argument for the notInSet comparison, all rows will be included in the result. ### Like Like performs pattern matching against `String` columns. The matcher is case-sensitive. Two special characters match partial entries: - **`%`** matches any sequence of characters. - **`_`** matches any single character. | String | Matcher | Is matching | | ------ | ------- | ----------- | | abc | a% | true | | abc | \_b% | true | | abc | a\_c | true | | abc | b\_ | false | We use like to match against a partial string. ```dart await User.db.find( session, where: (t) => t.name.like('A%') ); ``` In the example we fetch all users with a name that starts with A. There is a negated version of like that can be used to exclude rows from the result. ```dart await User.db.find( session, where: (t) => t.name.notLike('B%') ); ``` In the example we fetch all users with a name that does not start with B. ### Ilike The `ilike` method works the same as `like` but is case-insensitive. ```dart await User.db.find( session, where: (t) => t.name.ilike('a%') ); ``` In the example we fetch all users with a name that starts with a or A. There is a negated version, `notIlike`, that can be used to exclude rows from the result. ```dart await User.db.find( session, where: (t) => t.name.notIlike('b%') ); ``` In the example we fetch all users with a name that does not start with b or B. :::info On SQLite, both `like` and `ilike` use SQLite's `LIKE` operator, which is case-insensitive for ASCII characters. This means `like` is not case-sensitive on SQLite the way it is on Postgres. ::: ## Logical operators Logical operators are also supported when filtering, allowing you to chain multiple statements together to create more complex queries. The `&` operator is used to chain two statements together with an `and` operation. ```dart await User.db.find( session, where: (t) => (t.name.equals('Alice') & (t.age > 25)) ); ``` In the example we fetch all users with the name "Alice" *and* are older than 25. The `|` operator is used to chain two statements together with an `or` operation. ```dart await User.db.find( session, where: (t) => (t.name.like('A%') | t.name.like('B%')) ); ``` In the example we fetch all users that have a name that starts with A *or* B. The `~` operator is used to negate an expression with a `not` operation. ```dart await User.db.find( session, where: (t) => ~t.name.equals('Alice') ); ``` In the example we fetch all users that do *not* have the name "Alice". The `~` operator can also be used with more complex expressions: ```dart await User.db.find( session, where: (t) => ~(t.name.like('A%') | (t.age > 25)) ); ``` In the example we fetch all users that do *not* have a name starting with "A" *and* are *not* older than 25. :::info Place each operand within parentheses, as in the examples above. Dart's operator precedence binds `&` and `|` tighter than comparisons, so an expression like `t.name.like('A%') | t.age > 25` does not compile without the parentheses around `(t.age > 25)`. ::: ## Vector distance operators All vector field types support specialized distance operations for similarity search. Available vector distance operations: **Vector, HalfVector, and SparseVector fields:** - `distanceL2` - Euclidean (L2) distance. - `distanceInnerProduct` - Inner product distance. - `distanceCosine` - Cosine distance. - `distanceL1` - Manhattan or taxicab (L1) distance. **Bit vector fields:** - `distanceHamming` - Hamming distance. - `distanceJaccard` - Jaccard distance. A distance operation returns a numeric value, so you can compare it in `where`, order by it, and combine it with other filters. The canonical similarity search orders by distance and limits the result: ```dart // The vector to compare against. var queryVector = Vector([0.1, 0.2, 0.3, ...]); // Find the ten documents most similar to the query vector. var similarDocs = await Document.db.find( session, where: (t) => t.embedding.distanceCosine(queryVector) < 0.5, orderBy: (t) => t.embedding.distanceCosine(queryVector), limit: 10, ); ``` The same shape works for every distance operation, such as `distanceInnerProduct` against a `SparseVector` query or `distanceHamming` against a `Bit` query. The distance can also be range-filtered or combined with regular column filters: ```dart // Filter by distance range. var mediumSimilarity = await Document.db.find( session, where: (t) => t.embedding.distanceL2(queryVector).between(0.3, 0.8), ); // Combine with other filters. var filteredSimilarity = await Document.db.find( session, where: (t) => t.category.equals('article') & (t.embedding.distanceCosine(queryVector) < 0.7), orderBy: (t) => t.embedding.distanceCosine(queryVector), limit: 10, ); ``` :::tip For optimal performance with vector similarity searches, consider creating specialized vector indexes (HNSW or IVFFLAT) on your vector fields. See the [Vector indexes](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md#vector-indexes) section for more details. To fine-tune query execution, you can also set appropriate [runtime parameters](https://docs.serverpod.dev/concepts/data-and-the-database/database/runtime-parameters.md) for vector queries. ::: ## Relation operations When two models are connected with a [one-to-one](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one.md) or [one-to-many](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-many.md) object relation, you can filter on the related rows directly. ### One-to-one For 1:1 relations the columns of the relation can be accessed directly on the relation field. This enables filtering on related objects properties. ```dart await User.db.find( session, where: (t) => t.address.street.like('%road%') ); ``` In the example each user has a relation to an address with a street field. The query fetches all users whose related address has a street containing the word "road". ### One-to-many For 1:n relations, special filter methods let you filter on properties of the related rows as a group: how many there are, whether any or none exist, or whether all of them match a condition. #### Count Count can be used to count the number of related entries in a 1:n relation. The `count` always needs to be compared with a constant value. ```dart await User.db.find( session, where: (t) => t.orders.count() > 3 ); ``` In the example we fetch all users with more than three orders. We can apply a sub-filter to the `count` operator to filter the related entries before they are counted. ```dart await User.db.find( session, where: (t) => t.orders.count((o) => o.itemType.equals('book')) > 3 ); ``` In the example we fetch all users with more than three "book" orders. #### None None can be used to retrieve rows that have no related entries in a 1:n relation. If a related entry exists, the row is omitted from the result. ```dart await User.db.find( session, where: (t) => t.orders.none() ); ``` In the example we fetch all users that have no orders. We can apply a sub-filter to the `none` operator to filter the related entries. If any related entry matches the sub-filter, the row is omitted from the result. ```dart await User.db.find( session, where: (t) => t.orders.none((o) => o.itemType.equals('book')) ); ``` In the example we fetch all users that have no "book" orders. #### Any Any works similarly to the `any` method on lists in Dart. If there exists any related entry then include the row in the result. ```dart await User.db.find( session, where: (t) => t.orders.any() ); ``` In the example we fetch all users that have any order. We can apply a sub-filter to the `any` operator to filter the related entries. If any related entry matches the sub-filter, the row is included in the result. ```dart await User.db.find( session, where: (t) => t.orders.any((o) => o.itemType.equals('book')) ); ``` In the example we fetch all users that have any "book" order. #### Every Every works similarly to the `every` method on lists in Dart. If every related entry matches the sub-filter then include the row in the result. For the `every` operator the sub-filter is mandatory. ```dart await User.db.find( session, where: (t) => t.orders.every((o) => o.itemType.equals('book')) ); ``` In the example we fetch all users that have only "book" orders. ## Geography operators All geography field types (`GeographyPoint`, `GeographyLineString`, `GeographyPolygon`, `GeographyGeometryCollection`) support spatial filter and ordering operations. ### intersects Returns rows where the geography column spatially intersects the given geography value. Wraps `ST_Intersects`. ```dart var point = GeographyPoint(longitude: 2.35, latitude: 48.85); await Store.db.find( session, where: (t) => t.location.intersects(point), ); ``` ### distanceWithin Returns rows where the geography column is within a given distance (in metres) of the given geography value. Wraps `ST_DWithin`. ```dart var point = GeographyPoint(longitude: 2.35, latitude: 48.85); await Store.db.find( session, where: (t) => t.location.distanceWithin(point, 1000), ); ``` ### distance Returns the distance in metres between the geography column and the given geography value. Wraps `ST_Distance`. The result can be used in `orderBy` for nearest-first ordering, or compared numerically in `where`. ```dart var point = GeographyPoint(longitude: 2.35, latitude: 48.85); // Order results nearest-first await Store.db.find( session, orderBy: (t) => t.location.distance(point), ); // Filter by distance and order results nearest-first await Store.db.find( session, where: (t) => t.location.distance(point) < 5000, orderBy: (t) => t.location.distance(point), ); ``` ### contains Returns rows where the geography column fully contains the given geography value. Wraps `ST_Covers`. ```dart var point = GeographyPoint(longitude: 2.35, latitude: 48.85); await DeliveryZone.db.find( session, where: (t) => t.boundary.contains(point), ); ``` ### within Returns rows where the geography column is fully within the given geography value. Wraps `ST_CoveredBy`. ```dart var zone = GeographyPolygon( exteriorRing: [ GeographyPoint(longitude: 2.30, latitude: 48.82), GeographyPoint(longitude: 2.40, latitude: 48.82), GeographyPoint(longitude: 2.40, latitude: 48.90), GeographyPoint(longitude: 2.30, latitude: 48.90), GeographyPoint(longitude: 2.30, latitude: 48.82), ], ); await Store.db.find( session, where: (t) => t.location.within(zone), ); ``` :::tip For optimal performance with spatial queries, consider creating a spatial index on your geography fields. See the [Geography indexes](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md#geography-indexes) section for more details. ::: --- # Sort https://docs.serverpod.dev/concepts/data-and-the-database/database/sorting Sorting puts the results of a database query in a defined order. The `find` method has an `orderBy` parameter where you can specify a column for sorting. The parameter takes a callback as an argument. The callback receives a model-specific table descriptor, conventionally named `t`, though you can name the parameter anything. The same descriptor is also available as the static `t` field on the model, and that name is fixed. The table descriptor represents the model's database table and includes a field for each column. In the callback, return the column to sort by: ```dart var companies = await Company.db.find( session, orderBy: (t) => t.name, ); ``` In the example we fetch all companies and sort them by their name. By default, results are sorted in ascending order, so a bare column is equivalent to calling `.asc()` on it. To sort in descending order, call `.desc()` on the column: ```dart var companies = await Company.db.find( session, orderBy: (t) => t.name.desc(), ); ``` To order by several different columns, use `orderByList` instead. It cannot be used in conjunction with `orderBy`. ```dart var companies = await Company.db.find( session, orderByList: (t) => [t.name.desc(), t.id.asc()], ); ``` In the example we sort companies by their name in descending order, and then by their id in ascending order. The `orderBy` and `orderByList` parameters are not limited to `find`. They also appear on [`updateWhere`, `delete`, and `deleteWhere`](https://docs.serverpod.dev/concepts/data-and-the-database/database/crud.md), where they order the returned rows. When [paginating](https://docs.serverpod.dev/concepts/data-and-the-database/database/pagination.md) with `limit` and `offset`, always set an `orderBy` so pages stay consistent between requests. ## Sort on relations To sort based on a field from a related model, use the chained field reference. ```dart var companies = await Company.db.find( session, orderBy: (t) => t.ceo.name, ); ``` In the example we fetch all companies and sort them by their CEO's name. You can order results based on the count of a list relation (1:n). ```dart var companies = await Company.db.find( session, orderBy: (t) => t.employees.count(), ); ``` In the example we fetch all companies and sort them by the number of employees. The count used for sorting can also be filtered using a sub-filter. ```dart var companies = await Company.db.find( session, orderBy: (t) => t.employees.count( (employee) => employee.role.equals('developer'), ), ); ``` In the example we fetch all companies and sort them by the number of employees with the role of "developer". Vector and geography fields can be ordered by distance to a query value. See [Vector distance operators](https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering.md#vector-distance-operators) and [Geography operators](https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering.md#geography-operators). --- # Pagination https://docs.serverpod.dev/concepts/data-and-the-database/database/pagination Serverpod provides built-in support for pagination to help manage large datasets, allowing you to retrieve data in smaller chunks. Pagination is achieved using the `limit` and `offset` parameters. ## Limit The `limit` parameter specifies the maximum number of records to return from the query. This is equivalent to the number of rows on a page. ```dart var companies = await Company.db.find( session, limit: 10, ); ``` In the example we fetch the first 10 companies. ## Offset The `offset` parameter determines the starting point from which to retrieve records. It essentially skips the first `n` records. ```dart var companies = await Company.db.find( session, limit: 10, offset: 30, ); ``` In the example we skip the first 30 rows and fetch the 31st to 40th company. ## Using limit and offset for pagination Together, `limit` and `offset` can be used to implement pagination. ```dart int page = 3; int companiesPerPage = 10; var companies = await Company.db.find( session, orderBy: (t) => t.id, limit: companiesPerPage, offset: (page - 1) * companiesPerPage, ); ``` In the example we fetch the third page of companies, with 10 companies per page. ### Tips 1. **Performance**: Be aware that while `offset` can help in pagination, it may not be the most efficient way for very large datasets. Using an indexed column to filter results can sometimes be more performant. 2. **Consistency**: Due to possible data changes between paginated requests (like additions or deletions), the order of results might vary. It's recommended to use an `orderBy` parameter to ensure consistency across paginated results. 3. **Page numbering**: Page numbers usually start from 1. Adjust the offset calculation accordingly. ## Cursor-based pagination A limit-offset pagination may not be the best solution if the table is changed frequently and rows are added or removed between requests. Cursor-based pagination is an alternative method to the traditional limit-offset pagination. Instead of using an arbitrary offset to skip records, cursor-based pagination uses a unique record identifier (a *cursor*) to mark the starting or ending point of a dataset. This approach is particularly beneficial for large datasets as it offers consistent and efficient paginated results, even if the data is being updated frequently. ### How it works In cursor-based pagination, the client provides a cursor as a reference point, and the server returns data relative to that cursor. This cursor is usually an `id`. ### Implementing cursor-based pagination 1. **Initial request**: For the initial request, where no cursor is provided, retrieve the first `n` records: ```dart int recordsPerPage = 10; var companies = await Company.db.find( session, orderBy: (t) => t.id, limit: recordsPerPage, ); ``` 2. **Subsequent requests**: For the subsequent requests, use the cursor (for example, the last `id` from the previous result, typically sent by the client) to fetch the next set of records: ```dart int cursor = lastCompanyIdFromPreviousPage; var companies = await Company.db.find( session, where: (t) => t.id > cursor, orderBy: (t) => t.id, limit: recordsPerPage, ); ``` 3. **Returning the cursor**: When returning data to the client, also return the last row's `id`, so the client can send it back as the cursor for the next page. Define a small model that carries the page of rows together with its cursor, for example a `CompanyPage` with a `List` field and a nullable `lastCursor` field. ### Tips 1. **Choosing a cursor**: While IDs are commonly used as cursors, timestamps or other unique, sequentially ordered fields can also serve as effective cursors. 2. **Backward pagination**: To implement backward pagination, use the first item from the current page as the cursor and adjust the query accordingly. 3. **Combining with sorting**: Ensure the field used as a cursor aligns with the sorting order. For instance, if you're sorting data by a timestamp in descending order, the cursor should also be based on the timestamp. 4. **End of data**: If the returned data contains fewer items than the requested limit, it indicates that you've reached the end of the dataset. --- # Relations https://docs.serverpod.dev/concepts/data-and-the-database/database/relations A relation links rows in one table to rows in another through a foreign key, and the database keeps the link consistent. This is the alternative to embedding a model as a [`json` column](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md#data-representation): related data lives in its own table, can be queried on its own, and is never duplicated per row. Relations are declared in the model file with the `relation` keyword. There are two forms: - An **object relation** types the field as the related model, e.g. `address: Address?, relation`. The related object can be fetched together with the row through [relation queries](https://docs.serverpod.dev/concepts/data-and-the-database/database/relation-queries.md). - An **id relation** stores only the foreign key, e.g. `addressId: int, relation(parent=address)`. You read and set the id yourself, and nothing else is fetched. ## Choose a relation shape | Shape | Use when | Example on the page | | ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------- | | [One-to-one](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one.md) | A row pairs with at most one row of the other table. | A `User` with one `Address`. | | [One-to-many](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-many.md) | One row owns any number of related rows. | A `Company` with many `Employee` rows. | | [Many-to-many](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/many-to-many.md) | Rows on both sides connect freely, through a junction table. | `Student` and `Course` joined by `Enrollment`. | | [Self-relations](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/self-relations.md) | Rows relate to rows in the same table. | A `Cat` linking to its mother and kittens. | | [Relations with modules](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/modules.md) | Your tables relate to a module's tables, such as the signed-in user. | A `UserProfile` attached to an `AuthUser`. | Whatever the shape, [Referential actions](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/referential-actions.md) control what happens to related rows when the row they point to is deleted or its id changes, and [Deferrable constraints](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/deferrable-constraints.md) control when the foreign key is checked. ## Related - [Relation queries](https://docs.serverpod.dev/concepts/data-and-the-database/database/relation-queries.md): fetch, attach, and detach related rows. - [Filtering](https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering.md#relation-operations): filter on related rows, such as their count or contents. - [Tables](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md): how models map to tables in the first place. --- # One-to-one https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one One-to-one (1:1) relationships represent a unique association between two entities. At most one model can be connected on either side of the relation. This means we have to set a **unique index** on the foreign key in the database. Without the unique index the relation would be considered a one-to-many (1:n) relation. ## Defining the relationship In the following examples we show how to configure a 1:1 relationship between `User` and `Address`. ### With an id field In the most simple case, all we have to do is add an `id` field on one of the models. ```yaml # address.spy.yaml class: Address table: address fields: street: String # user.spy.yaml class: User table: user fields: # The foreign key field. addressId: int, relation(parent=address) indexes: user_address_unique_idx: fields: addressId unique: true ``` In the example, the `relation` keyword annotates the `addressId` field to hold the foreign key. The field's type must match the parent table's id type: `int` here, or `UuidValue` when the parent model uses a UUID id. The relation keyword needs to specify the `parent` parameter. The `parent` parameter defines which table the relation is towards, in this case the `Address` table. The addressId is **required** in this example because the field is not nullable. That means that each `User` must have a related `Address`. If you want to make the relation optional, make the datatype nullable (`int?`). When fetching a `User` from the database the `addressId` field will automatically be populated with the related `Address` object `id`. ### With an object While the previous example highlights manual handling of data, there's an alternative approach that simplifies data access using automated handling. By directly specifying the Address type in the User class, Serverpod can automatically handle the relation for you. ```yaml # address.spy.yaml class: Address table: address fields: street: String # user.spy.yaml class: User table: user fields: # The object relation field. address: Address?, relation indexes: user_address_unique_idx: fields: addressId unique: true ``` In this example, we define an object relation field by annotating the `address` field with the `relation` keyword where the type is another model, `Address?`. Serverpod then automatically generates a foreign key field (as seen in the last example) named `addressId` in the `User` class. This auto-generated field is non-nullable by default and is by default always named from the object relation field with the suffix `Id`. The object field, in this case `address`, must always be nullable (as indicated by `Address?`). An object relation field gives a big advantage when fetching data. Utilizing [relational queries](https://docs.serverpod.dev/concepts/data-and-the-database/database/relation-queries.md) enables filtering based on relation attributes or optionally including the related data in the result. No `parent` keyword is needed here because the relational table is inferred from the type on the field. ### Generated SQL For the [id field](#with-an-id-field) and [object](#with-an-object) examples above, Serverpod generates this SQL behind the scenes: ```sql CREATE TABLE "address" ( "id" bigserial PRIMARY KEY, "street" text NOT NULL ); CREATE TABLE "user" ( "id" bigserial PRIMARY KEY, "addressId" bigint NOT NULL ); CREATE UNIQUE INDEX "user_address_unique_idx" ON "user" USING btree ("addressId"); ALTER TABLE ONLY "user" ADD CONSTRAINT "user_fk_0" FOREIGN KEY("addressId") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; ``` The `ON DELETE` and `ON UPDATE` clauses come from the relation's referential actions, which default to `NO ACTION`. See [Referential actions](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/referential-actions.md) to change them. ### Optional relation To make the relation optional, add the `optional` keyword: ```yaml # user.spy.yaml class: User table: user fields: address: Address?, relation(optional) indexes: user_address_unique_idx: fields: addressId unique: true ``` With the introduction of the `optional` keyword in the relation, the automatically generated `addressId` field becomes nullable. This means that the `addressId` can either hold a foreign key to the related `address` table or be set to null, indicating no associated address. In the generated SQL, the `addressId` column loses its `NOT NULL`. ### Custom foreign key field The `field` parameter names the foreign key field used by an object relation. ```yaml # user.spy.yaml class: User table: user fields: address: Address?, relation(field=customIdField) indexes: user_address_unique_idx: fields: customIdField unique: true ``` In this example, `customIdField` holds the foreign key instead of the default auto-generated name `addressId`. The field does not have to be declared in `fields`. Serverpod generates it with the id type of the related model, just like it does for a relation without the `field` parameter, and the `optional` keyword makes it nullable: ```yaml fields: address: Address?, relation(optional, field=customIdField) ``` In the generated SQL, the column, the unique index, and the foreign key all use the custom name. Declare the field yourself when you need control over it, for example to give it a [column name override](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md#column-name-override) or a [scope](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#limiting-visibility-of-a-generated-class). A declared field must be nullable if the relation is `optional`: ```yaml # user.spy.yaml class: User table: user fields: customIdField: int?, column=fk_user_address_id address: Address?, relation(optional, field=customIdField) indexes: user_address_unique_idx: fields: customIdField unique: true ``` With the override, the generated column is named `fk_user_address_id`, and the unique index and the foreign key use that name. ## Independent relations defined on both sides You are able to define as many independent relations as you wish on each side of the relation. This is useful when you want to have multiple relations between two entities. ```yaml # user.spy.yaml class: User table: user fields: friendsAddress: Address?, relation indexes: user_address_unique_idx: fields: friendsAddressId unique: true # address.spy.yaml class: Address table: address fields: street: String resident: User?, relation indexes: address_user_unique_idx: fields: residentId unique: true ``` Both relations operate independently of each other, resulting in two distinct relationships with their respective unique indexes. ## Bidirectional relations If access to the same relation is desired from both sides, a bidirectional relation can be defined. ```yaml # user.spy.yaml class: User table: user fields: address: Address?, relation(name=user_address, field=addressId) indexes: user_address_unique_idx: fields: addressId unique: true # address.spy.yaml class: Address table: address fields: street: String user: User?, relation(name=user_address) ``` The example illustrates a 1:1 relationship between User and Address where both sides of the relationship are explicitly specified. Using the `name` parameter, we define a shared name for the relationship. It serves as the bridge connecting the `address` field in the User class to the `user` field in the Address class. Meaning that the same `User` referencing an `Address` is accessible from the `Address` as well. Without specifying the `name` parameter, you'd end up with two unrelated relationships. When the relationship is defined on both sides, you must mark which side holds the foreign key, because Serverpod cannot decide that for you. Either name the foreign key field with `field`, as above, or add the `fk` flag to keep the generated `addressId` name: ```yaml # user.spy.yaml class: User table: user fields: address: Address?, relation(name=user_address, fk) indexes: user_address_unique_idx: fields: addressId unique: true ``` Only one side of a relation can hold the key. As with any [custom foreign key field](#custom-foreign-key-field), `addressId` itself is generated and only needs to be declared if you want control over it. In a relationship where there is an object on both sides a unique index is always **required** on the foreign key field. --- # One-to-many https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-many One-to-many (1:n) relationships describe a scenario where multiple records from one table can relate to a single record in another table. An example would be the relationship between a company and its employees, where multiple employees can be employed at a single company. You can define the relation from the 'many' side (like `Employee`), from the 'one' side (like `Company`), or on both. Defining it from the 'one' side hides the foreign key from your class, which keeps the model minimal; defining it from the 'many' side gives you the foreign key as a field you can read and filter on directly. ## Defining the relationship In the following examples we show how to configure a 1:n relationship between `Company` and `Employee`. ### Implicit definition With an implicit setup, Serverpod determines and establishes the relationship based on the table and class structures. ```yaml # company.spy.yaml class: Company table: company fields: name: String employees: List?, relation # employee.spy.yaml class: Employee table: employee fields: name: String ``` In the example, we define a 1:n relation between `Company` and `Employee` by using the `List` type on the `employees` field together with the `relation` keyword. Serverpod adds the corresponding foreign key column to the 'many' side's table (e.g., `employee`), but it does not appear as a field on the class. When fetching companies, you can now include any or all employees in the query. 1:n relations also enable additional [filtering](https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering.md#one-to-many) and [sorting](https://docs.serverpod.dev/concepts/data-and-the-database/database/sorting.md#sort-on-relations) operations for [relational queries](https://docs.serverpod.dev/concepts/data-and-the-database/database/relation-queries.md). ### Explicit definition In an explicit definition, you directly specify the relationship in a one-to-many relation. This can be done through an [object relation](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one.md#with-an-object): ```yaml # company.spy.yaml class: Company table: company fields: name: String # employee.spy.yaml class: Employee table: employee fields: name: String company: Company?, relation ``` Or through a [foreign key field](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one.md#with-an-id-field): ```yaml # company.spy.yaml class: Company table: company fields: name: String # employee.spy.yaml class: Employee table: employee fields: name: String companyId: int, relation(parent=company) ``` The examples are 1:n relations because there is **no** unique index constraint on the foreign key field. This means that multiple employees can reference the same company. ## Bidirectional relations For a more comprehensive representation, you can define the relationship from both sides. Either through an [object relation](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one.md#with-an-object) on the many side: ```yaml # company.spy.yaml class: Company table: company fields: name: String employees: List?, relation(name=company_employees) # employee.spy.yaml class: Employee table: employee fields: name: String company: Company?, relation(name=company_employees) ``` Or through a [foreign key field](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one.md#with-an-id-field) on the many side: ```yaml # company.spy.yaml class: Company table: company fields: name: String employees: List?, relation(name=company_employees) # employee.spy.yaml class: Employee table: employee fields: name: String companyId: int, relation(name=company_employees, parent=company) ``` Just as in the 1:1 examples, the `name` parameter takes a unique string that links both sides together. --- # Many-to-many https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/many-to-many Many-to-many (n:m) relationships describe a scenario where multiple records from a table can relate to multiple records in another table. An example of this would be the relationship between students and courses, where a single student can enroll in multiple courses, and a single course can have multiple students. Serverpod models these relationships with a separate model you define, called a junction table, that records the relation: neither table references the other directly, and the junction table holds the foreign keys of both. ## When to use a junction table Reach for a many-to-many relation when records on both sides can each relate to many records on the other, like students and courses. If a record on one side belongs to only one record on the other, a [one-to-many](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-many.md) relation is simpler. A junction model also gives you a place to store data about the relationship itself. If the link between two records needs a date, a grade, or a status, those fields live on the junction model. A relationship that carries its own data always needs an explicit junction table, even when a plainer relation might otherwise do. ## Defining the relationship In the following examples we show how to configure an n:m relationship between `Student` and `Course`, with an `Enrollment` model as the junction table that stores the relationship explicitly. ### Many tables Both the `Course` and `Student` tables have a direct relationship with the `Enrollment` table but no direct relationship with each other. ```yaml # course.spy.yaml class: Course table: course fields: name: String enrollments: List?, relation(name=course_enrollments) ``` ```yaml # student.spy.yaml class: Student table: student fields: name: String enrollments: List?, relation(name=student_enrollments) ``` Note that the `name` argument is different, `course_enrollments` and `student_enrollments`, for the many tables. This is because each row in the junction table holds a relation to each of the two related tables, `Course` and `Student`. ### Junction table The `Enrollment` table acts as the bridge between `Course` and `Student`. It contains foreign keys from both tables, representing the many-to-many relationship. ```yaml # enrollment.spy.yaml class: Enrollment table: enrollment fields: student: Student?, relation(name=student_enrollments) course: Course?, relation(name=course_enrollments) indexes: enrollment_index_idx: fields: studentId, courseId unique: true ``` The unique index on the combination of `studentId` and `courseId` ensures that a student can only be enrolled in a particular course once. If omitted a student would be allowed to be enrolled in the same course multiple times. ## Managing junction records To link a student to a course, create an `Enrollment` row with both foreign keys set and insert it: ```dart var enrollment = Enrollment( studentId: student.id!, courseId: course.id!, ); await Enrollment.db.insertRow(session, enrollment); ``` The `studentId` and `courseId` fields are the foreign keys Serverpod generated from the two relations. To remove the link, delete the junction row: ```dart await Enrollment.db.deleteRow(session, enrollment); ``` Because `Student` and `Course` each hold a one-to-many relation to `Enrollment`, the generated `attach` and `detach` methods also operate on those relations. See [relation queries](https://docs.serverpod.dev/concepts/data-and-the-database/database/relation-queries.md#attach-and-detach-relations) for the attach and detach API. ## Common errors A few mistakes come up often when setting up a junction table: - **Referencing the generated foreign-key fields.** Defining `student: Student?, relation(...)` generates an implicit `studentId` column on the junction table. Use these generated `Id` names in indexes and filters, as the unique index above does. There is no separate field for you to declare. - **Relation names must match on both sides.** The `name` argument pairs the two halves of a relation, so `course_enrollments` on `Course.enrollments` has to match `course_enrollments` on `Enrollment.course`. A mismatch is reported as a missing named relation when you run code generation. - **Keep the relation fields persisted.** The relation fields on the junction are database columns. Marking one `!persist` drops the column, so the foreign key it backs can no longer be stored or queried. --- # Self-relations https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/self-relations A self-relation (a self-referential relation) occurs when a table has a foreign key that references its own primary key. This creates a relationship between different rows within the same table. ## One-to-one Imagine we have a blog and want to create links between our posts, where you can traverse forward and backward in the post history. Then we can create a self-referencing relation pointing to the next post in the chain. ```yaml class: Post table: post fields: content: String previous: Post?, relation(name=next_previous_post) next: Post?, relation(name=next_previous_post, optional, field=nextId, onDelete=SetNull) indexes: next_unique_idx: fields: nextId unique: true ``` In this example, there is a named relation holding the data on both sides of the relation. Serverpod generates the `nextId` field named by `field=`, which stores the id of the next post. The `optional` keyword makes it nullable, because the first post has nothing to point to. The next post represents the object on "this" side while the previous post is the corresponding object on the "other" side. The previous post is the one whose `nextId` points at this post. The `onDelete` behavior is covered in [Referential actions](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/referential-actions.md). ## One-to-many In a one-to-many self-referenced relation there is one object field connected to a list field. In this example we have modeled the relationship between a cat and her potential kittens. Each cat has at most one mother but can have many kittens. For brevity, we have only modeled the mother. ```yaml class: Cat table: cat fields: name: String mother: Cat?, relation(name=cat_kittens, optional, onDelete=SetNull) kittens: List?, relation(name=cat_kittens) ``` The field `motherId: int?` is injected into the Dart class, and it is nullable since we marked the `mother` field as an `optional` relation. A cat's kittens are the rows whose `motherId` equals this cat's `id`. A cat's mother is the row whose `id` equals this cat's `motherId`. ## Many-to-many Imagine a system where members can block other members, and you want to query both who a member is blocking and who is blocking them. This can be achieved by modeling the data as a many-to-many relationship. Each member has a list of all other members they are blocking and another list of all members that are blocking them. As with any [many-to-many](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/many-to-many.md) relation, a junction table holds the connection between the rows. ```yaml class: Member table: member fields: name: String blocking: List?, relation(name=member_blocked_by_me) blockedBy: List?, relation(name=member_blocking_me) ``` ```yaml class: Blocking table: blocking fields: blocked: Member?, relation(name=member_blocking_me, onDelete=Cascade) blockedBy: Member?, relation(name=member_blocked_by_me, onDelete=Cascade) indexes: blocking_blocked_unique_idx: fields: blockedId, blockedById unique: true ``` The junction table has an entry for who is blocking and another for who is getting blocked. Notice that the `blockedBy` field in the junction table is linked to the `blocking` field in the member table. We have also added a combined unique constraint on `blockedId` and `blockedById`, so a member can block another member only once. The cascade delete means that if a member is deleted all the blocking entries are also removed for that member. --- # Referential actions https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/referential-actions What happens to rows that reference a row you delete, or whose id changes? The `onUpdate` and `onDelete` properties on any relation answer that, and they map directly to the database's referential actions. In practice `onUpdate` rarely fires, since ids are stable. ## Available referential actions | Action | Description | | -------------- | --------------------------------------------------------------------------------------------------------- | | **NoAction** | If any constraint violation occurs, no action will be taken, and an error will be raised. | | **Restrict** | If any referencing rows still exist when the constraint is checked, an error is raised. | | **SetDefault** | The field will revert to its default value. Requires a default value on the field. | | **Cascade** | Any action taken on the parent (update/delete) will be mirrored in the child. | | **SetNull** | The field value is set to null. This action is permissible only if the field has been marked as optional. | ## Syntax Use the following syntax to apply referential actions: ```yaml relation(onUpdate=, onDelete=) ``` ## Default values If no referential actions are specified, both `onUpdate` and `onDelete` default to `NoAction`. This applies to [object relations](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one.md#with-an-object) and [id relations](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one.md#with-an-id-field) alike, so the two declarations below are equivalent to leaving the actions out: ```yaml parent: Model?, relation(onUpdate=NoAction, onDelete=NoAction) parentId: int?, relation(parent=model_table, onUpdate=NoAction, onDelete=NoAction) ``` Deleting a parent row that is still referenced therefore fails with a foreign key violation unless you set `onDelete=Cascade` or `onDelete=SetNull` explicitly. The order of `onUpdate` and `onDelete` in the relation does not matter. ### Full example ```yaml class: Example table: example fields: parentId: int?, relation(parent=example, onUpdate=SetNull, onDelete=NoAction) ``` In the given example, if the `example` parent is updated, the `parentId` will be set to null. If the parent is deleted, no action will be taken for parentId. --- # Deferrable constraints https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/deferrable-constraints The database checks a foreign key at the end of every statement. Insert a child row before its parent exists and that statement fails, even when the parent is inserted a line later in the same transaction. Marking the relation as deferrable moves the check to the end of the transaction, so the rows only need to line up by the time it commits. This is what makes circular references between two tables writable: with a per-statement check, neither row can be written first. ## Declare a deferrable relation Add `deferrable` or `deferred` to the relation on the side that holds the foreign key: ```yaml # employee.spy.yaml class: Employee table: employee fields: name: String companyId: int, relation(parent=company, deferrable) ``` | Keyword | Constraint | Checked | | ------------ | -------------------------------- | ------------------------------------------------------- | | `deferrable` | `DEFERRABLE INITIALLY IMMEDIATE` | After each statement, unless the transaction defers it. | | `deferred` | `DEFERRABLE INITIALLY DEFERRED` | At commit. | The two keywords are mutually exclusive. A relation declared with neither keyword cannot be deferred at runtime. Both work on [id relations](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one.md#with-an-id-field) and [object relations](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one.md#with-an-object). Like `onUpdate` and `onDelete`, they can only be set on the side holding the foreign key: ```yaml # employee.spy.yaml class: Employee table: employee fields: name: String company: Company?, relation(deferred) ``` Adding, changing, or removing the keyword changes the constraint in the database, so run `serverpod create-migration` afterwards. ## Defer the check in a transaction A `deferred` relation is checked at commit already, so ordinary transactions can write the rows in any order: ```dart await session.db.transaction((transaction) async { await Employee.db.insertRow(session, employee, transaction: transaction); await Company.db.insertRow(session, company, transaction: transaction); }); ``` A `deferrable` relation keeps the per-statement check until a transaction asks for it to be deferred. Set `deferConstraints` in the [transaction settings](https://docs.serverpod.dev/concepts/data-and-the-database/database/transactions.md) to defer every deferrable constraint for the duration of the transaction: ```dart await session.db.transaction( (transaction) async { await Employee.db.insertRow(session, employee, transaction: transaction); await Company.db.insertRow(session, company, transaction: transaction); }, settings: TransactionSettings(deferConstraints: true), ); ``` Either way the constraint is still enforced. If the parent row is missing when the transaction commits, the commit fails with a `DatabaseQueryException` and the whole transaction is rolled back. :::note On SQLite the `deferConstraints` setting defers every foreign key check in the transaction, whether or not the relation is declared deferrable. On PostgreSQL only constraints declared with `deferrable` or `deferred` can be deferred. ::: ## Related - [Referential actions](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/referential-actions.md): what happens to related rows when the row they point to is deleted or its id changes. - [Transactions](https://docs.serverpod.dev/concepts/data-and-the-database/database/transactions.md): isolation levels, savepoints, and rollback. --- # Relations with modules https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/modules Serverpod [modules](https://docs.serverpod.dev/concepts/server-fundamentals/modules.md) usually come with predefined tables and data structures. Sometimes it can be useful to extend them with your data structures by creating a relation to the module tables. Relations to modules come with a restriction: you do not own the module's table definition, so you cannot change its structure. Since you do not directly control the models inside the modules, it is recommended to create a so-called "bridge" table linking the module's model to your own. This is set up the same way as a normal [one-to-one](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one.md) relation. For example, to attach your own profile data to the signed-in user from the [authentication module](https://docs.serverpod.dev/concepts/authentication/basics.md): ```yaml class: UserProfile table: user_profile fields: authUser: module:serverpod_auth_core:AuthUser?, relation(onDelete=Cascade) age: int indexes: user_profile_auth_user_idx: fields: authUserId unique: true ``` Or by referencing the table name if you only want to access the id: ```yaml class: UserProfile table: user_profile fields: authUserId: UuidValue, relation(parent=serverpod_auth_core_user) age: int indexes: user_profile_auth_user_idx: fields: authUserId unique: true ``` Note that the foreign key's type must match the module table's id type, and `AuthUser` uses a `UuidValue` id. If you assign the module a nickname in your `generator.yaml`, use that nickname in the reference instead. See [modules](https://docs.serverpod.dev/concepts/server-fundamentals/modules.md). It is now possible to make any other relation to your model as described in [one-to-one](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one.md), [one-to-many](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-many.md), [many-to-many](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/many-to-many.md), and [self-relations](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/self-relations.md). ## Advanced example A one-to-many relation combined with the bridge table could look like this: ```yaml class: UserProfile table: user_profile fields: authUser: module:serverpod_auth_core:AuthUser?, relation(onDelete=Cascade) age: int company: Company?, relation(name=company_employee) indexes: user_profile_auth_user_idx: fields: authUserId unique: true company_unique_idx: fields: companyId unique: true ``` ```yaml class: Company table: company fields: name: String employees: List?, relation(name=company_employee) ``` For the full walkthrough of relating your data to signed-in users, see [Working with users](https://docs.serverpod.dev/concepts/authentication/working-with-users.md). --- # Relation queries (Joins) https://docs.serverpod.dev/concepts/data-and-the-database/database/relation-queries The Serverpod query framework supports filtering on, sorting on, and including relational data structures. In SQL this is often achieved using a join operation. The functionality is available if there exists any [one-to-one](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one.md) or [one-to-many](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-many.md) object relations between two models. ## Include relational data To include relational data in a query, use the `include` method. The `include` method has a typed interface and contains all the declared relations in your yaml file. ```dart var employee = await Employee.db.findById( session, employeeId, include: Employee.include( address: Address.include(), ), ); ``` The example above returns an employee including the related address object. ### Nested includes It is also possible to include deeply nested objects. ```dart var employee = await Employee.db.findById( session, employeeId, include: Employee.include( company: Company.include( address: Address.include(), ), ), ); ``` The example above returns an employee including the related company object that has the related address object included. Any relational object can be included or not when making a query but only the includes that are explicitly defined will be included in the result. ```dart var employee = await Employee.db.findById( session, employeeId, include: Employee.include( address: Address.include(), company: Company.include( address: Address.include(), ), ), ); ``` The example above includes several different objects configured by specifying the named parameters. ## Include relational lists Including a list of objects (1:n relation) can be done with the special `includeList` method. In the simplest case, the entire list is included. ```dart var company = await Company.db.findById( session, companyId, include: Company.include( employees: Employee.includeList(), ), ); ``` The example above returns a company with all related employees included. ### Nested includes The `includeList` method takes its nested includes through an `include` parameter, the same name used on `findById`. When including something on a list it means that every entry in the list will each have access to the nested object. ```dart var company = await Company.db.findById( session, companyId, include: Company.include( employees: Employee.includeList( include: Employee.include( address: Address.include(), ), ), ), ); ``` The example above returns a company with all related employees included. Each employee will have the related address object included. It is even possible to include lists within lists. ```dart var company = await Company.db.findById( session, companyId, include: Company.include( employees: Employee.includeList( include: Employee.include( tools: Tool.includeList(), ), ), ), ); ``` The example above returns a company with all related employees included. Each employee will have the related tools list included. :::note For each call to includeList (nested or not) the Serverpod Framework will perform one additional query to the database. ::: ### Filter and sort When working with large datasets, it's often necessary to [filter](https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering.md) and [sort](https://docs.serverpod.dev/concepts/data-and-the-database/database/sorting.md) the records to retrieve the most relevant data. Serverpod offers methods to refine the included list of related objects: #### Filter Use the `where` clause to filter the results based on certain conditions. ```dart var company = await Company.db.findById( session, companyId, include: Company.include( employees: Employee.includeList( where: (t) => t.name.ilike('a%') ), ), ); ``` The example above retrieves only employees whose names start with the letter 'a'. #### Sort The orderBy clause lets you sort the results based on a specific field. ```dart var company = await Company.db.findById( session, companyId, include: Company.include( employees: Employee.includeList( orderBy: (t) => t.name, ), ), ); ``` The example above sorts the employees by their names in ascending order. ### Pagination [Paginate](https://docs.serverpod.dev/concepts/data-and-the-database/database/pagination.md) results by specifying a limit on the number of records and an offset. ```dart var company = await Company.db.findById( session, companyId, include: Company.include( employees: Employee.includeList( limit: 10, offset: 10, ), ), ); ``` The example above retrieves the next 10 employees starting from the 11th record: Combined, these methods let you query, filter, and sort relational data efficiently. ## Attach and detach relations Managing relationships between tables is a common task. Serverpod provides methods to link (attach) and unlink (detach) related records: ### Attach single row Link an individual employee to a company. This operation associates an employee with a specific company: ```dart var company = await Company.db.findById(session, companyId); var employee = await Employee.db.findById(session, employeeId); await Company.db.attachRow.employees(session, company!, employee!); ``` ### Bulk attach rows For scenarios where you need to associate multiple employees with a company at once, use the bulk attach method. This operation is atomic, ensuring all or none of the records are linked: ```dart var company = await Company.db.findById(session, companyId); var employee = await Employee.db.findById(session, employeeId); await Company.db.attach.employees(session, company!, [employee!]); ``` ### Detach single row To remove the association between an employee and a company, use the detach row method: ```dart var employee = await Employee.db.findById(session, employeeId); await Company.db.detachRow.employees(session, employee!); ``` ### Bulk detach rows In cases where you need to remove associations for multiple employees simultaneously, use the bulk detach method. This operation is atomic: ```dart var employee = await Employee.db.findById(session, employeeId); await Company.db.detach.employees(session, [employee!]); ``` :::note When using the attach and detach methods the objects passed to them have to have the `id` field set. The detach method is also required to have the related nested object set if you make the call from the side that does not hold the foreign key. ::: --- # Vector & geography fields https://docs.serverpod.dev/concepts/data-and-the-database/database/vector-and-geography-fields Serverpod supports two families of specialized field types backed by PostgreSQL extensions: vector fields for high-dimensional embeddings (via pgvector) and geography fields for geospatial data (via PostGIS). Column types, JSON encodings, and SQLite storage are listed in [Field types](https://docs.serverpod.dev/concepts/data-and-the-database/models/field-types.md). ## Vector fields Vector types are used for storing high-dimensional vectors, which are especially useful for similarity search operations. When specifying vector types, the dimension is required between parentheses (for example, `Vector(1536)`). Common dimensions include: - 1536 (OpenAI embeddings) - 768 (many sentence transformers) - 384 (smaller models) All vector types support specialized distance operations for similarity search and filtering. See the [Vector distance operators](https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering.md#vector-distance-operators) section for details. To ensure optimal performance with vector similarity searches, consider creating specialized vector indexes on your vector fields. See the [Vector indexes](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md#vector-indexes) section for more details. :::info Vector fields require a PostgreSQL database with the `pgvector` extension installed. New projects have it. The embedded PostgreSQL bundles pgvector 0.8.3, and the `ghcr.io/serverpod/postgres:16` Docker image ships with it as well. SQLite does not support vector queries. To add the extension to an older Docker project or an external Postgres, see the [Upgrade to pgvector](https://docs.serverpod.dev/upgrading/archive/upgrade-to-pgvector.md) guide. ::: ### Vector The `Vector` type stores full-precision floating-point vectors for general-purpose embeddings. ```yaml class: Document table: document fields: ### The category of the document (e.g., article, tutorial). category: String ### The contents of the document. content: String ### A vector field for storing document embeddings embedding: Vector(1536) ``` ### HalfVector The `HalfVector` type uses half-precision (16-bit) floating-point numbers, providing memory savings with acceptable precision loss for several applications. ```yaml class: Document table: document fields: content: String ### Half-precision embedding for memory efficiency embedding: HalfVector(1536) ``` ### SparseVector The `SparseVector` type efficiently stores sparse vectors where most values are zero, which is ideal for high-dimensional data with few non-zero elements. ```yaml class: Document table: document fields: content: String ### Sparse vector for keyword-based embeddings keywords: SparseVector(10000) ``` ### Bit The `Bit` type stores binary vectors where each element is 0 or 1, offering maximum memory efficiency for binary embeddings. ```yaml class: Document table: document fields: content: String ### Binary vector for semantic hashing hash: Bit(256) ``` ### Reading vector values in Dart All four vector types implement `Iterable` and support zero-based indexed access with `[]`. Values read from the database can therefore be inspected directly, without converting them to a list first. ```dart var embedding = Vector([0.2, 0.4, 0.6]); print(embedding.length); // 3 print(embedding[1]); // 0.4 for (var value in embedding) { print(value); } var hasLargeValue = embedding.any((value) => value > 0.5); var values = embedding.toList(); ``` The `Vector`, `HalfVector`, and `SparseVector` types iterate over `double` values, while `Bit` iterates over `bool` values. ```dart var flags = Bit([true, false, true]); print(flags[1]); // false print(flags.where((value) => value).length); // 2 ``` A `SparseVector` iterates over every dimension, including the zero dimensions it does not store internally. Indexed access and `toList()` return `0.0` for those dimensions. ```dart var sparse = SparseVector([1.5, 0.0, 0.0, 2.5]); print(sparse.length); // 4 print(sparse[1]); // 0.0 print(sparse.toList()); // [1.5, 0.0, 0.0, 2.5] ``` ## Geography fields Geography types are used for storing geospatial data on the surface of the Earth. They are stored as PostGIS geography columns in PostgreSQL using the WGS 84 coordinate system (SRID 4326), which is the standard used by GPS. The SRID is fixed to `4326`, available in Dart as `Geography.defaultSrid`; configuring a different SRID per column is not yet supported. Like other fields, geography fields can be made optional by appending `?` to the type, for example `location: GeographyPoint?`. All geography types support spatial filter operations such as proximity search, intersection, containment, and distance-based ordering. See the [Geography operators](https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering.md#geography-operators) section for details. To ensure optimal performance with spatial queries, consider creating a spatial index on your geography fields. See the [Geography indexes](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md#geography-indexes) section for more details. :::info Geography fields require the PostGIS PostgreSQL extension. New projects have it. The embedded PostgreSQL bundles PostGIS 3.5.4, and the `ghcr.io/serverpod/postgres:16` Docker image ships with it as well. To set it up on an older Docker project or an external Postgres, see the [Upgrading to PostGIS support](https://docs.serverpod.dev/upgrading/upgrade-to-postgis.md) guide. ::: :::warning Spatial operations are only supported on PostgreSQL. On SQLite, geography values are stored as text in EWKT format and round-trip through CRUD operations, but spatial query operations are not supported and throw at query time. ::: ### GeographyPoint The `GeographyPoint` type stores a single geographic location defined by longitude and latitude. ```yaml class: Store table: store fields: name: String location: GeographyPoint address: String? ``` ### GeographyLineString The `GeographyLineString` type stores an ordered sequence of points forming a path or route. ```yaml class: DeliveryRoute table: delivery_route fields: name: String path: GeographyLineString description: String? ``` ### GeographyPolygon The `GeographyPolygon` type stores a closed region defined by an exterior ring and optional interior holes. ```yaml class: DeliveryZone table: delivery_zone fields: name: String boundary: GeographyPolygon description: String? ``` ### GeographyGeometryCollection The `GeographyGeometryCollection` type stores a collection of mixed geography types (points, lines, and polygons) as a single field. ```yaml class: Region table: region fields: name: String features: GeographyGeometryCollection ``` --- # Indexing https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing Queries that [filter](https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering.md) or [sort](https://docs.serverpod.dev/concepts/data-and-the-database/database/sorting.md) a large table slow down as rows accumulate, because the database has to scan every row. An index lets the database look up the matching rows directly instead. You declare indexes in the model file, and the next [migration](https://docs.serverpod.dev/concepts/data-and-the-database/database/migrations.md) creates them in the database. ## Add an index To add an index, add an `indexes` section to the model file. The `indexes` section is a map where the key is the name of the index and the value is a map with the index details. ```yaml class: Company table: company fields: name: String indexes: company_name_idx: fields: name ``` The `fields` keyword holds a comma-separated list of column names. These are the fields upon which the index is created. Note that the index can contain several fields. ```yaml class: Company table: company fields: name: String foundedAt: DateTime indexes: company_idx: fields: name, foundedAt ``` ## Make fields unique A unique index ensures that no two rows store the same value in the indexed fields. This is useful for example if you want to make sure that no two companies have the same name. For a single field, mark it with the `unique` keyword directly on the field definition: ```yaml class: Company table: company fields: name: String, unique ``` To make a combination of fields unique, or to name the index yourself, declare the index in the `indexes` section and set `unique: true`: ```yaml class: Company table: company fields: name: String indexes: company_name_idx: fields: name unique: true ``` The `unique` keyword on an index is a bool that defaults to false. When it is applied to a multi-column index, the combination of the fields must be unique. ### Composite unique constraints When a value should be unique only within a scope (for example, a setting key that is unique per user), use `unique(per=...)` on the field that must be unique within that scope. Serverpod auto-generates a composite unique index with the `per` columns first, followed by the annotated field. ```yaml class: UserSetting table: user_setting fields: userId: int key: String, unique(per=userId) ``` In this example, two rows can share the same `key` if they belong to different users, but the same user cannot have two rows with the same `key`. For a scope that spans multiple columns, pass a list of field names: ```yaml class: Product table: product fields: tenantId: int category: String sku: String, unique(per=[tenantId, category]) ``` You can also use the equivalent expanded form, where you name the index yourself: ```yaml class: Product table: product fields: tenantId: int category: String sku: String indexes: product_unique_idx: fields: tenantId, category, sku unique: true ``` ### Null values in unique indexes A unique index treats null values as distinct, so any number of rows can hold null in an indexed field without colliding. Set `nulls_distinct: false` to make them collide instead, which is what turns a nullable column into a "one row per key" constraint: ```yaml class: Product table: product fields: tenantId: int sku: String deletedAt: DateTime? indexes: product_active_sku_idx: fields: tenantId, sku, deletedAt unique: true nulls_distinct: false ``` In this example, a tenant can have at most one product with a given `sku` and no `deletedAt`. Deleted rows carry a timestamp, so they no longer collide with each other. The key is only allowed on unique indexes, and leaving it out keeps the database default. SQLite cannot express `nulls_distinct: false`, so creating a SQLite migration for a model that sets it fails. Setting it to `true` is accepted, since that matches what SQLite already does. ## Specify an index type Add a `type` key to specify the index type. ```yaml class: Company table: company fields: name: String indexes: company_name_idx: fields: name type: brin ``` If no type is specified, the default is `btree`. All [Postgres index types](https://www.postgresql.org/docs/current/indexes-types.html) are supported: `btree`, `hash`, `gist`, `spgist`, `gin`, and `brin`. :::info Index types other than `btree` are Postgres-only, including the GIN, vector, and geography indexes described below. On SQLite, indexes declared with other types are skipped when a migration is created, and a warning is logged. ::: ## GIN indexes GIN (Generalized Inverted Index) indexes are designed for efficiently querying composite values such as JSONB data. When all fields in an index are stored as `jsonb`, Serverpod automatically defaults the index type to `gin`: ```yaml class: Product table: product fields: tags: List, serializationDataType=jsonb indexes: product_tags_idx: fields: tags # type defaults to gin since all indexed fields are jsonb ``` You can also set the type explicitly: ```yaml indexes: product_tags_idx: fields: tags type: gin ``` ### Operator classes GIN indexes support different operator classes that control which query operators the index can accelerate. Use the `operatorClass` keyword to specify one: ```yaml indexes: product_tags_idx: fields: tags type: gin operatorClass: jsonbPathOps ``` | Operator Class | Description | Use Case | | -------------- | -------------------------------------------------- | --------------------------------------------------------------- | | `jsonbOps` | Default. Supports `@>`, `?`, `?\|`, `?&` operators | General-purpose JSONB querying | | `jsonbPathOps` | Supports only `@>` (containment) | Faster and smaller index when you only need containment queries | | `arrayOps` | For array containment queries | Array-typed columns | | `tsvectorOps` | For full-text search | Text search with `tsvector` columns | :::tip If you only need containment queries (`@>`), use `jsonbPathOps`: it produces a smaller and faster index than the default `jsonbOps`. ::: For details on configuring JSONB storage on your model fields, see [Storing serializable fields as JSONB](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md#storing-serializable-fields-as-jsonb). ## Vector indexes Vector similarity searches benefit from specialized indexes on vector fields (`Vector`, `HalfVector`, `SparseVector`, `Bit`). Serverpod supports the `hnsw` and `ivfflat` index types. :::info Each vector index can only be created on a single vector field. It is not possible to create a vector index on multiple fields of any kind. ::: ### HNSW indexes Hierarchical Navigable Small World (HNSW) indexes provide fast approximate nearest neighbor search: ```yaml class: Document table: document fields: content: String embedding: Vector(1536) keywords: SparseVector(10000) hash: Bit(256) indexes: document_embedding_hnsw_idx: fields: embedding type: hnsw distanceFunction: cosine parameters: m: 16 ef_construction: 64 document_keywords_idx: fields: keywords type: hnsw distanceFunction: innerProduct parameters: m: 16 ef_construction: 64 document_hash_idx: fields: hash type: hnsw distanceFunction: hamming parameters: m: 16 ef_construction: 64 ``` Available HNSW parameters: - `m`: Maximum number of bidirectional links for each node (default: 16) - `ef_construction`: Size of the dynamic candidate list (default: 64) Serverpod validates that `ef_construction` is at least `2 * m`, and rejects the model file otherwise. ### IVFFLAT indexes Inverted File with Flat compression (IVFFLAT) indexes are suitable for large datasets: ```yaml class: Document table: document fields: content: String embedding: Vector(1536) indexes: document_embedding_ivfflat_idx: fields: embedding type: ivfflat distanceFunction: innerProduct parameters: lists: 100 ``` Available IVFFLAT parameters: - `lists`: Number of inverted lists (default: 100) ### Distance functions Supported distance functions for vector indexes (`distanceFunction` parameter): | Distance Function | Description | Use Case | | ----------------- | ----------------------------- | ---------------------------- | | `l2` | Euclidean distance | Default for most embeddings | | `innerProduct` | Inner product | When vectors are normalized | | `cosine` | Cosine distance | Text embeddings | | `l1` | Manhattan or taxicab distance | Sparse/high-dimensional data | | `hamming` | Hamming distance | Binary vectors (Bit type) | | `jaccard` | Jaccard distance | Binary vectors (Bit type) | Different vector types have specific limitations when creating indexes: - **SparseVector**: Can only use HNSW indexes (IVFFLAT is not supported). - **HalfVector**: When using IVFFLAT indexes, the L1 distance function is not supported. - **Bit**: Only supports `hamming` (default) and `jaccard` distance functions. :::tip If more than one distance function is going to be frequently used on the same vector field, consider creating one index for each distance function to ensure optimal performance. ::: For more details on vector indexes and their configuration, refer to the [pgvector extension documentation](https://github.com/pgvector/pgvector/tree/master?tab=readme-ov-file#indexing). ## Geography indexes Geography columns benefit from spatial indexes, which significantly improve the performance of spatial queries such as proximity searches, intersection tests, and containment checks. Two index types are available for geography fields: - `gist` - Generalized Search Tree, the default and the right choice for most workloads. - `spgist` - Space-Partitioned Generalized Search Tree. If no `type` is specified for an index on a geography field, it defaults to `gist`. ```yaml class: Store table: store fields: name: String location: GeographyPoint indexes: store_location_idx: fields: location type: gist ``` Use `spgist` by setting the index `type` explicitly: ```yaml class: DeliveryZone table: delivery_zone fields: name: String boundary: GeographyPolygon indexes: delivery_zone_boundary_idx: fields: boundary type: spgist ``` :::tip A spatial index accelerates all spatial operations (`intersects`, `distanceWithin`, `distance`, `contains`, `within`). For tables with many rows and frequent spatial queries, adding one is strongly recommended. ::: Two restrictions apply to geography indexes: - Geography fields only support the `gist` and `spgist` index types. Specifying any other type fails code generation with `The "type" property must be one of: gist, spgist.` - An index may cover several geography columns, but it cannot mix geography and non-geography fields. Doing so fails with `Mixing geography and non-geography fields in the same index is not allowed.` :::info Indexes of type `spgist` on the geography type require a recent version of PostGIS. If your PostgreSQL instance ships an older PostGIS, use `gist` instead. ::: --- # Transactions https://docs.serverpod.dev/concepts/data-and-the-database/database/transactions A transaction bundles several database operations into one unit that either fully succeeds or fully fails. Other concurrent operations never see the intermediate states. If any step fails, none of the steps affect the database. Serverpod handles database transactions through the `session.db.transaction` method. The method takes a callback function that receives a transaction object. The transaction is committed when the callback function returns, and rolled back if an exception is thrown. Any return value of the callback function is returned by the `transaction` method. Pass the transaction object to each database operation method to include them in the same atomic operation: ```dart var result = await session.db.transaction((transaction) async { // Do some database queries here. await Company.db.insertRow(session, company, transaction: transaction); await Employee.db.insertRow(session, employee, transaction: transaction); // Optionally return a value. return true; }); ``` In the example we insert a company and an employee in the same transaction. If any of the operations fail, the entire transaction will be rolled back and no changes will be made to the database. If the transaction is successful, the return value will be `true`. :::tip The Serverpod test tools use this rollback behavior to isolate test cases: by default, each test runs inside a transaction that is rolled back when the test ends. See [rollback configuration](https://docs.serverpod.dev/concepts/testing/configuration.md#rollbackdatabase) in the testing docs. ::: ## Cancel a transaction To roll back a transaction deliberately, without throwing an exception, call `cancel` on the transaction object: ```dart var result = await session.db.transaction((transaction) async { await Company.db.insertRow(session, company, transaction: transaction); if (companyLimitReached) { await transaction.cancel(); return false; } return true; }); ``` Cancelling rolls back all of the transaction's changes, while the `transaction` method still returns the callback's return value. Database calls made after the cancel have no effect and might throw, depending on the database driver. ## Transaction isolation The transaction isolation level can be configured when initiating a transaction. The isolation level determines how the transaction interacts with concurrent database operations. If no isolation level is supplied, the level is determined by the database engine. :::info The default isolation level for the Postgres database engine is `IsolationLevel.readCommitted`. ::: :::info Setting an isolation level is only supported on Postgres. On SQLite, the `isolationLevel` setting is ignored, and transactions always run serialized. ::: To set the isolation level, configure the `isolationLevel` property of the `TransactionSettings` object: ```dart await session.db.transaction( (transaction) async { await Company.db.insertRow(session, company, transaction: transaction); await Employee.db.insertRow(session, employee, transaction: transaction); }, settings: TransactionSettings(isolationLevel: IsolationLevel.serializable), ); ``` In the example the isolation level is set to `IsolationLevel.serializable`. The available isolation levels are: | Isolation Level | Constant | Description | | ---------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Read uncommitted | `IsolationLevel.readUncommitted` | Exhibits the same behavior as `IsolationLevel.readCommitted` in Postgres | | Read committed | `IsolationLevel.readCommitted` | Each statement in the transaction sees a snapshot of the database as of the beginning of that statement. | | Repeatable read | `IsolationLevel.repeatableRead` | The transaction only observes rows committed before the first statement in the transaction was executed giving a consistent view of the database. If any conflicting writes among concurrent transactions occur, an exception is thrown. | | Serializable | `IsolationLevel.serializable` | Gives the same guarantees as `IsolationLevel.repeatableRead` but also throws if read rows are updated by other transactions. | For a detailed explanation of the different isolation levels, see the [PostgreSQL documentation](https://www.postgresql.org/docs/current/transaction-iso.html). ## Deferred constraint checking Foreign keys are checked at the end of every statement, so a transaction that inserts a child row before its parent fails on the child insert. Mark the relation as [deferrable](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/deferrable-constraints.md) and set `deferConstraints` to postpone the check until the transaction commits. Rows can then be written in any order, as long as every foreign key is satisfied at commit. ```dart await session.db.transaction( (transaction) async { // The child Employee row is inserted before the parent Company row. await Employee.db.insertRow(session, employee, transaction: transaction); await Company.db.insertRow(session, company, transaction: transaction); }, settings: TransactionSettings(deferConstraints: true), ); ``` Relations declared with `deferred` are already checked at commit and need no setting. ## Transaction failure exceptions When the database rejects a query inside the transaction, Serverpod throws a `DatabaseQueryException`. This can happen, for example, when concurrent writes conflict with the selected transaction isolation level, or when Postgres detects a deadlock. The exact database error code depends on why Postgres rejected the query. Serverpod exposes Postgres error code constants through `PgErrorCode`, so you can compare them with the `code` field on `DatabaseQueryException`. ```dart try { await session.db.transaction( (transaction) async { await Company.db.updateRow( session, company, transaction: transaction, ); }, settings: TransactionSettings(isolationLevel: IsolationLevel.serializable), ); } on DatabaseQueryException catch (e) { if (e.code == PgErrorCode.serializationFailure || e.code == PgErrorCode.deadlockDetected) { // Retry the transaction or report a write conflict to the caller. return; } rethrow; } ``` For all PostgreSQL error codes, see the [PostgreSQL error code appendix](https://www.postgresql.org/docs/current/errcodes-appendix.html). ## Savepoints A savepoint is a special mark inside a transaction that allows all commands that are executed after it was established to be rolled back, restoring the transaction state to what it was at the time of the savepoint. Read more about savepoints in the [PostgreSQL documentation](https://www.postgresql.org/docs/current/sql-savepoint.html). ### Creating savepoints To create a savepoint, call the `createSavepoint` method on the transaction object: ```dart await session.db.transaction((transaction) async { await Company.db.insertRow(session, company, transaction: transaction); // Create savepoint var savepoint = await transaction.createSavepoint(); await Employee.db.insertRow(session, employee, transaction: transaction); }); ``` In the example, we create a savepoint after inserting a company but before inserting the employee. This gives us the option to roll back to the savepoint and preserve the company insertion. #### Rolling back to savepoints Once a savepoint is created, you can roll back to it by calling the `rollback` method on the savepoint object: ```dart await session.db.transaction((transaction) async { // Changes preserved in the database await Company.db.insertRow(session, company, transaction: transaction); // Create savepoint var savepoint = await transaction.createSavepoint(); await Employee.db.insertRow(session, employee, transaction: transaction); // Changes rolled back await savepoint.rollback(); }); ``` In the example, we create a savepoint after inserting a company. We then insert an employee but invoke a rollback to our savepoint. This results in the database preserving the company but not the employee insertion. #### Releasing savepoints Savepoints can also be released, which means that the changes made after the savepoint are preserved in the transaction. Releasing a savepoint will also render any subsequent savepoints invalid. To release a savepoint, call the `release` method on the savepoint object: ```dart await session.db.transaction((transaction) async { // Create two savepoints var savepoint = await transaction.createSavepoint(); var secondSavepoint = await transaction.createSavepoint(); await Company.db.insertRow(session, company, transaction: transaction); await savepoint.release(); }); ``` In the example, two savepoints are created. After the company is inserted the first savepoint is released, which renders the second savepoint invalid. If the second savepoint is used to rollback, an exception will be thrown. --- # Row locking https://docs.serverpod.dev/concepts/data-and-the-database/database/row-locking Row-level locking allows you to lock specific rows in the database to prevent other transactions from modifying them while you work. This is essential for safely handling concurrent updates, such as processing payments, managing inventory, or any scenario where two transactions might conflict. :::warning All row locking operations require a [transaction](https://docs.serverpod.dev/concepts/data-and-the-database/database/transactions.md). An exception will be thrown if you attempt to acquire a lock without one. ::: For the following examples we will use this model: ```yaml class: Company table: company fields: name: String ``` ## Locking rows with a read You can lock rows as part of a read operation by passing the `lockMode` parameter to `find`, `findFirstRow`, or `findById`. The locked rows are returned and held until the transaction completes. ```dart await session.db.transaction((transaction) async { var companies = await Company.db.find( session, where: (t) => t.name.equals('Serverpod'), lockMode: LockMode.forUpdate, transaction: transaction, ); // Rows are locked. It is safe to update without conflicts. for (var company in companies) { company.name = 'Updated name'; await Company.db.updateRow(session, company, transaction: transaction); } }); ``` When a row is locked, other transactions that attempt to acquire a conflicting lock on the same rows will wait until the lock is released. Regular reads without a `lockMode` are not affected and can still read the rows freely. If waiting is not desired, you can configure the [lock behavior](#lock-behavior) to either throw an exception immediately or skip locked rows. The `findFirstRow` and `findById` methods also support locking. Here's an example using `findById`: ```dart await session.db.transaction((transaction) async { var company = await Company.db.findById( session, companyId, lockMode: LockMode.forUpdate, transaction: transaction, ); if (company != null) { company.name = 'Updated name'; await Company.db.updateRow(session, company, transaction: transaction); } }); ``` ## Locking rows without fetching data If you only need to lock rows without reading their data, use the `lockRows` method. This acquires locks with less overhead since no row data is transferred. ```dart await session.db.transaction((transaction) async { await Company.db.lockRows( session, where: (t) => t.name.equals('Serverpod'), lockMode: LockMode.forUpdate, transaction: transaction, ); // Rows are locked. Perform updates using other methods. }); ``` ## Lock modes The `lockMode` parameter determines the type of lock acquired. Different lock modes allow varying levels of concurrent access. | Lock Mode | Constant | Description | | ----------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | For update | `LockMode.forUpdate` | Exclusive lock that blocks all other locks. Use when you intend to update or delete the locked rows. | | For no key update | `LockMode.forNoKeyUpdate` | Exclusive lock that allows `forKeyShare` locks. Use when updating non-key columns only. | | For share | `LockMode.forShare` | Shared lock that blocks exclusive locks but allows other shared locks. Use when you need to ensure rows don't change while reading. | | For key share | `LockMode.forKeyShare` | Weakest lock that only blocks changes to key columns. | For a detailed explanation of how lock modes interact, see the [PostgreSQL documentation](https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-ROWS). On SQLite, row locking does nothing, since SQLite only supports one write transaction at a time. ## Lock behavior The `lockBehavior` parameter controls what happens when a requested row is already locked by another transaction. If not specified, the default behavior is to wait. | Behavior | Constant | Description | | ----------- | ------------------------- | ---------------------------------------------------------------------- | | Wait | `LockBehavior.wait` | Wait until the lock becomes available. This is the default. | | No wait | `LockBehavior.noWait` | Throw an exception immediately if any row is already locked. | | Skip locked | `LockBehavior.skipLocked` | Skip rows that are currently locked and return only the unlocked rows. | ```dart await session.db.transaction((transaction) async { var companies = await Company.db.find( session, where: (t) => t.id < 100, lockMode: LockMode.forUpdate, lockBehavior: LockBehavior.skipLocked, transaction: transaction, ); // Only unlocked rows are returned. }); ``` :::info `LockBehavior.skipLocked` is particularly useful for implementing job queues or work distribution, where multiple workers can each grab unlocked rows without waiting on each other. ::: --- # Raw access https://docs.serverpod.dev/concepts/data-and-the-database/database/raw-access Serverpod provides methods to execute raw SQL queries directly on the database for advanced scenarios. They are available on `session.db`. ## `unsafeQuery` Executes a single SQL query and returns a `DatabaseResult` containing the results. This method uses the extended query protocol, allowing for parameter binding to prevent SQL injection. ```dart DatabaseResult result = await session.db.unsafeQuery( r'SELECT * FROM mytable WHERE id = @id', parameters: QueryParameters.named({'id': 1}), ); ``` ## `unsafeExecute` Executes a single SQL query without returning any results. Use this for statements that modify data, such as `INSERT`, `UPDATE`, or `DELETE`. Returns the number of rows affected. ```dart int result = await session.db.unsafeExecute( r'DELETE FROM mytable WHERE id = @id', parameters: QueryParameters.named({'id': 1}), ); ``` ## `unsafeSimpleQuery` Similar to `unsafeQuery`, but uses the simple query protocol. This protocol does not support parameter binding, making it more susceptible to SQL injection. **Use with extreme caution and only when absolutely necessary.** Simple query mode is suitable for: - Queries containing multiple statements. - Situations where the extended query protocol is not available (e.g., replication mode or with proxies like PGBouncer). ```dart DatabaseResult result = await session.db.unsafeSimpleQuery( r'SELECT * FROM mytable WHERE id = 1; SELECT * FROM othertable;', ); ``` ## `unsafeSimpleExecute` Similar to `unsafeExecute`, but uses the simple query protocol. It does not return any results. **Use with extreme caution and only when absolutely necessary.** Simple query mode is suitable for the same scenarios as `unsafeSimpleQuery`. ```dart int result = await session.db.unsafeSimpleExecute( r'DELETE FROM mytable WHERE id = 1; DELETE FROM othertable;', ); ``` :::info Raw SQL is passed to the database as-is, so write your statements in the dialect of the [backend](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md#database-backends) your server runs against, PostgreSQL or SQLite. SQLite has no separate simple query protocol, and there the simple methods behave like their regular counterparts without parameter binding. ::: ## Query parameters To protect against SQL injection attacks, always use query parameters when passing values into raw SQL queries. The library provides two types of query parameters: - **Named parameters:** Use `@` to denote named parameters in your query and pass a `Map` of parameter names and values. - **Positional parameters:** Use `$1`, `$2`, etc., to denote positional parameters and pass a `List` of parameter values in the correct order. ```dart // Named parameters var result = await session.db.unsafeQuery( r'SELECT id FROM apparel WHERE color = @color AND size = @size', parameters: QueryParameters.named({ 'color': 'green', 'size': 'XL', }), ); // Positional parameters var result = await session.db.unsafeQuery( r'SELECT id FROM apparel WHERE color = $1 AND size = $2', parameters: QueryParameters.positional(['green', 'XL']), ); ``` :::danger Always sanitize your input when using raw query methods. For the `unsafeQuery` and `unsafeExecute` methods, use query parameters to prevent SQL injection. Avoid using `unsafeSimpleQuery` and `unsafeSimpleExecute` unless the simple query protocol is strictly required. ::: ## Working with results The query methods return a `DatabaseResult`, a read-only list of rows. Each row is itself a read-only list of column values, in the order the query selected them. Call `toColumnMap()` on a row to get the values keyed by column name instead. ```dart var result = await session.db.unsafeQuery( r'SELECT id, name FROM company WHERE id = @id', parameters: QueryParameters.named({'id': 1}), ); for (var row in result) { var id = row[0] as int; var map = row.toColumnMap(); // {'id': 1, 'name': 'Serverpod'} } ``` Beyond the rows, the result exposes two properties: - `affectedRowCount`: the number of rows affected by the query. - `schema`: the columns of the result, each with a `columnName`. When a query selects multiple columns with the same name, `toColumnMap()` keeps the last value for that name. ## Intercept database access To apply tracing, metrics, tenant scoping, or policy enforcement across every database operation of a session, including the raw methods above, see [Database interceptors](https://docs.serverpod.dev/concepts/data-and-the-database/database/database-interceptors.md). --- # Migrations https://docs.serverpod.dev/concepts/data-and-the-database/database/migrations Serverpod comes with a migration system that helps you keep your database schema up to date as your project evolves. Database migrations provide a structured way of upgrading your database while maintaining existing data. A migration is a set of database operations (for example, creating a table or adding a column) required to update the database schema to match the requirements of the project. Each migration handles both initializing a new database and rolling an existing one forward from a previous state. If you ever get out of sync with the migration system, repair migrations can be used to bring the database schema up to date with the migration system. Repair migrations identify the differences between the two and create a unique migration that brings the live database schema in sync with a migration database schema. ## Opt out of migrations It is possible to selectively opt out of the migration system per table basis, by setting the `managedMigration` key to false in your model. When this flag is set to false the generated migrations will not define any SQL code for this table. You will instead have to manually define and manage the life cycle of this table. ```yaml class: Example table: example managedMigration: false fields: name: String ``` If you want to transition a manually managed table to then be managed by Serverpod you first need to set this flag to `true`. Then you have two options: - Delete the old table you had created by yourself, and this will let Serverpod manage the schema from a clean state. However, this means that you would lose any data that was stored in the table. - Make sure that the table schema matches the expected schema you have configured. This can be done by either manually making sure the schema aligns, or by creating a [repair migration](#creating-a-repair-migration) to get back into the correct state. ## Creating a migration To create a migration navigate to your project's `server` package directory and run the `create-migration` command. ```bash $ serverpod create-migration ``` The command reads the database schema from the last migration, then compares it to the database schema necessary to accommodate the projects, and any module dependencies, current database requirements. If differences are identified, a new migration is created in the `migrations` directory to roll the database forward. If no previous migration exists it will create a migration assuming there is no initial state. If the project has [client-side database](https://docs.serverpod.dev/concepts/data-and-the-database/database/client-side-database.md) tables, matching SQLite migrations are also generated in the client package's `lib/migrations/` directory. They are applied on the device when the client database session opens. See the [Pre-migration project upgrade path](https://docs.serverpod.dev/upgrading/archive/upgrade-to-one-point-two.md) section for more information on how to get started with migrations for any project created before migrations were introduced in Serverpod. ### Force create migration The command stops without creating a migration in two situations, and each has its own override flag: - When no changes are detected between the latest migration and the schema the project requires, the command skips creating a migration. Pass `--empty` to create the migration anyway, for example as a starting point for [custom SQL](#writing-custom-sql). - When the change carries a risk of destroying data, the command aborts with a warning. Pass `--force` to override the warning and create the migration. ```bash $ serverpod create-migration --force $ serverpod create-migration --empty ``` ### Tag migration Tags can be useful to identify migrations that introduced specific changes to the project. Tags are appended to the migration name and can be added with the `--tag` option. A tag can contain only lowercase letters, numbers, and dashes. ```bash $ serverpod create-migration --tag "v1-0-0" ``` This would create a migration named `-v1-0-0`: ```text ├── migrations │ └── 20231205080937028-v1-0-0 ``` ### Writing custom SQL Migrations are plain SQL, so you can edit a generated `migration.sql` to do work the generator does not produce on its own, such as seeding reference data, backfilling a new column, or transforming existing data alongside a schema change. Open the `migration.sql` file in the migration's directory and add your statements. Serverpod applies the file as-is when it rolls the database forward. Custom SQL is worth reaching for when: - Initial or reference data needs to be in place as soon as the schema exists. - A schema change requires moving or transforming existing data, for example splitting one column into two. You own the correctness of anything you add. Make sure it matches the schema at that point in the migration history and rolls forward cleanly from the previous migration. Serverpod does not validate hand-written SQL. Once a migration has been applied, treat it as immutable and create a new migration for further changes instead of editing it. ### Migrations directory structure The `migrations` directory contains a folder for each migration that is created, looking like this for a project with two migrations: ```text ├── migrations │ ├── 20231205080937028 │ ├── 20231205081959122 │ └── migration_registry.txt ``` Every migration is denoted by a directory labeled with a timestamp indicating when the migration was generated. Within the directory, there is a `migration_registry.txt` file. This file is automatically created whenever migrations are generated and serves the purpose of cataloging the migrations. Its primary function is to identify migration conflicts. For each migration, five files are created: - **definition.json** - Contains the complete definition of the database schema, including any database schema changes from Serverpod module dependencies. This file is parsed by the Serverpod CLI to determine the target database schema for the migration. - **definition.sql** - Contains SQL statements to create the complete database schema. This file is applied when initializing a new database. - **definition\_project.json** - Contains the definition of the database schema for only your project. This file is parsed by the Serverpod CLI to determine what tables are different by Serverpod modules. - **migration.json** - Contains the actions that are part of the migration. This file is parsed by the Serverpod CLI. - **migration.sql** - Contains SQL statements to update the database schema from the last migration to the current version. This file is applied when rolling the database forward. ## Apply migrations ### During development The `serverpod start` command handles migrations for you. Pending migrations are applied automatically on the first boot of a session. While the session runs, with its terminal focused: - Press **M** to create a migration from your model changes and apply it. - Press **P** to create a repair migration and apply it. - Press **A** to retry when applying a migration fails. Hold **Shift** with **M** or **P** to force the migration. See [Running your server](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md#manage-migrations-from-the-terminal) for the full set of shortcuts. ### In production or CI To apply migrations explicitly, start the server runtime with the `--apply-migrations` flag from your `server` package directory. Migrations are applied as part of the startup sequence and the framework asserts that each migration is only applied once to the database. ```bash $ dart run bin/main.dart --apply-migrations ``` If a migration fails, or the database doesn't match the latest migration afterward, the outcome depends on the run mode: - **Any run mode except `development`:** the server logs `Failed to apply database migrations.` or `Database does not match target state.` and keeps starting. - **`development`:** the server exits with code 1. Migrations can also be applied with the [maintenance role](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md#choose-a-server-role). The server applies the migrations and then exits, which suits CI jobs and other automated processes. If a migration fails, or the database doesn't match the latest migration afterward, it exits with code 1 in every run mode, so the job fails. ```bash $ dart run bin/main.dart --role maintenance --apply-migrations ``` If migrations are applied at the same time as repair migration, the repair migration is applied first. ### On Serverpod Cloud Serverpod Cloud applies pending migrations for you on every deploy. If a migration fails, the server logs `Failed to apply database migrations.` Fix the migration and redeploy. See [Cloud database](https://docs.serverpod.dev/cloud/concepts/database.md#migrations-run-on-every-deploy) for the full flow. ## Creating a repair migration If the database has been manually modified the database schema may be out of sync with the migration system. In this case, a repair migration can be created to bring the database schema up to date with the migration system. By default, the command connects to and pulls a live database schema from a running development server. To create a repair migration, navigate to your project's `server` package directory and run the `create-repair-migration` command. ```bash $ serverpod create-repair-migration ``` This creates a repair migration in the `repair-migration` directory targeting the project's latest migration. A repair migration is represented by a single SQL file that contains the SQL statements necessary to bring the database schema up to date with the migration system. :::warning To restore the integrity of the database schema, repair migrations will attempt to remove any tables that are not part of the migration system. To preserve manually created or managed tables the [repair migration](#repair-migrations-directory-structure) needs to be modified accordingly before application. ::: Since each repair migration is created for a specific live database schema, Serverpod will overwrite the latest repair migration each time a new repair migration is created. ### Migration database source By default, the repair migration system connects to your `development` database using the information specified in your Serverpod config. To use a different database source, the `--mode` option is used. ```bash $ serverpod create-repair-migration --mode production ``` The command connects and pulls the live database schema from a running server. ### Targeting a specific migration Repair migrations can also target a specific migration version by specifying the migration name with the `--version` option. ```bash $ serverpod create-repair-migration --version 20230821135718-v1-0-0 ``` This makes it possible to revert your database schema back to any older migration version. ### Force create repair migration The repair migration command aborts and displays an error under two conditions: 1. When no changes are identified between the database schema in the latest migration and the schema required by the project. 2. When there is a risk of data loss. To override these safeguards and force the creation of a repair migration, use the `--force` flag. ```bash $ serverpod create-repair-migration --force ``` ### Tag repair migration Repair migrations can be tagged just like regular migrations. Tags are appended to the migration name and can be added with the `--tag` option. ```bash $ serverpod create-repair-migration --tag "reset-migrations" ``` This would create a repair migration named `-reset-migrations` in the `repair-migration` directory: ```text ├── repair-migration │ └── 20230821135718-reset-migrations.sql ``` ### Repair migrations directory structure The `repair-migration` directory only exists if a repair migration has been created and contains a single SQL file containing statements to repair the database schema. ```text ├── repair-migration │ └── 20230821135718-v1-0-0.sql ``` ## Applying a repair migration The repair migration is applied using the server runtime. To apply a repair migration, start the server with the `--apply-repair-migration` flag. The repair migration is applied as part of the startup sequence and the framework asserts that each repair migration is only applied once to the database. ```bash $ dart run bin/main.dart --apply-repair-migration ``` The repair migration can also be applied with the [maintenance role](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md#choose-a-server-role), where the server exits after applying it, with code 1 if it fails. ```bash $ dart run bin/main.dart --role maintenance --apply-repair-migration ``` If a repair migration is applied at the same time as migrations, the repair migration is applied first. ## Rolling back migrations Utilizing repair migrations it is easy to roll back the project to any migration. This is useful if you want to revert the database schema to a previous state. To roll back to a previous migration, create a repair migration targeting the migration you want to roll back to, then apply the repair migration. Note that data is not rolled back, only the database schema. To roll back to a previous migration, first create a repair migration targeting the desired migration version: ```bash $ serverpod create-repair-migration --version 20230821135718-v1-0-0 --tag "roll-back-to-v1-0-0" ``` Then apply the repair migration, any repair migration will only be applied once: ```bash $ dart run bin/main.dart --apply-repair-migration ``` ## Troubleshooting **Check the flag name when migrations do not apply.** The flag is `--apply-migrations`, plural. Passing the singular `--apply-migration` fails to parse, so Serverpod logs `Failed to parse command line arguments. Using default values.` and falls back to defaults. Migrations are not applied, and any other flags on the same command are dropped as well. If migrations are not running, check the flag spelling first. **Deleting a migration does not undo it.** Removing a migration directory does not roll the schema back. To undo a migration, create a repair migration targeting the version you want to return to and apply it, as described in [Rolling back migrations](#rolling-back-migrations). Only the schema is reverted; data is not. --- # Connection https://docs.serverpod.dev/concepts/data-and-the-database/database/connection The connection details and password for the database live in the `config` directory of your server package, and Serverpod connects using them when the server starts. For local development, no setup is needed by default: the server runs an [embedded PostgreSQL](https://docs.serverpod.dev/concepts/data-and-the-database/database/embedded-postgres.md) and manages it for you. This page is for connecting to a database you run yourself: a local or Docker Postgres, a remote server, or a managed instance such as Cloud SQL or RDS. ## Connection details Each environment configuration contains a `database` keyword that specifies the connection details. For your development build you can find the connection details in the `config/development.yaml` file. ```yaml ... database: host: localhost port: 8090 name: user: postgres ... ``` The `name` refers to the database name, `host` is the domain name or IP address pointing to your Postgres instance, `port` is the port that Postgres is listening to, and `user` is the username that is used to connect to the database. :::caution By default, Postgres listens on port 5432. Serverpod's development setups use port 8090 to avoid conflicts. If you host your own instance, double-check that the correct port is specified in your configuration files. ::: SQLite projects need no connection details beyond a file path, and no password; see [Database backends](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md#database-backends) for choosing and configuring a backend. ### Configure search paths If using Postgres, you can customize the search paths for your database connection, which helps when you work with multiple schemas. By default, Postgres uses the `public` schema unless otherwise specified. To override this, use the optional `searchPaths` setting in your configuration: ```yaml ... database: host: localhost port: 8090 name: user: postgres searchPaths: custom, public ... ``` In this example, Postgres looks for tables in the `custom` schema first, and falls back to `public` if needed. :::tip It is also possible to set the search paths using [runtime parameters](https://docs.serverpod.dev/concepts/data-and-the-database/database/runtime-parameters.md) directly on the server startup (or on a specific transaction). If the paths are set on both the configuration file and as runtime parameters, the runtime parameters take precedence. ::: ### Configure connection pool size By default, Serverpod uses a connection pool with a maximum of 10 connections to the database. You can customize this limit using the `maxConnectionCount` setting: ```yaml ... database: host: localhost port: 8090 name: user: postgres maxConnectionCount: 20 ... ``` To allow unlimited connections, set `maxConnectionCount` to `0` or a negative value. You can also configure this setting via the environment variable `SERVERPOD_DATABASE_MAX_CONNECTION_COUNT`. On SQLite, this setting controls the number of read-only transactions that can run concurrently. Only one write transaction can run at a time. ## Database password The database password is stored in a separate file called `passwords.yaml` in the same `config` directory. The password for each environment is stored under the `database` keyword in the file. ```yaml ... development: database: '' ... ``` ## The development database By default, the development database is an [embedded PostgreSQL](https://docs.serverpod.dev/concepts/data-and-the-database/database/embedded-postgres.md), started and stopped with the server; see [Running your server](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md). Nothing on this page is needed for it. Projects set up with the Docker alternative instead run their development database from the server package's `docker-compose.yaml`. To use it in a project configured for the embedded PostgreSQL, first remove `dataPath` from the run mode's configuration. Start the database from the root of the `server` package: ```bash $ docker compose up --build --detach ``` To stop the database run: ```bash $ docker compose stop ``` To remove the database and **delete** all associated data, run: ```bash $ docker compose down -v ``` ## Connect to a custom Postgres instance You can connect to any Postgres instance you run or rent. There are a few things to take into consideration: - Make sure that your Postgres instance is up and running and is reachable from your Serverpod server. - You will need to create a user with a password, and a database. ### Connect to a local Postgres server If you want to connect to a local Postgres server (with the default setup), the `development.yaml` works fine if you set the correct port, user, and database, and update the password in the `passwords.yaml` file. ### Connect to a remote Postgres server To connect to a remote Postgres server (that you have installed on a VPS or VDS), you need to follow a couple of steps: - Make sure that the Postgres server has a reachable network address and that it accepts incoming traffic. - You may need to open the database port on the machine. This may include configuring its firewall. - Update your Serverpod `database` config to use the public network address, database name, port, user, and password. ### Connect to Google Cloud SQL You can connect to a Google Cloud SQL Postgres instance in two ways: 1. Setting up the *Public IP Authorized networks* (with your Serverpod server IP) and changing the database host string to the *Cloud SQL public IP*. 2. Using the *Connection String* if you are hosting your Serverpod server on Google Cloud Run and changing the database host string to the Cloud SQL: `/cloudsql/my-project:server-location:database-name/.s.PGSQL.5432`. The next step is to update the database password in `passwords.yaml` and the connection details for the desired environment in the `config` folder. :::info If you are using the `isUnixSocket` don't forget to add **"/.s.PGSQL.5432"** to the end of the `host` IP address. Otherwise, your Google Cloud Run instance will not be able to connect to the database. ::: ### Connect to AWS RDS You can connect to an AWS RDS Instance in two ways: 1. Enable public access to the database and configure VPC/Subnets to accept your Serverpod's IP address. 2. Use the Endpoint `database-name.some-unique-id.server-location.rds.amazonaws.com` to connect to it from AWS ECS. The next step is to update the database password in `passwords.yaml` and the connection details for the desired environment in the `config` folder. --- # Runtime parameters https://docs.serverpod.dev/concepts/data-and-the-database/database/runtime-parameters Runtime parameters in Serverpod allow you to fine-tune the behavior of the database engine for specific queries or workloads. This can significantly improve performance, especially for complex queries or large datasets. :::warning Setting runtime parameters affects PostgreSQL's query planning and execution. Always test different parameter combinations with your specific dataset and query patterns to find the optimal configuration. ::: :::info Runtime parameters are only supported for PostgreSQL. Setting runtime parameters on SQLite is a no-op. ::: ## Parameter scopes Runtime parameters can be applied with different scopes: - **Global scope**: Parameters set at Serverpod startup that affect all database connections and sessions. - **Transaction scope**: Parameters that only affect queries within a specific transaction. :::info Due to connection pooling, runtime parameters cannot be set at the session level as this would create inconsistencies between different database connections. Parameters must be either global (set at startup) or transaction-scoped. ::: ### Setting global runtime parameters on startup To set global runtime parameters that apply to all database sessions, configure them when initializing your Serverpod instance. Add the runtime parameters configuration in the `run` function of your `lib/server.dart` file: ```dart import 'src/generated/serverpod.dart'; /// The starting point of the Serverpod server. void run(List args) async { final pod = Serverpod( args, // Set global runtime parameters that apply to all database sessions. runtimeParametersBuilder: (params) => [ params.vectorIndexQuery(enableIndexScan: true), params.hnswIndexQuery(efSearch: 64), ], ); // Start the server await pod.start(); } ``` ### Setting transaction-scoped runtime parameters To set runtime parameters that only apply to queries within a specific transaction, use the `setRuntimeParameters` method on the transaction object: ```dart // Transaction scope - only affects current transaction await session.db.transaction((transaction) async { await transaction.setRuntimeParameters((params) => [ params.hnswIndexQuery(efSearch: 40), ]); var results = await Document.db.find( session, where: (t) => t.embedding.distanceCosine(queryVector) < 0.5, limit: 10, orderBy: (t) => t.embedding.distanceCosine(queryVector), transaction: transaction, ); return results; }); ``` These parameters will override any global settings for the duration of the transaction, allowing you to fine-tune query behavior without affecting other transactions or sessions. ## Parameter groups Runtime parameters are set as groups of related settings that control various aspects of query execution. You can use the builder pattern to create these parameter groups, that will always set all parameters in a consistent way. Existing parameter groups include: - **Vector index query parameters**: Control the general behavior of index building and querying. - **HNSW index query parameters**: Control the behavior of HNSW index queries. - **IVFFLAT index query parameters**: Control the behavior of IVFFLAT index queries. - **Schema search paths**: Control the search paths for database schemas. It is also possible to [create custom parameter groups](#creating-custom-runtime-parameters). ### Combining multiple parameter groups You can combine different runtime parameter groups at once with the builder callback: ```dart await session.db.transaction((transaction) async { await transaction.setRuntimeParameters((params) => [ params.vectorIndexQuery(enableIndexScan: true), params.hnswIndexQuery(efSearch: 64), ]); // Your queries here... }); ``` ### Creating custom runtime parameters Custom parameter groups can be created in one of the following ways: #### Using `MapRuntimeParameters` Create an instance of the `MapRuntimeParameters` class, which allows you to set specific parameters by name: ```dart import 'package:serverpod/database.dart'; await session.db.transaction((transaction) async { // Clear specific parameters within this transaction await transaction.setRuntimeParameters((params) => [ MapRuntimeParameters({ 'param1': null, 'param2': 2, 'param3': true, }), ]); // Your queries here... }); ``` #### Extending `RuntimeParameters` Extend the base `RuntimeParameters` class directly, overriding the `options` getter: ```dart import 'package:serverpod/database.dart'; class CustomRuntimeParameters extends RuntimeParameters { final int param1; final int param2; CustomRuntimeParameters({ required this.param1, required this.param2, }); @override Map get options => { 'custom_param': param1, 'another_param': param2, }; } ``` #### Extending the builder class with custom methods Create an extension on `RuntimeParametersBuilder` to add custom methods that return instances of your custom runtime parameters class: ```dart import 'package:serverpod/database.dart'; extension CustomRuntimeParametersExtension on RuntimeParametersBuilder { // Directly with `MapRuntimeParameters`, without a custom class. MapRuntimeParameters mapCustomParameters({ required int param1, required int param2, }) { return MapRuntimeParameters({ 'custom_param': param1, 'another_param': param2, }); } // With a custom runtime parameters class as declared above. CustomRuntimeParameters classCustomParameters({ required int param1, required int param2, }) { return CustomRuntimeParameters( param1: param1, param2: param2, ); } } ``` This allows you to use your custom parameters with the same builder pattern: ```dart await session.db.transaction((transaction) async { await transaction.setRuntimeParameters((params) => [ params.mapCustomParameters(param1: 100, param2: 200), params.classCustomParameters(param1: 300, param2: 400), params.hnswIndexQuery(efSearch: 64), // Can combine with built-in parameters ]); // Your queries here... }); ``` ### Parameter values Values for runtime parameters can be set to specific types, such as `int`, `double`, `bool`, or `String`. When setting a parameter, you can also set it to `null` to reset it to its default value. Some custom enums, like `IterativeScan`, can also be used to control specific behaviors. ## Database schema search paths You can configure database schema search paths on either global level or within transactions for better schema resolution: ```dart await session.db.transaction((transaction) async { // Set custom search paths for schema resolution await transaction.setRuntimeParameters((params) => [ params.searchPaths(['my_schema', 'public']), ]); // Your queries here will use the custom search paths... // Or reset to default search paths await transaction.setRuntimeParameters((params) => [ params.searchPaths(), // No arguments resets to default ]); // Other queries here with default paths... }); ``` ## Vector runtime parameters When working with vector similarity searches, you can fine-tune query performance by setting runtime parameters that control the behavior of vector indexes. :::info Runtime parameters are particularly useful when you need to balance between query speed and result quality. For real-time applications, you might prefer faster but less accurate results, while for analytical workloads, you might prioritize accuracy over speed. ::: ### General vector query optimization Use the builder pattern within transactions to configure general PostgreSQL settings that affect vector query performance: ```dart await session.db.transaction((transaction) async { // Configure general vector query parameters await transaction.setRuntimeParameters((params) => [ params.vectorIndexQuery( enableIndexScan: true, // Enable/disable index scans enableSeqScan: false, // Enable/disable sequential scans minParallelTableScanSize: 1024, // Min number of 8KB chunks to scan before parallelizing scans parallelSetupCost: 1000, // Estimated cost of launching parallel worker processes maintenanceWorkMem: 65536, // Memory in KB for operations such as index creation maxParallelMaintenanceWorkers: 2, // Increase to speed up index creation on large tables maxParallelWorkersPerGather: 2, // Increase to speed up queries without an index ), ]); // Perform vector search with optimized PostgreSQL settings var results = await Document.db.find( session, where: (t) => t.embedding.distanceCosine(queryVector) < 0.7, orderBy: (t) => t.embedding.distanceCosine(queryVector), limit: 50, transaction: transaction, ); return results; }); ``` ### HNSW index optimization For queries using HNSW indexes, use the builder pattern within transactions to control search behavior: ```dart import 'package:serverpod/serverpod.dart'; await session.db.transaction((transaction) async { // Configure HNSW runtime parameters await transaction.setRuntimeParameters((params) => [ params.hnswIndexQuery( efSearch: 40, // Higher values = better recall, slower search iterativeScan: IterativeScan.relaxed, // Relaxed scan for better recall maxScanTuples: 20000, // Limit number of tuples to scan scanMemMultiplier: 2, // Memory multiplier for scanning ), ]); // Your HNSW queries here... }); ``` ### IVFFLAT index optimization For queries using IVFFLAT indexes, use the builder pattern within transactions to control search behavior: ```dart await session.db.transaction((transaction) async { // Configure IVFFLAT runtime parameters await transaction.setRuntimeParameters((params) => [ params.ivfflatIndexQuery( probes: 10, // Number of probes to search (higher = better recall) iterativeScan: IterativeScan.relaxed, // Relaxed scan for better recall maxProbes: 20, // Maximum number of probes to use ), ]); // Your IVFFLAT queries here... }); ``` :::note The `IterativeScan.strict` mode is not supported for IVFFLAT indexes. Use `IterativeScan.relaxed` or `IterativeScan.off` (default) instead. ::: ### Iterative scan modes The `IterativeScan` enum controls how vector indexes handle scanning: - **`IterativeScan.off`**: Disables iterative scanning, using the default `pgvector` behavior. - **`IterativeScan.strict`**: Ensures results are in exact order by distance (better precision). - **`IterativeScan.relaxed`**: Allows slightly out-of-order results but provides better recall. ## Testing with runtime parameters When writing tests that use runtime parameters, you'll typically want to configure them either globally for the test suite or within specific test transactions. On transaction scope, it is the same as in production code, where you set the parameters within the transaction to ensure they only apply to that specific test case. ### Global test configuration For tests that require specific runtime parameters throughout the test suite, you can configure them using the `withServerpod` test helper: ```dart import 'package:test/test.dart'; import 'test_tools/serverpod_test_tools.dart'; void main() { withServerpod( 'Given a server with vector runtime parameters', runtimeParametersBuilder: (params) => [ params.vectorIndexQuery(enableIndexScan: true), params.hnswIndexQuery(efSearch: 100), ], (sessionBuilder, endpoints) { // Tests here will use the configured runtime parameters }, ); } ``` This is the same as configuring the runtime parameters in the `run` function, but it allows you to set them specifically for your test environment. ### Ensuring vector extension for runtime parameter tests When testing runtime parameters, it is possible to find unexpected behavior of some parameters returning `null` if queried before the vector extension is loaded. To avoid this and ensure consistent behavior, call the `ensureVectorLoaded` method before running such tests. **This is not necessary for production code**. The only use case that it does make a difference is when explicitly querying runtime parameters that are related to vector operations before the vector extension is loaded: ```dart import 'package:serverpod_test/serverpod_test.dart'; void main() { withServerpod('Given withServerpod without runtimeParametersBuilder', (sessionBuilder, endpoints) { var session = sessionBuilder.build(); test( 'when querying runtime parameters globally then expect default values', () async { // Ensure vector extension is loaded before, so that all vector runtime // parameters have their default values set correctly. await session.db.ensureVectorLoaded(); var hnswCheckQuery = HnswIndexQueryOptions().buildCheckValues(); var hnswResult = await session.db.unsafeQuery(hnswCheckQuery); var hnswRow = hnswResult.first.toColumnMap(); // This would return null if the vector extension is not loaded, making // the expect unpredictable depending on previous tests/operations. expect(hnswRow['hnsw_ef_search'], '40'); await session.db.transaction((transaction) async { await transaction.setRuntimeParameters((params) => [ params.hnswIndexQuery(efSearch: 200), ]); // Your test queries here... }); }); }); } ``` --- # Client-side database https://docs.serverpod.dev/concepts/data-and-the-database/database/client-side-database The client-side database stores data on the device your app runs on, such as a phone or a browser, in a SQLite database. You query it with the same generated model methods you use on the server. This is useful for local caches, offline data, and state that belongs to the app rather than the server. ## Enable the client-side database A table model opts into the device database with the `database` keyword. Setting it to `client` generates the table only on the device, and `all` generates it on both the server and the device. The full value set is covered on the [Tables](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md#choosing-where-a-table-lives) page. ```yaml class: Company table: company database: client ``` When at least one table model has `database: client` or `database: all`, the generated `Client` class gets a `createSession` method that opens a SQLite database file and returns a `ClientDatabaseSession`. :::info The generated code depends on the `serverpod_database` package. Add it to your client package's `pubspec.yaml`. ::: ## Create a session On Flutter, resolve a file path and call `createSession`: ```dart import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:my_project_client/my_project_client.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // Point the client at your server as usual. final client = Client('http://localhost:8080/'); // Resolve the database path. final path = await resolveDatabasePath('app.db'); // Create the session to use in database operations. final session = await client.createSession(path, isDebugMode: kDebugMode); } Future resolveDatabasePath(String fileName) async { if (kIsWeb) return fileName; final dir = await getApplicationSupportDirectory(); return p.join(dir.path, fileName); } ``` Creating the session opens the database file, so keep one session for the lifetime of the app, for example in your state management, instead of creating a new one per operation. Call `close()` on the session to close the underlying database when you are done with it. On the web there is no file system, which is why the example passes a bare name instead of a path. :::note On the web, SQLite runs as WebAssembly in a web worker. Your Flutter app's `web/` directory needs two files, which the [`sqlite_async` web setup](https://pub.dev/packages/sqlite_async#web) links to: - `sqlite3.wasm`, from the release that matches the `sqlite3` version in your `pubspec.lock`. Without it, `createSession` fails. - `db_worker.js`, from the release that matches the `sqlite_async` version in your `pubspec.lock`. Without it, the database runs on the page's main thread and is not safe to use from several tabs. ::: :::info On native platforms, SQLite runs in WAL mode, so `-shm` and `-wal` files may exist next to the database file while the session is open. ::: ## Run migrations on the device Client tables get their own migrations. Whenever a migration is created for a project with client database tables, matching SQLite migrations are also generated under the client package's `lib/migrations/` directory. By default, `createSession` applies pending client migrations when it opens the database (`runMigrations: true`). Passing `false` skips them, which can leave the database out of sync with your models, so only do so if you apply migrations through another code path. With `isDebugMode: true`, the database integrity is verified after migrations are applied. On SQLite this includes a foreign key check that throws a `SqliteMigrationForeignKeyViolationException` if it finds violating rows. See [Database exceptions](https://docs.serverpod.dev/concepts/data-and-the-database/database/exceptions.md#sqlite-foreign-key-checks). On Flutter, pass `kDebugMode` so the verification runs in debug builds only. ## Use the database The session works like a server-side session in database calls. Pass it to the generated model methods: ```dart var companies = await Company.db.find( session, where: (t) => t.name.like('%Ltd'), orderBy: (t) => t.name, ); ``` [CRUD](https://docs.serverpod.dev/concepts/data-and-the-database/database/crud.md), [filtering](https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering.md), [sorting](https://docs.serverpod.dev/concepts/data-and-the-database/database/sorting.md), [pagination](https://docs.serverpod.dev/concepts/data-and-the-database/database/pagination.md), [relations](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations.md), and [transactions](https://docs.serverpod.dev/concepts/data-and-the-database/database/transactions.md) all work as they do on the server. Because the database is SQLite, the SQLite-specific behaviors apply: - `like` and `ilike` are both case-insensitive for ASCII characters. See [filtering](https://docs.serverpod.dev/concepts/data-and-the-database/database/filtering.md#ilike). - Transaction isolation levels are ignored, and transactions always run serialized. See [transactions](https://docs.serverpod.dev/concepts/data-and-the-database/database/transactions.md#transaction-isolation). - [Row locking](https://docs.serverpod.dev/concepts/data-and-the-database/database/row-locking.md) calls do nothing, since SQLite allows only one write transaction at a time. - [Vector](https://docs.serverpod.dev/concepts/data-and-the-database/database/vector-and-geography-fields.md#vector-fields) queries are not supported, and [geography](https://docs.serverpod.dev/concepts/data-and-the-database/database/vector-and-geography-fields.md#geography-fields) values round-trip through CRUD but throw on spatial query operations. - Column types and encodings differ from Postgres. See [Field types](https://docs.serverpod.dev/concepts/data-and-the-database/models/field-types.md#postgres-vs-sqlite). ## Sync tables with the server :::warning Offline sync is experimental. The `serverpod_offline_sync` module is still in development and not yet ready for production use. ::: A table with `database: sync` behaves like `database: all` and is additionally kept in sync between the device and the server by the `serverpod_offline_sync` module. Changes made while offline are merged on the next sync. The module tracks every operation and resolves conflicts itself, so your code never intervenes. Enable the experimental feature and register the module in `config/generator.yaml` (or pass `--experimental-features databaseSync` on the command line): ```yaml modules: serverpod_offline_sync: nickname: offline_sync experimental_features: databaseSync: true ``` Add `serverpod_offline_sync_server` to the server package and `serverpod_offline_sync_client` to the client package. Then mark the model. Sync tables need a UUID primary key. Serverpod adds a `spaceId` field for you, which links each row to the space that owns it: the user's personal space or a shared space. ```yaml class: Person table: person database: sync fields: id: UuidValue?, defaultPersist=random_v7 name: String ``` Code generation wires the sync engine into the generated `Serverpod` class, so the server needs no further changes. On the device, open the database with `createSyncSession` instead of `createSession`, then sync once or keep syncing: ```dart final session = await client.createSyncSession( path, isDebugMode: kDebugMode, persistentUserId: persistentUserId, ); await client.offlineSync.syncOnce(session); // Or keep the device and the server in sync while the app runs: final syncSession = client.offlineSync.syncContinuously(session); ``` The `createSyncSession` method returns an `OfflineSyncDatabaseSession` and takes the same `runMigrations` and `isDebugMode` parameters as `createSession`. The user must be signed in before syncing, or the sync calls fail as unauthorized. For personal and shared spaces, `persistentUserId`, and the rest of the API, see the [`serverpod_offline_sync` README](https://pub.dev/packages/serverpod_offline_sync). --- # Database exceptions https://docs.serverpod.dev/concepts/data-and-the-database/database/exceptions Serverpod wraps database failures in exceptions that are subtypes of `DatabaseException`. This gives you one common type to catch for database failures and more specific types when you want to handle a known failure, such as a unique constraint violation or a single-row operation that matched no row. ```dart try { await Company.db.updateById( session, companyId, columnValues: (t) => [t.name('New name')], ); } on DatabaseUnexpectedResultException { // No row with the provided id was updated. } on DatabaseQueryException catch (e) { session.log( 'Database query failed with code ${e.code}', exception: e, ); } on DatabaseException catch (e) { session.log('Database operation failed', exception: e); } ``` When a database exception is not caught inside an endpoint, it follows Serverpod's normal endpoint exception handling and is logged as an uncaught server exception. Serverpod does not serialize database exception details and send them to the app. They stay server-side in the logs. See [Error handling and exceptions](https://docs.serverpod.dev/concepts/endpoints-and-apis/error-handling-and-exceptions.md) for how uncaught exceptions reach the app. ## Exception types | Exception | Subtype of | When it is thrown | | --------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `DatabaseException` | `Exception` | The base type for database exceptions. Catch this when you want one handler for any database failure. | | `DatabaseUnexpectedResultException` | `DatabaseException` | A single-row operation did not affect exactly one row: `insertRow` returned a different number of rows, or `updateRow`, `updateById`, or `deleteRow` matched no row. `upsertRow` returns `null` instead when the existing row doesn't match its `updateWhere` expression. | | `DatabaseQueryException` | `DatabaseException` | The database rejected a query. Carries the adapter's error details, see below. | | `DatabaseUniqueViolationException` | `DatabaseQueryException` | A write violated a unique index or primary key. | | `DatabaseForeignKeyViolationException` | `DatabaseQueryException` | A write violated a foreign key constraint, including at commit for [deferrable constraints](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/deferrable-constraints.md). | | `SqliteDatabaseLockedException` | `DatabaseQueryException` | SQLite could not acquire a lock, typically because a query ran without the `transaction` of an already active transaction, or two transactions ran concurrently. | | `SqliteMigrationForeignKeyViolationException` | `DatabaseException` | A SQLite foreign key integrity check after migrations found one or more violating rows. | ## Query exception details The `DatabaseQueryException` type and its subclasses expose optional fields from the underlying database adapter. These fields are useful for logging and for handling known database errors: - `code` - `detail` - `hint` - `tableName` - `columnName` - `constraintName` - `position` These values are database-adapter details, so write defensive code that handles `null` values. PostgreSQL fills in the violated constraint name. SQLite does not. To react to a specific failure, prefer the typed subclass over inspecting the fields: ```dart try { await Company.db.insertRow(session, company); } on DatabaseUniqueViolationException { // Handle a duplicate company name here. return; } ``` Other query failures keep their original type and stack trace, so a broader `on DatabaseQueryException` handler can log them. ## Operation exceptions `DatabaseUnexpectedResultException` describes cases where Serverpod expected one row to be affected but the database result did not match that expectation. For example, `updateById` throws it when no row exists for the id you pass in. Batch and filtered operations have their own documented behavior. Some methods, such as `updateWhere` or `deleteWhere`, can validly affect zero rows and return an empty list instead of throwing. ## SQLite foreign key checks When using SQLite, Serverpod runs a foreign key integrity check after applying migrations in the development run mode. If SQLite reports invalid foreign key references, Serverpod throws `SqliteMigrationForeignKeyViolationException`. Its `violations` field holds the rows returned by SQLite's `PRAGMA foreign_key_check`. --- # Database interceptors https://docs.serverpod.dev/concepts/data-and-the-database/database/database-interceptors A database interceptor lets you wrap every database operation of a session in the same behavior, such as query tracing, tenant scoping, or a guard that rejects writes in a protected environment. Serverpod calls the interceptor once when it creates a session, passing the session and the framework database, and whatever the interceptor returns becomes `session.db` for the rest of that session. Register the interceptor when you construct `Serverpod`: ```dart var pod = Serverpod( args, databaseInterceptor: (session, inner) { session.log('Created a database layer for this session.'); return inner; }, ); ``` Returning `inner` leaves the framework database in place, so this example only observes session creation. Repeated reads of `session.db` return the same instance for the lifetime of the session. ## Return a custom database layer To intercept the operations themselves, return your own implementation of the `Database` interface exported by `package:serverpod/serverpod.dart`: ```dart var pod = Serverpod( args, databaseInterceptor: (session, inner) { return PolicyDatabase( session: session, inner: inner, ); }, ); ``` The `Database` class has no public constructor to subclass, so implement the interface and let your IDE generate the overrides. Hold on to the supplied `inner` database and delegate every operation the layer does not change. The object you return becomes `session.db`, so generated calls such as `User.db.find(session)` pass through it as well. Typical uses are: - Recording query traces, timings, and metrics. - Applying a tenant scope to every supported query. - Enforcing read and write policies. - Blocking unsafe operations or writes in protected environments. Pass transaction objects, [runtime parameters](https://docs.serverpod.dev/concepts/data-and-the-database/database/runtime-parameters.md), ordering, pagination, [row locks](https://docs.serverpod.dev/concepts/data-and-the-database/database/row-locking.md), and return options through without changing their meaning. A layer that enforces a policy also needs to cover [raw access](https://docs.serverpod.dev/concepts/data-and-the-database/database/raw-access.md) and transactions, otherwise a caller can reach the database around it. Do not hold on to the session or its inner database after the session closes. :::warning Custom database layers build on the `Database` API from `serverpod_database`, which can change in a breaking way in a minor Serverpod release. Review and test your implementation whenever you upgrade Serverpod. ::: --- # Embedded PostgreSQL https://docs.serverpod.dev/concepts/data-and-the-database/database/embedded-postgres New projects run an embedded PostgreSQL in the `development` and `test` run modes. It is a real PostgreSQL server that Serverpod starts and stops together with your server, so local development gets the same database engine as production without installing PostgreSQL or running Docker. It is meant for local development and testing only. There are no backups, no replication, and nothing supervising the process, so staging and production connect to a managed or separately operated PostgreSQL instead. See [Connection](https://docs.serverpod.dev/concepts/data-and-the-database/database/connection.md) for those setups. ## Enable it Embedded PostgreSQL is enabled by the `dataPath` setting, which generated projects already include: ```yaml title="config/development.yaml" database: host: localhost port: 8090 name: myproject user: postgres dataPath: .serverpod/development/pgdata ``` The rest of the database settings stay as they are. Serverpod uses `name` and `user` when it creates the database and reads the password from `config/passwords.yaml`, exactly as it would for an external instance. Remove `dataPath` to connect to the `host` and `port` instead. A relative `dataPath` is resolved from the root of the server package, and each run mode needs its own directory, for example `.serverpod/test/pgdata` in `config/test.yaml`. The directory holds a complete database, so keep it out of version control. ## What happens when the server starts The first start downloads the PostgreSQL binaries for your operating system and architecture into a per-user cache, which every later start and every other project reuses. Binaries are available for Linux and macOS on x64 and Arm64, and for Windows on x64. The cache location depends on your operating system: - **macOS:** `~/Library/Caches/serverpod/pg-binaries` - **Linux:** `$XDG_CACHE_HOME/serverpod/pg-binaries`, or `~/.cache/serverpod/pg-binaries` when `XDG_CACHE_HOME` is not set - **Windows:** `%LOCALAPPDATA%\serverpod\Cache\pg-binaries` Set `SERVERPOD_PG_CACHE_DIR` to keep the binaries in a different directory. Serverpod starts the database before it opens its connection pool, and connects over a Unix domain socket instead of a TCP port, so several projects can run at once without competing for ports. If another Serverpod process already runs a database in the same `dataPath`, the new process attaches to it rather than starting a second one. The files under `dataPath` are kept when the server stops, so your data is still there on the next start. After an unclean exit, Serverpod clears the leftover process state and brings the database back up. ## Connect a database tool Most clients like `psql` and `pgAdmin` connect over TCP, not the Unix socket the server uses. To inspect the data with one of them, start the database on its own while the server is stopped: ```bash $ serverpod database start ``` It boots the database configured for the `development` run mode and keeps it listening on the configured port until you stop it with Ctrl+C. Connect with the `name` and `user` from that run mode's configuration, and the password from `config/passwords.yaml`. Pass `--mode` to pick another run mode and `--port` to listen somewhere else: ```bash $ serverpod database start --mode test --port 9090 ``` ## Run integration tests Integration tests bring their own database, so there is nothing to install or start first: ```bash $ dart test ``` The started database server will be held open until the test process exits. Every suite in a test process shares the same database server, but each `withServerpod` group creates a randomly named database of its own on it that will be dropped on teardown. That is where the isolation comes from: groups run in parallel without seeing each other's data or leaving anything behind between runs. The data directory is not temporary. Like the one for `development`, it stays after the tests, and only the server process is reclaimed when the test process exits. See [Get started with testing](https://docs.serverpod.dev/concepts/testing/get-started.md) for the rest of the setup. :::note If a test process is hard-killed, the database server will not be stopped and data can be left behind. This data won't harm future runs because of the named database isolation, but it can occupy disk space. If the directory happens to grow too large, it is fully safe to delete it and let the next test recreate it from scratch. ::: ## Reset the database To start from an empty database, stop the server and delete the directory `dataPath` points at. Serverpod creates a new one on the next start. :::warning Deleting `dataPath` permanently deletes the database and all local data stored in it. ::: ## Related - [Connection](https://docs.serverpod.dev/concepts/data-and-the-database/database/connection.md): connecting to a PostgreSQL instance you run yourself. - [Configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md#database-backends): the database section of the run mode configuration. - [`serverpod database`](https://docs.serverpod.dev/concepts/cli/commands/database.md): every option of the command above. - [serverpod\_embedded\_postgres](https://pub.dev/packages/serverpod_embedded_postgres): the package behind this, for tools that need a PostgreSQL process of their own. --- # Get started with authentication https://docs.serverpod.dev/concepts/authentication/get-started When you create a project with `serverpod create`, email sign-in is already built in. The server is configured, the database is ready, and your app is connected to it. You only need to turn on the sign-in screen and run the app. This guide walks you through that, then shows how to test signing up and signing in. ![Sign-in with Serverpod](https://docs.serverpod.dev/img/authentication/sign-in-widget-device.png) ## Prerequisites - A project created with `serverpod create` on Serverpod 4.0 or later. For older projects, see [Setup](https://docs.serverpod.dev/concepts/authentication/setup.md) first to add the authentication module manually. - The Flutter SDK installed, so you can run the app. - Docker installed and running, if your project uses a Docker Postgres. Projects on the embedded Postgres option don't need Docker. :::note If you run on macOS (`flutter run -d macos`), add the Keychain Sharing entitlement before testing sign-in. See [Set up authentication on macOS](https://docs.serverpod.dev/concepts/authentication/macos-authentication.md). ::: ## Show the sign-in screen Your app already includes a sign-in screen. It is turned off by default. Turn it on with two small edits to your app's `main.dart`. First, import the screen: ```dart import 'screens/sign_in_screen.dart'; ``` Then show it as the home screen. In `MyHomePage.build()`, comment out the existing `body:` line and uncomment the `SignInScreen` block right below it, so it reads: ```dart // body: const GreetingsScreen(), body: SignInScreen( child: GreetingsScreen( onSignOut: () async { await client.auth.signOutDevice(); }, ), ), ``` That is the only change your app needs. ## Start your project From the project root, start everything with one command: ```bash serverpod start ``` This sets up the database and starts the server and your app. The app opens on the sign-in screen. To create an account: 1. Choose to create a new account, enter your email, and continue. 2. Look in the server console for the verification code. While testing, it is logged instead of emailed: ```text Registration code for you@example.com: <12345678> ``` 3. Enter the code, then set a password to finish. Once you are signed in, the app shows your content with a sign-out button. ## Next steps - Send verification emails from your own email service instead of the default. See [Use your own email provider](https://docs.serverpod.dev/concepts/authentication/providers/email/setup.md#use-your-own-email-provider). - Customize the sign-in screen. See [UI components](https://docs.serverpod.dev/concepts/authentication/ui-components.md). - Build your own sign-in UI with the email controller. See [Customizing the UI](https://docs.serverpod.dev/concepts/authentication/providers/email/customizing-the-ui.md). - Change password rules, code length, and rate limits. See [Email configuration](https://docs.serverpod.dev/concepts/authentication/providers/email/configuration.md). - Require sign-in on your endpoints. See [The basics](https://docs.serverpod.dev/concepts/authentication/basics.md#requiring-authentication-on-endpoints). - Add more sign-in options, like Google or Apple. See [Identity providers configuration](https://docs.serverpod.dev/concepts/authentication/setup.md#identity-providers-configuration). --- # Set up the authentication module https://docs.serverpod.dev/concepts/authentication/setup Serverpod comes with built-in user management and authentication. It is possible to build a [custom authentication implementation](https://docs.serverpod.dev/concepts/authentication/custom-overrides.md), but the recommended way to authenticate users is to use the `serverpod_auth_idp` module. The module lets users sign in through identity providers. An identity provider is a service that verifies who the user is, such as email with password, Google, or Apple. The module also handles basic user information, such as user names and profile pictures. This page walks through the manual setup: install the module on the server, the client, and the app, then wire up the sign-in UI. New projects created with `serverpod create` already include this setup. For that path, see [Get started](https://docs.serverpod.dev/concepts/authentication/get-started.md). ![Sign-in with Serverpod](https://docs.serverpod.dev/img/authentication/sign-in-widget-device.png) ## Server setup Add the authentication module as a dependency to the server project's `pubspec.yaml`. Use the same version numbers as for Serverpod itself for all dependencies. ```yaml dependencies: ... serverpod_auth_idp_server: 4.0.0 ``` The `serverpod_auth_idp_server` package contains all components required to configure authentication services. ### Configure authentication services In your main `server.dart` file, configure the authentication system using the `pod.initializeAuthServices()` extension method. The configuration needs at least one token manager. A token manager issues and validates the tokens that keep users signed in after they authenticate. The example below uses JWT tokens and reads its secrets from your password store. The [token manager configuration](#token-manager-configuration) section explains the options. ```dart import 'package:serverpod_auth_idp_server/core.dart'; import 'src/generated/serverpod.dart'; void run(List args) async { final pod = Serverpod(args); // Set up authentication services // The `pod.getPassword()` will get the value from `config/passwords.yaml`. pod.initializeAuthServices( tokenManagerBuilders: [ JwtConfig( // Pepper used to hash the refresh token secret. refreshTokenHashPepper: pod.getPassword('jwtRefreshTokenHashPepper')!, // Algorithm used to sign the tokens (`hmacSha512`, `hmacSha256` or `ecdsaSha512`). algorithm: JwtAlgorithm.hmacSha512( // Private key to sign the tokens. Must be a valid HMAC SHA-512 key. SecretKey(pod.getPassword('jwtHmacSha512PrivateKey')!), ) ), ], ); await pod.start(); } ``` See [storing secrets](#storing-secrets) for what the pepper in the example is and where to keep it. JWT-based authentication also needs a refresh endpoint. The app calls it to renew expired access tokens without asking the user to sign in again. Extend the abstract endpoint to expose it on the server. Create the file anywhere under your server's `lib/` directory, for example `_server/lib/src/endpoints/`. The generator picks it up: ```dart import 'package:serverpod_auth_idp_server/core.dart' as core; class RefreshJwtTokensEndpoint extends core.RefreshJwtTokensEndpoint {} ``` ### Token manager configuration Token managers issue and validate the tokens that keep users signed in. Configure at least one. The first one in the list is the primary manager, which issues new tokens. Any others are used to validate and manage the tokens they issued. Serverpod provides two built-in token manager builders: - `JwtConfig` to use JWT-based authentication. See [JWT token manager](https://docs.serverpod.dev/concepts/authentication/token-managers/jwt-token-manager.md) for details. - `ServerSideSessionsConfig` to use server-side sessions authentication. See [Server-side sessions token manager](https://docs.serverpod.dev/concepts/authentication/token-managers/server-side-sessions-token-manager.md) for details. For more details on how to configure token managers or create custom ones, see the dedicated [token managers](https://docs.serverpod.dev/concepts/authentication/token-managers/managing-tokens.md) documentation. ### Identity providers configuration Identity providers handle authentication with different methods (Email, Google, Apple, etc.). Each provider has its own configuration: - **[Email](https://docs.serverpod.dev/concepts/authentication/providers/email/setup.md)**: sign-up and sign-in with email and password. - **[Anonymous](https://docs.serverpod.dev/concepts/authentication/providers/anonymous/setup.md)** (experimental): accounts without any credentials, for trying the app before registering. - **[Google](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md)**, **[Apple](https://docs.serverpod.dev/concepts/authentication/providers/apple/setup.md)**, **[Facebook](https://docs.serverpod.dev/concepts/authentication/providers/facebook/setup.md)**, **[GitHub](https://docs.serverpod.dev/concepts/authentication/providers/github/setup.md)**, and **[Microsoft](https://docs.serverpod.dev/concepts/authentication/providers/microsoft/setup.md)**: sign-in with the respective account. - **[Firebase](https://docs.serverpod.dev/concepts/authentication/providers/firebase/setup.md)**: reuse Firebase Authentication, including its phone and social sign-ins. - **[Passkey](https://docs.serverpod.dev/concepts/authentication/providers/passkey/setup.md)** (experimental): passwordless sign-in with passkeys. - **[Custom providers](https://docs.serverpod.dev/concepts/authentication/providers/custom-providers/overview.md)**: build your own, including OAuth2-based ones. The list of identity providers keeps growing. If you want to contribute a new provider, see the [contribution guidelines](https://docs.serverpod.dev/contribute.md). By default, endpoints for all providers are disabled. To enable a provider: 1. Pass its config object to the `identityProviderBuilders` parameter of the `pod.initializeAuthServices()` method. ```dart pod.initializeAuthServices( tokenManagerBuilders: [ JwtConfigFromPasswords(), ], identityProviderBuilders: [ EmailIdpConfig( /* configuration options */ ), ], ); ``` :::tip Some providers need credentials from an external service, such as the Google client secret. The provider's config object takes these as required parameters. ::: 2. Extend the identity provider abstract endpoint. ```dart import 'package:serverpod_auth_idp_server/providers/email.dart'; class EmailIdpEndpoint extends EmailIdpBaseEndpoint {} ``` 3. Start the server with `serverpod start`. It generates the client code and endpoint methods for the provider, then runs the server with hot reload. ```bash $ serverpod start ``` 4. Create and apply the migration that initializes the database for the provider. In the `serverpod start` terminal, press **M**. The migration is created and applied in one step. If applying fails, press **A** to retry it. :::info If this is the first time creating migrations after adding the module, besides the provider tables, all authentication module tables will also be created. More detailed migration instructions can be found in the [migration guide](https://docs.serverpod.dev/concepts/data-and-the-database/database/migrations.md). ::: ### Storing secrets A pepper is a secret string mixed into values before they are hashed, so stored hashes cannot be brute-forced from a database leak alone. It is one application-wide secret, kept outside the database. Peppers and private keys should be stored securely. The example above uses `pod.getPassword()` which reads from your `config/passwords.yaml` file or environment variables in the format `SERVERPOD_PASSWORD_='value'`. Add secrets to `config/passwords.yaml`: ```yaml development: serverSideSessionKeyHashPepper: 'your-session-pepper-here' jwtRefreshTokenHashPepper: 'your-refresh-token-pepper-here' jwtHmacSha512PrivateKey: 'your-private-key-here' emailSecretHashPepper: 'your-email-pepper-here' googleClientSecret: '{"web":{"client_id":"...","client_secret":"...","redirect_uris":["..."]}}' # ... other secrets ``` Or use environment variables in the expected format: ```bash export SERVERPOD_PASSWORD_serverSideSessionKeyHashPepper='your-session-pepper-here' export SERVERPOD_PASSWORD_jwtRefreshTokenHashPepper='your-refresh-token-pepper-here' export SERVERPOD_PASSWORD_jwtHmacSha512PrivateKey='your-private-key-here' export SERVERPOD_PASSWORD_emailSecretHashPepper='your-email-pepper-here' export SERVERPOD_PASSWORD_googleClientSecret='{"web":{"client_id":"...","client_secret":"...","redirect_uris":["..."]}}' # ... other secrets ``` :::info Builders that need secrets have a `FromPasswords` variant that reads them from well-known key names, so you do not need to call `pod.getPassword()` yourself. Any other configuration options are still passed as parameters. For example: ```dart final jwtConfig = JwtConfigFromPasswords(); final serverSideSessionsConfig = ServerSideSessionsConfigFromPasswords(); final emailIdpConfig = EmailIdpConfigFromPasswords(); final googleIdpConfig = GoogleIdpConfigFromPasswords(); final appleIdpConfig = AppleIdpConfigFromPasswords(); final passkeyIdpConfig = PasskeyIdpConfigFromPasswords(); ``` ::: :::warning Never commit `config/passwords.yaml` to version control. Be sure to add it to your `.gitignore` file. Prefer environment variables or secure secret management in production. ::: ## Client setup The client is the generated Dart package that your app uses to call the server (the `_client` package in your project). Add the `serverpod_auth_idp_client` package to its `pubspec.yaml`. Use the same version numbers as for Serverpod itself for all dependencies. ```yaml dependencies: ... serverpod_auth_idp_client: 4.0.0 ``` ## App setup First, add these packages to your app's `pubspec.yaml` file. Some providers, such as Facebook and Firebase, need an extra package, which their setup page names. ```yaml dependencies: flutter: sdk: flutter serverpod_auth_idp_flutter: 4.0.0 serverpod_flutter: 4.0.0 your_client: path: ../your_client ``` Next, you need to set up a `FlutterAuthSessionManager`, which keeps track of the user's authentication state. It handles authentication tokens, token storage and refresh, and user session management. ```dart import 'package:flutter/material.dart'; import 'package:serverpod_flutter/serverpod_flutter.dart'; import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; import 'package:your_client/your_client.dart'; late Client client; void main() async { WidgetsFlutterBinding.ensureInitialized(); const serverUrl = 'http://localhost:8080/'; // Create the client with the auth session manager client = Client(serverUrl) ..connectivityMonitor = FlutterConnectivityMonitor() ..authSessionManager = FlutterAuthSessionManager(); // Initialize authentication (restores session from storage and validates) await client.auth.initialize(); runApp(MyApp()); } ``` The `FlutterAuthSessionManager` provides useful properties and methods for managing authentication state. :::info Web apps On the web, enable [cookie-based authentication](https://docs.serverpod.dev/concepts/authentication/web-authentication.md) so sign-in tokens are kept in `httpOnly` cookies instead of JavaScript-readable storage. ::: :::tip The `client.auth` getter is a shortcut for `client.authSessionManager`. If your project defines its own endpoint class named `AuthEndpoint`, the generated client uses the `auth` name for that endpoint instead. In that case, call `client.authSessionManager.initialize()` in the example above. ::: ### Initialize authentication The `initialize()` method restores any existing session from storage and validates it with the server. It should be called when your app starts: ```dart await client.auth.initialize(); ``` This is equivalent to calling `restore()` followed by `validateAuthentication()`. If the authentication is no longer valid, the user is automatically signed out. See [Client-side authentication](https://docs.serverpod.dev/concepts/authentication/basics.md#client-side-authentication) for more details on how to interact with the authentication state from the client. :::note macOS apps need a Keychain Sharing entitlement before authentication sessions can be stored. See [Set up authentication on macOS](https://docs.serverpod.dev/concepts/authentication/macos-authentication.md). ::: ### Web callback page (`auth.html`) :::note You only need this if your app targets the **web** platform and uses an identity provider that signs the user in through an OAuth2 redirect. That includes **GitHub**, **Microsoft**, **Google** on web, and custom OAuth2-based providers. Skip this section if your app does not target web, or if it only uses email, anonymous, passkey, Apple, Facebook, or Firebase sign-in. ::: When the user finishes signing in at the provider's page (for example, `accounts.google.com`), the provider redirects the browser to a URL on your site with the sign-in result attached. Your Flutter app cannot receive that redirect directly because the browser navigates fully away from it. The `auth.html` file is a small static page that catches the redirect, reads the result, and hands it back to your running Flutter app through `postMessage` (or `localStorage`, depending on how the sign-in was launched). You create one `auth.html` and share it across every identity provider that needs it. You have two ways to deliver it. If Serverpod serves your Flutter web app, register the `FlutterWebAuth2CallbackRoute` from `serverpod_auth_idp_server` on your web server and point the provider at that route. The page posts the result back to its own origin, so the browser only delivers it when your app is served from that same origin (same scheme, host, and port). Otherwise, host the file yourself. In your Flutter project's `web/` folder, add a file named `auth.html` with this content, which is identical to what the route serves: ```html Authentication complete

Authentication is complete. If this does not happen automatically, please close the window.

``` When you set up a provider that uses this callback, you will register the full URL of `auth.html` in **two** places, and they must match exactly: - **In the provider's OAuth client configuration**: for example, **Authorized redirect URIs** in Google Cloud Console, or **Authorization callback URL** in a GitHub OAuth app. - **In the Flutter sign-in initializer**, via the `redirectUri` argument (e.g., `client.auth.initializeGoogleSignIn(..., redirectUri: ...)`). When you host the file yourself, the URL is your Flutter web app's origin plus `/auth.html`. For example, `http://localhost:49660/auth.html` during local development or `https://yourdomain.com/auth.html` in production. When the server serves the page, the URL is that same origin plus the path you registered the route at, for example `https://yourdomain.com/auth/callback`. The provider's setup page walks through the exact values for that provider. ### Present the authentication UI The `serverpod_auth_idp_flutter` package provides a `SignInWidget` that automatically detects enabled identity providers and displays the appropriate sign-in options. ```dart import 'package:flutter/material.dart'; import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; import 'package:your_client/your_client.dart'; class SignInPage extends StatelessWidget { final Client client; const SignInPage({required this.client, super.key}); @override Widget build(BuildContext context) { return Scaffold( body: SignInWidget( client: client, onAuthenticated: () { // Do something when the user is authenticated. // // NOTE: You should not navigate to the home screen here, otherwise // the user will have to sign in again every time they open the app. }, onError: (error) { // Handle errors ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: $error')), ); }, ), ); } } ``` This widget is a convenient way to use identity providers out-of-the-box, but you can also fully customize it or replace it with your own implementation. See the [UI components](https://docs.serverpod.dev/concepts/authentication/ui-components.md) documentation for more details. #### Updating the UI based on authentication state Do not navigate to another screen from the `onAuthenticated` callback, or the user will have to sign in again every time they open the app. Instead, listen to authentication state changes with the `authInfoListenable` getter and switch screens based on the state. See [Client-side authentication](https://docs.serverpod.dev/concepts/authentication/basics.md#monitor-authentication-changes) for details. ## Related - [Get started](https://docs.serverpod.dev/concepts/authentication/get-started.md): the quick path for projects created with `serverpod create`. - [The basics](https://docs.serverpod.dev/concepts/authentication/basics.md): how authentication works on the server and in the app. - [Token managers](https://docs.serverpod.dev/concepts/authentication/token-managers/managing-tokens.md): choose between JWT and server-side sessions. - [Web setup](https://docs.serverpod.dev/concepts/authentication/web-authentication.md): keep web sign-in tokens in httpOnly cookies. - [UI components](https://docs.serverpod.dev/concepts/authentication/ui-components.md): customize or replace the sign-in UI. --- # Authentication basics https://docs.serverpod.dev/concepts/authentication/basics Serverpod automatically checks if the user is signed in and if the user has the right privileges to access each endpoint. When using the authentication module, tokens are tracked, refreshed, and included in requests for you. The `Session` object provides information about the current user. A `Session` is the request context that every endpoint method receives. It is not the user's login session. Access the current authentication through the synchronous `authenticated` getter of the `Session` object. It exposes a `userIdentifier`, a `String` that uniquely identifies the signed-in user. Use this id whenever you refer to a user. ```dart Future myMethod(Session session) async { final authenticationInfo = session.authenticated; final userIdentifier = authenticationInfo?.userIdentifier; ... } ``` You can also use the Session object to check if a user is authenticated: ```dart Future myMethod(Session session) async { var isSignedIn = session.isUserSignedIn; ... } ``` ## Requiring authentication on endpoints To restrict an endpoint to signed-in users, override the `requireLogin` property of the `Endpoint` class. ```dart class MyEndpoint extends Endpoint { @override bool get requireLogin => true; Future myMethod(Session session) async { ... } ... } ``` ## Explicitly allowing unauthenticated access In some cases, you may want to explicitly allow certain endpoints or methods to be accessed without authentication. Serverpod provides the `@unauthenticatedClientCall` annotation for this purpose. When an endpoint or method is annotated with `@unauthenticatedClientCall`: - No authentication will be added to the header on the client when calling it. - The server will treat the call as unauthenticated, regardless of any existing authentication state. You can use this annotation in two ways: 1. On the entire endpoint class to make all methods unauthenticated: ```dart @unauthenticatedClientCall class UnauthenticatedEndpoint extends Endpoint { Future someMethod(Session session) async { return session.isUserSignedIn; // Will always return false } Stream someStream(Session session) async* { yield session.isUserSignedIn; // Will always return false } } ``` 2. On specific methods to make only those methods unauthenticated: ```dart class PartiallyUnauthenticatedEndpoint extends Endpoint { @unauthenticatedClientCall Future publicMethod(Session session) async { return session.isUserSignedIn; // Will always return false } Future authenticatedMethod(Session session) async { return session.isUserSignedIn; } } ``` This is particularly useful for endpoints that must not receive authentication, such as the [JWT refresh endpoint](https://docs.serverpod.dev/concepts/authentication/token-managers/jwt-token-manager.md). :::warning Using `@unauthenticatedClientCall` on an endpoint or method that also has `requireLogin` set to true will lead to a conflict. The client suppresses the authentication header, but the server expects it, so calls to such endpoints or methods always fail with an authentication error. ::: ## Authorization on endpoints Scopes define what an authenticated user is allowed to do. Each scope is an independent capability identified by a string name. Scopes do not inherit from each other. Holding `Scope.admin` does not grant any other scope. Each endpoint exposes two properties for access control: - `requireLogin`: the user must be signed in, with no specific scope required. - `requiredScopes`: the user must hold every scope in the set. To restrict access by scope, override the `requiredScopes` property: ```dart class MyEndpoint extends Endpoint { @override Set get requiredScopes => {Scope.admin}; Future myMethod(Session session) async { ... } ... } ``` Serverpod ships with `Scope.admin` for built-in admin functionality in Serverpod modules. Define your own scope names for application-specific access control. When `requiredScopes` is non-empty, authentication is required even if `requireLogin` is false. ### Custom scopes Define constants for your domain by extending the `Scope` class: ```dart class CustomScope extends Scope { const CustomScope(String name) : super(name); static const userRead = CustomScope('userRead'); static const userWrite = CustomScope('userWrite'); } ``` Then use the custom scopes on your endpoints: ```dart class MyEndpoint extends Endpoint { @override Set get requiredScopes => {CustomScope.userRead, CustomScope.userWrite}; Future myMethod(Session session) async { ... } ... } ``` The user must hold both `userRead` and `userWrite` to access this endpoint. You can reuse the same scope constants across multiple endpoints to build different access combinations. Keep scope names stable once deployed, as renaming a scope will revoke it from any user who had it. You can also define shared scope requirements in a base endpoint class. See [Endpoint inheritance](https://docs.serverpod.dev/concepts/endpoints-and-apis/endpoint-inheritance.md) for details. :::caution A scope is identified by its string name only. If two scope constants share the same string, granting one also grants the other, since the server cannot tell them apart. Give every scope a unique string. ::: ### How scopes combine When an endpoint lists multiple scopes in `requiredScopes`, the user must have all of them. Serverpod evaluates scopes with AND logic. There is no OR matching and no scope hierarchy. Consider a user management feature with two admin endpoints: one for aggregated user analytics and one for editing user data. Not every admin should be able to modify users, so the endpoints require different scope combinations: ```dart class UserAnalyticsEndpoint extends Endpoint { @override Set get requiredScopes => {Scope.admin}; Future getStats(Session session) async { ... } } class UserEditEndpoint extends Endpoint { @override Set get requiredScopes => { Scope.admin, CustomScope.userWrite, }; Future updateUser(Session session, UserData data) async { ... } } ``` An admin user with only `Scope.admin` can call `UserAnalyticsEndpoint` but not `UserEditEndpoint`. To allow editing, grant the user both scopes, as shown in [Managing scopes](#managing-scopes) below. This lets you compose capabilities at the endpoint level instead of building nested roles. ### Managing scopes New users are created without any scopes. To update a user's scopes, use the `update` method from `AuthServices.instance.authUsers`. This method replaces all previously stored scopes: ```dart import 'package:serverpod_auth_idp_server/core.dart'; await AuthServices.instance.authUsers.update( session, authUserId: authUserId, scopes: {Scope.admin}, ); ``` Changing a user's scopes does not affect existing sessions or tokens until the user signs in again or their tokens are revoked. Serverpod closes open method streams when a revoked scope overlaps what the endpoint requires. See [Managing tokens](https://docs.serverpod.dev/concepts/authentication/token-managers/managing-tokens.md#revoking-tokens) for revoking tokens across devices. ### HTTP responses When access is denied, Serverpod returns: - **401 Unauthorized**: no token was provided, or the token is invalid. - **403 Forbidden**: the user is authenticated but missing one or more required scopes. ## Client-side authentication On the client side, authentication state is managed through the `FlutterAuthSessionManager`, which is accessible via `client.auth`. :::note On macOS, the `FlutterAuthSessionManager` stores tokens in the Keychain. New projects need a Keychain Sharing entitlement before sign-in works. See [Set up authentication on macOS](https://docs.serverpod.dev/concepts/authentication/macos-authentication.md). ::: :::note If you are building a pure Dart application, use the `ClientAuthSessionManager` from the `serverpod_auth_core_client` package instead. It works the same way, except it has no `authInfoListenable` getter, which is tied to the Flutter framework. ::: ### Check authentication state To check if the user is signed in: ```dart bool isSignedIn = client.auth.isAuthenticated; ``` Returns `true` if the user is signed in, or `false` otherwise. ### Access current authentication info To retrieve information about the current authentication: ```dart AuthSuccess? authInfo = client.auth.authInfo; ``` Returns an `AuthSuccess` object if the user is currently signed in, or `null` if the user is not. ### Register authentication To register a signed-in user, call: ```dart await client.auth.updateSignedInUser(authInfo); ``` This persists the authentication information. Identity providers call this method to register a signed-in user. For more details on providers, see [Custom Providers](https://docs.serverpod.dev/concepts/authentication/providers/custom-providers/overview.md). When the signed-in user changes, open method streams close, and new streams connect as the current user. A token refresh for the same user keeps them open. ### Monitor authentication changes The `FlutterAuthSessionManager` exposes `authInfoListenable`, a `ValueListenable`. Listen to it to update the UI whenever the user signs in or out: ```dart @override void initState() { super.initState(); // Listen to authentication state changes. client.auth.authInfoListenable.addListener(_onAuthStateChanged); } // Don't forget to remove the listener when the widget is disposed. @override void dispose() { client.auth.authInfoListenable.removeListener(_onAuthStateChanged); super.dispose(); } void _onAuthStateChanged() { setState(() { // UI will rebuild when auth state changes. }); } ``` The listener is triggered whenever the user's sign-in state changes. ### Validate the session and handle expiry Serverpod refreshes tokens automatically while the user stays signed in, so most apps never handle tokens directly. Expiry becomes visible only when the refresh token itself has expired or been revoked: the client can no longer refresh, and the stored session is no longer valid. Call `validateAuthentication` to check the current session against the server and sign the user out if it is no longer valid: ```dart await client.auth.validateAuthentication(); // throws on transient errors; retry if needed ``` The method force-refreshes the token and confirms with the server that the user is still signed in. If the session is no longer valid, it signs the user out on the current device. A transient problem, such as a network error or timeout, does not sign the user out. The exception is thrown instead, so you can catch it and retry. At app startup, use `initialize` to restore a stored session and validate it in one step: ```dart await client.auth.initialize(); ``` The `initialize` method runs `restore` followed by `validateAuthentication`. It returns `true` when validation completed, which is not the same as the user being signed in. Read `client.auth.isAuthenticated` for that. If the stored session has expired, the user is signed out. If validation cannot complete because of a network or server error, `initialize` returns `false` and leaves the stored session in place so you can retry later, which keeps offline users signed in. If the validation call times out, the timeout is thrown instead, so catch it if you want the same retry behavior. Because signing out updates the authentication state, a listener registered on `authInfoListenable` (see [Monitor authentication changes](#monitor-authentication-changes)) fires when a session expires, so you can route the user back to a sign-in screen from one place. ## Signing out users The `FlutterAuthSessionManager` provides methods for handling user sign-outs, whether from a single device or all devices. They call the `StatusEndpoint` methods under the hood, which are also directly accessible on the client through the `client.modules.serverpod_auth_core.status` getter. For revoking authentication across devices from the server, see [Managing tokens](https://docs.serverpod.dev/concepts/authentication/token-managers/managing-tokens.md#revoking-tokens). ### Sign out current device To sign the user out from the current device: ```dart await client.auth.signOutDevice(); ``` Returns `true` if the server call succeeds, or `false` if it fails. Either way, the app clears its local authentication state. ### Sign out all devices To sign the user out across all devices: ```dart await client.auth.signOutAllDevices(); ``` Returns `true` if the user is successfully signed out from all devices, or `false` if it fails. Either way, the app clears its local authentication state. ## Related - [Setup](https://docs.serverpod.dev/concepts/authentication/setup.md): install and configure the authentication module. - [Working with users](https://docs.serverpod.dev/concepts/authentication/working-with-users.md): user profiles, callbacks, and admin operations. - [Token managers](https://docs.serverpod.dev/concepts/authentication/token-managers/managing-tokens.md): how tokens are issued, validated, and revoked. - [Custom overrides](https://docs.serverpod.dev/concepts/authentication/custom-overrides.md): replace the built-in authentication with your own. --- # Working with users https://docs.serverpod.dev/concepts/authentication/working-with-users The authentication module provides convenient ways to work with your authenticated users and their related profile data. ## Authenticated users All authenticated users have an authentication identifier that uniquely identifies them across the server. This can be retrieved from the `session` object as a `String` through the `userIdentifier` property or as a `UuidValue` from the `authUserId` extension provided by the authentication module. ```dart var userIdString = session.authenticated?.userIdentifier; // requires `import 'package:serverpod_auth_idp_server/core.dart';` var userIdUuidValue = session.authenticated?.authUserId; ``` Further operations on the authenticated user can be performed using the `AuthUsers` class which is provided by the `AuthServices` instance. ```dart await AuthServices.instance.authUsers.delete(session, authUserId: userIdUuidValue!); ``` For the full list of operations, see the [AuthUsers](https://pub.dev/documentation/serverpod_auth_core_server/latest/serverpod_auth_core_server/AuthUsers-class.html) class documentation. ## Blocking users You can block users to prevent them from signing in to your app. When a blocked user attempts to authenticate, an `AuthUserBlockedException` will be thrown, and the authentication will fail. ### Blocking or unblocking a user To block/unblock a user, use the `update` method of the `AuthUsers` class: ```dart await AuthServices.instance.authUsers.update( session, authUserId: authUserId, blocked: true, // or false to unblock ); ``` Users can also be created with the blocked status set from the start: ```dart await AuthServices.instance.authUsers.create( session, blocked: true, ); ``` :::note When a user is blocked, they will not be able to sign in until they are unblocked. However, blocking a user does not automatically revoke their existing tokens. Be sure to revoke existing sessions for a complete block operation. See [Revoking tokens](https://docs.serverpod.dev/concepts/authentication/token-managers/managing-tokens.md#revoking-tokens) for more details. ::: ## User creation callbacks You can react when an auth user is created and control their initial scopes and blocked status by using the `AuthUsersConfig` callbacks. Configure them when initializing auth services on the `pod` object. :::warning Both callbacks receive a `transaction` parameter that should be used on all operations performed inside the callback. Failing to pass the transaction to database operations might lead to entries not being found or changes not being rolled back together. ::: ### Reacting to the user created event Use the `onAfterAuthUserCreated` callback to run logic after a new auth user has been created (for example, to create related domain data or send a welcome notification). The callback receives the current session, the newly created auth user, and the ongoing transaction. ```dart pod.initializeAuthServices( ... authUsersConfig: AuthUsersConfig( onAfterAuthUserCreated: (session, authUser, {required transaction}) { // Do something with the new auth user (e.g. create related data) }, ), ); ``` ### Setting default scopes and blocked status Use the `onBeforeAuthUserCreated` callback to set default scopes or blocked status for new auth users. The callback receives the session, the scopes and blocked value that would be used by default, and the transaction. Return a record with the `scopes` and `blocked` values you want to apply. You can add or remove scopes, or force the user to be blocked. ```dart pod.initializeAuthServices( ... authUsersConfig: AuthUsersConfig( onBeforeAuthUserCreated: (session, scopes, blocked, {required transaction}) { // Set default scopes (e.g. add Scope.admin) and optionally block the user return (scopes: {...scopes, Scope.admin}, blocked: blocked); }, ), ); ``` ## User profiles Authenticated users get a profile with information about the signed-in user. Read it with the `userProfile` extension on `AuthenticationInfo`. The extension returns a `UserProfileModel`, which is the read model sent to the app. The `UserProfile` class itself is only used for database access. ```dart var userProfile = await session.authenticated?.userProfile(session); ``` The result is `null` only when no user is signed in. If the user is signed in but has no profile, the call throws `UserProfileNotFoundException`. The profile contains a basic set of information about the user, such as their full name, email address, and profile picture. This information is automatically populated when the user signs in. Based on the authentication method used, different data may be available. It's a common task to read or update user information on your server. The `UserProfiles` class provides many convenient methods for working with user profiles and is accessible through the `AuthServices` instance. ```dart await AuthServices.instance.userProfiles.changeFullName(session, authUserId, 'my name'); ``` For the full list of operations, see the [UserProfiles](https://pub.dev/documentation/serverpod_auth_core_server/latest/serverpod_auth_core_server/UserProfiles-class.html) class documentation. ### User profile callbacks You can react when a user profile is created or updated by using the `UserProfileConfig` callbacks. Configure them when initializing auth services on the `pod` object via the `userProfileConfig` parameter. :::warning All callbacks receive a `transaction` parameter that should be used on all database operations performed inside the callback. Failing to pass the transaction might lead to entries not being found or changes not being rolled back together. ::: | Callback | When | Purpose | | ---------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------- | | `onBeforeUserProfileCreated` | Before a profile is created | Receive and return `UserProfileData` to modify defaults (user name, full name, email). | | `onAfterUserProfileCreated` | After a profile is created | Run logic that depends on the new profile (e.g. related data, default image). | | `onBeforeUserProfileUpdated` | Before a profile is updated (name, full name, or picture) | Receive and return `UserProfileData` to enforce rules or transform values. | | `onAfterUserProfileUpdated` | After a profile is updated | Sync related data or trigger side effects. | Example using `onAfterUserProfileCreated`: ```dart pod.initializeAuthServices( ... userProfileConfig: UserProfileConfig( onAfterUserProfileCreated: (session, userProfile, {required transaction}) async { // Do something with the new profile (e.g. create related data, set default image) }, ), ); ``` ### Accessing user profiles from the app To access the user profile from your Flutter app, you can use the `userProfileInfo` endpoint that is included in the authentication module: ```dart final userProfile = await client.modules.serverpod_auth_core.userProfileInfo.get(); ``` This returns a `UserProfileModel` object containing the signed-in user's profile information such as their name, email, and profile picture. ### Extending the user profile edit endpoint The authentication module provides a `UserProfileEditBaseEndpoint` abstract class that you can extend to expose user profile editing functionality to your app. This base endpoint includes methods for: - Removing user images - Setting user images - Changing user names - Changing full names For a step-by-step guide to profile photo upload, display, and storage, see [Manage user profile photos](https://docs.serverpod.dev/concepts/authentication/profile-photos.md). To enable profile editing in your app, create a concrete endpoint class on your server by extending `UserProfileEditBaseEndpoint`: ```dart import 'package:serverpod/serverpod.dart'; import 'package:serverpod_auth_idp_server/core.dart'; class UserProfileEditEndpoint extends UserProfileEditBaseEndpoint {} ``` This endpoint will have both `get` (same as `userProfileInfo`) and all profile editing methods. You can then call these methods from your Flutter app: ```dart // Get the user's profile final profile = await client.userProfileEdit.get(); // Change the user's full name final updatedProfile = await client.userProfileEdit.changeFullName('John Doe'); // Change the user's username final updatedProfile = await client.userProfileEdit.changeUserName('johndoe'); // Remove the user's profile image final updatedProfile = await client.userProfileEdit.removeUserImage(); // Set a new profile image final ByteData imageData = // ... load image data final updatedProfile = await client.userProfileEdit.setUserImage(imageData); ``` :::note The `UserProfileEditBaseEndpoint` requires authentication and operates on the currently authenticated user's profile. ::: You can also extend the endpoint class to add custom profile editing functionality: ```dart class UserProfileEditEndpoint extends UserProfileEditBaseEndpoint { Future myCustomProfileEdit(Session session, String bio) async { final userProfile = await session.authenticated?.userProfile(session); // Your custom logic here... return userProfile; } } ``` ### Setting a default user image Some providers, such as Google, expose the user's picture. In that case it is fetched and set as the profile picture automatically. When no picture is available, you can generate a default image with the `onAfterUserProfileCreated` callback in `UserProfileConfig` (see [User profile callbacks](#user-profile-callbacks) for the full set). For the configuration example and the image settings, see [Profile photos](https://docs.serverpod.dev/concepts/authentication/profile-photos.md#configure-image-size-and-format). ## Attaching additional information The recommended way to attach additional information to an authenticated user is to use a relation in the database. This makes it easy to query the data later based on the user's authentication identifier. ```yaml class: MyDomainData table: my_domain_data fields: ### The [AuthUser] this profile belongs to authUser: module:serverpod_auth_core:AuthUser?, relation(onDelete=Cascade) additionalInfo: String indexes: auth_user_id_unique_idx: fields: authUserId unique: true ``` The model above creates a relation to the `AuthUser` table and ensures that each user can only have one `MyDomainData` object. The `onDelete=Cascade` ensures that when the `AuthUser` is deleted, the `MyDomainData` object is also deleted. :::note The `AuthUser` model is declared in the `serverpod_auth_core` module, which is automatically included in your project as a dependency of the `serverpod_auth_idp` module. If you are not ignoring the generated files in your `analysis_options.yaml`, you might need to explicitly add the `serverpod_auth_core` module to your project to prevent `depend_on_referenced_packages` lint errors. The general recommendation, however, is to ignore linting on generated files: ```yaml # analysis_options.yaml analyzer: exclude: - lib/src/generated/** ``` ::: :::tip When referencing module classes in your model files, you can use a nickname for the module instead of the full module name. See the [modules documentation](https://docs.serverpod.dev/concepts/server-fundamentals/modules.md) for more information. ::: Query the additional information with the user's `authUserId`: ```dart final authUserId = session.authenticated?.authUserId; final additionalInfo = await MyDomainData.db.findFirstRow( session, where: (t) => t.authUserId.equals(authUserId!), ); ``` ## Merging accounts When a user adds a sign-in method that already belongs to a different account, the two accounts have to become one. Serverpod does not merge them on its own: you decide when to offer the merge, and run it once the user accepts. A merge runs as an ordered list of hooks (declared in `mergeHooks`). All hooks run inside one transaction, so a failure at any step leaves both accounts untouched. By default, `AccountMergeConfig` fills the list with these hooks: | Hook | What it does | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `defaultIdpMergeHandler` | Calls `mergeAuthUsers` on every initialized identity provider (e.g. Email, Google, Apple, etc.). | | `defaultCoreDataMergeHandler` | Merges the scopes and the blocked flag of the `AuthUser` records, moves the refresh tokens and server-side sessions, and merges the user profiles. A user blocked on either account stays blocked after the merge. | | `applicationMergeHandler` | Moves your application's own data. **This is the one you write.** | | `defaultMergeCleanupHandler` | Deletes the `AuthUser` that was merged away. | ### Configure the merge Your application's own data is the part Serverpod cannot move for you. Pass the `applicationMergeHandler` to `pod.initializeAuthServices()` that reassigns your rows from the removed user to the kept one: ```dart pod.initializeAuthServices( tokenManagerBuilders: [...], accountMergeConfig: AccountMergeConfig( applicationMergeHandler: ( Session session, { required UuidValue userToKeepId, required UuidValue userToRemoveId, required Transaction transaction, }) async { await MyDomainData.db.updateWhere( session, where: (t) => t.authUserId.equals(userToRemoveId), columnValues: (t) => [t.authUserId(userToKeepId)], transaction: transaction, ); }, ), ); ``` Your handler only needs to move data over. The `defaultMergeCleanupHandler` runs after it and deletes the removed `AuthUser`, which cascades to all rows that reference it with `onDelete=Cascade`. Without a handler, merging throws. That is the default for applications that never merge accounts. To reorder the built-in hooks or replace one of them, use `AccountMergeConfig.custom` and pass the full list yourself. ### Merge two users ```dart await AuthServices.instance.accountMerger.merge( session, userToKeepId: userToKeepId, userToRemoveId: userToRemoveId, ); ``` Both users must exist and be different from each other, otherwise the call throws. ## Related - [The basics](https://docs.serverpod.dev/concepts/authentication/basics.md): authentication state, scopes, and endpoint access control. - [Profile photos](https://docs.serverpod.dev/concepts/authentication/profile-photos.md): upload, display, and default profile images. - [Setup](https://docs.serverpod.dev/concepts/authentication/setup.md): configure the authentication services these callbacks hook into. - [Creating an OAuth2-based identity provider](https://docs.serverpod.dev/concepts/authentication/providers/custom-providers/oauth2-utility/creating-an-oauth2-based-identity-provider.md): implement `mergeAuthUsers` so a custom provider takes part in a merge. --- # Set up anonymous sign-in https://docs.serverpod.dev/concepts/authentication/providers/anonymous/setup :::warning The anonymous identity provider is **experimental**. It doesn't support account linking yet. To move an anonymous user's data to an account from another provider, merge the accounts. See [Merging accounts](https://docs.serverpod.dev/concepts/authentication/working-with-users.md#merging-accounts). ::: To properly configure anonymous authentication, you must allow anonymous access in your Serverpod auth configuration. :::caution You need to install the auth module before you continue, see [Setup](https://docs.serverpod.dev/concepts/authentication/setup.md). ::: ## Server-side configuration In your main `server.dart` file, configure the anonymous identity provider using the `AnonymousIdpConfig` object and add it to your `pod.initializeAuthServices()` configuration: ```dart import 'package:serverpod_auth_idp_server/core.dart'; import 'package:serverpod_auth_idp_server/providers/anonymous.dart'; import 'src/generated/serverpod.dart'; void run(List args) async { final pod = Serverpod(args); pod.initializeAuthServices( tokenManagerBuilders: [ JwtConfigFromPasswords(), ], identityProviderBuilders: [ // Configure the anonymous identity provider AnonymousIdpConfig(), ], ); await pod.start(); } ``` Then, extend the abstract endpoint to expose it on the server: ```dart import 'package:serverpod_auth_idp_server/providers/anonymous.dart'; class AnonymousIdpEndpoint extends AnonymousIdpBaseEndpoint {} ``` Then, start the server with `serverpod start` to generate the client code, then create and apply the migration that initializes the database for the provider (in the `serverpod start` terminal, press **M**). More detailed instructions can be found in the general [identity providers setup section](https://docs.serverpod.dev/concepts/authentication/setup.md#identity-providers-configuration). ### Basic configuration options Although the anonymous identity provider can be used directly with no other configuration, it is recommended to add some form of app attestation to prevent abuse in production environments. See the [Using a token for app attestation section](https://docs.serverpod.dev/concepts/authentication/providers/anonymous/customizations.md#using-a-token-for-app-attestation) for more details. For other configuration options such as callbacks (before/after account creation) and rate limiting, see the [customizations page](https://docs.serverpod.dev/concepts/authentication/providers/anonymous/customizations.md). ## Client-side configuration If you have configured the `SignInWidget` as described in the [setup section](https://docs.serverpod.dev/concepts/authentication/setup.md#present-the-authentication-ui), the anonymous identity provider will be automatically detected and displayed in the sign-in widget as a "Continue without account" option. You can also use the `AnonymousSignInWidget` to include anonymous sign-in in your own custom UI: ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; AnonymousSignInWidget( client: client, onAuthenticated: () { // Do something when the user is authenticated. // // NOTE: You should not navigate to the home screen here, otherwise // the user will have to sign in again every time they open the app. }, onError: (error) { // Handle errors ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: $error')), ); }, ) ``` The widget displays a "Continue without account" button that creates an anonymous session when pressed. For details on customizing the button (size, shape), using a custom widget with `SignInWidget`, or building a fully custom UI with `AnonymousAuthController`, see the [customizations page](https://docs.serverpod.dev/concepts/authentication/providers/anonymous/customizations.md#customize-the-sign-in-button). --- # Customize anonymous sign-in https://docs.serverpod.dev/concepts/authentication/providers/anonymous/customizations This page covers configuration options and UI customizations for the anonymous identity provider beyond the basic setup. On the server, you can tie sign-in to an app attestation token, react to new account creation, and adjust the rate limit. In the app, you can use the `AnonymousSignInWidget` to display the anonymous sign-in button in your own layout, or the `AnonymousAuthController` to build a completely custom authentication interface. ## Server configuration All server-side options for the anonymous provider are set on the `AnonymousIdpConfig`. ### Using a token for app attestation The anonymous `login` endpoint accepts an optional **token** that is forwarded to your `onBeforeAnonymousAccountCreated` callback. This lets you tie anonymous sign-in to an app attestation or app-check provider (e.g. [Firebase App Check](https://firebase.google.com/docs/app-check)) so only requests from your real app can create anonymous accounts. :::warning Using the anonymous provider without a token for app attestation is not recommended due to the risk of abuse. Make sure to configure an attestation before releasing your app to the public. ::: #### Configuring the server In `onBeforeAnonymousAccountCreated`, receive the optional `token` and verify it with your app-check provider. If verification fails or the token is missing (when you require it), throw an `AnonymousAccountBlockedException` with reason `denied` to block account creation. ```dart AnonymousIdpConfig( onBeforeAnonymousAccountCreated: ( Session session, { String? token, required Transaction? transaction, }) async { if (token == null || token.isEmpty) { throw AnonymousAccountBlockedException( reason: AnonymousAccountBlockedExceptionReason.denied, ); } // Verify the token with your app-check provider (e.g. Firebase App Check). // Example: call Firebase's verifyAppCheckToken REST API or your provider's // verification endpoint. If invalid, throw AnonymousAccountBlockedException. final isValid = await _verifyAppCheckToken(session, token); if (!isValid) { throw AnonymousAccountBlockedException( reason: AnonymousAccountBlockedExceptionReason.denied, ); } }, ) ``` For Firebase App Check, you can verify the token from a custom backend using the [Firebase App Check REST API](https://firebase.google.com/docs/app-check/custom-resource-backend) (`verifyAppCheckToken`). Other app-check or attestation providers can be integrated the same way. The app sends a token, and the server validates it in the callback and denies creation if it is invalid. For the app side of this flow, see [Configuring the Flutter app](#configuring-the-flutter-app). ### Reacting to anonymous account creation Besides the `onBeforeAnonymousAccountCreated` callback to allow or deny creation, you can also use the `onAfterAnonymousAccountCreated` callback to run logic after a new anonymous account has been created (e.g. analytics or side effects). ```dart AnonymousIdpConfig( onAfterAnonymousAccountCreated: ( Session session, { required UuidValue authUserId, required Transaction? transaction, }) async { // e.g. track creation for analytics or send to your logging service }, ) ``` ### Rate limiting The anonymous provider includes built-in rate limiting per IP address to prevent abuse. The default is 100 anonymous account creations per hour per IP. You can customize the rate limit in the `AnonymousIdpConfig` using the `perIpAddressRateLimit` parameter: ```dart // RateLimit is exported by the email provider library. import 'package:serverpod_auth_idp_server/providers/email.dart'; AnonymousIdpConfig( perIpAddressRateLimit: const RateLimit( maxAttempts: 50, timeframe: Duration(hours: 1), ), ) ``` When the limit is exceeded, the provider throws an `AnonymousAccountBlockedException` with reason `tooManyAttempts`. ## App configuration On the app side, the main configuration is supplying the attestation token described in [Using a token for app attestation](#using-a-token-for-app-attestation). ### Configuring the Flutter app Obtain a token from your app-check provider and pass it to the login call by setting `createAnonymousToken` on `AnonymousSignInWidget` or `AnonymousAuthController`. That callback is invoked when the user taps "Continue without account". The returned token is sent to the server as the `token` argument of the anonymous login endpoint. ```dart import 'package:firebase_app_check/firebase_app_check.dart'; import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; AnonymousSignInWidget( client: client, createAnonymousToken: () async { // Get a Firebase App Check token (or similar) to prove the request comes // from your app to prevent abuse. final appCheckToken = await FirebaseAppCheck.instance.getToken(); return appCheckToken; }, onAuthenticated: () { /* ... */ }, onError: (error) { /* ... */ }, ) ``` ## Customize the sign-in button When the button renders inside a `SignInWidget`, fields set on `buttonStyle` override its same-named arguments, as described in [Styling the buttons](https://docs.serverpod.dev/concepts/authentication/ui-components.md#styling-the-buttons). :::info The `SignInWidget` uses the `AnonymousSignInWidget` internally when the anonymous provider is enabled. You can supply a custom `AnonymousSignInWidget` to the `SignInWidget` to override the default (e.g. to pass `createAnonymousToken` or change size and shape). ```dart SignInWidget( client: client, anonymousSignInWidget: AnonymousSignInWidget( client: client, createAnonymousToken: () async => await getAppCheckToken(), size: SignInButtonSize.medium, shape: SignInButtonShape.rectangular, // A custom widget replaces the built-in handling, so pass your own callbacks. onAuthenticated: () { /* ... */ }, onError: (error) { /* ... */ }, ), ) ``` ::: ### Using the `AnonymousSignInWidget` The `AnonymousSignInWidget` displays a single "Continue without account" button that starts the anonymous sign-in flow when pressed. You can customize the widget's behavior and appearance using its constructor parameters: ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; AnonymousSignInWidget( client: client, createAnonymousToken: () async { // Optional: provide a token for app attestation (e.g. Firebase App Check) return await getAppCheckToken(); }, onAuthenticated: () { // Do something when the user is authenticated. // // NOTE: You should not navigate to the home screen here, otherwise // the user will have to sign in again every time they open the app. }, onError: (error) { // Handle errors }, // Button customization. The values shown are the defaults. size: SignInButtonSize.large, // or medium, small shape: SignInButtonShape.pill, // or rounded, rectangular ) ``` Optionally, you can provide an externally managed `AnonymousAuthController` instance to the widget. A controller and a `client` are mutually exclusive, and `onAuthenticated` and `onError` belong on the controller in that case. Passing them alongside a controller trips an assertion, so a debug build throws. ```dart AnonymousSignInWidget( controller: controller, size: SignInButtonSize.medium, shape: SignInButtonShape.rectangular, ) ``` #### Customizing the button appearance The button renders flat, with no background fill and no border, and follows your app's theme brightness for its label color. It sets its own colors and corner radius, so a `TextButtonThemeData` does not reach it and an `ElevatedButtonThemeData` cannot change those. Properties the button leaves unset, such as `side` and `textStyle`, still fall through from that theme. To change the label's text style, pass `textStyle` to the widget: ```dart AnonymousSignInWidget( client: client, textStyle: const TextStyle(fontWeight: FontWeight.w600), ) ``` Inside a `SignInWidget`, style every provider button at once with `buttonStyle` instead. See [Styling the buttons](https://docs.serverpod.dev/concepts/authentication/ui-components.md#styling-the-buttons). The button is at least 240 pixels wide and at most 400. Place it in a `SizedBox`, `Expanded`, or `Flex` to control layout. ## Build a custom UI with AnonymousAuthController For full control over the UI, use the `AnonymousAuthController` class. It provides the anonymous sign-in logic without any built-in widget, so you can trigger login from your own button or flow and build a completely custom layout. ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; final controller = AnonymousAuthController( client: client, createAnonymousToken: () async => await getAppCheckToken(), onAuthenticated: () { // Do something when the user is authenticated. }, onError: (error) { // Handle errors }, ); ``` ### AnonymousAuthController state management The controller notifies listeners when its state changes. Use these properties to drive your UI: ```dart // Check if a request is in progress final isLoading = controller.isLoading; // Check current state (idle, loading, error, authenticated) final state = controller.state; // Listen to state changes controller.addListener(() { setState(() { // Rebuild when controller state changes }); }); ``` ### AnonymousAuthController methods The controller exposes a single action for anonymous sign-in: ```dart // Start anonymous sign-in. // Obtains token if `createAnonymousToken` is set, then calls the login endpoint. await controller.login(); ``` Call `controller.login()` from your custom button's `onPressed`, or from any other trigger (e.g. after a delay or when the user performs an action). The controller handles loading state, success, and errors and invokes `onAuthenticated` or `onError` as appropriate. :::tip Remember to dispose the controller when it is no longer needed (e.g. in your widget's `dispose`), unless the widget manages its own controller and disposes it for you. ::: ## Related - [Setup](https://docs.serverpod.dev/concepts/authentication/providers/anonymous/setup.md): enable the anonymous provider on the server and show the sign-in button in your app. - [UI components](https://docs.serverpod.dev/concepts/authentication/ui-components.md): the shared sign-in screen and styling for all provider buttons. - [Working with users](https://docs.serverpod.dev/concepts/authentication/working-with-users.md): access the authenticated user and profile data on the server. --- # Set up email sign-in https://docs.serverpod.dev/concepts/authentication/providers/email/setup Sign in with Email verifies the user's address with a code, both when they register and when they reset their password. :::caution You need to install the auth module before you continue, see [Setup](https://docs.serverpod.dev/concepts/authentication/setup.md). ::: ## Server-side configuration Newly generated projects already configure the email identity provider in `pod.initializeAuthServices()` in your main `server.dart` file: ```dart pod.initializeAuthServices( tokenManagerBuilders: [ JwtConfigFromPasswords(), ], identityProviderBuilders: [ ServerpodCloudEmailIdpConfig( appDisplayName: 'My App', ), ], ); ``` Set `appDisplayName` to the name recipients should see in the verification emails. In the `development` and `test` run modes the codes are written to the server log instead of being sent, so you can complete the flow locally. In the `staging` and `production` run modes, the codes are sent as email through the Serverpod Cloud email service. Sending is authenticated with the `scloudAuthEmailKey` password, which Serverpod Cloud sets for you on deploy. The password is read when an email is sent rather than at startup, so a self-hosted server still starts without it, but it cannot deliver any codes. If you host the server yourself, switch to [your own email provider](#use-your-own-email-provider) before you go to staging or production. If a code cannot be sent, the failure is recorded in the session log and the sign-in flow continues unchanged, so check your [server logs](https://docs.serverpod.dev/concepts/operations/logging.md) when a user reports a missing code. ### Expose the endpoints Newly generated projects already include the email endpoint at `lib/src/auth/email_idp_endpoint.dart` and the migration that initializes the database, so running `serverpod start` is all that is needed. If you are adding the auth module to an existing project, extend the abstract endpoint yourself. Create the file anywhere under your server's `lib/` directory (for example, `_server/lib/src/endpoints/`). The generator picks it up: ```dart import 'package:serverpod_auth_idp_server/providers/email.dart'; class EmailIdpEndpoint extends EmailIdpBaseEndpoint {} ``` Then start the server with `serverpod start` to generate the client code, and create and apply the migration that initializes the database for the provider (in the `serverpod start` terminal, press **M**). More detailed instructions can be found in the general [identity providers setup section](https://docs.serverpod.dev/concepts/authentication/setup.md#identity-providers-configuration). ### Use your own email provider Serverpod Cloud delivery is there to get sign-in working quickly, and it sends a standard message carrying your `appDisplayName`. You might prefer using a custom email provider to have full control over the body, layout, and language of the emails. For servers hosted outside of Serverpod Cloud, it is the only option. Changing the email provider is done by replacing `ServerpodCloudEmailIdpConfig` with `EmailIdpConfigFromPasswords`, which requires you to pass your own callbacks for the two codes. One convenient option is the [mailer](https://pub.dev/packages/mailer) package, which can send emails through any SMTP service. Most email providers, such as Resend, SendGrid, or Mandrill, support SMTP. ```dart import 'package:serverpod_auth_idp_server/core.dart'; import 'package:serverpod_auth_idp_server/providers/email.dart'; import 'src/generated/serverpod.dart'; void run(List args) async { final pod = Serverpod(args); pod.initializeAuthServices( tokenManagerBuilders: [ JwtConfigFromPasswords(), ], identityProviderBuilders: [ // Configure the Email Identity Provider // This is the basic configuration for the Email IDP to work. EmailIdpConfigFromPasswords( // Callback to send the registration verification code to the user. sendRegistrationVerificationCode: _sendRegistrationCode, // Callback to send the password reset verification code to the user. sendPasswordResetVerificationCode: _sendPasswordResetCode, ), ], ); await pod.start(); } void _sendRegistrationCode( Session session, { required String email, required UuidValue accountRequestId, required String verificationCode, required Transaction? transaction, }) { // NOTE: Here you call your mail service to send the verification code to // the user. For testing, we will log the verification code. session.log('[EmailIDP] Registration code ($email): $verificationCode'); } void _sendPasswordResetCode( Session session, { required String email, required UuidValue passwordResetRequestId, required String verificationCode, required Transaction? transaction, }) { // NOTE: Here you call your mail service to send the verification code to // the user. For testing, we will log the verification code. session.log('[EmailIDP] Password reset code ($email): $verificationCode'); } ``` #### Basic configuration options - `sendRegistrationVerificationCode`: A callback that will be called to send the registration verification code to the user. Here you should call the email sending service to send the verification code to the user. - `sendPasswordResetVerificationCode`: A callback that will be called to send the password reset verification code to the user. Here you should call the email sending service to send the verification code to the user. Both configurations hash passwords and verification codes with a secret pepper, which `EmailIdpConfigFromPasswords` reads from the `emailSecretHashPepper` key in `config/passwords.yaml` or the `SERVERPOD_PASSWORD_emailSecretHashPepper` environment variable. Newly generated projects already have a value for every run mode. To pass the pepper directly instead, use `EmailIdpConfig` with its `secretHashPepper` parameter. For more details on configuration options, such as customizing password requirements, verification code generation, rate limiting, and more, see the [configuration section](https://docs.serverpod.dev/concepts/authentication/providers/email/configuration.md). ## Client-side configuration If you have configured the `SignInWidget` as described in the [setup section](https://docs.serverpod.dev/concepts/authentication/setup.md#present-the-authentication-ui), the Email identity provider will be automatically detected and displayed in the sign-in widget. You can also use the `EmailSignInWidget` to include the email authentication flow in your own custom UI. ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; EmailSignInWidget( client: client, onAuthenticated: () { // Do something when the user is authenticated. // // NOTE: You should not navigate to the home screen here, otherwise // the user will have to sign in again every time they open the app. }, onError: (error) { // Handle errors ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: $error')), ); }, ) ``` The widget automatically handles: - Login with email and password. - Registration with terms acceptance and email verification. - Password reset flow with email verification. - Navigation between screens. For details on how to display the email authentication UI in your Flutter app, see the [customizing the UI section](https://docs.serverpod.dev/concepts/authentication/providers/email/customizing-the-ui.md). --- # Configure email sign-in https://docs.serverpod.dev/concepts/authentication/providers/email/configuration This page covers configuration options for the email identity provider beyond the basic setup. ## Configuration options New projects use `ServerpodCloudEmailIdpConfig`. It reads the [pepper](#peppering) from the `emailSecretHashPepper` password, but it does not take the other options on this page. To set them, switch to `EmailIdpConfigFromPasswords` and pass your own `sendRegistrationVerificationCode` and `sendPasswordResetVerificationCode` callbacks. See [Use your own email provider](https://docs.serverpod.dev/concepts/authentication/providers/email/setup.md#use-your-own-email-provider). Below is a non-exhaustive list of some of the most common configuration options. For more details on all options, check the `EmailIdpConfig` in-code documentation. ### Peppering A pepper is a server-side secret mixed into a password before it is hashed and stored, so a database leak alone is not enough to crack the hashes. See [storing secrets](https://docs.serverpod.dev/concepts/authentication/setup.md#storing-secrets) for where to keep it. The pepper is configured via the `secretHashPepper` property in `EmailIdpConfig`, or read from the `emailSecretHashPepper` password by `EmailIdpConfigFromPasswords`, as described in the [server-side configuration](https://docs.serverpod.dev/concepts/authentication/providers/email/setup.md#server-side-configuration) section. Its [recommended pepper length](https://www.ietf.org/archive/id/draft-ietf-kitten-password-storage-04.html#name-storage-2) is 32 bytes. :::warning If the pepper is changed, all passwords in the database will need to be re-hashed with the new pepper, or the old pepper needs to be added as a fallback pepper. Store the pepper securely and never expose it to the client. ::: #### Rotating secret hash peppers To keep old passwords valid after a pepper change, use the `fallbackSecretHashPeppers` configuration option to add the old pepper as a fallback pepper. ```dart final emailIdpConfig = EmailIdpConfigFromPasswords( fallbackSecretHashPeppers: [ pod.getPassword('oldEmailSecretHashPepper')!, ], ); ``` ### Customizing password requirements By default, the minimum password length is set to 8 characters. If you wish to modify this requirement, you can use the `passwordValidationFunction` configuration option. ```dart final emailIdpConfig = EmailIdpConfigFromPasswords( passwordValidationFunction: (password) { // Require at least 12 characters, one uppercase, one lowercase, one number if (password.length < 12) return false; if (!password.contains(RegExp(r'[A-Z]'))) return false; if (!password.contains(RegExp(r'[a-z]'))) return false; if (!password.contains(RegExp(r'[0-9]'))) return false; return true; }, ); ``` :::tip This is useful to ensure password policies on the server-side. It is a best practice to pair it with a configuration on the client-side to provide a better UX when creating a new password. The `EmailSignInWidget` and `EmailAuthController` have a `passwordRequirements` parameter that can be used to configure the password requirements. ::: ### Custom verification code generation You can customize how verification codes are generated: ```dart final emailIdpConfig = EmailIdpConfigFromPasswords( registrationVerificationCodeGenerator: () { // Generate a 6-digit numeric code final random = Random.secure(); return List.generate(6, (_) => random.nextInt(10)).join(); }, ); ``` :::warning Remember to configure the `verificationCodeConfig` parameter on the `EmailSignInWidget` to match the length and allowed characters of the verification code you generate. Otherwise, users will never be able to enter the verification code correctly. See the [customizing the UI section](https://docs.serverpod.dev/concepts/authentication/providers/email/customizing-the-ui.md) for more details. ::: #### Bypassing verification code in development To simplify testing, it is possible to bypass the verification code by using a custom verification code generator that always returns the same code. This can be configured only for the development run mode as below: ```dart pod.initializeAuthServices( tokenManagerBuilders: [ JwtConfigFromPasswords(), ], identityProviderBuilders: [ EmailIdpConfigFromPasswords( registrationVerificationCodeGenerator: pod.runMode == 'development' ? () => '11111111' // Digits only, and the same length as production. : defaultVerificationCodeGenerator, ), ], ); ``` ### Reacting to events Beyond the `sendRegistrationVerificationCode` and `sendPasswordResetVerificationCode` callbacks to send verification codes to the user, there are a few callbacks that can be used to react to events, such as `onAfterAccountCreated` and `onPasswordResetCompleted`. These can be useful for sending emails to the user to communicate the successful operation. ### Rate limiting The email provider includes built-in rate limiting to prevent abuse, with some sensible defaults: - **Failed login attempts**: Limited to 5 attempts per 5 minutes - **Password reset attempts**: Limited to 3 attempts per hour - **Verification code attempts**: Limited to 3 attempts per code lifetime of 15 minutes You can customize all rate limits in the `EmailIdpConfig`: ```dart final emailIdpConfig = EmailIdpConfigFromPasswords( registrationVerificationCodeLifetime: Duration(minutes: 15), registrationVerificationCodeAllowedAttempts: 3, passwordResetVerificationCodeLifetime: Duration(minutes: 15), passwordResetVerificationCodeAllowedAttempts: 3, failedLoginRateLimit: RateLimit( maxAttempts: 10, timeframe: Duration(minutes: 10), ), maxPasswordResetAttempts: RateLimit( maxAttempts: 5, timeframe: Duration(hours: 2), ), ); ``` --- # Customize the email sign-in UI https://docs.serverpod.dev/concepts/authentication/providers/email/customizing-the-ui When using the email identity provider, you can customize the UI to your liking. You can use the `EmailSignInWidget` to display the email authentication flow in your own custom UI, or you can use the `EmailAuthController` to build a completely custom authentication interface. :::info The `SignInWidget` uses the `EmailSignInWidget` internally to display the email authentication flow. You can also supply a custom `EmailSignInWidget` to the `SignInWidget` to override the default behavior. ```dart SignInWidget( client: client, emailSignInWidget: EmailSignInWidget( client: client, // Open on the login screen instead of the default registration screen startScreen: EmailFlowScreen.login, // A custom widget replaces the built-in handling, so pass your own callbacks. onAuthenticated: () { /* ... */ }, onError: (error) { /* ... */ }, ), ) ``` ::: ## Using the `EmailSignInWidget` The `EmailSignInWidget` handles the complete email authentication flow including login, registration, email verification, and password reset. You can customize the widget's behavior using its constructor parameters: ```dart EmailSignInWidget( client: client, startScreen: EmailFlowScreen.login, // or startRegistration // Customize the verification code configuration // The default matches the default server verification code generation verificationCodeConfig: VerificationCodeConfig( length: 6, keyboardType: TextInputType.number, allowedCharactersPattern: RegExp(r'[0-9]'), // The wait before the user can request a new code. resendCountdownDuration: Duration(minutes: 1), ), // Custom email validation function. Throw InvalidEmailException so the // widget shows the message. Other exceptions block sign-in silently. emailValidation: (email) { if (!email.contains('@example.com')) { throw const InvalidEmailException('Only @example.com emails allowed'); } }, // Customize the password requirements // If omitted, uses the default configuration below passwordRequirements: [ PasswordRequirement.minLength(12), PasswordRequirement.containsUppercase(), PasswordRequirement.containsLowercase(), PasswordRequirement.containsNumber(), PasswordRequirement.containsSpecialCharacter(), ], onTermsAndConditionsPressed: () { // Open terms and conditions }, onPrivacyPolicyPressed: () { // Open privacy policy }, onAuthenticated: () { // Do something when the user is authenticated. // // NOTE: You should not navigate to the home screen here, otherwise // the user will have to sign in again every time they open the app. }, onError: (error) { // Handle errors }, ) ``` Optionally, you can provide an externally managed `EmailAuthController` instance to the widget. A controller and a `client` are mutually exclusive, and `onAuthenticated` and `onError` belong on the controller in that case. Passing either alongside a controller trips an assertion, so a debug build throws. The controller carries the sign-in options, while `verificationCodeConfig`, `onTermsAndConditionsPressed`, and `onPrivacyPolicyPressed` stay on the widget. ```dart EmailSignInWidget( controller: controller, ) ``` :::info The terms and conditions and privacy policy checkbox on the registration screen are optional and disabled by default. The checkbox is shown as soon as you provide either `onTermsAndConditionsPressed` or `onPrivacyPolicyPressed`. ::: ### Customizing the default widget's appearance Since the `EmailSignInWidget` uses the Material Design system, it will react to your app's Material theme. You can also wrap it in a `Theme` widget to apply a custom theme. ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; Theme( data: Theme.of(context).copyWith( colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), // Use the AuthIdpTheme to customize the verification code input look extensions: >[ AuthIdpTheme.defaultTheme( errorPinTheme: PinTheme(...), ), ], ), child: EmailSignInWidget(client: client), ) ``` ## Building a custom UI with the `EmailAuthController` For more control over the UI, you can use the `EmailAuthController` class, which provides all the authentication logic without any UI components. This allows you to build a completely custom authentication interface. ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; final controller = EmailAuthController( client: client, startScreen: EmailFlowScreen.login, onAuthenticated: () { // Do something when the user is authenticated. // // NOTE: You should not navigate to the home screen here, otherwise // the user will have to sign in again every time they open the app. }, onError: (error) { // Handle errors }, ); ``` ### EmailAuthController state management Your widget should render the appropriate screen based on the `currentScreen` property of the controller. You can also use the below state properties to build your UI: ```dart // Check current screen final currentScreen = controller.currentScreen; // Check if loading final isLoading = controller.isLoading; // Check if authenticated final isAuthenticated = controller.isAuthenticated; // Get error message final errorMessage = controller.errorMessage; // Listen to state changes controller.addListener(() { setState(() { // Rebuild UI when controller state changes }); }); ``` ## The email authentication flow The email authentication flow uses the following screens: - `EmailFlowScreen.login` - Login with email and password - `EmailFlowScreen.startRegistration` - Start registration with email - `EmailFlowScreen.verifyRegistration` - Verify registration code - `EmailFlowScreen.completeRegistration` - Set password to complete registration - `EmailFlowScreen.requestPasswordReset` - Request password reset - `EmailFlowScreen.verifyPasswordReset` - Verify password reset code - `EmailFlowScreen.completePasswordReset` - Set new password ### Navigating between screens The controller handles navigation between screens automatically when calling the methods associated with each screen, but you can also navigate manually: ```dart // Navigate to a specific screen controller.navigateTo(EmailFlowScreen.startRegistration); // Navigate back if (controller.canNavigateBack) { controller.navigateBack(); } ``` ### EmailAuthController methods The controller provides methods for each step of the authentication flow: :::info The `EmailAuthController` already exposes internal text controllers for the email, password and verification code inputs. You can use these to build your own custom UI. All controllers are shared between login, registration and password reset flows for good UX. The `EmailAuthController` will handle the cleanup of the text controllers upon navigation as needed. ::: #### Login ```dart // Set email and password in the controller's text controllers controller.emailController.text = 'user@example.com'; controller.passwordController.text = 'password123'; // Call login await controller.login(); ``` #### Registration flow The registration flow consists of three steps: 1. **Start Registration** - Request a verification code: ```dart controller.emailController.text = 'user@example.com'; await controller.startRegistration(); // Controller navigates to verification screen ``` 2. **Verify Registration Code** - Enter the verification code: ```dart controller.verificationCodeController.text = '12345678'; await controller.verifyRegistrationCode(); // Controller navigates to password setup screen ``` 3. **Complete Registration** - Set password: ```dart controller.passwordController.text = 'securePassword123'; await controller.finishRegistration(); // User is now authenticated ``` #### Password reset flow The password reset flow also consists of three steps: 1. **Request Password Reset**: ```dart controller.emailController.text = 'user@example.com'; await controller.startPasswordReset(); // Controller navigates to verification screen ``` 2. **Verify Reset Code**: ```dart controller.verificationCodeController.text = '12345678'; await controller.verifyPasswordResetCode(); // Controller navigates to new password screen ``` 3. **Complete Password Reset**: ```dart controller.passwordController.text = 'newSecurePassword123'; await controller.finishPasswordReset(); // User is authenticated with new password ``` ### Resending verification codes To resend a verification code: ```dart await controller.resendVerificationCode(); ``` This will resend the code based on the current screen (registration or password reset). --- # Email admin operations https://docs.serverpod.dev/concepts/authentication/providers/email/admin-operations The email identity provider provides admin operations through `EmailIdpAdmin` for managing email accounts and cleaning up expired or dangling requests. These operations are useful for administrative tasks, maintenance, and preventing database bloat. ## Accessing the `EmailIdpAdmin` You can access the admin operations through the `AuthServices.instance.emailIdp` property: ```dart import 'package:serverpod_auth_idp_server/providers/email.dart'; import 'package:serverpod_auth_idp_server/core.dart'; // Get the EmailIdp instance final emailIdp = AuthServices.instance.emailIdp; // Access admin operations final admin = emailIdp.admin; ``` ## Account management The admin API provides methods for managing email accounts: ### Finding accounts ```dart // Find an account by email final account = await admin.findAccount( session, email: 'user@example.com', ); ``` ### Creating accounts ```dart // Create an email authentication account final emailAccountId = await admin.createEmailAuthentication( session, authUserId: userId, email: 'user@example.com', password: 'securePassword123', ); ``` ### Deleting accounts ```dart // Delete an account by email await admin.deleteEmailAccount( session, email: 'user@example.com', ); // Delete all email accounts for a user await admin.deleteEmailAccountByAuthUserId( session, authUserId: userId, ); ``` ### Setting passwords ```dart // Set or update a password for an account await admin.setPassword( session, email: 'user@example.com', password: 'newSecurePassword123', ); ``` :::warning The `setPassword` method does not validate the password against the configured password policy. Make sure to validate the password before calling this method if needed. ::: ## Finding active account requests You can also check for active account requests: ```dart final accountRequest = await admin.findActiveEmailAccountRequest( session, accountRequestId: requestId, ); ``` This is useful for checking the status of a registration request or verifying if a request is still valid. ## Cleanup operations Over time, expired account requests, password reset requests, and failed login attempts can accumulate in the database, leading to database bloat and potential performance issues. It's important to periodically clean these up to prevent database bloat. Such requests are not automatically cleaned up since they can be useful for auditing purposes, so it is up to each application to decide when to clean them up. ### Cleaning up expired account requests Account requests that have expired (users who started registration but never completed it) should be cleaned up: ```dart // Delete all expired account requests await admin.deleteExpiredAccountRequests(session); // Delete a specific account request await admin.deleteEmailAccountRequestById( session, requestId, ); ``` ### Cleaning up expired password reset requests Password reset requests that have expired (users who requested a password reset but never completed it) should be cleaned up: ```dart // Delete all expired password reset requests await admin.deleteExpiredPasswordResetRequests(session); // Delete password reset request attempts for a specific email // Useful when you want to allow a user to request a new password // even though they have hit the rate limit await admin.deletePasswordResetRequestsAttemptsForEmail( session, email: 'user@example.com', ); ``` ### Cleaning up failed login attempts Failed login attempts, tracked for rate limiting, should also be cleaned up when no longer useful for auditing purposes: ```dart // Delete all failed login attempts await admin.deleteFailedLoginAttempts(session); // Delete failed login attempts older than a specific duration await admin.deleteFailedLoginAttempts( session, olderThan: Duration(days: 30), ); ``` --- # Set up Google sign-in https://docs.serverpod.dev/concepts/authentication/providers/google/setup Sign in with Google requires a Google Cloud project. You also need platform-specific OAuth credentials depending on which platforms you target. ## Prerequisites Before following this guide, make sure you have: - A Google account with access to [Google Cloud Console](https://console.cloud.google.com/). - A running Serverpod project (server, client, and Flutter app packages from `serverpod create`). - The Serverpod auth module installed and configured per the [authentication setup](https://docs.serverpod.dev/concepts/authentication/setup.md). If your project was generated with an older Serverpod version, follow that guide first to add `serverpod_auth_idp_server` and `serverpod_auth_idp_flutter` and to configure `pod.initializeAuthServices()` before continuing. ## Get your Google credentials All platforms require a Web application OAuth client (used by the server). iOS and Android additionally require their own platform-specific OAuth clients. ### Create a Google Cloud project 1. Go to [Create a project](https://console.cloud.google.com/projectcreate). 2. Enter a **Project name** (for example, `My Serverpod App`) and click **Create**. ### Configure Google Auth Platform 1. Navigate to the [Google Auth Platform overview](https://console.cloud.google.com/auth/overview) and click **Get started** if you haven't enabled it yet. ![Google Auth Platform overview](https://docs.serverpod.dev/img/authentication/providers/google/4-auth-platform-overview.png) 2. **Project configuration**: Complete the setup wizard by filling in the required fields across each step (App Information, Audience, Contact Information) and click **Create**. ![Project configuration wizard](https://docs.serverpod.dev/img/authentication/providers/google/4b-project-configuration.png) 3. **Branding**: After completing the wizard, navigate to the [Branding](https://console.cloud.google.com/auth/branding) page from the sidebar. Fill in the remaining fields: app logo, app homepage link, privacy policy link, terms of service link, developer contact email, and **authorized domains**. These details appear on the OAuth consent screen shown to users during sign-in. Add the **root domain** you will deploy under to **Authorized domains**. Google stores only the top private domain, so a single root entry covers every subdomain you deploy under it. If you deploy on Serverpod Cloud, add `serverpod.space`. It is already verified by Serverpod, so you only need to add it here, no DNS verification is required on your end. For custom domains, see [Verify your authorized domain](#1-verify-your-authorized-domain). ![Branding configuration](https://docs.serverpod.dev/img/authentication/providers/google/10-branding.png) 4. **Data access**: Navigate to the [Data Access](https://console.cloud.google.com/auth/scopes) page and add the required scopes: `.../auth/userinfo.email` and `.../auth/userinfo.profile`. ![Scopes configuration](https://docs.serverpod.dev/img/authentication/providers/google/1-scopes.png) :::tip If you need access to additional Google APIs (for example, Calendar or Drive), you can add more scopes here. See [Accessing Google APIs](https://docs.serverpod.dev/concepts/authentication/providers/google/customizations.md#accessing-google-apis) for details on requesting additional scopes and using them with the `getExtraGoogleInfoCallback` on the server. ::: 5. **Audience**: Navigate to the [Audience](https://console.cloud.google.com/auth/audience) page. While in development, the app is in **Testing** mode, which means only users you explicitly add as test users can sign in (up to 100). Add your email as a test user so you can test the integration. ![Audience and test users](https://docs.serverpod.dev/img/authentication/providers/google/7-audience.png) :::tip Leave the app in **Testing** mode for now. You can [publish it](#5-publish-the-oauth-consent-screen) after verifying that sign-in works end to end. ::: ### Create the server OAuth client (Web application) All platforms (iOS, Android, and Web) require a **Web application** OAuth client for the server. This is the only client type that provides a **client secret**, which Serverpod needs to verify sign-in tokens on the server side. 1. In the Google Auth Platform, navigate to **Clients** and click **Create Client**. 2. Select **Web application** as the application type. 3. Leave **Authorized JavaScript origins** and **Authorized redirect URIs** empty for now. [Web setup](#web) below fills them in if you target web. ![Clients configuration](https://docs.serverpod.dev/img/authentication/providers/google/5-clients.png) 4. Click **Create**. 5. Copy the **Client ID** and **Client secret** shown on screen. You will need both in the next step. ### Store your credentials Your server's `config/passwords.yaml` already has `development:`, `staging:`, and `production:` sections from the project template. Add the `googleClientSecret` key to the `development:` section using the client ID and client secret you copied: ```yaml development: # ... existing keys (database, redis, serviceSecret, etc.) ... googleClientSecret: | { "web": { "client_id": "your-client-id.apps.googleusercontent.com", "client_secret": "your-client-secret", "redirect_uris": [] } } ``` Replace `your-client-id` and `your-client-secret` with the values from the Google Auth Platform. If you target web, [Web setup](#web) below adds the callback URL to `redirect_uris`. For production, add the same `googleClientSecret` entry to the `production:` section of `passwords.yaml` (with your production redirect URI), or set the `SERVERPOD_PASSWORD_googleClientSecret` environment variable on your production server. :::note **Carefully maintain correct indentation for YAML block scalars.** The `googleClientSecret` block uses a `|`. Any indentation error makes the JSON fail to parse, and the server throws at startup when `GoogleIdpConfigFromPasswords()` loads the secret. ::: ## Server-side configuration ### Add the Google identity provider Your server's `server.dart` file (for example, `my_project_server/lib/server.dart`) should already contain a `pod.initializeAuthServices()` call if your project was created with the Serverpod project template (`serverpod create`). If it's not there, see [Setup](https://docs.serverpod.dev/concepts/authentication/setup.md) first to configure the auth module and JWT settings. Add the Google import and `GoogleIdpConfigFromPasswords()` to the existing `identityProviderBuilders` list: ```dart import 'package:serverpod_auth_idp_server/providers/google.dart'; ``` ```dart pod.initializeAuthServices( tokenManagerBuilders: [ JwtConfigFromPasswords(), ], identityProviderBuilders: [ // ... any existing providers (e.g., EmailIdpConfigFromPasswords) ... GoogleIdpConfigFromPasswords(), ], ); ``` The `GoogleIdpConfigFromPasswords()` constructor automatically loads the client secret from the `googleClientSecret` key in `config/passwords.yaml` (or the `SERVERPOD_PASSWORD_googleClientSecret` environment variable). :::tip If you need more control over how the client secret is loaded, you can use `GoogleIdpConfig(clientSecret: GoogleClientSecret.fromJsonString(...))` instead. See the [customizations](https://docs.serverpod.dev/concepts/authentication/providers/google/customizations.md) page for details. ::: ### Create the endpoint Create a new endpoint file in your server project (for example, `my_project_server/lib/src/auth/google_idp_endpoint.dart`) alongside the existing auth endpoints. Extending the base class registers the sign-in methods with your server so your app can call them to complete the authentication flow: ```dart import 'package:serverpod_auth_idp_server/providers/google.dart'; class GoogleIdpEndpoint extends GoogleIdpBaseEndpoint {} ``` ### Start the server Start the server from your server project directory (for example, `my_project_server/`): ```bash serverpod start ``` Then create and apply the migration for the provider's tables: in the `serverpod start` terminal, press **M** to create and apply the migration. :::warning Skipping the migration will cause the server to crash at runtime when the Google provider tries to read or write user data. More detailed instructions can be found in the general [identity providers setup section](https://docs.serverpod.dev/concepts/authentication/setup.md#identity-providers-configuration). ::: ## Client-side configuration The Android and iOS integrations use the [google\_sign\_in](https://pub.dev/packages/google_sign_in) package under the hood, so any documentation there should also apply to this setup. ### iOS 1. In the Google Auth Platform, navigate to **Clients** and click **Create Client**. 2. Select **iOS** as the application type. 3. Fill in your app's **Bundle ID** and any other required information. 4. Click **Create** and download the `.plist` file. ![Create iOS OAuth client](https://docs.serverpod.dev/img/authentication/providers/google/8-ios-client-create.png) 5. Open the `Info.plist` file in your Flutter project (for example, `my_project_flutter/ios/Runner/Info.plist`) and add the following keys inside the top-level ``: ```xml ... GIDClientID your_ios_client_id GIDServerClientID your_server_client_id ``` Replace `your_ios_client_id` with the `CLIENT_ID` value from the downloaded plist file, and `your_server_client_id` with the client ID from the [Web application OAuth client](#create-the-server-oauth-client-web-application) you created earlier. #### Add the URL scheme To allow navigation back to the app after sign-in, add the URL scheme to the same `Info.plist` file. The scheme is the reversed client ID of your iOS app (found as `REVERSED_CLIENT_ID` in the downloaded plist file). Add the following inside the top-level ``: ```xml ... CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLSchemes com.googleusercontent.apps.your_client_id ``` Replace the URL scheme with your actual reversed client ID. :::warning Without the URL scheme, the OAuth callback never returns to your app and sign-in silently hangs. ::: ### Android 1. In the Google Auth Platform, navigate to **Clients** and click **Create Client**. 2. Select **Android** as the application type. 3. Fill in your app's **Package name** and **SHA-1 certificate fingerprint**. You can get the debug SHA-1 hash by running this from your Flutter project's `android/` directory (for example, `my_project_flutter/android/`): ```bash ./gradlew signingReport ``` 4. Click **Create**. This registers your app's package name and signing key with Google. ![Create Android OAuth client](https://docs.serverpod.dev/img/authentication/providers/google/9-android-client-create.png) 5. On Android, the sign-in SDK also needs to know your server's client ID. Pass the [Web application OAuth client](#create-the-server-oauth-client-web-application)'s ID as `serverClientId` when you initialize the client (covered in [Initialize the Google sign-in service](#initialize-the-google-sign-in-service) below). You can also pass it at build time with `--dart-define`. See [Configuring client IDs on the app](https://docs.serverpod.dev/concepts/authentication/providers/google/customizations.md#configuring-client-ids-on-the-app). :::note If your app uses Firebase (the `com.google.gms.google-services` Gradle plugin), you can skip step 5: the plugin supplies the server client ID from `google-services.json`. Re-download that file after creating the Web application client so it includes the web client entry. ::: :::warning When testing against a local server, the Android emulator cannot reach `localhost`: sign-in completes, but the endpoint call fails with a connection error. See [troubleshooting](https://docs.serverpod.dev/concepts/authentication/providers/google/troubleshooting.md#endpoint-calls-fail-on-android-with-connection-refused) for pointing the app at your machine. ::: ### Web On web, Google completes sign-in by redirecting the browser to a callback URL you control. This flow requires Serverpod to serve your Flutter web app on the **same origin** (same scheme, host, and port) as the callback route. :::warning The web flow only works from the **built** app served by Serverpod (`http://localhost:8082/` locally on default projects). Running the app with `flutter run -d chrome` fails, because Flutter's dev server is a different origin than Serverpod and the browser blocks the sign-in callback. See [troubleshooting](https://docs.serverpod.dev/concepts/authentication/providers/google/troubleshooting.md#sign-in-callback-fails-locally-with-flutter-run--d-chrome). For a hot-reload workflow, use the [separately-hosted Flutter web](https://docs.serverpod.dev/concepts/authentication/providers/google/customizations.md#separately-hosted-flutter-web) flow instead. ::: To test locally, build your Flutter web app into Serverpod's `web/app/` directory and start the server: ```bash flutter build web --base-href / --output ../my_project_server/web/app # from your Flutter project serverpod start --no-flutter # from your server project ``` Replace `my_project_server` with your server package directory. Pass `--no-flutter` so `serverpod start` serves your prebuilt web app. Without the flag, it also runs a Flutter web dev server on a different port, which does not share Serverpod's origin. Open `http://localhost:8082/` to test. Projects created with the website option serve the app under `/app` instead. Build those with `--base-href /app/` and open `/app`. The examples below use port `8082` (Serverpod's default from `config/development.yaml`). 1. In your server project's `server.dart` file, before `pod.start()`, register the callback route: ```dart import 'package:serverpod_auth_idp_server/core.dart'; // ... pod.webServer.addRoute( FlutterWebAuth2CallbackRoute(), '/auth/callback', ); ``` The path `/auth/callback` can be anything, as long as it matches the URL you register with Google in step 2 and pass to `initializeGoogleSignIn` later. In production, pass `host:` to restrict the route to your production domain: ```dart pod.webServer.addRoute( FlutterWebAuth2CallbackRoute(host: 'my-awesome-project.serverpod.space'), '/auth/callback', ); ``` :::tip The same route works for every OAuth2 PKCE provider. Register it once and reuse the callback URL for Google, GitHub, and any future provider. ::: 2. Update the server OAuth client you created in the [previous section](#create-the-server-oauth-client-web-application) and add the following: - **Authorized JavaScript origins**: your Flutter web app's origin (for example, `http://localhost:8082` locally, or `https://my-awesome-project.serverpod.space` in production). - **Authorized redirect URIs**: the full URL of the route you registered (for example, `http://localhost:8082/auth/callback` locally, or `https://my-awesome-project.serverpod.space/auth/callback` in production). Add the same URL to `redirect_uris` in the `googleClientSecret` entry of `passwords.yaml` from [Store your credentials](#store-your-credentials). ![Web credentials configuration](https://docs.serverpod.dev/img/authentication/providers/google/2-credentials.png) 3. Pass the same URL to `initializeGoogleSignIn` via the `redirectUri` argument when you initialize the client (covered in [Initialize the Google sign-in service](#initialize-the-google-sign-in-service) below). :::tip You can also pass the redirect URI via `--dart-define`. See [Configuring the web redirect URI](https://docs.serverpod.dev/concepts/authentication/providers/google/customizations.md#configuring-the-web-redirect-uri) for the pattern. ::: ## Present the authentication UI ### Initialize the Google sign-in service In your Flutter app's `lib/client.dart`, the template creates the `Client` and calls `client.auth.initialize()` inside `initializeClient()`. Add `initializeGoogleSignIn()` right after that call: ```dart unawaited(client.auth.initialize()); client.auth.initializeGoogleSignIn(); ``` **On web**, the call needs `clientId` and `redirectUri` (matching the route from [Web](#web)). **On Android**, it needs `serverClientId` from [Android](#android). On iOS, the SDK reads its config from `Info.plist`, and passing `serverClientId` is fine since it holds the same value as `GIDServerClientID`: ```dart if (kIsWeb) { client.auth.initializeGoogleSignIn( clientId: '.apps.googleusercontent.com', redirectUri: 'http://localhost:8082/auth/callback', ); } else { client.auth.initializeGoogleSignIn( serverClientId: '.apps.googleusercontent.com', ); } ``` Swap the redirect URI for your production URL when deploying. See [Configuring the web redirect URI](https://docs.serverpod.dev/concepts/authentication/providers/google/customizations.md#configuring-the-web-redirect-uri) to avoid hard-coding it per environment. :::warning On web, the app Serverpod serves is the build you created in [Web setup](#web). After changing `client.dart` (for example the `redirectUri`), run the build command again and hard-reload the browser. A stale build keeps sending the old values, and sign-in fails with [redirect\_uri\_mismatch](https://docs.serverpod.dev/concepts/authentication/providers/google/troubleshooting.md#sign-in-fails-with-redirect_uri_mismatch). ::: ### Show the Google sign-in button New projects include a `SignInScreen` widget at `lib/screens/sign_in_screen.dart`. The version below is trimmed to the essentials. It listens to `client.auth.authInfoListenable` and swaps between `SignInWidget` while the user is signed out and the `child` you pass it once they sign in. The `SignInWidget` auto-detects which identity provider endpoints are registered on the server, so once `GoogleIdpEndpoint` is exposed and the client code has been regenerated, the Google button appears inside it. ```dart import 'package:flutter/material.dart'; import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; import '../client.dart'; class SignInScreen extends StatefulWidget { final Widget child; const SignInScreen({super.key, required this.child}); @override State createState() => _SignInScreenState(); } class _SignInScreenState extends State { bool _isSignedIn = false; @override void initState() { super.initState(); client.auth.authInfoListenable.addListener(_updateSignedInState); _isSignedIn = client.auth.isAuthenticated; } @override void dispose() { client.auth.authInfoListenable.removeListener(_updateSignedInState); super.dispose(); } void _updateSignedInState() { setState(() { _isSignedIn = client.auth.isAuthenticated; }); } @override Widget build(BuildContext context) { return _isSignedIn ? widget.child : Center( child: SignInWidget( client: client, onError: (error) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Authentication failed: $error')), ); }, ), ); } } ``` :::warning The `SignInScreen` listener is what swaps to your authenticated UI. If you also pass an `onAuthenticated` callback to `SignInWidget`, use it for transient feedback only (snackbars, analytics). Driving navigation from `onAuthenticated` instead of the listener sends the user back to sign-in on every app restart even though their session is still valid. ::: In `main.dart`, the template wires this into `MyHomePage.build()`'s `Scaffold` behind a commented block. Comment out `body: const GreetingsScreen()` and uncomment the `SignInScreen(...)` block beneath it: ```dart body: SignInScreen( child: GreetingsScreen( onSignOut: () async { await client.auth.signOutDevice(); }, ), ), ``` The running app does not include your `initializeGoogleSignIn` call until you rebuild or restart it: - **Web:** Serverpod serves the built app, so run the `flutter build web` command from [Web](#web) again, then hard-reload the browser. Until then, the Google button stays hidden. - **Android and iOS:** hot reload does not re-run `initializeClient()`, so hot restart the app by pressing **R** in the `serverpod start` terminal, or rerun `flutter run`. Before you restart, Android sign-in fails with [serverClientId must be provided](https://docs.serverpod.dev/concepts/authentication/providers/google/troubleshooting.md#sign-in-fails-on-android-with-serverclientid-must-be-provided). The `SignInWidget` renders the standard Google sign-in button: ![Google sign-in button](https://docs.serverpod.dev/img/authentication/providers/google/3-button.png) To change the button's theme or build a fully custom UI, see [Customizations](https://docs.serverpod.dev/concepts/authentication/providers/google/customizations.md#customize-the-sign-in-button). :::tip If you run into issues, see the [troubleshooting guide](https://docs.serverpod.dev/concepts/authentication/providers/google/troubleshooting.md). ::: ## Publishing to production Before going live, complete the following steps: ### 1. Verify your authorized domain Google's **Authorized domains** field on the [Branding](https://console.cloud.google.com/auth/branding) page accepts only the **top private domain** (the root). Once the root is verified, every subdomain under it is automatically authorized, and you do not need to add each project subdomain separately. If you deploy on Serverpod Cloud under a `*.serverpod.space` subdomain, `serverpod.space` is already verified by Serverpod. Add `serverpod.space` to **Authorized domains** in the Google Auth Platform; no DNS verification is required on your end. For a custom domain, verify ownership of your root domain (for example, `example.com`) at [Google Search Console](https://search.google.com/search-console) by adding the DNS TXT record Google provides. After verification completes, add the root to **Authorized domains** in the Google Auth Platform. :::tip A single verified root authorizes all of its subdomains. If Google rejects a domain you add, you are likely entering a full subdomain instead of the root. ::: ### 2. Update the OAuth redirect URIs Go back to the [server OAuth client](#create-the-server-oauth-client-web-application) in the Google Auth Platform and add your production URLs: - **Authorized JavaScript origins**: your production Flutter web app's origin (for example, `https://my-awesome-project.serverpod.space`). - **Authorized redirect URIs**: the production callback URL. From the standard [Web setup](#web), this is the route URL you registered (for example, `https://my-awesome-project.serverpod.space/auth/callback`). For the [separately-hosted Flutter web](https://docs.serverpod.dev/concepts/authentication/providers/google/customizations.md#separately-hosted-flutter-web) fallback, it's the full URL where `auth.html` is served on your production Flutter web host (for example, `https://app.example.com/auth.html`). Replace the URLs with your actual production address. On Serverpod Cloud, your project is served from `https://.serverpod.space`. ### 3. Set production credentials Production runs out of the `production:` section of `passwords.yaml`, which is separate from the `development:` section you populated during setup. Adding production credentials does not replace your development ones, both stay in place and Serverpod picks the right set based on the run mode. The production `googleClientSecret` reuses the same web client ID and secret from setup, but lists your production redirect URI rather than the development one. If you use a different OAuth client for production, [create another web client](#create-the-server-oauth-client-web-application) first and use its values below. Pick the path that matches your deployment: #### Self-hosted Add `googleClientSecret` to the `production:` section of `passwords.yaml` with the production redirect URI: ```yaml production: # ... existing keys ... googleClientSecret: | { "web": { "client_id": ".apps.googleusercontent.com", "client_secret": "your-client-secret", "redirect_uris": ["https://my-awesome-project.serverpod.space/auth/callback"] } } ``` Alternatively, set the `SERVERPOD_PASSWORD_googleClientSecret` [environment variable](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md#via-environment-variables) on your production server with the same JSON value. #### Serverpod Cloud Use `https://.serverpod.space/auth/callback` as the redirect URI in the JSON (matching the route you registered in [Web setup](#web)). Save it to a file and use `serverpod cloud password set` with `--from-file`: ```bash serverpod cloud password set googleClientSecret --from-file path/to/google-client-secret.json ``` Run this from your linked server project directory, or pass `--project ` on the call. See the [Serverpod Cloud passwords guide](https://docs.serverpod.dev/cloud/concepts/passwords-secrets-env-vars.md) for project linking and other options. ### 4. Update the Android OAuth client with the release SHA-1 The Android OAuth client you created during setup uses your debug SHA-1 fingerprint. Release builds are signed with a different key, so you need to add the release SHA-1 as well. If you use Google Play App Signing (the default for new apps), get the SHA-1 from the Play Console: **Setup** > **App integrity** > **App signing key certificate**. Make sure to use the **app signing key** SHA-1, not the upload key SHA-1. If you manage your own release keystore, get the SHA-1 from it directly: ```bash keytool -list -v -keystore your-release-key.jks -alias your-key-alias ``` Once you have the SHA-1, go back to your Android OAuth client in the Google Auth Platform and add it under **SHA-1 certificate fingerprint**. :::warning Forgetting this step is one of the most common reasons Google sign-in works in debug builds but silently fails after publishing to the Play Store. ::: ### 5. Publish the OAuth consent screen While the app is in **Testing** mode, only the test users you added on the [Audience](https://console.cloud.google.com/auth/audience) page, in the Google Auth Platform, can sign in. All other users will see an error. Navigate to the **Audience** page and click **Publish App** to allow any Google account to sign in. If your app uses sensitive or restricted scopes, Google may require a verification review before publishing. --- # Customize Google sign-in https://docs.serverpod.dev/concepts/authentication/providers/google/customizations This page covers configuration and UI options for the Google identity provider beyond the basic setup. On the server, you can control how the client secret is loaded and hook into the sign-in flow with callbacks. In the app, you can configure client IDs and redirect URIs, customize the sign-in button with the `GoogleSignInWidget`, or build a completely custom authentication interface with the `GoogleAuthController`. ## Server configuration These options control how the Google identity provider behaves on the server. ### Configuration options Below is a non-exhaustive list of some of the most common configuration options. For more details on all options, check the `GoogleIdpConfig` in-code documentation. The Google identity provider can be configured using one of two classes: - **`GoogleIdpConfigFromPasswords`**: Automatically loads the client secret from the `googleClientSecret` key in `passwords.yaml` (or the `SERVERPOD_PASSWORD_googleClientSecret` environment variable). This is the class used in the [setup guide](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md) and is recommended for most projects. - **`GoogleIdpConfig`**: Requires you to pass a `GoogleClientSecret` object directly. Use this when you need to load credentials from a custom source, such as a JSON file, a secrets manager, or a programmatically constructed map. The `GoogleIdpConfigFromPasswords` class is a convenience wrapper around `GoogleIdpConfig` that handles credential loading for you. Both classes accept the same optional callbacks, such as `googleAccountDetailsValidation` and `getExtraGoogleInfoCallback`, shown below. The examples on this page use `GoogleIdpConfigFromPasswords` unless the section specifically demonstrates manual client secret loading. #### Load the client secret using GoogleIdpConfig When using `GoogleIdpConfig`, you must provide the client secret explicitly. You can load the secret in several ways: **From JSON string (recommended for production):** ```dart final googleIdpConfig = GoogleIdpConfig( clientSecret: GoogleClientSecret.fromJsonString( pod.getPassword('googleClientSecret')!, ), ); ``` **From JSON file:** ```dart final googleIdpConfig = GoogleIdpConfig( clientSecret: GoogleClientSecret.fromJsonFile( File('config/google_client_secret.json'), ), ); ``` **From JSON map:** ```dart final googleIdpConfig = GoogleIdpConfig( clientSecret: GoogleClientSecret.fromJson({ 'web': { 'client_id': 'your-client-id.apps.googleusercontent.com', 'client_secret': 'your-client-secret', 'redirect_uris': [ 'https://your-domain.com/auth/callback', ], }, }), ); ``` #### Custom account validation You can customize the validation for Google account details before allowing sign-in. The default validation rejects sign-in unless `verifiedEmail` is true and both `name` and `fullName` are present. ```dart final googleIdpConfig = GoogleIdpConfigFromPasswords( // Optional: Custom validation for Google account details googleAccountDetailsValidation: (accountDetails) { // Throw an exception if account doesn't meet custom requirements if (accountDetails.verifiedEmail != true || !accountDetails.email.endsWith('@example.com')) { throw GoogleUserInfoMissingDataException(); } }, ); ``` #### Accessing Google APIs on the server On the server side, you can access Google APIs using the access token. The `getExtraGoogleInfoCallback` in `GoogleIdpConfig` receives the access token and can be used to call Google APIs: ```dart import 'package:http/http.dart' as http; final googleIdpConfig = GoogleIdpConfigFromPasswords( // Optional: Extract additional info from Google APIs getExtraGoogleInfoCallback: (session, { required accountDetails, required accessToken, required transaction, }) async { // Use accessToken to call Google APIs and store additional info // Example: Access YouTube API final response = await http.get( Uri.https('www.googleapis.com', '/youtube/v3/channels', { 'part': 'snippet', 'mine': 'true', }), headers: {'Authorization': 'Bearer $accessToken'}, ); // Process response and store additional info in the database }, ); ``` To request additional scopes at sign-in, see [Accessing Google APIs](#accessing-google-apis) under App configuration. #### Reacting to auth user creation The `onBeforeAuthUserCreated` and `onAfterAuthUserCreated` hooks are global callbacks configured on `AuthUsersConfig` in `initializeAuthServices`. They are not specific to Google. They fire for every identity provider. See [user creation callbacks](https://docs.serverpod.dev/concepts/authentication/working-with-users.md#user-creation-callbacks) for full details on both hooks. ### GoogleIdpConfig parameter reference | Parameter | Type | Required | Description | | -------------------------------- | ------------------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `clientSecret` | `GoogleClientSecret` | Yes | The Google OAuth client secret loaded from JSON. Can be loaded via `fromJsonString`, `fromJsonFile`, or `fromJson`. | | `googleAccountDetailsValidation` | `GoogleAccountDetailsValidation` | No | Custom validation callback for Google account details before allowing sign-in. Throws an exception to reject the account. | | `getExtraGoogleInfoCallback` | `GetExtraGoogleInfoCallback?` | No | Callback that receives the access token after sign-in, allowing you to call additional Google APIs and store extra user data. | | `onAfterGoogleAccountCreated` | `AfterGoogleAccountCreatedFunction?` | No | Callback invoked after a new Google account has been created and linked to an auth user. Runs inside the same transaction as account creation. | | `clockSkewTolerance` | `Duration` | No | Tolerance for clock skew when validating Google ID token timestamps. Defaults to the framework's default tolerance. | ## App configuration These options are set in your Flutter app rather than on the server. ### Accessing Google APIs The default setup allows access to basic user information, such as email, profile image, and name. You may require additional access scopes, such as accessing a user's calendar, contacts, or files. To do this, you will need to: - Add the required scopes to the [Data Access](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#configure-google-auth-platform) page in the Google Auth Platform. - Request access to the scopes when signing in. Do this by setting the `scopes` parameter of the `GoogleSignInWidget` or `GoogleAuthController`. For a full list of available scopes, see the [Google OAuth 2.0 Scopes reference](https://developers.google.com/identity/protocols/oauth2/scopes). :::info Adding additional scopes may require approval by Google. On the OAuth consent screen, you can see which of your scopes are considered sensitive. ::: To use the granted scopes from the server with the access token, see [Accessing Google APIs on the server](#accessing-google-apis-on-the-server). ### Lightweight sign-in on the Flutter app Lightweight sign-in is a feature that attempts to authenticate users previously logged in with Google automatically with minimal or no user interaction. When enabled, the Google authentication controller will try to sign the user in using platform-specific lightweight authentication methods. This feature is disabled by default, but can be enabled from the `GoogleSignInWidget` or `GoogleAuthController`. ```dart GoogleSignInWidget( client: client, attemptLightweightSignIn: true, // Enable lightweight sign-in onAuthenticated: () { // User was automatically signed in }, ) ``` :::note Lightweight sign-in runs automatically when the controller is initialized (typically at app launch). If it fails (no previous session, or the user dismisses the prompt), the regular sign-in button remains available. On web, the option has no effect in this version. It only applies to Android and iOS. ::: ### Configuring client IDs on the app If no client IDs are provided programmatically, the underlying `google_sign_in` package falls back to `GoogleService-Info.plist` on iOS. On Android, the `google-services.json` fallback only works when the app uses the Firebase `com.google.gms.google-services` Gradle plugin. A plain project must pass the IDs in code or with `--dart-define`, or sign-in fails (see [troubleshooting](https://docs.serverpod.dev/concepts/authentication/providers/google/troubleshooting.md#sign-in-fails-on-android-with-serverclientid-must-be-provided)). To set them programmatically, you can use the following methods. #### Passing client IDs in code You can pass the client IDs directly when initializing the Google sign-in service: ```dart client.auth.initializeGoogleSignIn( clientId: '.apps.googleusercontent.com', serverClientId: '.apps.googleusercontent.com', ); ``` This approach is useful when you need different client IDs per platform and want to manage them in your Dart code. #### Using environment variables Alternatively, you can pass client IDs during build time using the `--dart-define` option. The Google sign-in provider supports the following environment variables: - `GOOGLE_CLIENT_ID`: The platform-specific OAuth client ID - `GOOGLE_SERVER_CLIENT_ID`: The server (web application) OAuth client ID If `clientId` and `serverClientId` values are not supplied when initializing the service, the provider will automatically fetch them from these environment variables. **Example usage:** ```bash flutter run \ -d "" \ --dart-define="GOOGLE_CLIENT_ID=.apps.googleusercontent.com" \ --dart-define="GOOGLE_SERVER_CLIENT_ID=.apps.googleusercontent.com" ``` This approach is useful when you need to: - Manage separate client IDs for different platforms (Android, iOS, web) in a centralized way - Avoid committing client IDs to version control - Configure different credentials for different build environments (development, staging, production) :::tip You can also set these environment variables in your IDE's run configuration or CI/CD pipeline to avoid passing them manually each time. ::: ### Configuring the web redirect URI You can pass the web redirect URI to `initializeGoogleSignIn` via `--dart-define`. This is useful when building for different environments (development, staging, production) without changing `client.dart`: ```dart if (kIsWeb) { client.auth.initializeGoogleSignIn( clientId: const String.fromEnvironment('GOOGLE_CLIENT_ID'), redirectUri: const String.fromEnvironment('GOOGLE_WEB_REDIRECT_URI'), ); } else { client.auth.initializeGoogleSignIn(); } ``` ```bash flutter run -d chrome \ --dart-define="GOOGLE_CLIENT_ID=.apps.googleusercontent.com" \ --dart-define="GOOGLE_WEB_REDIRECT_URI=" ``` Use the redirect URI that matches the environment you are building for: the integrated-route URL (e.g., `http://localhost:8082/auth/callback`) when Serverpod serves your web app, or your production URL (e.g., `https://my-awesome-project.serverpod.space/auth/callback`). For `flutter run -d chrome`, where the app runs on its own origin, use the [separately-hosted flow](#separately-hosted-flutter-web) instead. ### Separately-hosted Flutter web Use this flow when your Flutter web app and Serverpod are on different origins. Common cases: `flutter run -d chrome` locally with Serverpod on a separate port, or a CDN-hosted Flutter build with a separate API server. 1. Place a static `auth.html` file in your Flutter project's `web/` folder. A single copy is shared across every identity provider that uses an OAuth2 redirect, so create it once. Follow [Web callback page (`auth.html`)](https://docs.serverpod.dev/concepts/authentication/setup.md#web-callback-page-authhtml) in the authentication setup guide. 2. Run Flutter on a fixed port. The examples use `49660`, but any free port works, as long as you keep it consistent everywhere: ```bash flutter run -d chrome --web-port=49660 ``` 3. Update the [server OAuth client](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#create-the-server-oauth-client-web-application) with **Flutter's dev server origin** (not Serverpod's, since Flutter serves `auth.html`): - **Authorized JavaScript origins**: `http://localhost:49660` locally, `https://app.example.com` in production. - **Authorized redirect URIs**: `http://localhost:49660/auth.html` locally, `https://app.example.com/auth.html` in production. 4. Pass the same URL to `initializeGoogleSignIn` via the `redirectUri` argument instead of the route URL. ## Customize the sign-in button See [Styling the buttons](https://docs.serverpod.dev/concepts/authentication/ui-components.md#styling-the-buttons) for how the `GoogleSignInWidget` parameters interact with the `buttonStyle` set on `SignInWidget`. :::info The `SignInWidget` uses the `GoogleSignInWidget` internally to display the Google sign-in flow. You can also supply a custom `GoogleSignInWidget` to the `SignInWidget` to override the default behavior. ```dart SignInWidget( client: client, googleSignInWidget: GoogleSignInWidget( client: client, // Shape and label survive inside SignInWidget, unless its buttonStyle // sets them. Brand colors do not. shape: SignInButtonShape.rounded, text: SignInButtonTextVariant.signInWith, // A custom widget replaces the built-in handling, so pass your own callbacks. onAuthenticated: () { /* ... */ }, onError: (error) { /* ... */ }, ), ) ``` ::: ### Using the `GoogleSignInWidget` The `GoogleSignInWidget` handles the complete Google sign-in flow for iOS, Android, and web. You can customize the widget's appearance and behavior: ```dart GoogleSignInWidget( client: client, // Button customization. The values shown are the defaults. style: GoogleButtonStyle.outline, // or filledBlue, filledBlack size: SignInButtonSize.large, // or medium, small text: SignInButtonTextVariant.continueWith, // or signInWith, signUpWith, signIn shape: SignInButtonShape.pill, // or rounded, rectangular logoAlignment: SignInButtonLogoAlignment.center, // or left minimumWidth: 240, // at most 400 textStyle: null, // TextStyle for the label // Scopes to request from Google // These are the default scopes, you can add additional scopes as needed. scopes: const [ 'https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile', ], // Whether to attempt lightweight sign-in (Android and iOS only) attemptLightweightSignIn: false, onAuthenticated: () { // Do something when the user is authenticated. // // NOTE: You should not navigate to the home screen here, otherwise // the user will have to sign in again every time they open the app. }, onError: (error) { // Handle errors }, ) ``` ## Build a custom UI with GoogleAuthController For more control over the UI, you can use the `GoogleAuthController` class, which provides all the authentication logic without any UI components. This allows you to build a completely custom authentication interface. ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; final controller = GoogleAuthController( client: client, onAuthenticated: () { // Do something when the user is authenticated. // // NOTE: You should not navigate to the home screen here, otherwise // the user will have to sign in again every time they open the app. }, onError: (error) { // Handle errors }, attemptLightweightSignIn: false, scopes: const [ 'https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile', ], ); // Initiate sign-in await controller.signIn(); ``` :::note On web, sign-in always runs through the OAuth2 redirect flow. Call `initializeGoogleSignIn` with both `clientId` and `redirectUri` before calling `signIn()`. When either value cannot be resolved, `initializeGoogleSignIn` throws an `ArgumentError`. Skipping the call entirely leaves the controller in the error state after `signIn()`. Set them up as described in [Web setup](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#web). ::: ### GoogleAuthController state management Your widget should render the appropriate UI based on the `state` property of the controller. You can also use the below state properties to build your UI: ```dart // Check current state final state = controller.state; // GoogleAuthState enum // Check if loading final isLoading = controller.isLoading; // Check if authenticated final isAuthenticated = controller.isAuthenticated; // Get error message final errorMessage = controller.errorMessage; // Listen to state changes controller.addListener(() { setState(() { // Rebuild UI when controller state changes }); }); ``` #### GoogleAuthController states - `GoogleAuthState.initializing` - Controller is initializing. - `GoogleAuthState.idle` - Ready for user interaction. - `GoogleAuthState.loading` - Processing a sign-in request. - `GoogleAuthState.error` - An error occurred. - `GoogleAuthState.authenticated` - Authentication was successful. ## Related - [Setup](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md): configure the Google Auth Platform and register the identity provider. - [Troubleshooting](https://docs.serverpod.dev/concepts/authentication/providers/google/troubleshooting.md): fix common Google sign-in errors. - [UI components](https://docs.serverpod.dev/concepts/authentication/ui-components.md): style the sign-in buttons and localize the built-in UI. - [Working with users](https://docs.serverpod.dev/concepts/authentication/working-with-users.md): react to user creation and manage user data. --- # Google admin operations https://docs.serverpod.dev/concepts/authentication/providers/google/admin-operations The Google identity provider exposes admin operations through `GoogleIdpAdmin`. Use them from server-side code to look up a Serverpod user by Google user ID and to attach a Google identity to an existing user. :::warning Call these methods only from secure server-side code. Do not expose them through client endpoints without authorization checks. ::: ## Accessing the GoogleIdpAdmin The admin operations are available through `AuthServices.instance.googleIdp` after you add the Google identity provider (see [setup](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#add-the-google-identity-provider)). ```dart import 'package:serverpod_auth_idp_server/providers/google.dart'; import 'package:serverpod_auth_idp_server/core.dart'; final googleIdp = AuthServices.instance.googleIdp; final admin = googleIdp.admin; ``` ## Finding accounts The `findUserByGoogleUserId` method is static. Pass the Google user ID (`userIdentifier`) when you have it and need the linked Serverpod auth user ID. ```dart final userId = await GoogleIdpAdmin.findUserByGoogleUserId( session, userIdentifier: 'google-user-id', ); ``` The method returns `null` if no Google account row exists for that ID. ## Linking Google authentication Link an existing Serverpod user to a Google account. This is useful when migrating users from another provider or when an admin tool attaches Google sign-in to a user that already exists. First load the account details from a Google ID token (and an access token when you have one), then link them: ```dart final accountDetails = await admin.fetchAccountDetails( session, idToken: googleIdToken, accessToken: googleAccessToken, ); final googleAccount = await admin.linkGoogleAuthentication( session, authUserId: existingUserId, accountDetails: accountDetails, ); ``` The `accessToken` parameter may be `null` when you only have the ID token. The `linkGoogleAuthentication` method returns the created `GoogleAccount`. ## Related - [Setup](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md): configure the Google Auth Platform and register the identity provider. - [Customizations](https://docs.serverpod.dev/concepts/authentication/providers/google/customizations.md): configuration options and sign-in UI customization. - [Troubleshooting](https://docs.serverpod.dev/concepts/authentication/providers/google/troubleshooting.md): fix common Google sign-in errors. - [Working with users](https://docs.serverpod.dev/concepts/authentication/working-with-users.md): the server-side user APIs these operations complement. --- # Troubleshoot Google sign-in https://docs.serverpod.dev/concepts/authentication/providers/google/troubleshooting This page helps you identify common Sign in with Google failures, explains why they occur, and shows how to resolve them. For platform-specific issues with the underlying Flutter package, see the [google\_sign\_in\_android troubleshooting guide](https://pub.dev/packages/google_sign_in_android#troubleshooting). ## Setup checklist Go through this before investigating a specific error. Most problems come from a missed step. #### Google Cloud - [ ] Create a **Google Cloud project** in the [Google Cloud Console](https://console.cloud.google.com/). - [ ] In **Google Auth Platform**, complete the initial setup (wizard) and add the required scopes on **Data Access** (`.../auth/userinfo.email` and `.../auth/userinfo.profile`). - [ ] On **Branding** ([Branding](https://console.cloud.google.com/auth/branding)), complete the OAuth consent screen (logo, homepage, privacy policy, terms of service, and developer contact) and add the **root domain** (top private domain) under **Authorized domains**. Google stores only the root, so a single verified entry covers all of its subdomains. On Serverpod Cloud, add `serverpod.space` (already verified by Serverpod, no DNS setup needed). For custom domains, see [Verify your authorized domain](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#1-verify-your-authorized-domain). - [ ] Add **test users** on **Audience** while in **Testing** mode ([Audience](https://console.cloud.google.com/auth/audience)), or **Publish app** when everyone should be able to sign in. - [ ] Create a **Web application** OAuth client. For web sign-in, set **Authorized JavaScript origins** to your Flutter web app's origin (for example, `https://my-awesome-project.serverpod.space`) and **Authorized redirect URIs** to the route URL from [Web setup](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#web) (for example, `https://my-awesome-project.serverpod.space/auth/callback`), or the `auth.html` URL if you use the [separately-hosted Flutter web](https://docs.serverpod.dev/concepts/authentication/providers/google/customizations.md#separately-hosted-flutter-web) fallback (for example, `http://localhost:49660/auth.html`). Copy the **Client ID** and **Client secret**. - [ ] Add `googleClientSecret` to `config/passwords.yaml` with your client ID, client secret, and matching `redirect_uris` (the same callback URL as above). For production, this is the route URL you registered via `FlutterWebAuth2CallbackRoute` (for example, `https://my-awesome-project.serverpod.space/auth/callback`) from [Web setup](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#web), or the production `auth.html` URL on your Flutter web host if you use the [separately-hosted Flutter web](https://docs.serverpod.dev/concepts/authentication/providers/google/customizations.md#separately-hosted-flutter-web) fallback. See [Publishing to production](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#publishing-to-production). #### Server - [ ] For new or customized servers, confirm auth services and JWT are configured per [Authentication setup](https://docs.serverpod.dev/concepts/authentication/setup.md#identity-providers-configuration) before adding Google. - [ ] Add `GoogleIdpConfigFromPasswords()` to `identityProviderBuilders` in `server.dart`. - [ ] Create a `GoogleIdpEndpoint` file in `lib/src/auth/`. - [ ] Start the server with `serverpod start`, then create and apply the migration (press **M**). #### Client - [ ] Add `client.auth.initializeGoogleSignIn()` after `client.auth.initialize()` inside `initializeClient()` in your Flutter app's `lib/client.dart`. On web, pass `clientId` and `redirectUri` (the full callback URL, either the route URL or the `auth.html` URL, depending on your [Web setup](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#web)). On Android, pass `serverClientId` (the Web client's ID) unless your app uses the Firebase Gradle plugin. - [ ] Surface Google sign-in in the UI with `SignInWidget` or `GoogleSignInWidget` (see [Present the authentication UI](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#present-the-authentication-ui)). - [ ] Create an **iOS** OAuth client in the **same** Google Cloud project as the Web client, using the same **Bundle ID** as the app. Set `GIDClientID` from the iOS client, `GIDServerClientID` to the **Web** client's ID, and add the reversed-client-ID **URL scheme** in `Info.plist` (*iOS only*). - [ ] Create an **Android** OAuth client in the **same** project, with the same **package name** and **SHA-1** as the build you run (*Android only*). - [ ] Set up the web callback (*web only*). Pick one: - **Standard:** Register `FlutterWebAuth2CallbackRoute` on `pod.webServer` in `server.dart` before `pod.start()` per [Web setup](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#web). - **Separately-hosted fallback:** Create `web/auth.html` in your Flutter project as described in [Web callback page (`auth.html`)](https://docs.serverpod.dev/concepts/authentication/setup.md#web-callback-page-authhtml) and run Flutter on a **fixed** `--web-port` so the origin does not change every run. See [separately-hosted Flutter web](https://docs.serverpod.dev/concepts/authentication/providers/google/customizations.md#separately-hosted-flutter-web). ## Sign-in fails with redirect\_uri\_mismatch **Problem:** The OAuth flow fails with a `redirect_uri_mismatch` error from Google. **Cause:** The redirect URI sent during sign-in does not exactly match one of the URIs registered on the Web OAuth client. Google requires an exact match (scheme, host, port, path, and casing). **Resolution:** In the Google Auth Platform, navigate to **Clients**, select your Web application client, and verify that the URIs under **Authorized JavaScript origins** and **Authorized redirect URIs** match what your app actually uses: - **Authorized JavaScript origins** must contain your Flutter web app's origin (for example, `http://localhost:49660` locally, or `https://my-awesome-project.serverpod.space` in production). - **Authorized redirect URIs** must contain the full callback URL: the route URL from [Web setup](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#web) (for example, `https://my-awesome-project.serverpod.space/auth/callback`), or the full `auth.html` URL if you use the [separately-hosted Flutter web](https://docs.serverpod.dev/concepts/authentication/providers/google/customizations.md#separately-hosted-flutter-web) fallback (for example, `http://localhost:49660/auth.html` locally). The same callback URL must also appear in `client.auth.initializeGoogleSignIn(..., redirectUri: ...)` in your Flutter app. The `redirect_uris` key in `config/passwords.yaml` must be present for the JSON to parse, but its contents are not used for this check. Common mistakes: - Trailing slashes, port differences, or `http` vs `https`. - Forgetting the callback path on the redirect URI. The bare origin is not enough. - For separately-hosted Flutter web, the Flutter dev server running on a random port. Pass `--web-port=` to `flutter run` so the origin is stable. - A stale build on the standard [Web setup](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#web) flow. The app Serverpod serves is a compiled snapshot, so a `redirectUri` change in `client.dart` takes effect only after re-running `flutter build web`. Rebuild and hard-reload the browser, because the service worker can cache the old bundle. ## Production redirect URIs rejected by Google **Problem:** When adding your production domain to **Authorized redirect URIs** on the Web OAuth client, Google rejects it with an error about unauthorized domains. **Cause:** The redirect URI's root domain (the top private domain) has not been verified and added to **Authorized domains** on the Branding page. Google requires the root to be a verified Authorized Domain before it accepts redirect URIs that use it. **Resolution:** Add the root domain (for example, `serverpod.space` or your custom root) to **Authorized domains**, then retry the redirect URI. On Serverpod Cloud, the `serverpod.space` root is already verified, so you only need to add `serverpod.space` to **Authorized domains**. For a custom domain, verify ownership at [Google Search Console](https://search.google.com/search-console) first. See [Verify your authorized domain](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#1-verify-your-authorized-domain). ## Sign-in works for you but not for other users **Problem:** Sign-in works for your Google account but other users get an error screen from Google saying the app is not verified or access is denied. **Cause:** Your Google Auth Platform app is still in **Testing** mode. Only users explicitly added as test users can sign in (up to 100). **Resolution:** Navigate to the [Audience](https://console.cloud.google.com/auth/audience) page and click **Publish App** to allow any Google account to sign in. If your app uses sensitive or restricted scopes, Google may require a verification review before publishing. ## Sign-in callback fails locally with `flutter run -d chrome` **Problem:** You followed [Web setup](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#web) and registered `FlutterWebAuth2CallbackRoute`. Sign-in completes at Google, the browser redirects, but the Flutter app never receives the result. Only affects `flutter run -d chrome` local dev. **Cause:** The integrated route requires Serverpod and your Flutter web app to be on the **same origin** (same scheme, host, AND port). With `flutter run -d chrome`, Flutter runs on its own dev server port (for example, `49660`) while Serverpod is on `8082`, so they are different origins. The browser blocks the callback page's `postMessage` across origins. **Resolution:** Use the [separately-hosted Flutter web](https://docs.serverpod.dev/concepts/authentication/providers/google/customizations.md#separately-hosted-flutter-web) flow for local dev. It serves `auth.html` from Flutter's own dev server, same-origin with your Flutter app. For production, the integrated route works once Serverpod serves your Flutter build (via `FlutterRoute`, mounted at `/` on default projects, or `/app` when the website option is enabled). ## Sign-in callback never returns to the Flutter app **Problem:** The Google sign-in window completes successfully, but the Flutter app never receives the result. The browser sits on a blank page, or `signIn()` hangs. **Cause:** The browser was redirected to a URL that does not serve the callback page, or the page is loaded but cannot post the result back to the Flutter app (origin mismatch). **Resolution:** 1. Confirm your callback page is reachable. Open the `redirectUri` directly in a browser tab. You should see the "Authentication complete" page. - For the standard [Web setup](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#web), confirm `FlutterWebAuth2CallbackRoute` is registered on `pod.webServer` before `pod.start()` and that the path matches the URL you opened. - For the [separately-hosted Flutter web](https://docs.serverpod.dev/concepts/authentication/providers/google/customizations.md#separately-hosted-flutter-web) fallback, confirm `web/auth.html` exists in your Flutter project and contains the script described in [Web callback page (`auth.html`)](https://docs.serverpod.dev/concepts/authentication/setup.md#web-callback-page-authhtml). If the file is missing, the redirect URL returns a 404. 2. Confirm the `redirectUri` passed to `initializeGoogleSignIn` exactly matches the URL where the callback is served, and that both share scheme, host, and port with your Flutter web app (the browser blocks `postMessage` across origins). ## Server fails to parse googleClientSecret from passwords.yaml **Problem:** The server crashes on startup with a JSON parsing error related to `googleClientSecret`. **Cause:** The YAML block scalar indentation is incorrect. The `googleClientSecret` key uses `|` (literal block scalar), which requires every line of the JSON to be indented at the same level relative to the key. **Resolution:** Make sure the JSON block is indented consistently under the `|`: ```yaml development: googleClientSecret: | { "web": { "client_id": "...", "client_secret": "...", "redirect_uris": [] } } ``` Every line of the JSON must be indented by at least one level more than `googleClientSecret:`. Mixing tabs and spaces can also cause issues. ## Sign-in fails on Android with PlatformException(sign\_in\_failed) or clientConfigurationError **Problem:** Google sign-in throws a `PlatformException(sign_in_failed, ...)` or a `GoogleSignInException` with `clientConfigurationError` on Android but works on other platforms. **Cause:** The SHA-1 fingerprint registered in your Android OAuth client does not match the signing key used to build the app. This commonly happens when switching between debug and release builds, or when the app is signed with a different keystore than the one registered. **Resolution:** 1. Check which SHA-1 your debug build is using: ```bash ./gradlew signingReport ``` 2. In the Google Auth Platform, navigate to **Clients** and verify your Android OAuth client has the correct SHA-1 fingerprint. 3. If you are testing a release build, use the SHA-1 from your production keystore: ```bash keytool -list -v -keystore /path/to/keystore ``` 4. After updating the SHA-1, it can take a few minutes for Google to propagate the change. ## Sign-in works in debug but fails in release **Problem:** Google sign-in works in debug mode but fails silently or with `sign_in_failed` in a release build. **Cause:** Debug and release builds use different signing keys. The SHA-1 fingerprint registered in your Android OAuth client only matches the debug keystore. **Resolution:** Register the SHA-1 fingerprint from your release keystore as an additional fingerprint in the Google Auth Platform. You can add multiple SHA-1 fingerprints to the same Android OAuth client, or create separate clients for debug and release. ## Sign-in fails on Android with "serverClientId must be provided" **Problem:** Sign-in fails on Android with `GoogleSignInException(code GoogleSignInExceptionCode.clientConfigurationError, serverClientId must be provided on Android, null)`. **Cause:** On Android, the `google_sign_in` SDK requires the server (Web application) client ID, and nothing supplies it. A plain Flutter project does not read `google-services.json`: that file is consumed by the `com.google.gms.google-services` Gradle plugin, which only Firebase-based projects apply. **Resolution:** Pass the Web application client ID when initializing, as shown in [Initialize the Google sign-in service](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#initialize-the-google-sign-in-service): ```dart client.auth.initializeGoogleSignIn( serverClientId: '.apps.googleusercontent.com', ); ``` You can also supply it at build time with `--dart-define=GOOGLE_SERVER_CLIENT_ID=...`. See [Configuring client IDs on the app](https://docs.serverpod.dev/concepts/authentication/providers/google/customizations.md#configuring-client-ids-on-the-app). For Firebase-based projects using the Gradle plugin, make sure a Web application OAuth client exists in the same Google Cloud project and re-download `google-services.json` so it includes the web client entry. ## Endpoint calls fail on Android with connection refused **Problem:** Sign-in completes at Google, but the app then fails with `ServerpodClientNetworkException: ... Connection refused ... uri=http://localhost:8080/...`. **Cause:** On Android, `localhost` is the emulator or device itself, not the machine running your server. The project template's `assets/config.json` sets `apiUrl` to `http://localhost:8080`, and that value takes precedence over the framework's platform-aware default (see [server URL resolution](https://docs.serverpod.dev/concepts/endpoints-and-apis.md)). **Resolution:** Point the app at your server explicitly when running on Android: ```bash flutter run --dart-define=SERVER_URL=http://10.0.2.2:8080/ ``` On the Android emulator, `10.0.2.2` maps to the host machine. On a physical device, use your computer's LAN IP address instead (for example, `http://192.168.1.20:8080/`), with the phone on the same network. ## Server crashes on first Google sign-in with "no such table" **Problem:** The server builds and starts, but crashes when a user tries Google sign-in. The error cites a missing table (like `serverpod_auth_idp_google_account`). **Cause:** The database migration that creates the provider's tables was never created or applied. **Resolution:** In the running `serverpod start` terminal, press **M** to create and apply the migration. ## Google sign-in button does not appear **Problem:** The `SignInWidget` renders, but the Google button is missing. **Cause:** The `SignInWidget` shows the Google button when the client has a registered `GoogleIdpEndpoint`. On web, the Google sign-in service must also be initialized. The common misses: - `GoogleIdpEndpoint` is missing on the server, or the client was not regenerated after adding it. - On web, the running app does not include your `initializeGoogleSignIn` call yet. The app Serverpod serves is a build, and hot reload does not re-run `initializeClient()`. - On web, `initializeGoogleSignIn` threw an `ArgumentError` because `clientId` or `redirectUri` is missing. See [clientId or redirectUri is required on web](#clientid-or-redirecturi-is-required-when-initializing-google-sign-in-on-web). **Resolution:** Confirm `GoogleIdpEndpoint` exists on the server and run `serverpod generate`. On web, run the `flutter build web` command from [Web setup](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#web) again and hard-reload the browser. If you run the app with `flutter run` instead, hot restart it. ## Lightweight sign-in (One Tap) not appearing **Problem:** You enabled `attemptLightweightSignIn: true` but the silent sign-in doesn't trigger. **Cause:** On web, `attemptLightweightSignIn` has no effect at all in this version, so nothing appearing there is expected. On Android and iOS, lightweight sign-in requires that the user has signed in with Google on the device before and has a Google account configured. **Resolution:** On mobile, this is expected behavior for first-time users, since the prompt only appears for returning users, and Google may suppress it temporarily after repeated dismissals. The regular sign-in button remains available as a fallback. ## iOS sign-in prompt doesn't show **Problem:** Tapping the Google sign-in button on iOS has no effect or throws an error about a missing client ID. **Cause:** The `GIDClientID` or `GIDServerClientID` keys are missing or incorrect in `Info.plist`, or the URL scheme is not registered. **Resolution:** 1. Open `ios/Runner/Info.plist` and verify that `GIDClientID` is set to the `CLIENT_ID` from your iOS OAuth client plist, and `GIDServerClientID` is set to the client ID from your Web application OAuth client. 2. Verify the URL scheme (`CFBundleURLSchemes`) contains the reversed client ID from the iOS plist (the `REVERSED_CLIENT_ID` value). 3. Clean the build and run again. ## clientId or redirectUri is required when initializing Google Sign-In on web **Problem:** The Flutter app throws an `ArgumentError` at startup with one of these messages: - `redirectUri is required when initializing Google Sign-In on web` - `clientId is required when initializing Google Sign-In on web with a redirect URI` **Cause:** On web, `initializeGoogleSignIn` needs both `clientId` and `redirectUri`. If you omit `clientId`, it reads the value from the `GOOGLE_CLIENT_ID` `--dart-define`. There is no such fallback for `redirectUri`. **Resolution:** Pass both values on web: ```dart client.auth.initializeGoogleSignIn( clientId: kIsWeb ? 'your-web-client-id.apps.googleusercontent.com' : null, redirectUri: kIsWeb ? 'https://my-awesome-project.serverpod.space/auth/callback' : null, ); ``` You can also pass `clientId` via `--dart-define` instead of in code: ```bash flutter run --dart-define=GOOGLE_CLIENT_ID=your-web-client-id.apps.googleusercontent.com ... ``` ## Google API calls fail after one hour **Problem:** Your server calls Google APIs (for example, Calendar or Drive) with the access token captured during sign-in, but requests start returning `401 Unauthorized` after about an hour. **Cause:** Google access tokens expire after 3,600 seconds (one hour). Serverpod captures the token during sign-in for the `getExtraGoogleInfoCallback`, and does not refresh it afterwards. **Resolution:** Fetch what you need inside `getExtraGoogleInfoCallback` while the token is fresh. For ongoing access, ask the user to sign in again, or run your own token exchange in a custom endpoint so you control the refresh token. ## Related - [Setup](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md): configure the Google Auth Platform and register the identity provider. - [Customizations](https://docs.serverpod.dev/concepts/authentication/providers/google/customizations.md): configuration options and sign-in UI customization. - [UI components](https://docs.serverpod.dev/concepts/authentication/ui-components.md): the sign-in widgets and how to compose them. --- # Set up Apple sign-in https://docs.serverpod.dev/concepts/authentication/providers/apple/setup ## Prerequisites Before you start, make sure you have: - A Serverpod project with the new auth module installed. New projects created with `serverpod create` include it by default. If you are upgrading an older project, follow the [auth module setup guide](https://docs.serverpod.dev/concepts/authentication/setup.md) first. - An active subscription to the [Apple Developer Program](https://developer.apple.com/programs/). Sign in with Apple requires this even for local development. - Xcode installed if you target iOS or macOS. ## Get your credentials All platforms require an App ID, a Sign in with Apple key, and a Service ID. Only the Android and web flow uses the Service ID at runtime, but sign-in initialization requires it everywhere. ### Register your App ID 1. In [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list), click **Identifiers → +**. 2. Select **App IDs** and click **Continue**. ![Register a new identifier: App IDs selected](https://docs.serverpod.dev/img/authentication/providers/apple/4-app-id-create.png) 3. Select **App** as the type and click **Continue**. 4. Fill in a description and your app's **Bundle ID** (for example, `com.example.app`). 5. Scroll down to **Capabilities**, find **Sign in with Apple**, and check it. Keep it set as a **primary App ID**. ![App ID capabilities: Sign in with Apple enabled](https://docs.serverpod.dev/img/authentication/providers/apple/5-app-id-capability.png) 6. Click **Continue**, then **Register**. ### Create a Service ID 1. In Certificates, Identifiers & Profiles, click **Identifiers → +**. 2. Select **Services IDs** and click **Continue**. ![Register a new identifier: Services IDs selected](https://docs.serverpod.dev/img/authentication/providers/apple/6-service-id-create.png) 3. Enter a description and a unique **Identifier** (for example, `com.example.service`). This value becomes your `serviceIdentifier`. Click **Continue**, then **Register**. 4. Click on the Service ID you created above. Check **Sign in with Apple** and click **Configure**. 5. In the modal, set: - **Primary App ID**: the App ID from the previous section - **Domains and Subdomains**: your domain (for example, `example.com`) - **Return URLs**: your server's callback route (for example, `https://example.com/auth/callback`) ![Web Authentication Configuration: Primary App ID, domains, and return URLs](https://docs.serverpod.dev/img/authentication/providers/apple/7-service-id-configure.png) 6. Click **Next**, then **Done**, then **Save**. :::warning All return URLs must use **HTTPS**. Apple rejects HTTP URLs, including `localhost`. For local development, expose your server over HTTPS using a tunnelling service, like ngrok or Cloudflare Tunnel. ::: :::note Register the value you will use for `appleRedirectUri`, the server callback route, under **Return URLs**. Apple validates that redirect. The separate `appleWebRedirectUri` is a page in your own web app that the server sends the browser to afterwards, so Apple never sees it and it does not belong here. ::: ### Create a Sign in with Apple key 1. In Certificates, Identifiers & Profiles, click **Keys → +**. 2. Enter a key name, check **Sign in with Apple**, and click **Configure**. Select your primary App ID and click **Save**. ![Configure key: Sign in with Apple checked, primary App ID selected](https://docs.serverpod.dev/img/authentication/providers/apple/8-key-create.png) 3. Click **Continue**, then **Register**. ![Register a New Key: review screen](https://docs.serverpod.dev/img/authentication/providers/apple/8-key-register.png) 4. Download the `.p8` key file immediately: **you can only download it once**. Note the **Key ID** shown on this page. ![Download Your Key: one-time download warning with Key ID visible](https://docs.serverpod.dev/img/authentication/providers/apple/8-key-download.png) 5. Find your **Team ID** in your [Apple Developer Account](https://developer.apple.com/account) under Membership. :::note Each primary App ID can have a maximum of two private keys. If you reach the limit, revoke an existing key before creating a new one. See [Create a Sign in with Apple private key](https://developer.apple.com/help/account/configure-app-capabilities/create-a-sign-in-with-apple-private-key/) for details. ::: ### Store your credentials Your server's `config/passwords.yaml` already has `development:`, `staging:`, and `production:` sections from the project template. Add the Apple credentials to the `development:` section: ```yaml development: # ... existing keys (database, redis, serviceSecret, etc.) ... appleServiceIdentifier: 'com.example.service' appleBundleIdentifier: 'com.example.app' appleRedirectUri: 'https://example.com/auth/callback' appleTeamId: 'ABC123DEF4' appleKeyId: 'XYZ789ABC0' appleKey: | -----BEGIN PRIVATE KEY----- MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg... -----END PRIVATE KEY----- # Web only (the web app URL the server redirects the browser back to). appleWebRedirectUri: 'https://example.com/auth/apple-complete' # Android only. appleAndroidPackageIdentifier: 'com.example.app' ``` :::tip Paste the raw `.p8` file contents as-is. Do not pre-generate a JWT. Serverpod handles that internally. If sign-in fails, see the [troubleshooting guide](https://docs.serverpod.dev/concepts/authentication/providers/apple/troubleshooting.md). ::: When you are ready to ship, see [Publishing to production](#publishing-to-production) for the production credential setup. ## Server-side configuration ### Add the Apple identity provider :::caution If your project doesn't have the auth module installed yet (for example, you're upgrading an older project), follow the [auth module Setup](https://docs.serverpod.dev/concepts/authentication/setup.md) guide first before continuing. ::: In your server's `server.dart`, import the Apple provider and add it to the existing `identityProviderBuilders` list on `pod.initializeAuthServices()`: ```dart import 'package:serverpod_auth_idp_server/providers/apple.dart'; // ... pod.initializeAuthServices( tokenManagerBuilders: [ JwtConfigFromPasswords(), ], identityProviderBuilders: [ // ... any existing providers (e.g., EmailIdpConfigFromPasswords) ... AppleIdpConfigFromPasswords(), ], ); ``` The `AppleIdpConfigFromPasswords()` constructor reads the eight `apple*` keys from `config/passwords.yaml` (or the corresponding `SERVERPOD_PASSWORD_` environment variables), so you do not have to wire up each credential manually. :::tip If you need more control over how the credentials are loaded, you can use `AppleIdpConfig(...)` with manual `pod.getPassword()` calls instead. See the [customizations](https://docs.serverpod.dev/concepts/authentication/providers/apple/customizations.md) page for details. ::: ### Configure web routes Sign in with Apple requires web routes for handling callbacks and revocation notifications. Add this call before `pod.start()`: ```dart pod.configureAppleIdpRoutes( revokedNotificationRoutePath: '/hooks/apple-notification', webAuthenticationCallbackRoutePath: '/auth/callback', ); ``` The `webAuthenticationCallbackRoutePath` must match the **Return URL** you registered on your Service ID. The `revokedNotificationRoutePath` is called by Apple when a user revokes access from their Apple ID settings. ### Create the endpoint Create a new endpoint file in your server project (for example, `my_project_server/lib/src/auth/apple_idp_endpoint.dart`). Extending the base class registers the sign-in methods with your server so your app can call them: ```dart import 'package:serverpod_auth_idp_server/providers/apple.dart'; class AppleIdpEndpoint extends AppleIdpBaseEndpoint {} ``` ### Start the server Start the server from your server project directory (for example, `my_project_server/`): ```bash serverpod start ``` Then create and apply the migration for the provider's tables: in the `serverpod start` terminal, press **M** to create and apply the migration. :::note Skipping the migration will cause the server to crash at runtime when the Apple provider tries to read or write user data. More detailed instructions can be found in the general [identity providers setup section](https://docs.serverpod.dev/concepts/authentication/setup.md#identity-providers-configuration). ::: ## Client-side configuration The Flutter app created with `serverpod create` already has `serverpod_auth_idp_flutter` in `pubspec.yaml`. If your app doesn't, add it: ```bash flutter pub add serverpod_auth_idp_flutter ``` It uses [sign\_in\_with\_apple](https://pub.dev/packages/sign_in_with_apple) under the hood for platform-specific sign-in flows. :::note Sign in with Apple may not work correctly on all Simulator versions. If you run into issues during development, test on a physical device to confirm whether the problem is Simulator-specific. ::: ### iOS and macOS Enable the Sign in with Apple capability in your Xcode project: 1. Open your project in Xcode. 2. Select your target. 3. Go to **Signing & Capabilities**. 4. Click **+ Capability**. 5. Add **Sign in with Apple**. ![Add capabilities](https://docs.serverpod.dev/img/authentication/providers/apple/1-xcode-add.png) ![Sign in with Apple](https://docs.serverpod.dev/img/authentication/providers/apple/2-xcode-sign-in-with-apple.png) ### Android Sign in with Apple on Android works through a web-based OAuth flow. When the user completes authentication, Apple redirects to your server's callback route, which then redirects back to your app using an Android intent URI with the `signinwithapple` scheme. The redirect URI and `appleAndroidPackageIdentifier` were already configured in the [Store your credentials](#store-your-credentials) and [Service ID](#create-a-service-id) steps. The only remaining step is to register the `signinwithapple` URI scheme in your `AndroidManifest.xml`: ```xml ``` :::warning This intent filter is required. Without it, the OAuth callback never returns to your app and sign-in silently hangs. ::: ### Web Sign in with Apple on web requires the Apple JS SDK. Add the following script to your Flutter app's `web/index.html` inside the `` tag: ```html ``` The redirect URI and `appleWebRedirectUri` were already configured in the [Store your credentials](#store-your-credentials) and [Service ID](#create-a-service-id) steps. ## Present the authentication UI ### Initialize the Sign in with Apple service In your Flutter app's `lib/client.dart`, the template already sets up the `Client` and calls `client.auth.initialize()` inside `initializeClient()`. Add `client.auth.initializeAppleSignIn()` right after it: ```dart unawaited(client.auth.initialize()); client.auth.initializeAppleSignIn(); ``` The sign-in service needs your Service ID and redirect URI on every platform, even though only the web and Android flow uses them. Pass them as build-time environment variables using `--dart-define`: ```bash flutter run \ -d "" \ --dart-define="APPLE_SERVICE_IDENTIFIER=com.example.service" \ --dart-define="APPLE_REDIRECT_URI=https://example.com/auth/callback" ``` Use the same values you configured in the [Service ID](#create-a-service-id) and [Store your credentials](#store-your-credentials) steps. You can also pass the values directly as parameters instead. See the [customizations page](https://docs.serverpod.dev/concepts/authentication/providers/apple/customizations.md#configuring-sign-in-with-apple-on-the-app) for details. ### Add the sign-in widget If you have configured the `SignInWidget` as described in the [setup section](https://docs.serverpod.dev/concepts/authentication/setup.md#present-the-authentication-ui), the Apple identity provider will be automatically detected and displayed in the sign-in widget. You can also use the `AppleSignInWidget` directly in your widget tree to include the Apple authentication flow in your own custom UI: ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; AppleSignInWidget( client: client, onAuthenticated: () { // Do something when the user is authenticated. // // NOTE: You should not navigate to the home screen here, otherwise // the user will have to sign in again every time they open the app. }, onError: (error) { // Handle errors ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: $error')), ); }, ) ``` This renders a Sign in with Apple button like this: ![Sign in with Apple button](https://docs.serverpod.dev/img/authentication/providers/apple/3-button.png) The widget automatically handles: - Sign in with Apple flow for iOS, macOS, Android, and web. - Token management. - Underlying `sign_in_with_apple` package error handling. For details on how to customize the Sign in with Apple UI in your Flutter app, see the [customizations page](https://docs.serverpod.dev/concepts/authentication/providers/apple/customizations.md#customize-the-sign-in-button). :::warning Apple sends the user's email and name only on the **first sign-in**. Serverpod stores them automatically during that first authentication. If the first sign-in never reaches your server, they cannot be retrieved later. See [User email is null after sign-in](https://docs.serverpod.dev/concepts/authentication/providers/apple/troubleshooting.md#user-email-is-null-after-sign-in). ::: ## Publishing to production ### Update the Apple Developer Portal Add your production domain and callback URL to the Service ID. The development tunnel URL and the production URL can stay registered at the same time. 1. In [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list), open your Service ID. 2. Under **Sign in with Apple**, click **Configure**. 3. Add your production domain to **Domains and Subdomains** (for example, `example.com`). 4. Add your production callback to **Return URLs** (for example, `https://example.com/auth/callback`). 5. Click **Next**, **Done**, then **Save**. ### Set production credentials Production runs out of the `production:` section of `passwords.yaml`, which is separate from the `development:` section you populated during setup. Adding production credentials does not replace your development ones. Both stay in place, and Serverpod picks the right set based on the run mode. Most credentials, like the Team ID, Key ID, and `.p8` private key, can be reused from development. The values that typically differ are the URLs (`appleRedirectUri` and `appleWebRedirectUri`), which should point at your production domain rather than your development tunnel. If you use a different App ID or Service ID for production, register them in the [Apple Developer Portal](https://developer.apple.com/account/resources/identifiers/list) first and use those identifiers below. Pick the path that matches your deployment: #### Self-hosted Add the `apple*` keys to the `production:` section of `passwords.yaml` with production values, or set them as environment variables on the production server using the `SERVERPOD_PASSWORD_` prefix (for example, `SERVERPOD_PASSWORD_appleServiceIdentifier`, `SERVERPOD_PASSWORD_appleKey`). #### Serverpod Cloud Use `serverpod cloud password set` for each credential. The `appleKey` value spans multiple lines, so pass it via `--from-file`: ```bash serverpod cloud password set appleServiceIdentifier "com.example.service" serverpod cloud password set appleBundleIdentifier "com.example.app" serverpod cloud password set appleRedirectUri "https://example.com/auth/callback" serverpod cloud password set appleTeamId "ABC123DEF4" serverpod cloud password set appleKeyId "XYZ789ABC0" serverpod cloud password set appleKey --from-file ./AuthKey_XYZ789ABC0.p8 serverpod cloud password set appleWebRedirectUri "https://example.com/auth/apple-complete" serverpod cloud password set appleAndroidPackageIdentifier "com.example.app" ``` Run these from your linked server project directory, or pass `--project ` on each call. See the [Serverpod Cloud passwords guide](https://docs.serverpod.dev/cloud/concepts/passwords-secrets-env-vars.md) for project linking and other options. ### Update app builds Release builds need the production Service ID and redirect URI on every platform, the same as during development. Pass them via `--dart-define`: ```bash flutter build web \ --dart-define="APPLE_SERVICE_IDENTIFIER=com.example.service" \ --dart-define="APPLE_REDIRECT_URI=https://example.com/auth/callback" ``` :::tip If you run into issues, see the [troubleshooting guide](https://docs.serverpod.dev/concepts/authentication/providers/apple/troubleshooting.md). ::: --- # Customize Apple sign-in https://docs.serverpod.dev/concepts/authentication/providers/apple/customizations This page covers additional configuration options for the Apple identity provider beyond the basic setup. On the server, you can control how credentials are loaded, react to account creation, and configure the web routes. In your app, you can configure Sign in with Apple, customize the sign-in button with the `AppleSignInWidget`, or build a completely custom interface with the `AppleAuthController`. ## Server configuration Below is a non-exhaustive list of some of the most common configuration options. For more details on all options, check the `AppleIdpConfig` in-code documentation. ### Loading Apple credentials The `AppleIdpConfigFromPasswords()` constructor reads the eight `apple*` keys from `config/passwords.yaml` (or the matching `SERVERPOD_PASSWORD_` environment variables) for you. This is the path used in the [setup guide](https://docs.serverpod.dev/concepts/authentication/providers/apple/setup.md#add-the-apple-identity-provider) and is the recommended default: ```dart final appleIdpConfig = AppleIdpConfigFromPasswords(); ``` Use `AppleIdpConfig(...)` directly when you need to pull credentials from a custom source, transform them at startup, or omit `passwords.yaml` entirely. You are responsible for resolving each value: ```dart final appleIdpConfig = AppleIdpConfig( serviceIdentifier: pod.getPassword('appleServiceIdentifier')!, bundleIdentifier: pod.getPassword('appleBundleIdentifier')!, redirectUri: pod.getPassword('appleRedirectUri')!, teamId: pod.getPassword('appleTeamId')!, keyId: pod.getPassword('appleKeyId')!, key: pod.getPassword('appleKey')!, webRedirectUri: pod.getPassword('appleWebRedirectUri'), androidPackageIdentifier: pod.getPassword('appleAndroidPackageIdentifier'), ); ``` ### Reacting to account creation For Apple-specific logic, use `onAfterAppleAccountCreated` on `AppleIdpConfig`. It receives the created `AppleAccount` row, so you can read the Apple identifier and the name Apple returned on first sign-in. ```dart AppleIdpConfigFromPasswords( onAfterAppleAccountCreated: (session, authUser, appleAccount, {required transaction}) async { session.log('Apple account created: ${appleAccount.userIdentifier}'); }, ) ``` For logic that should run whichever provider the user signed in with, use [`onAfterAuthUserCreated`](https://docs.serverpod.dev/concepts/authentication/working-with-users.md#reacting-to-the-user-created-event) on `AuthUsersConfig` instead. It fires the first time any provider creates an auth user. ```dart pod.initializeAuthServices( tokenManagerBuilders: [ JwtConfigFromPasswords(), ], identityProviderBuilders: [ AppleIdpConfigFromPasswords(), ], authUsersConfig: AuthUsersConfig( onAfterAuthUserCreated: (session, authUser, {required transaction}) async { // authUser.id is the new user's UUID, use it to create any // app-specific records that must exist before the user's first request. await UserData.db.insertRow( session, UserData(authUserId: authUser.id, createdAt: authUser.createdAt), transaction: transaction, ); }, ), ); ``` :::info This callback runs inside the same database transaction as the auth user creation. Throwing an exception inside it will abort the entire process and the user will not be created. If you perform external side-effects (e.g. analytics, sending emails), wrap them in a try/catch so an unrelated failure does not block sign-in. ::: ### Web routes configuration Sign in with Apple requires web routes for handling callbacks and notifications. These routes must be configured both on Apple's side and in your Serverpod server, using the `pod.configureAppleIdpRoutes()` method: ```dart pod.configureAppleIdpRoutes( revokedNotificationRoutePath: '/hooks/apple-notification', webAuthenticationCallbackRoutePath: '/auth/callback', ); ``` - `revokedNotificationRoutePath` (default: `'/hooks/apple-notification'`): The path Apple calls when a user revokes authorization. Register this URL in your Apple Developer Portal for server-to-server notifications. - `webAuthenticationCallbackRoutePath` (default: `'/auth/apple/callback'`): The path Apple redirects to after the user completes web-based sign-in. Must match the return URL registered on your Service ID. :::note When a user revokes access from their Apple ID settings, Apple sends a notification to `revokedNotificationRoutePath`. Registering the route is enough: Serverpod revokes the Apple authorization and the tokens it issued through Apple sign-in for that user. Clean up only your own derived records. ::: ### AppleIdpConfig parameters | Parameter | Type | Required | `passwords.yaml` key | Description | | -------------------------- | --------- | ------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `serviceIdentifier` | `String` | Yes | `appleServiceIdentifier` | The Services ID identifier (e.g. `com.example.service`). Required on every platform, though only the Android and web OAuth flow uses it. | | `bundleIdentifier` | `String` | Yes | `appleBundleIdentifier` | The App ID bundle identifier (e.g. `com.example.app`). Used as the client ID for native Apple platform sign-in. | | `redirectUri` | `String` | Yes | `appleRedirectUri` | The server callback route Apple redirects to after sign-in. Sent with every authorization-code exchange, and validated by Apple in the Android and web flow. Must be HTTPS and match the return URL registered on your Service ID. | | `teamId` | `String` | Yes | `appleTeamId` | The 10-character Team ID from your Apple Developer account. Used to sign the client secret JWT. | | `keyId` | `String` | Yes | `appleKeyId` | The Key ID of the Sign in with Apple private key. | | `key` | `String` | Yes | `appleKey` | The raw contents of the `.p8` private key file, including the `-----BEGIN PRIVATE KEY-----` header and footer. Do not pre-generate the JWT yourself. | | `webRedirectUri` | `String?` | web only | `appleWebRedirectUri` | The web app URL the browser is redirected to after the server receives Apple's callback. | | `androidPackageIdentifier` | `String?` | Android only | `appleAndroidPackageIdentifier` | The Android package name (e.g. `com.example.app`). When set, the callback route redirects Android sign-ins back to the app via an intent URI. | ## App configuration These options control the values your app passes to `initializeAppleSignIn()`. ### Configuring Sign in with Apple on the app Your app needs the Service ID and the server callback URL. The setup guide passes them via `--dart-define`. If you would rather hardcode them or resolve them at runtime, pass them directly to `initializeAppleSignIn()` instead: ```dart client.auth.initializeAppleSignIn( serviceIdentifier: 'com.example.service', redirectUri: 'https://example.com/auth/callback', ); ``` When both are passed, they take precedence over the `APPLE_SERVICE_IDENTIFIER` and `APPLE_REDIRECT_URI` build variables. The `redirectUri` must match the **Return URL** registered on your Apple Service ID and the value used by `pod.configureAppleIdpRoutes()`. :::note Only the web and Android flow consumes these values, but `initializeAppleSignIn` requires them on every platform. Pass them (or the matching dart-defines) even in an iOS-only app, or initialization throws. ::: #### Using environment variables The build variables `APPLE_SERVICE_IDENTIFIER` and `APPLE_REDIRECT_URI` are read by `initializeAppleSignIn()` whenever you do not pass the values as parameters: - `APPLE_SERVICE_IDENTIFIER`: your Services ID identifier (e.g. `com.example.service`) - `APPLE_REDIRECT_URI`: the server callback URL (e.g. `https://example.com/auth/callback`) Pass them at build time with `--dart-define`: ```bash flutter run \ --dart-define="APPLE_SERVICE_IDENTIFIER=com.example.service" \ --dart-define="APPLE_REDIRECT_URI=https://example.com/auth/callback" ``` This approach is useful when you need to: - Manage configuration separately for different platforms (Android, web) in a centralized way - Avoid committing sensitive configuration to version control - Configure different credentials for different build environments (development, staging, production) :::tip You can set `--dart-define` values in your IDE run configuration or CI/CD pipeline instead of passing them on every `flutter run` command. ::: ## Customize the sign-in button For the `buttonStyle` precedence rules that apply to all built-in buttons, see [Styling the buttons](https://docs.serverpod.dev/concepts/authentication/ui-components.md#styling-the-buttons). :::info The `SignInWidget` uses the `AppleSignInWidget` internally to display the Apple sign-in flow. You can also supply a custom `AppleSignInWidget` to the `SignInWidget` to override the default behavior. ```dart SignInWidget( client: client, appleSignInWidget: AppleSignInWidget( client: client, // Shape and label survive inside SignInWidget, unless its buttonStyle // sets them. Brand colors do not. shape: SignInButtonShape.rounded, text: SignInButtonTextVariant.signInWith, // A custom widget replaces the built-in handling, so pass your own callbacks. onAuthenticated: () { /* ... */ }, onError: (error) { /* ... */ }, ), ) ``` ::: ### Using the `AppleSignInWidget` The `AppleSignInWidget` handles the complete Apple sign-in flow for iOS, macOS, Android, and web. You can customize the widget's appearance and behavior: ```dart // AppleIDAuthorizationScopes comes from the sign_in_with_apple package. // Add it to your app's dependencies to import it. import 'package:sign_in_with_apple/sign_in_with_apple.dart'; AppleSignInWidget( client: client, // Button customization. The values shown are the defaults. style: AppleButtonStyle.black, // or white, whiteOutlined size: SignInButtonSize.large, // or medium, small text: SignInButtonTextVariant.continueWith, // or signInWith, signUpWith, signIn shape: SignInButtonShape.pill, // or rounded, rectangular logoAlignment: SignInButtonLogoAlignment.center, // or left minimumWidth: 240, // at most 400 textStyle: null, // TextStyle for the label // Scopes to request from Apple. // These are the default, and the only ones Sign in with Apple supports. scopes: const [ AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName, ], onAuthenticated: () { // Do something when the user is authenticated. // // NOTE: You should not navigate to the home screen here, otherwise // the user will have to sign in again every time they open the app. }, onError: (error) { // Handle errors }, ) ``` ## Build a custom UI with AppleAuthController For more control over the UI, you can use the `AppleAuthController` class, which provides all the authentication logic without any UI components. This allows you to build a completely custom authentication interface. ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; // Also import sign_in_with_apple here for AppleIDAuthorizationScopes. final controller = AppleAuthController( client: client, onAuthenticated: () { // Do something when the user is authenticated. // // NOTE: You should not navigate to the home screen here, otherwise // the user will have to sign in again every time they open the app. }, onError: (error) { // Handle errors }, scopes: const [ AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName, ], ); // Initiate sign-in await controller.signIn(); ``` ### AppleAuthController state management Your widget should render the appropriate UI based on the `state` property of the controller. You can also use the below state properties to build your UI: ```dart // Check current state final state = controller.state; // AppleAuthState enum // Check if loading final isLoading = controller.isLoading; // Check if authenticated final isAuthenticated = controller.isAuthenticated; // Get error message final errorMessage = controller.errorMessage; // Listen to state changes controller.addListener(() { setState(() { // Rebuild UI when controller state changes }); }); ``` #### AppleAuthController states - `AppleAuthState.idle` - Ready for user interaction. - `AppleAuthState.loading` - Processing a sign-in request. - `AppleAuthState.error` - An error occurred. - `AppleAuthState.authenticated` - Authentication was successful. ## Related - [Setup](https://docs.serverpod.dev/concepts/authentication/providers/apple/setup.md): configure Sign in with Apple on the server and in your app. - [Troubleshooting](https://docs.serverpod.dev/concepts/authentication/providers/apple/troubleshooting.md): fix common Apple sign-in errors. - [UI components](https://docs.serverpod.dev/concepts/authentication/ui-components.md): style the sign-in buttons and localize the built-in UI. - [Working with users](https://docs.serverpod.dev/concepts/authentication/working-with-users.md): manage auth users and react to account events. --- # Apple admin operations https://docs.serverpod.dev/concepts/authentication/providers/apple/admin-operations The Apple identity provider exposes admin operations through `AppleIdpAdmin`. Use them from server-side code to check whether Sign in with Apple accounts are still in good standing and to react when Apple has revoked an account. :::warning Call these methods only from secure server-side code. Do not expose them through client endpoints without authorization checks. ::: ## Accessing the AppleIdpAdmin The admin operations are available through `AuthServices.instance.appleIdp` after you add the Apple identity provider (see [setup](https://docs.serverpod.dev/concepts/authentication/providers/apple/setup.md#add-the-apple-identity-provider)). ```dart import 'package:serverpod_auth_idp_server/providers/apple.dart'; import 'package:serverpod_auth_idp_server/core.dart'; final appleIdp = AuthServices.instance.appleIdp; final admin = appleIdp.admin; ``` ## Checking account status The `checkAccountStatus` method walks Apple accounts that have not been checked in the last 24 hours and validates their stored refresh tokens with Apple. Call it from a scheduled task (for example a [recurring task](https://docs.serverpod.dev/concepts/scheduling/recurring-tasks.md)). When Apple has revoked the authorization, the `onExpiredUserAuthentication` callback receives that user's auth user ID. Revoke every session created through Sign in with Apple for that user. ```dart await admin.checkAccountStatus( session, onExpiredUserAuthentication: (authUserId) { // Revoke sessions for this auth user. }, ); ``` The method loads accounts in batches of 100 by default (`databaseBatchSize`). ## Related - [Setup](https://docs.serverpod.dev/concepts/authentication/providers/apple/setup.md): configure Sign in with Apple on the server and in your app. - [Customizations](https://docs.serverpod.dev/concepts/authentication/providers/apple/customizations.md): configuration options and sign-in UI customization. - [Troubleshooting](https://docs.serverpod.dev/concepts/authentication/providers/apple/troubleshooting.md): covers the revoked-notification route, the push counterpart of the polling this page documents. - [Working with users](https://docs.serverpod.dev/concepts/authentication/working-with-users.md): manage auth users and react to account events. --- # Troubleshoot Apple sign-in https://docs.serverpod.dev/concepts/authentication/providers/apple/troubleshooting This page helps you identify common Sign in with Apple failures, explains why they occur, and shows how to resolve them. For Apple's full list of OAuth error codes, see [TN3107: Resolving Sign in with Apple response errors](https://developer.apple.com/documentation/technotes/tn3107-resolving-sign-in-with-apple-response-errors). ## Setup checklist Go through this before investigating a specific error. Most problems come from a missed step. #### Apple Developer Portal - [ ] Enable **Sign in with Apple** on your App ID at [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list). - [ ] Create a **Service ID** for OAuth. - [ ] On the Service ID, check **Sign in with Apple**, click **Configure**, and select your **Primary App ID**. - [ ] Add your **Domains and Subdomains** (e.g. `example.com`) and **Return URLs** on the Service ID. - [ ] Confirm the **return URL** on the Service ID uses `https://` (not `http://` or `localhost`). - [ ] Create a **Sign in with Apple key** and download the `.p8` file. #### Server - [ ] Add `serverpod_auth_idp_server` to your server's `pubspec.yaml`. - [ ] Add the Apple credentials to `config/passwords.yaml` with the raw `.p8` file contents (not a pre-generated JWT). - [ ] Double-check the **`.p8` key** is indented consistently under `appleKey: |`. - [ ] Add `AppleIdpConfigFromPasswords()` to `identityProviderBuilders` in `server.dart`. - [ ] Call **`pod.configureAppleIdpRoutes(...)`** on the server before the pod starts. - [ ] Create an `AppleIdpEndpoint` file in `lib/src/auth/`. - [ ] Start the server with `serverpod start`, then create and apply the migration (press **M**). #### Client - [ ] Add `serverpod_auth_idp_flutter` to your Flutter app's `pubspec.yaml`. - [ ] Add `client.auth.initializeAppleSignIn()` after `client.auth.initialize()` inside `initializeClient()` in your Flutter app's `lib/client.dart`. - [ ] Add **Sign in with Apple** under Signing & Capabilities in Xcode (*iOS/macOS only*). - [ ] Add the **Apple JS SDK** script to `web/index.html` (*web only*). - [ ] Pass **`APPLE_SERVICE_IDENTIFIER`** and **`APPLE_REDIRECT_URI`** via `--dart-define`. Initialization throws an `ArgumentError` without them, on every platform. - [ ] Add the **`signinwithapple`** intent filter to `AndroidManifest.xml` (*Android only*). - [ ] Add **Apple's mail servers** to your SPF record if you email users who might use Hide My Email. ## Sign-in fails with `invalid_client` every time **Problem:** Every authentication attempt gives an `invalid_client` error from Apple. **Cause:** The `appleKey` value in `passwords.yaml` is not indented correctly. The key gets corrupted during parsing. The server starts without error, but Apple sees an invalid signature. **Resolution:** Paste the raw `.p8` key under `appleKey` with consistent indentation. All lines of the key must line up with the one that starts `-----BEGIN PRIVATE KEY-----`. For example: ```yaml appleKey: | -----BEGIN PRIVATE KEY----- MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg... -----END PRIVATE KEY----- ``` Alternatively, set `appleKey` via the `SERVERPOD_PASSWORD_appleKey` environment variable to avoid YAML indentation entirely. ## Sign-in starts failing with `invalid_client` after months of success **Problem:** Sign-in was working for months, then suddenly fails with `invalid_client` and you haven't changed code. **Cause:** The `appleKey` value holds a pre-generated client secret JWT, not the raw `.p8` key. Apple makes JWTs expire after six months. When it expires, all sign-ins fail. **Resolution:** Replace any JWT in `appleKey` with the raw `.p8` private key (include the full header and footer). Serverpod will create fresh short-lived JWTs automatically. No need to handle JWTs yourself. See [Creating a client secret](https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret). ## Sign-in fails with `invalid_grant` **Problem:** Authentication fails with an `invalid_grant` error from Apple. **Cause:** Apple's authorization codes are single-use and expire after five minutes. This error occurs when: - The authorization code was already exchanged (e.g. the request was retried after a network failure). - The server clock is significantly out of sync, causing the client secret JWT to appear expired before Apple processes it. **Resolution:** - Do not retry requests that carry an Apple authorization code. If the flow fails, restart it from the beginning. - Ensure your server's system clock is synchronized via NTP. A drift of more than a few seconds will cause JWT validation to fail on Apple's side. ## Wrong identifier passed for web or Android sign-in **Problem:** Sign-in on Android or web fails immediately, or Apple returns `invalid_client` / `invalid_request` even though credentials look correct. **Cause:** There are two separate identifiers in Apple's system and they are easy to mix up: - **App ID** (`bundleIdentifier`): the bundle identifier of your iOS/macOS app (e.g. `com.example.app`). Used for native Apple platform sign-in only. - **Services ID** (`serviceIdentifier`): a separate identifier you create in the Apple Developer Portal specifically for web and Android OAuth (e.g. `com.example.service`). This acts as the OAuth client ID. Passing the App ID bundle identifier where the Services ID is expected will cause Apple to reject the request. **Resolution:** Check `passwords.yaml` and confirm: - `appleServiceIdentifier` is set to your **Services ID** (the one created under Identifiers → Services IDs). - `appleBundleIdentifier` is set to your **App ID** bundle identifier. If you use `--dart-define`, confirm `APPLE_SERVICE_IDENTIFIER` is the Services ID, not the bundle ID. ## Sign-in hangs on Android **Problem:** The OAuth flow opens a browser, but never returns to the app. Sign-in seems to finish but the app doesn't get the callback. **Cause:** The `signinwithapple` URI scheme isn't registered in `AndroidManifest.xml`, so Android drops the callback. **Resolution:** Add this activity to `AndroidManifest.xml`: ```xml ``` ## Server crashes on first Apple sign-in with "no such table" **Problem:** The server builds and starts, but crashes when a user tries Apple sign-in. The error cites a missing table (like `serverpod_auth_idp_apple_account`). **Cause:** The database migration that creates the provider's tables was never created or applied. **Resolution:** In the running `serverpod start` terminal, press **M** to create and apply the migration. ## Apple rejects the redirect URI with `invalid_request` **Problem:** The web OAuth flow fails with `invalid_request` and Apple's error page says the redirect URI is invalid. **Cause:** You're using HTTP instead of HTTPS for the redirect. Apple requires HTTPS and does not allow `localhost`. **Resolution:** Always use an HTTPS URL for your redirect. For local development, run your server behind an HTTPS tunnel. Register the tunneled `https://` URL as your return URL in Apple's Developer Portal, and update `appleRedirectUri` in `passwords.yaml` to match. ## Emails aren't delivered to some users **Problem:** Transactional emails (password resets, notifications) work for most people, but some never receive them. **Cause:** Some users chose Apple's "Hide My Email" during sign-in. Mail to relay addresses like `@privaterelay.appleid.com` fails if your domain's SPF record doesn't include Apple's mail servers. **Resolution:** Add Apple's mail servers to your SPF record. See [Configure private email relay service](https://developer.apple.com/help/account/configure-app-capabilities/configure-private-email-relay-service/) for SPF settings and instructions. ## User email is `null` after sign-in **Problem:** The user's email is missing or `null` after sign-in, or it's present on first sign-in but missing after that. **Cause:** Apple sends the email address and name only once, during the initial authorization. Later sign-ins carry only the stable `sub` identifier. Serverpod stores both values on the `AppleAccount` row when it first creates the account. The usual reason they are missing is that the first authorization never completed on your server, for example because the endpoint was added afterwards. **Resolution:** The module does not backfill these fields on later sign-ins. Ask the user for their email in your app. If the user removes your app in their Apple ID settings (**Sign in with Apple > Stop Using Apple ID**) and signs in again, Apple resends the values, but the module keeps the old account row. Sign-in itself is unaffected either way, since accounts are keyed by Apple's stable identifier, not the email. See [Authenticating users with Sign in with Apple](https://developer.apple.com/documentation/sign_in_with_apple/authenticating-users-with-sign-in-with-apple). ## iOS sign-in prompt doesn't show **Problem:** Tapping the Sign in with Apple button on iOS has no effect, or the Apple authentication UI never appears. **Cause:** The App ID isn't set up with the Sign in with Apple capability, or your Xcode project isn't using that App ID. **Resolution:** 1. In [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list), find your App ID. Check that **Sign in with Apple** is enabled under Capabilities. 2. In Xcode, select your target, open **Signing & Capabilities**, and check that **Sign in with Apple** is listed. If not, click **+ Capability** to add it. 3. Download and install your new provisioning profile if needed. ## Sign-in does not work in the iOS Simulator **Problem:** Sign in with Apple silently fails or the native authentication sheet does not appear when running in the iOS Simulator, but works fine on a physical device. **Cause:** Some Simulator versions do not fully support the native Sign in with Apple flow. This is a known Simulator limitation, not a code or configuration issue. **Resolution:** Test on a physical device to confirm the problem is Simulator-specific. If sign-in works on a real device, no changes are needed. ## Web sign-in fails with `TypeError: type ... is not a subtype of type 'JSObject'` **Problem:** Clicking the Apple button on web throws a `TypeError` mentioning `JSObject` or a minified type like `minified:CM`. **Cause:** The Apple JS SDK is not loaded. The `sign_in_with_apple` package calls `AppleID.auth.init()` on the page, but that function only exists after Apple's script is loaded in the HTML. **Resolution:** Add the Apple JS SDK to your Flutter app's `web/index.html` inside the `` tag: ```html ``` ## macOS sign-in shows "Sign Up Not Completed" **Problem:** The native Sign in with Apple sheet appears on macOS, but immediately shows "Sign Up Not Completed" without completing authentication. **Cause:** This is almost always a signing or entitlements mismatch on the macOS target. Sign in with Apple needs the bundle ID, Team ID, and entitlements to line up with an App ID that has the capability enabled. **Resolution:** Check, in this order: 1. In Xcode, open the macOS target's **Signing & Capabilities** tab and confirm that **Sign in with Apple** is listed. If not, click **+ Capability** to add it. 2. Confirm the macOS bundle ID matches the App ID that has Sign in with Apple enabled in the Apple Developer Portal, and that the signing **Team** matches the same Apple Developer account. 3. If your macOS app is sandboxed, make sure the sandbox entitlements include `com.apple.security.network.client`. Without outbound network access, the request to Apple's servers fails silently. 4. Regenerate or re-download the provisioning profile after any of the above changes so the new entitlements are picked up. ## User stays signed in after removing Apple access **Problem:** A user removes your app from Apple ID settings (`Settings > [your name] > Sign-In & Security > Sign in with Apple > Stop Using Apple ID`) but is still logged in to your app. **Cause:** Apple's revocation notification never reaches your server. Once it does, Serverpod revokes the Apple authorization and the tokens it issued through Apple sign-in automatically. **Resolution:** Check that `pod.configureAppleIdpRoutes()` registers a `revokedNotificationRoutePath`, that the route's public HTTPS URL is registered as the server-to-server notification endpoint in the Apple Developer Portal, and that the URL is reachable from the internet. See [Processing changes for Sign in with Apple accounts](https://developer.apple.com/documentation/signinwithapple/processing-changes-for-sign-in-with-apple-accounts) for how the notification works. ## Related - [Setup](https://docs.serverpod.dev/concepts/authentication/providers/apple/setup.md): configure Sign in with Apple on the server and in your app. - [Customizations](https://docs.serverpod.dev/concepts/authentication/providers/apple/customizations.md): configuration options and sign-in UI customization. - [UI components](https://docs.serverpod.dev/concepts/authentication/ui-components.md): the sign-in widgets and how to compose them. --- # Set up Facebook sign-in https://docs.serverpod.dev/concepts/authentication/providers/facebook/setup Sign in with Facebook uses a **Facebook Login app** and the external `serverpod_auth_idp_flutter_facebook` package. Unlike providers built into the core auth module, Facebook sign-in needs this extra package, which wraps the native Facebook SDK to handle the flow on iOS, Android, web, and macOS. ## Prerequisites Before following this guide, make sure you have: - A Facebook account with access to [Facebook for Developers](https://developers.facebook.com/). - A running Serverpod project (server, client, and Flutter app packages from `serverpod create`). - The Serverpod auth module installed and configured per the [authentication setup](https://docs.serverpod.dev/concepts/authentication/setup.md). If your project was generated with an older Serverpod version, follow that guide first to add `serverpod_auth_idp_server` and `serverpod_auth_idp_flutter` and to configure `pod.initializeAuthServices()` before continuing. ## Create your Facebook app 1. Go to [Facebook for Developers](https://developers.facebook.com/apps/creation/) and log in with your Facebook account. 2. Click **Create an app** and follow the setup wizard: - **App details**: Enter your **App name** and **App contact email**. ![Create app details](https://docs.serverpod.dev/img/authentication/providers/facebook/12-create-app-details.png) - **Use cases**: Select **"Authenticate and request data from users with Facebook Login"**. ![Select the Facebook Login use case](https://docs.serverpod.dev/img/authentication/providers/facebook/13-select-use-case.png) - **Business**: Optionally select a **Business Portfolio** if available. - Complete any additional **Requirements**. 3. Click through to the **Overview** page and go to your app **Dashboard**. ### Configure Facebook Login After creating your app, click the **Customize** button next to **"Authenticate and request data from users with Facebook Login"** on the **Dashboard** or **Use cases** page. This opens the customization page with the following menu options: ![Use Case Customize](https://docs.serverpod.dev/img/authentication/providers/facebook/1-use-case-customize.png) #### 1. Permissions and features Configure the permissions your application will request: - `public_profile`: Added by default, allows access to basic profile information. - `email`: Recommended. Allows to read a person's primary email address. ![Permissions and Features](https://docs.serverpod.dev/img/authentication/providers/facebook/2-permissions.png) #### 2. Settings Configure the authentication settings: - Turn **Login with the JavaScript SDK** to **Yes**. Web and macOS sign-in use the Facebook JavaScript SDK, and the flow fails at runtime with "JSSDK Option is Not Toggled" if this is left off. You can skip this if you only target iOS and Android. ![Login with the JavaScript SDK](https://docs.serverpod.dev/img/authentication/providers/facebook/11-js-sdk-toggle.png) - In **Allowed Domains for the JavaScript SDK**, add: - `https://www.facebook.com` (required to avoid CORS issues) - Your own domain if you plan to support web authentication (for example, `https://yourdomain.com`) ![Allowed Domains](https://docs.serverpod.dev/img/authentication/providers/facebook/3-allowed-domains.png) #### 3. Quickstart Set up your platforms by selecting and configuring: - **iOS** - **Android** - **Web** - **Other** (for additional platforms like macOS) Save your changes after completing the configuration. ### Get app credentials 1. Go to **App settings** > **Basic** in your Facebook App Dashboard to retrieve: - **App ID** - **App secret** 2. Go to **App settings** > **Advanced** to retrieve: - **Client token** :::tip The **App secret** is sensitive. Keep it confidential and never commit it to version control. The **Client token** is used by the mobile SDKs on Android and iOS. Web and macOS initialize with the App ID alone. ::: ## Server-side configuration ### Store your credentials Your server's `config/passwords.yaml` already has `development:`, `staging:`, and `production:` sections from the project template. Add `facebookAppId` and `facebookAppSecret` to the `development:` section using the values from your Facebook app: ```yaml development: # ... existing keys (database, redis, serviceSecret, etc.) ... facebookAppId: 'your-facebook-app-id' facebookAppSecret: 'your-facebook-app-secret' ``` For production, add the same two keys to the `production:` section, or set the `SERVERPOD_PASSWORD_facebookAppId` and `SERVERPOD_PASSWORD_facebookAppSecret` environment variables on your production server. See [Publishing to production](#publishing-to-production) below for the full prod walkthrough. :::warning **Never commit `config/passwords.yaml` to version control.** It contains your app secret. Use environment variables or a secrets manager in production. ::: ### Add the Facebook identity provider Your server's `server.dart` file (for example, `my_project_server/lib/server.dart`) should already contain a `pod.initializeAuthServices()` call if your project was created with the Serverpod project template (`serverpod create`). If it's not there, see [Setup](https://docs.serverpod.dev/concepts/authentication/setup.md) first to configure the auth module and JWT settings. Add the Facebook import and `FacebookIdpConfigFromPasswords()` to the existing `identityProviderBuilders` list: ```dart import 'package:serverpod_auth_idp_server/providers/facebook.dart'; ``` ```dart pod.initializeAuthServices( tokenManagerBuilders: [ JwtConfigFromPasswords(), ], identityProviderBuilders: [ // ... any existing providers (e.g., EmailIdpConfigFromPasswords) ... FacebookIdpConfigFromPasswords(), ], ); ``` The `FacebookIdpConfigFromPasswords()` constructor automatically loads the App ID and secret from the `facebookAppId` and `facebookAppSecret` keys in `config/passwords.yaml` (or the matching `SERVERPOD_PASSWORD_*` environment variables). :::tip If you need more control over how the credentials are loaded, use `FacebookIdpConfig(appId: ..., appSecret: ...)` instead. See [Customizations](https://docs.serverpod.dev/concepts/authentication/providers/facebook/customizations.md) for details. ::: ### Create the endpoint Create a new endpoint file in your server project (for example, `my_project_server/lib/src/auth/facebook_idp_endpoint.dart`) alongside the existing auth endpoints. Extending the base class registers the sign-in methods with your server so your app can call them to complete the authentication flow: ```dart import 'package:serverpod_auth_idp_server/providers/facebook.dart'; class FacebookIdpEndpoint extends FacebookIdpBaseEndpoint {} ``` ### Start the server Start the server from your server project directory (for example, `my_project_server/`): ```bash serverpod start ``` Then create and apply the migration for the provider's tables: in the `serverpod start` terminal, press **M** to create and apply the migration. :::warning Skipping the migration will cause the server to crash at runtime when the Facebook provider tries to read or write user data. More detailed instructions can be found in the general [identity providers setup section](https://docs.serverpod.dev/concepts/authentication/setup.md#identity-providers-configuration). ::: ## Client-side configuration ### Install required packages Add the `serverpod_auth_idp_flutter_facebook` package to your Flutter project: ```bash flutter pub add serverpod_auth_idp_flutter_facebook ``` This package uses [`flutter_facebook_auth`](https://pub.dev/packages/flutter_facebook_auth) to handle the authentication flow. The documentation for that package should in most cases also apply to this setup. :::info Unlike providers such as Google or Apple that are built into the core `serverpod_auth_idp_flutter` package, Facebook authentication is provided as a separate external package. This keeps the core package lightweight while allowing you to opt-in to Facebook authentication when needed. ::: ### Android The minimum Android SDK version required is 21. Update your `android/app/build.gradle` file: ```gradle defaultConfig { ... minSdkVersion 21 ... } ``` #### Add Facebook SDK configuration 1. Open or create `/android/app/src/main/res/values/strings.xml`: ```xml YOUR_FACEBOOK_APP_ID fbYOUR_FACEBOOK_APP_ID YOUR_CLIENT_TOKEN ``` Replace `YOUR_FACEBOOK_APP_ID` with your App ID and `YOUR_CLIENT_TOKEN` with your Client token (found in **Settings** > **Advanced** > **Client token** in the Facebook App Dashboard). 2. Open `/android/app/src/main/AndroidManifest.xml` and add the following: **Add internet permission before the `` element:** ```xml ``` **Add meta-data inside the `` element:** ```xml ... ... ``` #### Configure Package Visibility (Android 11+) For Android 11 and above, add the following inside the `` element: ```xml ... ``` #### Configure Android platform in Facebook In the Facebook App Dashboard, go to **Use cases** > **Customize** > **Quickstart** and select the **Android** tab. ![Quickstart Android](https://docs.serverpod.dev/img/authentication/providers/facebook/4-quickstart-android.png) Follow these steps: **Step 3: Tell Us about Your Android Project** Fill in your Android app details: - **Package Name**: Your app's package identifier (found in `android/app/build.gradle` as `applicationId` or in `AndroidManifest.xml` as `package`). - **Class Name**: Your main activity class name (typically `MainActivity`). ![Package Name](https://docs.serverpod.dev/img/authentication/providers/facebook/5-package-name.png) **Step 4: Add Your Development and Release Key Hashes** Generate and add your key hashes: **Debug Key Hash (for development):** macOS/Linux: ```bash keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore | openssl sha1 -binary | openssl base64 ``` Windows: ```bash keytool -exportcert -alias androiddebugkey -keystore "C:\Users\USERNAME\.android\debug.keystore" | "PATH_TO_OPENSSL_LIBRARY\bin\openssl" sha1 -binary | "PATH_TO_OPENSSL_LIBRARY\bin\openssl" base64 ``` **Release Key Hash (for production):** ```bash keytool -exportcert -alias YOUR_RELEASE_KEY_ALIAS -keystore YOUR_RELEASE_KEY_PATH | openssl sha1 -binary | openssl base64 ``` Paste the generated key hashes into the Facebook console and save. ![Add Key Hashes](https://docs.serverpod.dev/img/authentication/providers/facebook/6-save-key-hashes.png) :::note You can skip the remaining steps (1, 2, 5, 6+) as they are not required for Flutter apps or have already been covered. ::: For more detailed Android setup instructions, refer to the [flutter\_facebook\_auth Android documentation](https://facebook.meedu.app/docs/7.x.x/android). ### iOS The minimum iOS deployment target is 12.0. Update your `ios/Podfile`: ```ruby platform :ios, '12.0' ``` Also set the deployment target to 12.0 in Xcode (**Runner** > **General** > **Deployment Info**). #### Configure iOS platform in Facebook In the Facebook App Dashboard, go to **Use cases** > **Customize** > **Quickstart** and select the **iOS** tab. ![Quickstart iOS](https://docs.serverpod.dev/img/authentication/providers/facebook/7-quickstart-ios.png) Follow these steps: **Step 2: Add your Bundle Identifier** Enter your iOS app's Bundle Identifier (found in Xcode under **Runner** > **General** > **Identity** or in your `Info.plist`). ![iOS Bundle ID](https://docs.serverpod.dev/img/authentication/providers/facebook/8-ios-bundle-id.png) **Step 4: Configure Your info.plist** Open `ios/Runner/Info.plist` and add the following configuration (replace placeholders with your actual values): ```xml CFBundleURLTypes CFBundleURLSchemes fbYOUR_APP_ID FacebookAppID YOUR_APP_ID FacebookClientToken YOUR_CLIENT_TOKEN FacebookDisplayName YOUR_APP_NAME LSApplicationQueriesSchemes fbapi fb-messenger-share-api ``` Replace `YOUR_APP_ID` with your Facebook App ID, `YOUR_CLIENT_TOKEN` with your Client token, and `YOUR_APP_NAME` with your app's name. :::tip If you have other providers (like Google) implemented, merge the values for `CFBundleURLTypes` and `LSApplicationQueriesSchemes` instead of creating duplicate keys. ::: Additionally, ensure that the **Keychain Sharing** capability is enabled for your target in Xcode (**Runner** > **Signing & Capabilities** > **+ Capability** > **Keychain Sharing**). :::note You can skip the remaining steps (1, 3, 5-9) as they are not required for Flutter apps or have already been covered. ::: #### iOS App Tracking Transparency (ATT) On iOS, Facebook may issue **limited access tokens** when App Tracking Transparency (ATT) permission is not granted. These limited tokens cannot be validated by the server or used to retrieve user profile data, which will cause authentication to fail. To ensure full Facebook authentication functionality on iOS, you should request ATT permissions before initiating Facebook sign-in. You can use the [`app_tracking_transparency`](https://pub.dev/packages/app_tracking_transparency) package to handle this: ```dart import 'package:app_tracking_transparency/app_tracking_transparency.dart'; // Request tracking authorization before showing Facebook sign-in final status = await AppTrackingTransparency.requestTrackingAuthorization(); ``` :::warning Without ATT permission granted, Facebook authentication fails on iOS. Consider requesting this permission early in your app's flow or before showing the Facebook sign-in button. ::: For more detailed iOS setup instructions, refer to the [flutter\_facebook\_auth iOS documentation](https://facebook.meedu.app/docs/7.x.x/ios). ### Web For web platforms, you must initialize the FacebookSignInService with your App ID: ```dart await client.auth.initializeFacebookSignIn( appId: 'YOUR_FACEBOOK_APP_ID', ); ``` #### Configure Web platform in Facebook In the Facebook App Dashboard, go to **Use cases** > **Customize** > **Quickstart** and select the **Web** tab. ![Quickstart Web](https://docs.serverpod.dev/img/authentication/providers/facebook/9-quickstart-web.png) Follow these steps: **1. Add Site URL in Quickstart** Enter your website's Site URL (for example, `https://yourdomain.com`) and save your changes. :::note You can skip the remaining steps (2-5) as they are not required for Flutter apps or have already been covered. ::: **2. Configure Settings (Important)** Go to **Use cases** > **Customize** > **Settings** and configure the following: - Confirm **Login with the JavaScript SDK** is set to **Yes** (from the [Settings step](#2-settings) above). Web sign-in fails with "JSSDK Option is Not Toggled" without it. - Under **Allowed Domains for the JavaScript SDK**, add all domains where your app will be accessible: - Your development domain: `http://localhost:PORT` (for example, `http://localhost:8082` when serving the Flutter web build from Serverpod). - Your production domain: `https://yourdomain.com`. This enables Facebook authentication on these domains. Without it, Facebook sign-in fails due to CORS restrictions. ![Allowed Domains](https://docs.serverpod.dev/img/authentication/providers/facebook/3-allowed-domains.png) :::warning The Facebook JavaScript SDK is only allowed over `https`, but you can test the plugin on `localhost` (you will see an error message in your web console). ::: For more detailed web setup instructions, refer to the [flutter\_facebook\_auth web documentation](https://facebook.meedu.app/docs/7.x.x/web). ### macOS #### Enable Network Capabilities In `macos/Runner/Info.plist`, add: ```xml com.apple.security.network.server ``` In Xcode, select the **Runner** target, go to **Signing & Capabilities**, and enable **Outgoing Connections**. #### Enable Keychain Sharing macOS uses `flutter_secure_storage` to securely store session data. Add the **Keychain Sharing** capability in Xcode (**Runner** > **Signing & Capabilities** > **+ Capability** > **Keychain Sharing**). See [Set up authentication on macOS](https://docs.serverpod.dev/concepts/authentication/macos-authentication.md) for file-based setup steps and troubleshooting. #### Initialize the FacebookSignInService For macOS, you must initialize the FacebookSignInService with your App ID: ```dart await client.auth.initializeFacebookSignIn( appId: 'YOUR_FACEBOOK_APP_ID', ); ``` #### Configure macOS platform in Facebook **1. Configure settings** Go to **Use cases** > **Customize** > **Settings** and configure the following: - Enable **Login from Devices** under **Client OAuth settings**. - Enable **Login with the JavaScript SDK** under **Client OAuth settings**. - In **Allowed Domains for the JavaScript SDK**, add `https://www.facebook.com` to avoid CORS issues. ![Use cases Settings](https://docs.serverpod.dev/img/authentication/providers/facebook/10-settings.png) For more detailed macOS setup instructions, refer to the [flutter\_facebook\_auth macOS documentation](https://facebook.meedu.app/docs/7.x.x/macos). ## Present the authentication UI ### Initialize the Facebook sign-in service Initialize the service in your app's `lib/client.dart` using the `initializeFacebookSignIn()` extension method on `FlutterAuthSessionManager`, on the line after the existing `client.auth.initialize()` call in `initializeClient()`. ```dart import 'package:serverpod_auth_idp_flutter_facebook/serverpod_auth_idp_flutter_facebook.dart'; Future initializeClient() async { client = Client(await serverUrl) ..connectivityMonitor = FlutterConnectivityMonitor() ..authSessionManager = FlutterAuthSessionManager(); unawaited(client.auth.initialize()); client.auth.initializeFacebookSignIn(); } ``` :::note For web and macOS platforms, you must provide your Facebook App ID during initialization: ```dart client.auth.initializeFacebookSignIn( appId: 'YOUR_FACEBOOK_APP_ID', ); ``` For iOS and Android, the App ID is not required as the SDK reads credentials from native configuration. ::: ### Show the Facebook sign-in button If you use the template's `SignInWidget` (see [Present the authentication UI](https://docs.serverpod.dev/concepts/authentication/setup.md#present-the-authentication-ui)), the Facebook button is detected and shown automatically once the `serverpod_auth_idp_flutter_facebook` package is installed and the service is initialized. It handles the full sign-in flow, token management, and error handling on iOS, Android, web, and macOS. To customize the button or build a fully custom UI, see [Customizations](https://docs.serverpod.dev/concepts/authentication/providers/facebook/customizations.md#customize-the-sign-in-button). ## Publishing to production Before going live, complete the following steps. ### 1. Switch the app to Live mode New Facebook apps start in **Development mode**, where only people with a [role on the app](https://developers.facebook.com/docs/development/build-and-test/app-roles) (admins, developers, testers) can sign in. To let anyone sign in, toggle the app from **Development** to **Live** at the top of the [Facebook App Dashboard](https://developers.facebook.com/apps). :::note Going Live requires a valid **Privacy Policy URL** (**App settings** > **Basic**), and requesting permissions beyond `email` and `public_profile` requires **App Review**. See [Accessing Facebook APIs](https://docs.serverpod.dev/concepts/authentication/providers/facebook/customizations.md#accessing-facebook-apis). ::: ### 2. Add your production domains and platforms - **Web and macOS**: In **Use cases** > **Customize** > **Settings**, confirm **Login with the JavaScript SDK** is **Yes** and add your production domain to **Allowed Domains for the JavaScript SDK** (for example, `https://yourdomain.com`) alongside your development domain. Both can stay registered so dev and prod work at the same time. - **Android**: Add your **release key hash** (not only the debug one) to the Android platform in the Facebook app. Generate it from your release keystore: ```bash keytool -exportcert -alias YOUR_RELEASE_KEY_ALIAS -keystore YOUR_RELEASE_KEY_PATH | openssl sha1 -binary | openssl base64 ``` - **iOS**: No change is needed between dev and prod for the custom URL scheme. ### 3. Set production credentials Production runs out of the `production:` section of `passwords.yaml`, which is separate from the `development:` section you populated during setup. Adding production credentials does not replace your development ones. Both stay in place, and Serverpod picks the right set based on the run mode. You can reuse the same Facebook app for development and production, or [create a separate app](https://developers.facebook.com/apps/creation/) per environment and use its credentials. #### Self-hosted Add `facebookAppId` and `facebookAppSecret` to the `production:` section of `passwords.yaml`: ```yaml production: # ... existing keys ... facebookAppId: 'your-facebook-app-id' facebookAppSecret: 'your-facebook-app-secret' ``` Alternatively, set the `SERVERPOD_PASSWORD_facebookAppId` and `SERVERPOD_PASSWORD_facebookAppSecret` [environment variables](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md#via-environment-variables) on your production server with the same values. #### Serverpod Cloud Use `serverpod cloud password set` to upload each value. The App ID is public, so pass it as a positional argument. The App secret is sensitive, so read it from a file with `--from-file` to keep it out of your shell history: ```bash serverpod cloud password set facebookAppId your-facebook-app-id serverpod cloud password set facebookAppSecret --from-file path/to/facebook-app-secret.txt ``` Run these from your linked server project directory, or pass `--project ` on each call. See the [Serverpod Cloud passwords guide](https://docs.serverpod.dev/cloud/concepts/passwords-secrets-env-vars.md) for project linking and other options. ### 4. Provide the App ID to the production Flutter build On web and macOS the App ID must be available to the Flutter app at build time. When you omit `appId`, the provider reads it from the `FACEBOOK_APP_ID` compile-time environment variable, so a single `client.dart` works in dev and prod: ```dart await client.auth.initializeFacebookSignIn(); ``` Pass the value in your production build (or the `flutter_build` step for Serverpod Cloud): ```bash flutter build web --dart-define=FACEBOOK_APP_ID=your-facebook-app-id ``` On iOS and Android the App ID comes from the native configuration files, so no build-time value is needed. See [Configuring Facebook sign-in on the app](https://docs.serverpod.dev/concepts/authentication/providers/facebook/customizations.md#configuring-facebook-sign-in-on-the-app). :::tip If you run into issues, see the [troubleshooting guide](https://docs.serverpod.dev/concepts/authentication/providers/facebook/troubleshooting.md). ::: --- # Customize Facebook sign-in https://docs.serverpod.dev/concepts/authentication/providers/facebook/customizations This page covers additional configuration options for the Facebook identity provider beyond the basic setup. It also covers how to customize the sign-in UI. You can use the `FacebookSignInWidget` to display the Facebook sign-in flow in your own custom UI, or the `FacebookAuthController` to build a completely custom authentication interface. ## Server configuration Common configuration options for the Facebook provider. For more details on all options, check the `FacebookIdpConfig` in-code documentation. ### Load the credentials yourself The setup guide uses `FacebookIdpConfigFromPasswords`, which reads `facebookAppId` and `facebookAppSecret` from your password store. To control the loading yourself, use `FacebookIdpConfig` and pass the values directly: ```dart FacebookIdpConfig( appId: myAppId, appSecret: myAppSecret, ) ``` Both classes accept the same optional callbacks, such as `facebookAccountDetailsValidation` and `getExtraFacebookInfoCallback`, shown below. The examples on this page use `FacebookIdpConfigFromPasswords`, the class from the setup guide. ### Custom account validation You can customize the validation for Facebook account details before allowing sign-in. By default, the validation checks that the received account details contain a non-empty userIdentifier. The default validation logic: ```dart static void validateFacebookAccountDetails( final FacebookAccountDetails accountDetails, ) { if (accountDetails.userIdentifier.isEmpty) { throw FacebookUserInfoMissingDataException(); } } ``` To customize validation, provide your own `facebookAccountDetailsValidation` function: ```dart final facebookIdpConfig = FacebookIdpConfigFromPasswords( // Optional: Custom validation for Facebook account details facebookAccountDetailsValidation: (FacebookAccountDetails accountDetails) { // Throw an exception if account doesn't meet custom requirements if (accountDetails.userIdentifier.isEmpty) { throw FacebookUserInfoMissingDataException(); } // Example: Require email to be present if (accountDetails.email == null || accountDetails.email!.isEmpty) { throw FacebookUserInfoMissingDataException(); } }, ); ``` :::note Users may choose not to share their email or other information during the Facebook login flow. Adjust your validation function carefully to avoid blocking legitimate users. ::: ### FacebookAccountDetails The `facebookAccountDetailsValidation` callback receives a `FacebookAccountDetails` record with the following properties: | Property | Type | Description | | ---------------- | --------- | ------------------------------------------- | | `userIdentifier` | `String` | The Facebook user's unique identifier (UID) | | `email` | `String?` | The user's email address (may be null) | | `fullName` | `String?` | The user's full name from Facebook | | `firstName` | `String?` | The user's first name | | `lastName` | `String?` | The user's last name | | `image` | `Uri?` | URL to the user's profile image | Example of accessing these properties: ```dart facebookAccountDetailsValidation: (accountDetails) { print('Facebook UID: ${accountDetails.userIdentifier}'); print('Email: ${accountDetails.email}'); print('Display name: ${accountDetails.fullName}'); print('Profile image: ${accountDetails.image}'); // Custom validation logic if (accountDetails.email == null) { // Handle case where user didn't share email } }, ``` :::info The properties available depend on the permissions requested and what the user consented to share. ::: ### Accessing Facebook APIs on the server :::caution The `getExtraFacebookInfoCallback` below runs on **every** sign-in, not only the first. Cache what you fetch, and guard external calls with `try`/`catch` so a provider outage does not block sign-in. ::: On the server side, you can access Facebook APIs using the access token. The `getExtraFacebookInfoCallback` in `FacebookIdpConfig` receives the access token and can be used to call Facebook Graph APIs: ```dart import 'package:http/http.dart' as http; final facebookIdpConfig = FacebookIdpConfigFromPasswords( // Optional: Extract additional info from Facebook Graph APIs getExtraFacebookInfoCallback: (session, { required accountDetails, required accessToken, required transaction, }) async { // Use accessToken to call Facebook Graph APIs and store additional info // Example: Access user's friends list final response = await http.get( Uri.https('graph.facebook.com', '/v21.0/me/friends'), headers: {'Authorization': 'Bearer $accessToken'}, ); // Process response and store additional info in the database }, ); ``` ### Reacting to Facebook account creation You can use the `onAfterFacebookAccountCreated` callback to run logic after a new Facebook account has been created and linked to an auth user. This callback is only invoked for new accounts, not for returning users. This callback is complementary to the [core `onAfterAuthUserCreated` callback](https://docs.serverpod.dev/concepts/authentication/working-with-users.md#reacting-to-the-user-created-event) to perform side-effects that are specific to a login on this provider, like storing analytics, sending a welcome email, or storing additional data. ```dart final facebookIdpConfig = FacebookIdpConfigFromPasswords( onAfterFacebookAccountCreated: ( session, authUser, facebookAccount, { required transaction, }) async { // e.g. store additional data, send a welcome email, or log for analytics }, ); ``` :::info This callback runs inside the same database transaction as the account creation. Throwing an exception inside this callback will abort the process. If you perform external side-effects, make sure to safeguard them with a try/catch to prevent unwanted failures. ::: :::caution Scopes you assign here with `AuthServices.instance.authUsers.update()` do not apply to the login that is already in progress, because token issuance uses the scopes loaded before this callback runs. They take effect the next time the user signs in. To assign scopes at creation time instead, use [`onBeforeAuthUserCreated`](https://docs.serverpod.dev/concepts/authentication/working-with-users.md#user-creation-callbacks) together with `getExtraFacebookInfoCallback`, which runs before the auth user is created. ::: ### FacebookIdpConfig parameter reference | Parameter | Type | Required | Description | | ---------------------------------- | -------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `appId` | `String` | Yes | The app ID from your Facebook Developer app. | | `appSecret` | `String` | Yes | The app secret from your Facebook Developer app. | | `facebookAccountDetailsValidation` | `FacebookAccountDetailsValidation` | No | Custom validation callback for Facebook account details before allowing sign-in. Throws an exception to reject the account. Defaults to validating only that `userIdentifier` is non-empty. | | `getExtraFacebookInfoCallback` | `GetExtraFacebookInfoCallback?` | No | Callback that receives the access token after sign-in, allowing you to call the Facebook Graph API and store extra user data. Runs on every sign-in. | | `onAfterFacebookAccountCreated` | `AfterFacebookAccountCreatedFunction?` | No | Callback invoked after a new Facebook account is created and linked to an auth user. Fires only for new accounts. | ## App configuration These options are configured in your Flutter app rather than on the server. ### Configuring Facebook sign-in on the app When using the external `serverpod_auth_idp_flutter_facebook` package, you can configure the App ID in your Flutter application. You can pass the App ID directly when initializing the Facebook sign-in service: ```dart await client.auth.initializeFacebookSignIn( appId: 'YOUR_FACEBOOK_APP_ID', ); ``` If the `appId` value is not supplied when initializing the service, the provider will automatically fetch it from the `FACEBOOK_APP_ID` environment variable. You can set this variable at build time using the `--dart-define` option: ```bash flutter run -d \ --dart-define="FACEBOOK_APP_ID=your_app_id" ``` This approach is useful when you need to: - Set the App ID for web and macOS builds, where it is read from Dart. On Android and iOS the Facebook SDK reads it from the native configuration files instead. - Avoid committing App IDs to version control. - Configure different credentials for different build environments (development, staging, production). :::tip You can also set these environment variables in your IDE's run configuration or CI/CD pipeline to avoid passing them manually each time. ::: ### Accessing Facebook APIs The default setup allows access to basic user information, such as `name` and `email`. You may require additional permissions to access other Facebook APIs, such as accessing a user's friends, posts, or pages. The default permissions requested are: - `email`: Access to user's email address. - `public_profile`: Access to user's basic profile information. To request additional permissions, you will need to: - Ensure the required permissions are configured in your Facebook App settings (navigate to **Use cases** > **Customize** > **Permissions and features** in the [Facebook App Dashboard](https://developers.facebook.com/)). - Request access to the permissions when signing in. Do this by setting the `permissions` parameter of the `FacebookSignInWidget` or `FacebookAuthController`. A full list of available permissions can be found in the [Facebook permissions reference](https://developers.facebook.com/docs/permissions). :::info Adding additional permissions may require App Review depending on the sensitivity of the requested permissions and your app's use case. ::: ## Customize the sign-in button If you render the button inside `SignInWidget`, see [Styling the buttons](https://docs.serverpod.dev/concepts/authentication/ui-components.md#styling-the-buttons) for how its `buttonStyle` overrides the parameters set here. :::info The `SignInWidget` automatically detects and displays the Facebook sign-in flow when the `serverpod_auth_idp_flutter_facebook` package is installed and initialized. The Facebook provider registers itself dynamically with the sign-in widget. ::: ### Using the `FacebookSignInWidget` The `FacebookSignInWidget` handles the complete Facebook sign-in flow for iOS, Android, web, and macOS. You can customize the widget's appearance and behavior: ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; import 'package:serverpod_auth_idp_flutter_facebook/serverpod_auth_idp_flutter_facebook.dart'; FacebookSignInWidget( client: client, // Button customization. The values shown are the defaults. style: FacebookButtonStyle.blue, // or white size: SignInButtonSize.large, // or medium, small text: SignInButtonTextVariant.continueWith, // or signInWith, signUpWith, signIn shape: SignInButtonShape.pill, // or rounded, rectangular logoAlignment: SignInButtonLogoAlignment.center, // or left minimumWidth: 240, // at most 400 textStyle: null, // TextStyle for the label // Permissions to request from Facebook // These are the default permissions. permissions: const ['email', 'public_profile'], onAuthenticated: () { // Do something when the user is authenticated. // // NOTE: You should not navigate to the home screen here, otherwise // the user will have to sign in again every time they open the app. }, onError: (error) { // Handle errors }, ) ``` ## Build a custom UI with FacebookAuthController For more control over the UI, you can use the `FacebookAuthController` class, which provides all the authentication logic without any UI components. This allows you to build a completely custom authentication interface. ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; import 'package:serverpod_auth_idp_flutter_facebook/serverpod_auth_idp_flutter_facebook.dart'; final controller = FacebookAuthController( client: client, onAuthenticated: () { // Do something when the user is authenticated. // // NOTE: You should not navigate to the home screen here, otherwise // the user will have to sign in again every time they open the app. }, onError: (error) { // Handle errors }, permissions: const ['email', 'public_profile'], ); // Initiate sign-in await controller.signIn(); ``` ### FacebookAuthController state management Your widget should render the appropriate UI based on the `state` property of the controller. You can also use the below state properties to build your UI: ```dart // Check current state final state = controller.state; // FacebookAuthState enum // Check if loading final isLoading = controller.isLoading; // Check if authenticated final isAuthenticated = controller.isAuthenticated; // Get error message final errorMessage = controller.errorMessage; // Get error object final error = controller.error; // Listen to state changes controller.addListener(() { setState(() { // Rebuild UI when controller state changes }); }); ``` #### FacebookAuthController states - `FacebookAuthState.initializing` - Controller is initializing. - `FacebookAuthState.idle` - Ready for user interaction. - `FacebookAuthState.loading` - Processing a sign-in request. - `FacebookAuthState.error` - An error occurred. - `FacebookAuthState.authenticated` - Authentication was successful. ## Related - [Setup](https://docs.serverpod.dev/concepts/authentication/providers/facebook/setup.md): set up the Facebook identity provider on the server and in your app. - [Troubleshooting](https://docs.serverpod.dev/concepts/authentication/providers/facebook/troubleshooting.md): fix common Facebook sign-in errors. - [UI components](https://docs.serverpod.dev/concepts/authentication/ui-components.md): style the sign-in widget and its provider buttons. - [Working with users](https://docs.serverpod.dev/concepts/authentication/working-with-users.md): manage auth users and react to account events. --- # Troubleshoot Facebook sign-in https://docs.serverpod.dev/concepts/authentication/providers/facebook/troubleshooting This page helps you identify common Sign in with Facebook failures, explains why they occur, and shows how to resolve them. For underlying issues with the native SDK, see the [flutter\_facebook\_auth package](https://pub.dev/packages/flutter_facebook_auth). ## Setup checklist Go through this before investigating a specific error. Most problems come from a missed step. #### Facebook App Dashboard - [ ] Created an app with the **Authenticate and request data from users with Facebook Login** use case at [Facebook for Developers](https://developers.facebook.com/apps). - [ ] Requested the permissions you need under **Use cases** > **Customize** > **Permissions and features** (`email` and `public_profile` are enough for basic sign-in). - [ ] For web and macOS: set **Login with the JavaScript SDK** to **Yes** and added every serving origin to **Allowed Domains for the JavaScript SDK**. - [ ] Copied the **App ID** and **App secret** (**App settings** > **Basic**) and the **Client token** (**App settings** > **Advanced**). - [ ] Switched the app to **Live** mode (or added your account as a tester) so non-role users can sign in. #### Server - [ ] Added `facebookAppId` and `facebookAppSecret` to `config/passwords.yaml` under the matching environment (`development:` for local, `production:` for prod), or set the matching `SERVERPOD_PASSWORD_facebookAppId` and `SERVERPOD_PASSWORD_facebookAppSecret` environment variables. - [ ] Added `FacebookIdpConfigFromPasswords()` to `identityProviderBuilders` in `server.dart`. - [ ] Created a `FacebookIdpEndpoint` file in `lib/src/auth/`. - [ ] Started the server with `serverpod start`, then created and applied the migration (pressed **M**). #### Client - [ ] Installed the `serverpod_auth_idp_flutter_facebook` package. - [ ] Called `client.auth.initializeFacebookSignIn()` after `client.auth.initialize()` inside `initializeClient()` in your Flutter app's `lib/client.dart`. - [ ] On **web** and **macOS**, supplied the App ID (directly or via the `FACEBOOK_APP_ID` dart-define). - [ ] On **Android**, added `strings.xml` with the app ID, protocol scheme, and client token, plus the `` and `` entries in `AndroidManifest.xml`, and registered the debug or release key hash on the Facebook app. - [ ] On **iOS**, added the `CFBundleURLTypes`, `FacebookAppID`, `FacebookClientToken`, and `LSApplicationQueriesSchemes` entries to `Info.plist`. ## Web sign-in fails with "JSSDK Option is Not Toggled" **Problem:** On web (or macOS), clicking the Facebook button fails immediately with a message like "JSSDK Option is Not Toggled. Please toggle the Login with Javascript SDK Option to Yes in developers.facebook.com". **Cause:** Web and macOS sign-in use the Facebook JavaScript SDK, which the app must explicitly opt into. It is off by default on a new app. **Resolution:** In the [Facebook App Dashboard](https://developers.facebook.com/apps), go to **Use cases** > **Customize** > **Settings**, set **Login with the JavaScript SDK** to **Yes**, and save. See [Settings](https://docs.serverpod.dev/concepts/authentication/providers/facebook/setup.md#2-settings). ## Web sign-in is blocked by CORS or the domain is not allowed **Problem:** The Facebook popup does not load, or the browser console shows a CORS error or "can't load URL: the domain of this URL isn't included in the app's domains". **Cause:** The origin serving your Flutter web app is not listed in **Allowed Domains for the JavaScript SDK**. **Resolution:** Add every serving origin to **Use cases** > **Customize** > **Settings** > **Allowed Domains for the JavaScript SDK**, including `https://www.facebook.com`, your development origin (e.g., `http://localhost:8082`), and your production domain. The Facebook JavaScript SDK only works over `https` in production; `localhost` over `http` loads but logs a console warning. See [Web](https://docs.serverpod.dev/concepts/authentication/providers/facebook/setup.md#web). ## iOS sign-in fails after denying tracking permission **Problem:** On iOS, Facebook sign-in fails or the server cannot read the user's profile, especially after the user declines the App Tracking Transparency (ATT) prompt. **Cause:** When ATT permission is not granted, Facebook may issue a **limited access token**. Limited tokens cannot be validated by the server or used to fetch profile data, so authentication fails. **Resolution:** Request ATT authorization before starting the Facebook sign-in flow, for example with the [`app_tracking_transparency`](https://pub.dev/packages/app_tracking_transparency) package: ```dart import 'package:app_tracking_transparency/app_tracking_transparency.dart'; final status = await AppTrackingTransparency.requestTrackingAuthorization(); ``` See [iOS](https://docs.serverpod.dev/concepts/authentication/providers/facebook/setup.md#ios) for the full iOS configuration. ## Android opens Facebook but the sign-in never completes **Problem:** On Android, tapping the Facebook button opens the login flow, but after authorizing, the app returns an error or the session is never established. **Cause:** The key hash registered on the Facebook app does not match the keystore that signed the build, or the `strings.xml` / `AndroidManifest.xml` entries are missing or wrong. **Resolution:** 1. Confirm `android/app/src/main/res/values/strings.xml` contains `facebook_app_id`, `fb_login_protocol_scheme` (your app ID prefixed with `fb`), and `facebook_client_token`. 2. Confirm the `com.facebook.sdk.ApplicationId` and `com.facebook.sdk.ClientToken` `` entries and the `com.facebook.katana.provider.PlatformProvider` `` entry are in `AndroidManifest.xml`. 3. Register the key hash for the keystore you are building with. Generate the debug hash with: ```bash keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore | openssl sha1 -binary | openssl base64 ``` Add the output under the Android platform in the Facebook app. For release builds, generate and add the release keystore's hash too. See [Android](https://docs.serverpod.dev/concepts/authentication/providers/facebook/setup.md#android). ## Sign-in works for you but not for other users **Problem:** Sign-in works for your own Facebook account but other users get an error saying the app is not available or is still in development. **Cause:** The Facebook app is in **Development mode**, which only allows people with a role on the app (admins, developers, testers) to sign in. **Resolution:** Switch the app to **Live** mode at the top of the [Facebook App Dashboard](https://developers.facebook.com/apps), or add the specific users as testers under **App roles**. Going Live requires a Privacy Policy URL and, for permissions beyond `email` and `public_profile`, App Review. See [Publishing to production](https://docs.serverpod.dev/concepts/authentication/providers/facebook/setup.md#publishing-to-production). ## Sign-in succeeds but the user has no email **Problem:** The user signs in successfully, but the server-side `FacebookAccountDetails.email` value is `null`. **Cause:** Users can decline to share their email during the Facebook login flow, or the `email` permission was not requested. A custom validator that requires an email will then block the sign-in. **Resolution:** - Confirm the `email` permission is requested (it is included by default; see [Accessing Facebook APIs](https://docs.serverpod.dev/concepts/authentication/providers/facebook/customizations.md#accessing-facebook-apis)). - If you do not strictly need an email, keep the default validator, which only checks `userIdentifier`. Avoid rejecting accounts without an email unless you must. See [Custom account validation](https://docs.serverpod.dev/concepts/authentication/providers/facebook/customizations.md#custom-account-validation). - Surface a helpful message to the user instead of failing silently. ## Server crashes on startup with a missing password **Problem:** The server crashes on startup with a `PasswordNotFoundException` for `facebookAppId` or `facebookAppSecret`. **Cause:** The keys are missing from `config/passwords.yaml`, are spelled differently, or are not present under the active environment section. **Resolution:** Confirm both keys exist under the section matching your run mode (`development:` when running locally, `production:` when deployed): ```yaml development: facebookAppId: 'your-facebook-app-id' facebookAppSecret: 'your-facebook-app-secret' ``` Quoting the values is a safeguard. YAML parses unquoted values that look like numbers as numbers instead of strings. Facebook App IDs are numeric, so an unquoted `facebookAppId` crashes startup with an "Invalid password entries" error instead of this one. On Serverpod Cloud, set them with `serverpod cloud password set` instead. See [Publishing to production](https://docs.serverpod.dev/concepts/authentication/providers/facebook/setup.md#publishing-to-production). ## Server crashes on first Facebook sign-in with "no such table" **Problem:** The server builds and starts, but crashes when a user tries to sign in with Facebook. The error cites a missing table such as `serverpod_auth_idp_facebook_account`. **Cause:** The database migration that creates the provider's tables was never created or applied. **Resolution:** In the running `serverpod start` terminal, press **M** to create and apply the migration. See [Start the server](https://docs.serverpod.dev/concepts/authentication/providers/facebook/setup.md#start-the-server). ## Sign-in works in dev but fails after deploy **Problem:** Facebook sign-in works locally but fails in production. **Cause:** The production domain is not registered on the Facebook app, the app is still in Development mode, or the production Flutter build was not given the App ID. **Resolution:** 1. Add your production domain to **Allowed Domains for the JavaScript SDK** (web and macOS) and register the release key hash (Android). 2. Switch the app to **Live** mode. 3. Confirm the production server has `facebookAppId` and `facebookAppSecret` set, and that the production web build passes `--dart-define=FACEBOOK_APP_ID=...`. See [Publishing to production](https://docs.serverpod.dev/concepts/authentication/providers/facebook/setup.md#publishing-to-production). ## Related - [Setup](https://docs.serverpod.dev/concepts/authentication/providers/facebook/setup.md): set up the Facebook identity provider on the server and in your app. - [Customizations](https://docs.serverpod.dev/concepts/authentication/providers/facebook/customizations.md): configuration options and sign-in UI customization. - [UI components](https://docs.serverpod.dev/concepts/authentication/ui-components.md): the sign-in widgets and how to compose them. --- # Set up Firebase authentication https://docs.serverpod.dev/concepts/authentication/providers/firebase/setup Firebase authentication lets you use any Firebase sign-in method (email/password, phone, Google, Apple, Facebook, etc.) with your Serverpod backend. Firebase handles the sign-in flow through its own SDKs, while Serverpod syncs the authenticated user and manages the server-side session. ## Prerequisites Before you start, make sure you have: - A Serverpod project with the new auth module installed. New projects created with `serverpod create` include it by default. If you are upgrading an older project, follow the [auth module setup guide](https://docs.serverpod.dev/concepts/authentication/setup.md) first. - A Google account with access to the [Firebase Console](https://console.firebase.google.com/). - The [Firebase CLI installed](https://firebase.google.com/docs/cli#install_the_firebase_cli) and the FlutterFire CLI activated (`dart pub global activate flutterfire_cli`). You will use both later in the guide. ## Get your credentials ### Create a Firebase project 1. Go to the [Firebase Console](https://console.firebase.google.com/) and click **Create a project** (or **Add project** if you already have projects). 2. Enter a project name, accept the terms, select a parent resource if needed, and click **Continue**. ![Enter project name](https://docs.serverpod.dev/img/authentication/providers/firebase/2-project-name.png) 3. Follow the remaining prompts (Google Analytics is optional) and click **Create project**. ### Generate a service account key The server needs service account credentials to verify Firebase ID tokens. 1. In the Firebase Console, navigate to **Project settings** > **Service accounts**. ![Service accounts page](https://docs.serverpod.dev/img/authentication/providers/firebase/4-service-accounts.png) 2. Click **Generate new private key**. ![Generate new private key button](https://docs.serverpod.dev/img/authentication/providers/firebase/5-generate-key.png) 3. In the confirmation dialog, click **Generate key**. ![Generate key confirmation dialog](https://docs.serverpod.dev/img/authentication/providers/firebase/6-generate-key-confirm.png) 4. A JSON file downloads to your machine. This file contains your service account credentials. You will need it in the next step. ### Enable authentication methods 1. In the Firebase Console sidebar, navigate to **Product categories** > **Security** > **Authentication**. ![Navigate to Authentication](https://docs.serverpod.dev/img/authentication/providers/firebase/7-navigate-auth.png) 2. If this is your first time, click **Get started** to enable the Authentication service. ![Enable Authentication](https://docs.serverpod.dev/img/authentication/providers/firebase/8-enable-auth.png) 3. Select the **Sign-in method** tab. 4. Click on the provider you want to enable (e.g., **Email/Password**) and toggle it on. Click **Save**. ![Enable a sign-in provider](https://docs.serverpod.dev/img/authentication/providers/firebase/9-enable-provider.png) 5. Repeat for each provider you want to support (Phone, Google, Apple, etc.). Configure each provider according to the instructions shown in the Firebase Console. ![Sign-in providers list](https://docs.serverpod.dev/img/authentication/providers/firebase/10-sign-in-methods.png) :::note While enabling providers, you may see a **Firebase App Check** option in the sidebar or as a checkbox during provider configuration. **Leave App Check disabled while you are getting started.** Enabling it before your client integration is ready will cause every sign-in to fail with an App Check token error. You can turn it on later once everything works end-to-end. ::: ### Store the service account key Open `config/passwords.yaml` in your server project. Projects created with `serverpod create` already have `development:`, `staging:`, and `production:` sections; if yours doesn't, add a `development:` section. Then add the `firebaseServiceAccountKey` key under `development:` using the contents of the JSON file you downloaded: ```yaml development: # ... existing keys (database, redis, serviceSecret, etc.) ... firebaseServiceAccountKey: | { "type": "service_account", "project_id": "your-project-id", "private_key_id": "...", "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n", "client_email": "firebase-adminsdk-xxxxx@your-project-id.iam.gserviceaccount.com", "client_id": "...", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token" } ``` Production credentials are covered in [Publishing to production](#publishing-to-production) below. :::warning **Never commit `config/passwords.yaml` to version control.** It contains your service account key, which gives admin access to your Firebase project. Use environment variables or a secrets manager in production. ::: :::warning **Indent the JSON consistently under the `|` block scalar.** Any indentation error makes the JSON fail to parse, so the server throws at startup when the credentials load. Mixing tabs and spaces is a common cause. ::: ## Server-side configuration ### 1. Add the Firebase identity provider Open your server's `server.dart` file (e.g., `my_project_server/lib/server.dart`). It should already contain a `pod.initializeAuthServices()` call from the project template. :::note If `initializeAuthServices` is missing, the auth module is not installed. Verify `serverpod_auth_idp_server` is in your server's `pubspec.yaml` and follow the [auth module setup](https://docs.serverpod.dev/concepts/authentication/setup.md) before continuing. ::: Add the Firebase import and `FirebaseIdpConfigFromPasswords()` to the existing `identityProviderBuilders` list: ```dart import 'package:serverpod_auth_idp_server/providers/firebase.dart'; ``` ```dart pod.initializeAuthServices( tokenManagerBuilders: [ JwtConfigFromPasswords(), ], identityProviderBuilders: [ // ... any existing providers (e.g., EmailIdpConfigFromPasswords) ... FirebaseIdpConfigFromPasswords(), ], ); ``` The `FirebaseIdpConfigFromPasswords()` constructor automatically loads the service account key from the `firebaseServiceAccountKey` key in `config/passwords.yaml` (or the `SERVERPOD_PASSWORD_firebaseServiceAccountKey` environment variable). For loading credentials from other sources (file, JSON map, project ID only), see the [Configuration](https://docs.serverpod.dev/concepts/authentication/providers/firebase/configuration.md) page. ### 2. Create the endpoint Create a new endpoint file in your server project (e.g., `my_project_server/lib/src/auth/firebase_idp_endpoint.dart`) alongside the existing auth endpoints. Extending the base class registers the sign-in methods with your server so the Flutter client can call them to complete the authentication flow: ```dart import 'package:serverpod_auth_idp_server/providers/firebase.dart'; class FirebaseIdpEndpoint extends FirebaseIdpBaseEndpoint {} ``` ### 3. Start the server Start the server from your server project directory (e.g., `my_project_server/`): ```bash serverpod start ``` Then create and apply the migration for the provider's tables: in the `serverpod start` terminal, press **M** to create and apply the migration. :::note Skipping the migration will cause the server to crash at runtime when the Firebase provider tries to read or write user data. More detailed instructions can be found in the general [identity providers setup section](https://docs.serverpod.dev/concepts/authentication/setup.md#identity-providers-configuration). ::: ## Client-side configuration ### 1. Install required packages From your Flutter project directory (e.g., `my_project_flutter/`), add the Firebase and Serverpod authentication packages along with [`firebase_ui_auth`](https://pub.dev/packages/firebase_ui_auth), which provides the pre-built sign-in screens this guide uses: ```bash flutter pub add firebase_core firebase_auth firebase_ui_auth serverpod_auth_idp_flutter_firebase ``` :::note The `firebase_ui_auth` package is the fastest path to a working sign-in screen. If you want to build a fully custom UI on top of `firebase_auth` directly, see [Using firebase\_auth directly](https://docs.serverpod.dev/concepts/authentication/providers/firebase/customizing-the-ui.md#using-firebase_auth-directly). ::: ### 2. Configure FlutterFire First, log in to Firebase from your terminal: ```bash firebase login ``` This opens a browser window so you can authenticate with the same Google account you used for the Firebase Console. Then run the FlutterFire CLI from your Flutter project directory to configure Firebase for the platforms you support: ```bash flutterfire configure ``` Select your Firebase project when prompted, and choose the platforms you want to support. ![FlutterFire configure terminal output](https://docs.serverpod.dev/img/authentication/providers/firebase/11-flutterfire-configure.png) This generates a `firebase_options.dart` file with your platform-specific Firebase configuration, and registers each platform app with your Firebase project. :::note If your Flutter project folder name contains an underscore (or any character that is not valid in a reverse-DNS bundle identifier), FlutterFire's auto-registration step will fail for iOS and macOS. Pass the bundle ID explicitly when this happens. See [FlutterFire configure fails for iOS or macOS apps](https://docs.serverpod.dev/concepts/authentication/providers/firebase/troubleshooting.md#flutterfire-configure-fails-for-ios-or-macos-apps) in the troubleshooting guide. ::: ### 3. Initialize Firebase and Serverpod The template creates the `Client` in `lib/client.dart` and initializes it from `main()`. Initialize Firebase before the client, in `lib/main.dart`: ```dart import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'client.dart'; import 'firebase_options.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); await initializeClient(); runApp(const MyApp()); } ``` Then, in `lib/client.dart`, add `initializeFirebaseSignIn()` right after the existing `client.auth.initialize()` call in `initializeClient()`: ```dart import 'package:serverpod_auth_idp_flutter_firebase/serverpod_auth_idp_flutter_firebase.dart'; Future initializeClient() async { client = Client(await serverUrl) ..connectivityMonitor = FlutterConnectivityMonitor() ..authSessionManager = FlutterAuthSessionManager(); unawaited(client.auth.initialize()); client.auth.initializeFirebaseSignIn(); } ``` The `initializeFirebaseSignIn()` call signs the user out from Firebase automatically when they sign out from Serverpod, so both systems stay in sync. ## Present the authentication UI New Serverpod Flutter projects ship with a `sign_in_screen.dart` file (typically under `lib/screens/sign_in_screen.dart`) that gates the app behind authentication. Replace its contents with the gate widget below, which uses [`firebase_ui_auth`](https://pub.dev/packages/firebase_ui_auth) together with [`FirebaseAuthController`](https://pub.dev/documentation/serverpod_auth_idp_flutter_firebase/latest/serverpod_auth_idp_flutter_firebase/FirebaseAuthController-class.html) to sync the Firebase user with Serverpod: ```dart import 'package:flutter/material.dart'; import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; import 'package:firebase_ui_auth/firebase_ui_auth.dart' as firebase_ui; import 'package:serverpod_auth_idp_flutter_firebase/serverpod_auth_idp_flutter_firebase.dart'; import '../client.dart'; /// A screen that manages sign-in flow and wraps the rest of the app after authentication. class SignInScreen extends StatefulWidget { /// The widget to display after authentication. final Widget child; const SignInScreen({super.key, required this.child}); @override State createState() => _SignInScreenState(); } class _SignInScreenState extends State { // Controller to help with Firebase authentication. late final FirebaseAuthController controller; @override void initState() { super.initState(); // Initialize the controller with callbacks for authentication events. controller = FirebaseAuthController( client: client, onAuthenticated: () => context.showSnackBar( message: 'User authenticated.', backgroundColor: Colors.green, ), onError: (error) => context.showSnackBar( message: 'Authentication failed: $error', backgroundColor: Colors.red, ), ); // Rebuild when the controller's state changes, so the gate swaps to the // app once login() succeeds. The controller does not notify on sign-out. // Listen to client.auth.authInfoListenable as well if you need that. controller.addListener(_onControllerChanged); } void _onControllerChanged() { if (mounted) setState(() {}); } @override void dispose() { controller.removeListener(_onControllerChanged); controller.dispose(); super.dispose(); } /// Handle changes in authentication state: Log in a new Firebase user with Serverpod, if any. Future _handleAuthStateChange() async { final user = firebase_auth.FirebaseAuth.instance.currentUser; if (user != null) await controller.login(user); } @override Widget build(BuildContext context) { // If already authenticated, display the main app. if (controller.isAuthenticated) return widget.child; // Otherwise, display the sign-in screen. return Center( child: firebase_ui.SignInScreen( providers: [ // Use email/password sign-in provider. firebase_ui.EmailAuthProvider(), ], actions: [ // Handle when a user has signed in. firebase_ui.AuthStateChangeAction( (context, state) => _handleAuthStateChange(), ), // Handle when a user is created (first sign-in). firebase_ui.AuthStateChangeAction( (context, state) => _handleAuthStateChange(), ), ], ), ); } } // Extension to show a snackbar from a BuildContext. extension SnackbarExtension on BuildContext { /// Shows a [SnackBar] with a custom message and optional background color. void showSnackBar({ required String message, Color? backgroundColor, }) { ScaffoldMessenger.of(this).showSnackBar( SnackBar( content: Text(message), backgroundColor: backgroundColor, duration: const Duration(seconds: 5), ), ); } } ``` For a breakdown of the controller, action handlers, and how to build a custom authentication UI, see [Customizing the UI](https://docs.serverpod.dev/concepts/authentication/providers/firebase/customizing-the-ui.md). :::tip If you run into issues, see the [Troubleshooting](https://docs.serverpod.dev/concepts/authentication/providers/firebase/troubleshooting.md) guide. ::: ## Publishing to production Before going live, complete every step below. Skipping any of these is the most common cause of "works in dev, fails in prod" reports. ### 1. Decide which Firebase project production should use Most teams keep a separate Firebase project for production so dev experiments do not affect real users. If that is your setup, create the production Firebase project now and generate a fresh service account key from it (same steps as in [Generate a service account key](#generate-a-service-account-key) above). You can also reuse your dev Firebase project in production; in that case the service account key carries over and you can skip straight to step 2. ### 2. Add the production service account key Production credentials are added alongside your `development:` ones, not swapped in. Dev keeps using the `development:` key when you run locally, and production uses whatever you wire up below. Pick the path that matches your deployment: #### Self-hosted Add `firebaseServiceAccountKey` to the `production:` section of `config/passwords.yaml` using the same `|` block scalar shown earlier, or set it as the `SERVERPOD_PASSWORD_firebaseServiceAccountKey` environment variable on the production server. The env var path avoids committing the JSON to disk on the server. Whichever path you pick, keep `passwords.yaml` out of version control. #### Serverpod Cloud Use `serverpod cloud password set` and pass the JSON file with `--from-file`: ```bash serverpod cloud password set firebaseServiceAccountKey --from-file ./firebase-service-account.json ``` Run this from your linked server project directory, or pass `--project ` on each call. See the [Serverpod Cloud passwords guide](https://docs.serverpod.dev/cloud/concepts/passwords-secrets-env-vars.md) for project linking and when to use passwords, secrets, or variables. ### 3. Authorize your production domain In the Firebase Console, go to **Authentication** > **Settings** > **Authorized domains** and add the domain your app runs on (e.g., `my-awesome-project.serverpod.space`). Without this entry, OAuth-based sign-in (Google, Apple, etc.) will fail with a redirect URI mismatch in production. ### 4. Configure each platform app for production Depending on which sign-in methods and platforms you support, complete the steps that apply: - **Android (Google sign-in via Firebase):** Add your release SHA-1 fingerprint to the Android app in your Firebase project. If you use **Google Play App Signing** (the default for new apps), get the SHA-1 from the Play Console: **Setup** > **App integrity** > **App signing key certificate**. Use the **app signing key** SHA-1, not the upload key SHA-1. If you manage your own release keystore, run `keytool -list -v -keystore your-release-key.jks -alias your-key-alias`. Then re-run `flutterfire configure` so `google-services.json` is updated. - **iOS (phone or Apple sign-in):** Verify your bundle identifier is registered with the Firebase iOS app, and that `GoogleService-Info.plist` is included in the Xcode project's Runner target. Phone authentication also requires you to upload an APNs authentication key in **Project settings** > **Cloud Messaging**. - **Web:** Add every domain that will host your app (including custom domains and preview URLs) to the **Authorized domains** list from [step 3](#3-authorize-your-production-domain). :::warning Forgetting the release SHA-1 is the single most common reason Google sign-in works in debug builds but silently fails after publishing to the Play Store. ::: ### 5. Re-verify Firebase App Check If you disabled App Check for development, decide now whether to enable it. Enabling App Check requires extra integration on the client (attest tokens via Play Integrity, App Attest, or reCAPTCHA). Do not enable it without that integration in place, otherwise every authenticated request will be rejected. --- # Configure Firebase authentication https://docs.serverpod.dev/concepts/authentication/providers/firebase/configuration This page covers additional configuration options for the Firebase identity provider beyond the basic setup. ## Loading credentials with FirebaseIdpConfig The [setup guide](https://docs.serverpod.dev/concepts/authentication/providers/firebase/setup.md) uses `FirebaseIdpConfigFromPasswords`, which loads the service account key from `passwords.yaml` for you. When you need to load credentials from a different source (a file path, a secrets manager, or just a project ID), use `FirebaseIdpConfig` directly and pass a `FirebaseServiceAccountCredentials` instance. The `FirebaseServiceAccountCredentials` class provides four constructors. These are the only supported ways to construct it: **From a JSON string** (use this when reading the JSON from a secrets manager or environment variable): ```dart final firebaseIdpConfig = FirebaseIdpConfig( credentials: FirebaseServiceAccountCredentials.fromJsonString( pod.getPassword('firebaseServiceAccountKey')!, ), ); ``` **From a JSON file** (useful for local development or when secrets are mounted as files): ```dart import 'dart:io'; final firebaseIdpConfig = FirebaseIdpConfig( credentials: FirebaseServiceAccountCredentials.fromJsonFile( File('config/firebase_service_account_key.json'), ), ); ``` **From a JSON map** (useful when credentials are assembled programmatically, for example by pulling each field from `passwords.yaml` or a secrets manager): ```dart final firebaseIdpConfig = FirebaseIdpConfig( credentials: FirebaseServiceAccountCredentials.fromJson({ 'type': 'service_account', 'project_id': pod.getPassword('firebaseProjectId')!, 'private_key_id': pod.getPassword('firebasePrivateKeyId')!, 'private_key': pod.getPassword('firebasePrivateKey')!, 'client_email': pod.getPassword('firebaseClientEmail')!, 'client_id': pod.getPassword('firebaseClientId')!, 'auth_uri': 'https://accounts.google.com/o/oauth2/auth', 'token_uri': 'https://oauth2.googleapis.com/token', }), ); ``` :::warning Do not inline the service account fields (especially `private_key`) directly in source code. Load every sensitive field from a secure source such as `pod.getPassword()` (backed by `passwords.yaml` or `SERVERPOD_PASSWORD_*` environment variables) or a secrets manager. ::: **Project ID only** (Serverpod uses only this field and ignores the rest): ```dart final firebaseIdpConfig = FirebaseIdpConfig( credentials: const FirebaseServiceAccountCredentials( projectId: 'your-project-id', ), ); ``` :::note Only `projectId` is used from the service account JSON. The other fields are accepted so you can paste the downloaded file unchanged, but Serverpod does not use them. ID token signatures are verified against Google's public certificates, not against the service account key. ::: ## Custom account validation You can customize the validation for Firebase account details before allowing sign-in. By default every account is accepted, including one whose email is not verified. Firebase Email/Password accounts start unverified and users usually sign in straight after signing up, so rejecting them would block that flow. To require a verified email instead, pass the built-in validator: ```dart final firebaseIdpConfig = FirebaseIdpConfigFromPasswords( firebaseAccountDetailsValidation: FirebaseIdpConfig.requireVerifiedEmail, ); ``` It throws `FirebaseEmailNotVerifiedException`, which reaches the app so you can prompt the user to verify. Accounts with no email, such as phone sign-in, are still accepted. To customize validation, provide your own `firebaseAccountDetailsValidation` function: ```dart final firebaseIdpConfig = FirebaseIdpConfigFromPasswords( firebaseAccountDetailsValidation: (accountDetails) { // Require verified email (even for phone auth). Throw the serializable // FirebaseEmailNotVerifiedException so the app can tell this case apart. // A plain Exception reaches the app as FirebaseIdTokenVerificationException. if (accountDetails.verifiedEmail != true) { throw FirebaseEmailNotVerifiedException(); } // Restrict to specific email domain if (accountDetails.email != null && !accountDetails.email!.endsWith('@example.com')) { throw Exception('Only @example.com emails allowed'); } }, ); ``` ### FirebaseAccountDetails properties The `firebaseAccountDetailsValidation` callback receives a `FirebaseAccountDetails` record with the following properties: - `userIdentifier` (`String`): Firebase UID. - `email` (`String?`): Email address, or `null` for phone-only sign-in. - `fullName` (`String?`): Display name from Firebase. - `image` (`Uri?`): Profile image URL. - `verifiedEmail` (`bool?`): Whether the email is verified. - `phone` (`String?`): Phone number, only populated for phone authentication. Which properties are populated depends on the Firebase sign-in method the user chose. For example, `phone` is only populated for phone authentication, and `email` may be `null` if the user signed in with phone only. ## Reacting to auth user creation [`onBeforeAuthUserCreated`](https://pub.dev/documentation/serverpod_auth_idp_server/latest/core/AuthUsersConfig/onBeforeAuthUserCreated.html) and [`onAfterAuthUserCreated`](https://pub.dev/documentation/serverpod_auth_idp_server/latest/core/AuthUsersConfig/onAfterAuthUserCreated.html) are global callbacks on `AuthUsersConfig`. They fire for every identity provider, not just Firebase. See [Working with users](https://docs.serverpod.dev/concepts/authentication/working-with-users.md#reacting-to-the-user-created-event) for full details. The core callbacks cannot see Firebase account details, so provider-specific logic, such as a scope derived from the phone number, belongs in `onAfterFirebaseAccountCreated`, which receives the `FirebaseAccount`. The example below assigns a baseline scope to every new user: ```dart pod.initializeAuthServices( tokenManagerBuilders: [ JwtConfigFromPasswords(), ], identityProviderBuilders: [ FirebaseIdpConfigFromPasswords(), ], authUsersConfig: AuthUsersConfig( onBeforeAuthUserCreated: ( session, scopes, blocked, { required transaction, }) { return ( scopes: {...scopes, Scope('user')}, blocked: blocked, ); }, onAfterAuthUserCreated: ( session, authUser, { required transaction, }) async { // e.g. send a welcome email, log for analytics }, ), ); ``` For the Firebase-specific hook, pass `onAfterFirebaseAccountCreated` to the provider config. It receives the created `FirebaseAccount`, so it can read the phone number or the Firebase UID: ```dart FirebaseIdpConfigFromPasswords( onAfterFirebaseAccountCreated: (session, authUser, firebaseAccount, {required transaction}) async { if (firebaseAccount.phone != null) { await AuthServices.instance.authUsers.update( session, authUserId: authUser.id, scopes: {...authUser.scopes, Scope('phone-verified')}, transaction: transaction, ); } }, ) ``` :::warning These callbacks run inside the same database transaction as the account creation. Throwing an exception inside a callback aborts the sign-up. Wrap external side-effects (email sending, analytics) in `try`/`catch` so a third-party outage does not block new sign-ups. ::: ## FirebaseIdpConfig parameter reference | Parameter | Type | Required | Description | | ---------------------------------- | -------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `credentials` | `FirebaseServiceAccountCredentials` | Yes | Firebase service account credentials for verifying ID tokens. Can be loaded via `fromJsonString`, `fromJsonFile`, `fromJson`, or the default constructor with just `projectId`. When using `FirebaseIdpConfigFromPasswords`, this is loaded automatically from the `firebaseServiceAccountKey` key in `passwords.yaml` or the `SERVERPOD_PASSWORD_firebaseServiceAccountKey` environment variable. | | `firebaseAccountDetailsValidation` | `FirebaseAccountDetailsValidation` | No | Custom validation callback for Firebase account details before allowing sign-in. By default all account details are accepted, including unverified emails. Pass `FirebaseIdpConfig.requireVerifiedEmail` to reject accounts whose email has not been verified. | | `onAfterFirebaseAccountCreated` | `AfterFirebaseAccountCreatedFunction?` | No | Callback invoked after a new Firebase account has been created and linked to an auth user. Receives the session, the created `AuthUserModel`, the `FirebaseAccount`, and the active `Transaction`. Runs inside the same database transaction as account creation, so the `transaction` can be used to perform additional database operations atomically with sign-up. | | `clockSkewTolerance` | `Duration` | No | Tolerance for clock skew when validating Firebase ID token timestamps. Defaults to the framework's default clock skew tolerance. | --- # Customize the Firebase authentication UI https://docs.serverpod.dev/concepts/authentication/providers/firebase/customizing-the-ui When using the Firebase identity provider, you build your authentication UI on top of [`firebase_auth`](https://pub.dev/packages/firebase_auth), optionally with [`firebase_ui_auth`](https://pub.dev/packages/firebase_ui_auth) for the pre-built sign-in screens. The [`FirebaseAuthController`](https://pub.dev/documentation/serverpod_auth_idp_flutter_firebase/latest/serverpod_auth_idp_flutter_firebase/FirebaseAuthController-class.html) handles syncing the authenticated Firebase user with your Serverpod backend. This page breaks down the gate widget shown in the [setup guide](https://docs.serverpod.dev/concepts/authentication/providers/firebase/setup.md#present-the-authentication-ui) and then covers building a fully custom UI with `firebase_auth` directly. ## Anatomy of the gate widget The [setup guide](https://docs.serverpod.dev/concepts/authentication/providers/firebase/setup.md#present-the-authentication-ui) shows a `SignInScreen` widget that wraps your app and only renders the `child` once the user is authenticated. Three pieces drive that flow: ### FirebaseAuthController The controller manages the sync between Firebase authentication state and your Serverpod session. The `client` argument is the global Serverpod `Client` instance created in `lib/client.dart` (see [Initialize Firebase and Serverpod](https://docs.serverpod.dev/concepts/authentication/providers/firebase/setup.md#3-initialize-firebase-and-serverpod)). ```dart controller = FirebaseAuthController( client: client, onAuthenticated: () => context.showSnackBar( message: 'User authenticated.', backgroundColor: Colors.green, ), onError: (error) => context.showSnackBar( message: 'Authentication failed: $error', backgroundColor: Colors.red, ), ); ``` :::warning Do not navigate to a home screen inside `onAuthenticated`. This callback fires every time a sync succeeds, including on app restart. Instead, gate the UI on `controller.isAuthenticated` (as the setup example does), or listen to `client.auth.authInfoListenable`. ::: ### Action handlers The `firebase_ui_auth` package emits state changes through [`AuthStateChangeAction`](https://pub.dev/documentation/firebase_ui_auth/latest/firebase_ui_auth/AuthStateChangeAction-class.html). Two handlers cover the cases that need a Serverpod sync: - [`SignedIn`](https://pub.dev/documentation/firebase_ui_auth/latest/firebase_ui_auth/SignedIn-class.html): a returning user signed in. - [`UserCreated`](https://pub.dev/documentation/firebase_ui_auth/latest/firebase_ui_auth/UserCreated-class.html): a brand-new account was created. Both call [`controller.login(user)`](https://pub.dev/documentation/serverpod_auth_idp_flutter_firebase/latest/serverpod_auth_idp_flutter_firebase/FirebaseAuthController/login.html) so the Firebase user is registered with Serverpod: ```dart Future _handleAuthStateChange() async { final user = firebase_auth.FirebaseAuth.instance.currentUser; if (user != null) await controller.login(user); } ``` ### controller.isAuthenticated The `build` method checks `controller.isAuthenticated` to decide whether to show the sign-in UI or the wrapped `child`: ```dart if (controller.isAuthenticated) return widget.child; ``` Add more providers to the `firebase_ui.SignInScreen` as needed (see the [firebase\_ui\_auth documentation](https://pub.dev/packages/firebase_ui_auth)): ```dart providers: [ firebase_ui.EmailAuthProvider(), // firebase_ui.PhoneAuthProvider(), // firebase_ui.GoogleProvider(clientId: '...'), ], ``` ## Controller reference ### The login method After a user signs in through Firebase (using any method), pass the `firebase_auth.User` to the controller to sync with Serverpod: ```dart final firebaseUser = firebase_auth.FirebaseAuth.instance.currentUser; if (firebaseUser != null) { await controller.login(firebaseUser); } ``` ### State management The controller is a `ChangeNotifier`, so it notifies your widget whenever its state changes. Register a listener inside `initState` and call `setState` to trigger a rebuild: ```dart @override void initState() { super.initState(); controller = FirebaseAuthController(client: client); controller.addListener(() { if (mounted) setState(() {}); }); } ``` The controller exposes a few properties for your `build` method: - `controller.isLoading`: Whether the controller is processing a request. - `controller.isAuthenticated`: Whether the user is authenticated. - `controller.errorMessage`: The error message string, if any. - `controller.error`: The raw error object, for advanced error handling. Use them in your `build` method to render the right UI for the current state: ```dart @override Widget build(BuildContext context) { if (controller.isLoading) { return const Center(child: CircularProgressIndicator()); } if (controller.errorMessage != null) { return Center(child: Text('Error: ${controller.errorMessage}')); } if (controller.isAuthenticated) { return widget.child; } // Otherwise, show the sign-in UI. return const SignInForm(); } ``` If you need finer-grained control, switch on `controller.state` directly: ```dart @override Widget build(BuildContext context) { return switch (controller.state) { FirebaseAuthState.idle => const SignInForm(), FirebaseAuthState.loading => const Center(child: CircularProgressIndicator()), FirebaseAuthState.error => Center(child: Text('Error: ${controller.errorMessage}')), FirebaseAuthState.authenticated => widget.child, }; } ``` ## Using firebase\_auth directly For full control over the authentication UI, use the [`firebase_auth`](https://pub.dev/packages/firebase_auth) package directly. Build your own sign-in UI, call [`FirebaseAuth`](https://pub.dev/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth-class.html) methods, then pass the result to `controller.login()`: ```dart import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; final credential = await firebase_auth.FirebaseAuth.instance .signInWithEmailAndPassword( email: emailController.text, password: passwordController.text, ); final user = credential.user; if (user != null) { await controller.login(user); } ``` Refer to the [firebase\_auth documentation](https://pub.dev/packages/firebase_auth) for all available authentication methods. ### Listening to Firebase auth state changes For apps that need to react to Firebase auth state changes automatically (e.g., the user signs in on another device, or the session is restored after an app restart), subscribe to [`FirebaseAuth.instance.authStateChanges()`](https://pub.dev/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth/authStateChanges.html). The stream emits the current `User` whenever Firebase's auth state changes; pass that user to `controller.login()` to sync with Serverpod. ```dart firebase_auth.FirebaseAuth.instance.authStateChanges().listen((user) async { if (user == null) return; // Guard against re-syncing an already authenticated user. if (!controller.isAuthenticated) { await controller.login(user); } }); ``` :::warning Always check `controller.isAuthenticated` before calling `login()` inside the listener. The stream fires on every app start with the cached Firebase user, and `login()` would otherwise re-sync on every launch. ::: --- # Firebase admin operations https://docs.serverpod.dev/concepts/authentication/providers/firebase/admin-operations The Firebase identity provider provides admin operations through `FirebaseIdpAdmin` for managing Firebase-authenticated accounts on the server. Common use cases include linking an existing Serverpod user to a Firebase account, looking up accounts for support tools, and cleaning up orphaned accounts. :::warning Admin operations should only be called from secure server-side code. Do not expose these methods directly through client endpoints without proper authorization checks. ::: ## Accessing the FirebaseIdpAdmin You can access the admin operations through the `AuthServices.instance.firebaseIdp` property. This requires that the Firebase identity provider is already configured (see [setup](https://docs.serverpod.dev/concepts/authentication/providers/firebase/setup.md#1-add-the-firebase-identity-provider)). ```dart import 'package:serverpod_auth_idp_server/providers/firebase.dart'; import 'package:serverpod_auth_idp_server/core.dart'; final firebaseIdp = AuthServices.instance.firebaseIdp; final admin = firebaseIdp.admin; ``` ## Finding accounts Pick the finder that matches the identifier you have on hand: - `findAccountByEmail` when you have the user's email (e.g., from a support ticket). - `findAccountByAuthUserId` when you have a Serverpod auth user ID and want the linked Firebase account. - `findUserByFirebaseUserId` when you have a Firebase UID and want the Serverpod user it is linked to. ```dart final accountByEmail = await admin.findAccountByEmail( session, email: 'user@example.com', ); final accountByAuthUser = await admin.findAccountByAuthUserId( session, authUserId: authUserId, ); // findUserByFirebaseUserId is static, unlike the instance methods above. final userId = await FirebaseIdpAdmin.findUserByFirebaseUserId( session, userIdentifier: 'firebase-uid', ); ``` ## Linking Firebase authentication Link an existing Serverpod user to a Firebase account. This is useful when migrating users from another auth provider to Firebase, or when manually linking accounts in an admin tool. First, obtain the account details from a Firebase ID token, then link them to an existing user: ```dart // Fetch account details from a Firebase ID token final accountDetails = await admin.fetchAccountDetails( session, idToken: firebaseIdToken, ); // Link the account to an existing user await admin.linkFirebaseAuthentication( session, authUserId: existingUserId, accountDetails: accountDetails, ); ``` ## Deleting accounts Use `deleteFirebaseAccount` when you have a Firebase UID, or `deleteFirebaseAccountByAuthUserId` to remove every Firebase link attached to a single Serverpod user: ```dart final deletedByUid = await admin.deleteFirebaseAccount( session, userIdentifier: 'firebase-uid', ); final deletedByAuthUser = await admin.deleteFirebaseAccountByAuthUserId( session, authUserId: authUserId, ); ``` :::warning Deleting a Firebase account only removes the link between Firebase authentication and the Serverpod user. The Serverpod user stays in your database, and the Firebase user stays in your Firebase project. You must delete those separately if that is what you want. ::: ## FirebaseIdpUtils The `FirebaseIdpUtils` class provides a lower-level `authenticate` method for when you need to verify a Firebase ID token and find or create the associated Serverpod user in custom endpoint logic (outside the normal sign-in flow): ```dart final utils = firebaseIdp.utils; final authSuccess = await utils.authenticate( session, idToken: firebaseIdToken, transaction: transaction, ); ``` --- # Troubleshoot Firebase authentication https://docs.serverpod.dev/concepts/authentication/providers/firebase/troubleshooting This page helps you identify common Firebase authentication failures with Serverpod, explains why they occur, and shows how to resolve them. For issues with Firebase itself, see the [Firebase Auth documentation](https://firebase.google.com/docs/auth). ## Setup checklist Go through this before investigating a specific error. Most problems come from a missed step. #### Firebase Console - [ ] Create a **Firebase project** in the [Firebase Console](https://console.firebase.google.com/). - [ ] Add an app for **every platform you support** (iOS, Android, web) inside the Firebase project. Each platform needs its own registration. - [ ] Generate a **service account key** from **Project settings** > **Service accounts**. - [ ] Enable the **authentication methods** you want to use under **Security** > **Authentication** > **Sign-in method**. - [ ] Confirm **Firebase App Check** is **disabled** (enable it later only after the client integration is in place). - [ ] In **Authentication** > **Settings** > **Authorized domains**, the list includes `localhost` for development and your production domain when you ship. #### Server - [ ] Paste the service account JSON into `firebaseServiceAccountKey` in `config/passwords.yaml`. See [Store the service account key](https://docs.serverpod.dev/concepts/authentication/providers/firebase/setup.md#store-the-service-account-key). - [ ] Confirm the `project_id` inside `firebaseServiceAccountKey` matches the Firebase project the client is using. - [ ] Add `FirebaseIdpConfigFromPasswords()` to `identityProviderBuilders` in `server.dart`. - [ ] Create a `FirebaseIdpEndpoint` file in `lib/src/auth/` extending `FirebaseIdpBaseEndpoint`. - [ ] Start the server with `serverpod start`, then create and apply the migration (press **M**). #### Client - [ ] Install **`firebase_core`**, **`firebase_auth`**, and **`serverpod_auth_idp_flutter_firebase`** in your Flutter project. - [ ] Run **`flutterfire configure`** so `firebase_options.dart` is generated. - [ ] Verify the platform config files are in place: `GoogleService-Info.plist` (iOS, in the Runner target) and `google-services.json` (Android, in `android/app/`). - [ ] Import `firebase_options.dart` and call **`Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform)`** before creating the Serverpod client. - [ ] Call **`client.auth.initializeFirebaseSignIn()`** after `client.auth.initialize()` inside `initializeClient()` in your Flutter app's `lib/client.dart`. - [ ] Call **`controller.login(user)`** after Firebase authentication completes. ## Server crashes on first Firebase sign-in with "no such table" **Problem:** The server builds and starts, but crashes when a user tries Firebase sign-in. The error cites a missing table (like `serverpod_auth_idp_firebase_account`). **Cause:** The database migration that creates the provider's tables was never created or applied. **Resolution:** In the running `serverpod start` terminal, press **M** to create and apply the migration. ## Token verification fails with "invalid signature" **Problem:** The server rejects Firebase ID tokens with a signature verification error. **Cause:** The ID token itself failed verification against Google's public certificates. The token was truncated or corrupted in transit, or what was sent is not a Firebase ID token. A project mismatch produces `Invalid issuer` or `Audience does not match` instead, and broken JSON in `passwords.yaml` fails at startup (see the parse-error entry below). **Resolution:** 1. Log the token length in the app before sending it, and confirm the server receives the same value. 2. Confirm the app sends the Firebase **ID token** (`getIdToken()`), not an access token or a custom token. 3. For `Invalid issuer` or `Audience does not match`, verify the `project_id` in your `firebaseServiceAccountKey` matches the project in `firebase_options.dart`. ## Token verification fails with "token expired" **Problem:** The server rejects Firebase ID tokens with a token expiration error. **Cause:** Firebase ID tokens expire after one hour. If the server's system clock is significantly off, valid tokens may appear expired. **Resolution:** Check that the server's system clock is accurate. If the client token is genuinely expired (e.g., the user's app was backgrounded for a long time), the client should re-authenticate with Firebase to obtain a fresh ID token before calling `controller.login()`. ## Server fails to parse firebaseServiceAccountKey from passwords.yaml **Problem:** The server crashes on startup with a JSON parsing error related to `firebaseServiceAccountKey`. **Cause:** The YAML block scalar indentation is incorrect. The `firebaseServiceAccountKey` key uses `|` (literal block scalar), which requires every line of the JSON to be indented at the same level relative to the key. **Resolution:** Make sure the JSON block is indented consistently under the `|`: ```yaml development: firebaseServiceAccountKey: | { "type": "service_account", "project_id": "...", "private_key": "..." } ``` Every line of the JSON must be indented by at least one level more than `firebaseServiceAccountKey:`. Mixing tabs and spaces can also cause issues. ## FirebaseAuth.instance.currentUser is null after sign-in **Problem:** After the Firebase sign-in flow completes, `FirebaseAuth.instance.currentUser` returns null, so `controller.login(user)` never gets called. **Cause:** `Firebase.initializeApp()` was not called before attempting authentication, or was called with the wrong options. **Resolution:** Make sure `Firebase.initializeApp()` is called in your `main()` function before any Firebase operations: ```dart await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); ``` If you haven't run `flutterfire configure`, do so to generate the `firebase_options.dart` file. ## Sign-in succeeds in Firebase but fails to sync with Serverpod **Problem:** The user authenticates with Firebase (the Firebase UI shows success), but the Serverpod session is never created. The `onError` callback on `FirebaseAuthController` fires. **Cause:** The Firebase ID token could not be verified by the server. Check the server logs first, the error message will narrow this down to one of the following causes: 1. **Missing service account key:** The `firebaseServiceAccountKey` is not present in `passwords.yaml`, or the JSON is invalid. 2. **Missing endpoint:** You did not create the endpoint class extending `FirebaseIdpBaseEndpoint`. Without it, the client has no endpoint to call. 3. **Missing migration:** The provider's database tables don't exist yet. In the `serverpod start` terminal, press **M** to create and apply the migration. 4. **Project mismatch:** The service account key belongs to a different Firebase project than the one configured in your Flutter app. Compare `project_id` in `firebaseServiceAccountKey` against the project in `firebase_options.dart`. 5. **App Check enabled prematurely:** If you enabled Firebase App Check before the client integration is in place, every request will be rejected with an App Check assertion error. Disable App Check until the client is wired up. ## Email validation rejects phone-only users **Problem:** Users who sign in with phone authentication are rejected. The app receives `FirebaseEmailNotVerifiedException`, or a generic `FirebaseIdTokenVerificationException` when the validator throws a plain exception. **Cause:** A custom `firebaseAccountDetailsValidation` callback requires a verified email, but phone-only users don't have an email. The default validation accepts phone-only authentication, so this only happens with a custom validator. **Resolution:** Guard on the presence of an email before requiring verification. The built-in `FirebaseIdpConfig.requireVerifiedEmail` does exactly this, or write it yourself: ```dart firebaseAccountDetailsValidation: (accountDetails) { if (accountDetails.email != null && accountDetails.verifiedEmail != true) { throw FirebaseEmailNotVerifiedException(); } }, ``` ## User signed out of Serverpod but still signed in to Firebase **Problem:** After calling sign-out on Serverpod, the user's Firebase session remains active. The next time the app opens, Firebase still has an authenticated user. **Cause:** `initializeFirebaseSignIn()` was not called during app initialization. This method sets up the automatic sign-out sync between Serverpod and Firebase. **Resolution:** Make sure you call `initializeFirebaseSignIn()` after initializing the Serverpod client: ```dart client.auth.initialize(); client.auth.initializeFirebaseSignIn(); ``` ## FlutterFire configure fails or generates wrong config **Problem:** `flutterfire configure` fails, or the generated `firebase_options.dart` has wrong project details. **Cause:** The Firebase CLI is not installed, not logged in, or is pointing to the wrong project. **Resolution:** 1. [Install the Firebase CLI](https://firebase.google.com/docs/cli#install_the_firebase_cli). 2. Log in: `firebase login` 3. Install FlutterFire CLI: `dart pub global activate flutterfire_cli` 4. Run `flutterfire configure` and select the correct project when prompted. See the [FlutterFire CLI documentation](https://firebase.flutter.dev/docs/cli/) for more details. ## FlutterFire configure fails for iOS or macOS apps **Problem:** `flutterfire configure` registers the Android app successfully but fails when registering iOS or macOS apps, with an error about an invalid bundle identifier. **Cause:** When a platform app does not yet exist in your Firebase project, FlutterFire creates it and derives the default bundle ID from your Flutter project folder name. Apple bundle identifiers must be valid reverse-DNS strings, so any character that is not allowed in a DNS label (most commonly an underscore) breaks auto-registration. **Resolution:** Pass the bundle ID explicitly so FlutterFire does not derive it from the folder name: ```bash flutterfire configure \ --ios-bundle-id="dev.serverpod.authproviders" \ --macos-bundle-id="dev.serverpod.authproviders" ``` Use a valid reverse-DNS identifier for your app. If you have already created the iOS and macOS apps manually in the Firebase Console, FlutterFire will reuse those entries instead of trying to create new ones. ## Firebase UI auth actions not firing **Problem:** The `AuthStateChangeAction` or `AuthStateChangeAction` actions on the `SignInScreen` never fire, so `controller.login()` is never called. **Cause:** The action types don't match the authentication state changes from the providers you configured. For example, using `EmailAuthProvider` but only listening for one of the two states. **Resolution:** Make sure you have actions for both `SignedIn` (returning users) and `UserCreated` (new users). See the [Customizing the UI](https://docs.serverpod.dev/concepts/authentication/providers/firebase/customizing-the-ui.md#anatomy-of-the-gate-widget) page for the complete code example. ## Platform-specific Firebase SDK configuration issues **Problem:** Firebase operations fail on a specific platform (iOS, Android, or Web) with errors about missing configuration. **Cause:** The `flutterfire configure` command may not have configured all platforms, or platform-specific files were not placed correctly. **Resolution:** 1. Re-run `flutterfire configure` and ensure you select all platforms you want to support. 2. For **iOS**: Verify that `GoogleService-Info.plist` is included in the Xcode project's Runner target. 3. For **Android**: Verify that `google-services.json` is in `android/app/`. 4. For **Web**: Verify that the Firebase config is loaded in `web/index.html` or via `firebase_options.dart`. ## `Firebase.initializeApp()` throws `channel-error` on web in production **Problem:** A Flutter web app works fine in `flutter run -d chrome` (debug), but the production build throws `channel-error` from `FirebaseCoreHostApi.initializeCore` as soon as `Firebase.initializeApp()` runs. The browser console shows obfuscated frames like `aJs.$2` and `JX.fI` in `main.dart.js` with no clear message. **Cause:** Flutter's incremental build cache under `.dart_tool/flutter_build/` can silently drop `firebase_core_web`'s plugin registration. The on-disk plugin registrant file still lists `FirebaseCoreWeb.registerWith(registrar)`, but stale kernel `.dill` artifacts mean `dart2js` compiles `main.dart.js` without the wiring. `FirebasePlatform.instance` then stays as the default `MethodChannelFirebase`, and `Firebase.initializeApp()` goes through a pigeon channel with no handler. The error sounds native, but it is purely a Flutter web build-cache issue. Debug mode (DDC) does not tree-shake or cache the same way as `dart2js` (release), so it hides the bug entirely. **Resolution:** Run `flutter clean` before building so the registration is included: ```bash flutter clean flutter build web ``` For Serverpod Cloud projects, add `flutter clean` ahead of `flutter build web` in your `flutter_build` script (or whatever `scripts.pre_deploy` runs). :::tip To make minified stack traces readable while you debug an issue like this, build **locally** with `flutter build web --source-maps` and load that build (or the resulting `.map` file) in your browser's dev tools. Do **not** deploy the `.map` file to a public-facing host: it contains your full Dart source. If you need readable production stack traces continuously, upload source maps to an error-monitoring service (Sentry, Datadog) rather than serving them next to `main.dart.js`. ::: --- # Set up GitHub sign-in https://docs.serverpod.dev/concepts/authentication/providers/github/setup Sign in with GitHub uses OAuth2 credentials from a **GitHub App** registered on GitHub. ## Prerequisites Before following this guide, make sure you have: - A GitHub account with access to [Developer Settings](https://github.com/settings/apps). - A running Serverpod project (server, client, and Flutter app packages from `serverpod create`). - The Serverpod auth module installed and configured per the [authentication setup](https://docs.serverpod.dev/concepts/authentication/setup.md). If your project was generated with an older Serverpod version, follow that guide first to add `serverpod_auth_idp_server` and `serverpod_auth_idp_flutter` and to configure `pod.initializeAuthServices()` before continuing. ## Get your GitHub credentials ### Register a new GitHub App 1. Go to [Register new GitHub App](https://github.com/settings/apps/new). 2. Fill in the basics: - **GitHub App name**. - **Homepage URL**. - **Description** (optional). ![GitHub App basics](https://docs.serverpod.dev/img/authentication/providers/github/1-app-basics.png) ### Configure the callback URL The callback URL is where GitHub redirects the user after they authorize your app. 1. In the **Callback URL** field, enter the redirect URI for your app. GitHub Apps accept up to 10 entries, one per line. Add every platform you target: - **iOS, macOS, and Android**: `com.example.yourapp://auth`. Register the custom scheme in `AndroidManifest.xml`. iOS and macOS need no special configuration. - **Web**: `http://localhost:8082/auth/callback` locally, `https://my-awesome-project.serverpod.space/auth/callback` in production. ![Callback URL field](https://docs.serverpod.dev/img/authentication/providers/github/2-callback-url.png) 2. Leave **Expire user authorization tokens** enabled (GitHub's default). Token expiration is recommended for sign-in flows so leaked tokens have a short useful lifetime. Serverpod uses the access token once during sign-in and does not store or refresh it, so expiry does not affect signed-in users. 3. Leave **Request user authorization (OAuth) during installation** unchecked unless you need users to authorize immediately when they install the GitHub App on their account or organization. ### Disable webhooks Webhooks let your GitHub App receive events like pushes, pull requests, and issue activity. They are unrelated to sign-in and add complexity if you do not need them, so turn the section off for now. You can re-enable webhooks later without affecting the auth flow. Under **Webhook**, uncheck **Active** to disable the whole section. ![Disable webhook](https://docs.serverpod.dev/img/authentication/providers/github/3-webhook-disable.png) ### Set permissions For sign-in you only need access to the user's profile. 1. Under **Account permissions**, set **Email addresses** to **Read-only**. This lets your app read the user's primary verified email. 2. Leave all other permissions at **No access** unless your app needs them for non-auth reasons. ![Account permissions](https://docs.serverpod.dev/img/authentication/providers/github/4-permissions.png) :::note GitHub users can keep their email private. Even with the **Email addresses** permission, the `email` field on the account may be `null`. See [Custom account validation](https://docs.serverpod.dev/concepts/authentication/providers/github/customizations.md#custom-account-validation) for how to handle this without blocking sign-in. ::: ### Choose the installation scope 1. Under **Where can this GitHub App be installed?**, choose: - **Only on this account** for development or internal-only apps. - **Any account** for production apps that anyone can sign in to. ### Create the app and copy credentials 1. Click **Create GitHub App**. GitHub takes you to the new app's settings page. 2. Copy the **Client ID** shown on that page. 3. Click **Generate a new client secret**, then copy the secret immediately. GitHub only shows the secret once. ![Client ID and Generate secret button](https://docs.serverpod.dev/img/authentication/providers/github/5-credentials.png) :::warning **Keep the Client Secret confidential.** Do not commit it to version control. Use `config/passwords.yaml` (excluded from git) or environment variables. ::: ## Server-side configuration ### Store your credentials Your server's `config/passwords.yaml` already has `development:`, `staging:`, and `production:` sections from the project template. Add `githubClientId` and `githubClientSecret` to the `development:` section using the values you copied above: ```yaml development: # ... existing keys (database, redis, serviceSecret, etc.) ... githubClientId: 'your-github-client-id' githubClientSecret: 'your-github-client-secret' ``` For production, add the same two keys to the `production:` section, or set the `SERVERPOD_PASSWORD_githubClientId` and `SERVERPOD_PASSWORD_githubClientSecret` environment variables on your production server. See [Publishing to production](#publishing-to-production) below for the full prod walkthrough. :::warning **Never commit `config/passwords.yaml` to version control.** It contains your client secret. Use environment variables or a secrets manager in production. ::: ### Add the GitHub identity provider Your server's `server.dart` file (e.g., `my_project_server/lib/server.dart`) should already contain a `pod.initializeAuthServices()` call if your project was created with the Serverpod project template (`serverpod create`). If it's not there, see [Setup](https://docs.serverpod.dev/concepts/authentication/setup.md) first to configure the auth module and JWT settings. Add the GitHub import and `GitHubIdpConfigFromPasswords()` to the existing `identityProviderBuilders` list: ```dart import 'package:serverpod_auth_idp_server/providers/github.dart'; ``` ```dart pod.initializeAuthServices( tokenManagerBuilders: [ JwtConfigFromPasswords(), ], identityProviderBuilders: [ // ... any existing providers (e.g., EmailIdpConfigFromPasswords) ... GitHubIdpConfigFromPasswords(), ], ); ``` The `GitHubIdpConfigFromPasswords()` constructor automatically loads the client ID and secret from the `githubClientId` and `githubClientSecret` keys in `config/passwords.yaml` (or the matching `SERVERPOD_PASSWORD_*` environment variables). :::tip If you need more control over how the credentials are loaded, use `GitHubIdpConfig(clientId: ..., clientSecret: ...)` instead. See [Customizations](https://docs.serverpod.dev/concepts/authentication/providers/github/customizations.md) for details. ::: ### Create the endpoint Create a new endpoint file in your server project (e.g., `my_project_server/lib/src/auth/github_idp_endpoint.dart`) alongside the existing auth endpoints. Extending the base class registers the sign-in methods with your server so your app can call them to complete the authentication flow: ```dart import 'package:serverpod_auth_idp_server/providers/github.dart'; class GitHubIdpEndpoint extends GitHubIdpBaseEndpoint {} ``` ### Start the server Start the server from your server project directory (e.g., `my_project_server/`): ```bash serverpod start ``` Then create and apply the migration for the provider's tables: in the `serverpod start` terminal, press **M** to create and apply the migration. :::warning Skipping the migration will cause the server to crash at runtime when the GitHub provider tries to read or write user data. More detailed instructions can be found in the general [identity providers setup section](https://docs.serverpod.dev/concepts/authentication/setup.md#identity-providers-configuration). ::: ## Client-side configuration The GitHub identity provider uses [`flutter_web_auth_2`](https://pub.dev/packages/flutter_web_auth_2) under the hood to drive the OAuth2 redirect on every platform. The configuration differs slightly between platforms because each one has a different way of receiving the callback URL. ### iOS and macOS No special configuration is needed for a standard custom-scheme callback URL (e.g., `myapp://auth`). The `flutter_web_auth_2` package handles the redirect using `ASWebAuthenticationSession`. If you use **Universal Links** instead, your redirect URI must use `https` and you must follow the [flutter\_web\_auth\_2 iOS setup](https://pub.dev/packages/flutter_web_auth_2#ios) to register the associated domain. ### Android To capture the custom-scheme callback URL, add the following activity to your Flutter project's `android/app/src/main/AndroidManifest.xml`. Replace `your-callback-scheme` with the scheme you registered on your GitHub App (e.g., `myapp` for `myapp://auth`): ```xml ``` :::warning The scheme in `AndroidManifest.xml` must exactly match the scheme in your GitHub App's **Callback URL** and the `redirectUri` you pass to `initializeGitHubSignIn`. A mismatch causes the OAuth callback to never reach your app. ::: ### Web On web, GitHub completes sign-in by redirecting the browser to a callback URL you control. This flow requires Serverpod to serve your Flutter web app on the **same origin** as the callback route. To test locally, build your Flutter web app into Serverpod's `web/app/` directory: ```bash flutter build web --base-href / --output ../my_project_server/web/app # from your Flutter project serverpod start --no-flutter # from your server project ``` Pass `--no-flutter` so `serverpod start` serves your prebuilt web app. Without the flag, it also runs a Flutter web dev server on a different port, which does not share Serverpod's origin. For hot-reload workflows, use the [separately-hosted Flutter web](https://docs.serverpod.dev/concepts/authentication/providers/github/customizations.md#separately-hosted-flutter-web) flow instead. Open `http://localhost:8082/` to test. Projects created with the website option serve the app under `/app` instead. Build those with `--base-href /app/` and open `/app`. The examples below use port `8082` (Serverpod's default from `config/development.yaml`). 1. In your server project's `server.dart` file, before `pod.start()`, register the callback route: ```dart import 'package:serverpod_auth_idp_server/core.dart'; // ... pod.webServer.addRoute( FlutterWebAuth2CallbackRoute(), '/auth/callback', ); ``` The path `/auth/callback` can be anything, as long as it matches the URL you register with GitHub in step 2 and pass to `initializeGitHubSignIn` later. In production, pass `host:` to restrict the route to your production domain: ```dart pod.webServer.addRoute( FlutterWebAuth2CallbackRoute(host: 'my-awesome-project.serverpod.space'), '/auth/callback', ); ``` :::tip The same route works for every OAuth2 PKCE provider. Register it once and reuse the callback URL for GitHub, Google, and any future provider. ::: 2. Add the full callback URL to your GitHub App's **Callback URL** field (e.g., `http://localhost:8082/auth/callback` locally, `https://my-awesome-project.serverpod.space/auth/callback` in production). GitHub Apps accept up to 10 entries, so dev and prod entries can coexist. ## Present the authentication UI ### Initialize the GitHub sign-in service Open your Flutter app's `lib/client.dart`. The Serverpod template already creates the `Client` and calls `client.auth.initialize()` inside `initializeClient()`. Add `client.auth.initializeGitHubSignIn(...)` on the line immediately after it. The GitHub provider requires `clientId` and `redirectUri` on every platform because GitHub does not have native platform-specific clients (unlike Google or Apple): ```dart Future initializeClient() async { client = Client(await serverUrl) ..connectivityMonitor = FlutterConnectivityMonitor() ..authSessionManager = FlutterAuthSessionManager(); unawaited(client.auth.initialize()); await client.auth.initializeGitHubSignIn( clientId: 'your-github-client-id', redirectUri: 'com.example.yourapp://auth', ); } ``` Replace `your-github-client-id` with the **Client ID** from your GitHub App, and `redirectUri` with the matching callback URL you registered: a reverse-DNS custom scheme for mobile, or the route URL from [Web](#web) for Flutter web. Swap the redirect URI for your production URL when deploying. :::tip To keep these values out of `client.dart` and vary them per build, read them from `--dart-define`. See [Configuring client IDs on the app](https://docs.serverpod.dev/concepts/authentication/providers/github/customizations.md#configuring-client-ids-on-the-app) for the pattern. ::: ### Show the GitHub sign-in button The Serverpod template ships with a `SignInScreen` widget at `lib/screens/sign_in_screen.dart`. It listens to `client.auth.authInfoListenable` and swaps between `SignInWidget` while the user is signed out and the `child` you pass it once they sign in. The `SignInWidget` auto-detects which identity provider endpoints are registered on the server, so once `GitHubIdpEndpoint` is exposed and the client code has been regenerated, the GitHub button appears inside it. To customize the GitHub button or build a fully custom UI, see [Customizations](https://docs.serverpod.dev/concepts/authentication/providers/github/customizations.md#customize-the-sign-in-button). ## Publishing to production Before going live, complete the following steps: ### 1. Add the production callback URL Go back to your GitHub App's settings and add your production callback URL to **Callback URL** alongside the development one. Both should remain registered so dev and prod work simultaneously. - For web, add the production route URL you registered in [Web](#web) (e.g., `https://my-awesome-project.serverpod.space/auth/callback`). Make sure `pod.webServer.addRoute(FlutterWebAuth2CallbackRoute(host: 'my-awesome-project.serverpod.space'), '/auth/callback')` is called in `server.dart` so the route is registered in production. If you use the [separately-hosted Flutter web](https://docs.serverpod.dev/concepts/authentication/providers/github/customizations.md#separately-hosted-flutter-web) flow, register the production `auth.html` URL instead (e.g., `https://app.example.com/auth.html`). - For mobile custom schemes (e.g., `com.example.yourapp://auth`), no change is needed between dev and prod. ### 2. Set production credentials Production runs out of the `production:` section of `passwords.yaml`, which is separate from the `development:` section you populated during setup. Adding production credentials does not replace your development ones. Both stay in place, and Serverpod picks the right set based on the run mode. If you use the same GitHub App for development and production, you can reuse the same `githubClientId` and `githubClientSecret`. For separate environments, [register a second GitHub App](https://github.com/settings/apps/new) first and use its values. #### Self-hosted Add `githubClientId` and `githubClientSecret` to the `production:` section of `passwords.yaml`: ```yaml production: # ... existing keys ... githubClientId: 'your-github-client-id' githubClientSecret: 'your-github-client-secret' ``` Alternatively, set the `SERVERPOD_PASSWORD_githubClientId` and `SERVERPOD_PASSWORD_githubClientSecret` [environment variables](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md#via-environment-variables) on your production server with the same values. #### Serverpod Cloud Use `serverpod cloud password set` to upload each value. The Client ID is public, so pass it as a positional argument. The Client Secret is sensitive, so read it from a file with `--from-file` to keep it out of your shell history: ```bash serverpod cloud password set githubClientId your-github-client-id serverpod cloud password set githubClientSecret --from-file path/to/github-client-secret.txt ``` Run these from your linked server project directory, or pass `--project ` on each call (the flag is required unless the project is linked). See the [Serverpod Cloud passwords guide](https://docs.serverpod.dev/cloud/concepts/passwords-secrets-env-vars.md) for project linking and other options. ### 3. Verify the redirect URI in the Flutter build The production build of your Flutter app must initialize `GitHubSignInService` with the production `redirectUri`. The cleanest pattern is to read it from `--dart-define` so a single `client.dart` works in dev and prod. See [Configuring client IDs on the app](https://docs.serverpod.dev/concepts/authentication/providers/github/customizations.md#configuring-client-ids-on-the-app). :::tip If you run into issues, see the [troubleshooting guide](https://docs.serverpod.dev/concepts/authentication/providers/github/troubleshooting.md). ::: --- # Customize GitHub sign-in https://docs.serverpod.dev/concepts/authentication/providers/github/customizations This page covers additional configuration options for the GitHub identity provider beyond the basic setup. On the server, you can control how credentials are loaded and hook into the sign-in flow with callbacks. In your app, you can configure client IDs and redirect URIs, and customize the sign-in UI. Use the `GitHubSignInWidget` to display the GitHub sign-in flow in your own custom UI, or the `GitHubAuthController` to build a completely custom authentication interface. ## Server configuration The options in this section are set on the server when you register the GitHub identity provider. ### Configuration options Below is a non-exhaustive list of some of the most common configuration options. For more details on all options, check the `GitHubIdpConfig` in-code documentation. The GitHub identity provider can be configured using one of two classes: - **`GitHubIdpConfigFromPasswords`**: Automatically loads the client ID and secret from the `githubClientId` and `githubClientSecret` keys in `passwords.yaml` (or the matching `SERVERPOD_PASSWORD_*` environment variables). This is the class used in the [setup guide](https://docs.serverpod.dev/concepts/authentication/providers/github/setup.md) and is recommended for most projects. - **`GitHubIdpConfig`**: Requires you to pass the client ID and secret directly. Use this when you load credentials from a custom source, such as a secrets manager or a programmatically constructed config. The `GitHubIdpConfigFromPasswords` class is a convenience wrapper around `GitHubIdpConfig` that handles credential loading for you. Both classes accept the same optional callbacks shown in the sections below. The examples on this page use `GitHubIdpConfigFromPasswords` unless the section specifically demonstrates manual credential loading. #### Load credentials using GitHubIdpConfig When using `GitHubIdpConfig`, you must provide the client ID and secret explicitly. Read them from any source you want: ```dart final githubIdpConfig = GitHubIdpConfig( clientId: pod.getPassword('githubClientId')!, clientSecret: pod.getPassword('githubClientSecret')!, ); ``` Or from a secrets manager, hard-coded values for tests, or a custom loader: ```dart final githubIdpConfig = GitHubIdpConfig( clientId: await mySecretsManager.fetch('github-client-id'), clientSecret: await mySecretsManager.fetch('github-client-secret'), ); ``` #### Custom account validation You can customize the validation for GitHub account details before allowing sign-in. By default, the validation only checks that the received account details contain a non-empty `userIdentifier`. ```dart final githubIdpConfig = GitHubIdpConfigFromPasswords( githubAccountDetailsValidation: (accountDetails) { // Throw an exception if account doesn't meet custom requirements if (accountDetails.userIdentifier.isEmpty) { throw GitHubUserInfoMissingDataException(); } }, ); ``` :::note GitHub users can keep their email private, so `email` may be `null` even for valid accounts. Similarly, `name` is optional on GitHub profiles. To avoid blocking real users with private profiles from signing in, adjust your validation function with care. ::: ##### GitHubAccountDetails The `githubAccountDetailsValidation` callback receives a `GitHubAccountDetails` record with the following properties: | Property | Type | Description | | ---------------- | --------- | --------------------------------------------------- | | `userIdentifier` | `String` | The GitHub user's unique identifier (UID) | | `email` | `String?` | The user's email address (may be `null` if private) | | `name` | `String?` | The user's display name from GitHub | | `image` | `Uri?` | URL to the user's profile image | Example of accessing these properties: ```dart githubAccountDetailsValidation: (accountDetails) { print('GitHub UID: ${accountDetails.userIdentifier}'); print('Email: ${accountDetails.email}'); print('Display name: ${accountDetails.name}'); print('Profile image: ${accountDetails.image}'); // Custom validation logic if (accountDetails.email == null) { throw GitHubUserInfoMissingDataException(); } }, ``` #### Accessing GitHub APIs on the server On the server side, you can call GitHub's REST API using the access token returned by sign-in. The `getExtraGitHubInfoCallback` on `GitHubIdpConfig` receives the access token on every authentication attempt and can be used to fetch and store additional user data: ```dart import 'package:http/http.dart' as http; final githubIdpConfig = GitHubIdpConfigFromPasswords( getExtraGitHubInfoCallback: (session, { required accountDetails, required accessToken, required transaction, }) async { final response = await http.get( Uri.https('api.github.com', '/user/orgs'), headers: { 'Authorization': 'Bearer $accessToken', 'Accept': 'application/vnd.github+json', }, ); // Parse response and store organization membership in your own table, // linked by accountDetails.userIdentifier. }, ); ``` :::warning **Do not create `GitHubAccount`, `UserProfile`, or `AuthUser` models inside this callback.** The authentication flow already creates them. Creating them here breaks new-account detection and skips critical setup steps. Store any extra data in your own custom tables, linked by `accountDetails.userIdentifier`. ::: :::info This callback runs on **every** sign-in, not only the first. Keep operations lightweight or guard expensive work behind a check for whether the data already exists. Guard external calls with `try`/`catch`, because an uncaught exception in the callback makes the sign-in fail. ::: #### Reacting to GitHub account creation Use the `onAfterGitHubAccountCreated` callback to run logic after a new GitHub account has been created and linked to an auth user. This callback only fires for new accounts, not returning users. This callback is complementary to the global [`onAfterAuthUserCreated`](#reacting-to-auth-user-creation) hook and is for side-effects specific to a GitHub sign-in, like storing GitHub-specific analytics or sending a GitHub-themed welcome email. ```dart final githubIdpConfig = GitHubIdpConfigFromPasswords( onAfterGitHubAccountCreated: ( session, authUser, githubAccount, { required transaction, }) async { // e.g. store additional data, send a welcome email, or log for analytics }, ); ``` :::info This callback runs inside the same database transaction as the account creation. Throwing an exception inside this callback aborts the process. If you perform external side-effects, guard them with `try`/`catch` to prevent unwanted failures. ::: :::caution Scopes you assign here with `AuthServices.instance.authUsers.update()` do not apply to the login that is already in progress, because token issuance uses the scopes loaded before this callback runs. They take effect the next time the user signs in. To force them sooner, revoke the user's tokens so they sign in again. The `onBeforeAuthUserCreated` hook, covered below, assigns scopes at creation time, but it cannot use GitHub data, because `getExtraGitHubInfoCallback` runs after the auth user is created. ::: #### Reacting to auth user creation The `onBeforeAuthUserCreated` and `onAfterAuthUserCreated` hooks are global callbacks configured on `AuthUsersConfig` in `initializeAuthServices`. They are not specific to GitHub. They fire for every identity provider. See [user creation callbacks](https://docs.serverpod.dev/concepts/authentication/working-with-users.md#user-creation-callbacks) for full details on both hooks. ### GitHubIdpConfig parameter reference | Parameter | Type | Required | Description | | -------------------------------- | ------------------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `clientId` | `String` | Yes | The Client ID from your GitHub App or OAuth App. | | `clientSecret` | `String` | Yes | The Client Secret generated for your GitHub App or OAuth App. | | `githubAccountDetailsValidation` | `GitHubAccountDetailsValidation` | No | Custom validation callback for GitHub account details before allowing sign-in. Throws an exception to reject the account. Defaults to validating only that `userIdentifier` is non-empty. | | `getExtraGitHubInfoCallback` | `GetExtraGitHubInfoCallback?` | No | Callback that receives the access token after sign-in, allowing you to call additional GitHub APIs and store extra user data. Runs on every sign-in. | | `onAfterGitHubAccountCreated` | `AfterGitHubAccountCreatedFunction?` | No | Callback invoked after a new GitHub account is created and linked to an auth user. Fires only for new accounts. | ## App configuration The options in this section are set in your Flutter app when you initialize the GitHub sign-in service. ### Configuring client IDs on the app #### Passing client IDs in code You can pass the `clientId` and `redirectUri` directly when initializing the GitHub sign-in service: ```dart await client.auth.initializeGitHubSignIn( clientId: 'your-github-client-id', redirectUri: 'com.example.yourapp://auth', ); ``` This approach is useful when you need different `redirectUri` values per platform and want to keep them in your Dart code. #### Using environment variables Alternatively, pass them at build time using `--dart-define`. The GitHub sign-in provider supports the following environment variables: - `GITHUB_CLIENT_ID`: Your GitHub OAuth client ID. - `GITHUB_REDIRECT_URI`: The callback URI. Use the value matching the platform you build for: a reverse-DNS scheme for mobile, `https://your-domain.com/auth/callback` for Serverpod-hosted Flutter web, or the full `auth.html` URL for separately-hosted Flutter web. If `clientId` and `redirectUri` are not supplied when initializing the service, the provider automatically falls back to these environment variables. **Example usage:** ```bash flutter run -d chrome --web-port=49660 \ --dart-define="GITHUB_CLIENT_ID=your-github-client-id" \ --dart-define="GITHUB_REDIRECT_URI=http://localhost:49660/auth.html" ``` ```bash flutter build web \ --dart-define="GITHUB_CLIENT_ID=your-github-client-id" \ --dart-define="GITHUB_REDIRECT_URI=https://my-awesome-project.serverpod.space/auth/callback" ``` This approach is useful when you need to: - Configure different credentials for different build environments (development, staging, production). - Avoid committing client IDs to version control. - Inject platform-specific redirect URIs from your CI/CD pipeline. :::tip You can also set these environment variables in your IDE's run configuration or CI/CD pipeline to avoid passing them manually each time. ::: ### Separately-hosted Flutter web Use this flow when your Flutter web app and Serverpod are on different origins. Common cases: `flutter run -d chrome` locally with Serverpod on a separate port, or a CDN-hosted Flutter build with a separate API server. 1. Place a static `auth.html` file in your Flutter project's `web/` folder. A single copy is shared across every identity provider that uses an OAuth2 redirect, so create it once. Follow [Web callback page (`auth.html`)](https://docs.serverpod.dev/concepts/authentication/setup.md#web-callback-page-authhtml) in the authentication setup guide. 2. Run Flutter on a fixed port. The examples use `49660`, but any free port works; keep it consistent everywhere: ```bash flutter run -d chrome --web-port=49660 ``` 3. Register the full `auth.html` URL on your GitHub App's **Callback URL** field (e.g., `http://localhost:49660/auth.html` locally, `https://app.example.com/auth.html` in production). GitHub Apps accept up to 10 callback URLs, so dev and prod entries can coexist with the Serverpod-hosted route and mobile schemes. 4. Pass the same URL to `initializeGitHubSignIn` via the `redirectUri` argument instead of the route URL. ## Customize the sign-in button See [Styling the buttons](https://docs.serverpod.dev/concepts/authentication/ui-components.md#styling-the-buttons) for how a `buttonStyle` set on `SignInWidget` takes precedence over the appearance arguments shown below. :::info The `SignInWidget` uses the `GitHubSignInWidget` internally to display the GitHub sign-in flow. You can also supply a custom `GitHubSignInWidget` to the `SignInWidget` to override the default behavior. ```dart SignInWidget( client: client, githubSignInWidget: GitHubSignInWidget( client: client, // Shape and label survive inside SignInWidget, unless its buttonStyle // sets them. Brand colors do not. shape: SignInButtonShape.rounded, text: SignInButtonTextVariant.signInWith, // A custom widget replaces the built-in handling, so pass your own callbacks. onAuthenticated: () { /* ... */ }, onError: (error) { /* ... */ }, ), ) ``` ::: ### Using the `GitHubSignInWidget` The `GitHubSignInWidget` handles the complete GitHub sign-in flow for your Flutter app. You can customize the widget's appearance and behavior: ```dart GitHubSignInWidget( client: client, // Button customization. The values shown are the defaults. style: GitHubButtonStyle.black, // or white size: SignInButtonSize.large, // or medium, small text: SignInButtonTextVariant.continueWith, // or signInWith, signUpWith, signIn shape: SignInButtonShape.pill, // or rounded, rectangular logoAlignment: SignInButtonLogoAlignment.center, // or left minimumWidth: 240, // at most 400 textStyle: null, // TextStyle for the label // Scopes to request from GitHub // These are the default. scopes: const ['user', 'user:email', 'read:user'], onAuthenticated: () { // Do something when the user is authenticated. // // NOTE: You should not navigate to the home screen here, otherwise // the user will have to sign in again every time they open the app. }, onError: (error) { // Handle errors }, ) ``` :::note The `scopes` argument applies to **OAuth Apps**. For a **GitHub App**, the App's [Permissions](https://docs.serverpod.dev/concepts/authentication/providers/github/setup.md#set-permissions) configured on the GitHub side control access and the `scopes` argument is ignored. ::: ## Build a custom UI with GitHubAuthController For more control over the UI, you can use the `GitHubAuthController` class, which provides all the authentication logic without any UI components. This allows you to build a completely custom authentication interface. ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; final controller = GitHubAuthController( client: client, onAuthenticated: () { // Do something when the user is authenticated. // // NOTE: You should not navigate to the home screen here, otherwise // the user will have to sign in again every time they open the app. }, onError: (error) { // Handle errors }, scopes: const ['user', 'user:email', 'read:user'], ); // Initiate sign-in await controller.signIn(); ``` ### GitHubAuthController state management Your widget should render the appropriate UI based on the `state` property of the controller. You can also use the below state properties to build your UI: ```dart // Check current state final state = controller.state; // GitHubAuthState enum // Check if loading final isLoading = controller.isLoading; // Check if authenticated final isAuthenticated = controller.isAuthenticated; // Get error message final errorMessage = controller.errorMessage; // Listen to state changes controller.addListener(() { setState(() { // Rebuild UI when controller state changes }); }); ``` #### GitHubAuthController states - `GitHubAuthState.idle` - Ready for user interaction. - `GitHubAuthState.loading` - Processing a sign-in request. - `GitHubAuthState.error` - An error occurred. - `GitHubAuthState.authenticated` - Authentication was successful. ## Related - [Setup](https://docs.serverpod.dev/concepts/authentication/providers/github/setup.md): configure GitHub sign-in on the server and in your app. - [Troubleshooting](https://docs.serverpod.dev/concepts/authentication/providers/github/troubleshooting.md): fix common GitHub sign-in errors. - [UI components](https://docs.serverpod.dev/concepts/authentication/ui-components.md): style the sign-in buttons and localize the built-in UI. - [Working with users](https://docs.serverpod.dev/concepts/authentication/working-with-users.md): manage auth users and react to account events. --- # Troubleshoot GitHub sign-in https://docs.serverpod.dev/concepts/authentication/providers/github/troubleshooting This page helps you identify common Sign in with GitHub failures, explains why they occur, and shows how to resolve them. For underlying issues with the OAuth callback library, see the [flutter\_web\_auth\_2 documentation](https://pub.dev/packages/flutter_web_auth_2). ## Setup checklist Go through this before investigating a specific error. Most problems come from a missed step. #### GitHub Developer Settings - [ ] Created a **GitHub App** at [Developer Settings](https://github.com/settings/apps) (or an **OAuth App** if you specifically need one; OAuth Apps allow only a single Callback URL). - [ ] Registered every redirect URI you actually use under **Callback URL**: reverse-DNS custom schemes for mobile (e.g., `com.example.yourapp://auth`), and the appropriate web URL: `https://your-domain.com/auth/callback` for Serverpod-hosted Flutter web, or `https://your-domain.com/auth.html` for separately-hosted Flutter web. GitHub Apps accept up to 10 entries; OAuth Apps accept only one. - [ ] Set **Account permissions > Email addresses** to **Read-only** if you need the user's email. - [ ] Disabled **Active** under the Webhook section, unless you actually use webhooks for non-auth reasons. - [ ] Set **Where can this GitHub App be installed?** to **Any account** if users outside your account need to sign in. - [ ] Generated a **Client secret** and stored it (GitHub only shows it once). #### Server - [ ] Added `githubClientId` and `githubClientSecret` to `config/passwords.yaml` under the matching environment (`development:` for local, `production:` for prod), or set the matching `SERVERPOD_PASSWORD_githubClientId` and `SERVERPOD_PASSWORD_githubClientSecret` environment variables. - [ ] Added `GitHubIdpConfigFromPasswords()` to `identityProviderBuilders` in `server.dart`. - [ ] Created a `GitHubIdpEndpoint` file in `lib/src/auth/`. - [ ] Started the server with `serverpod start`, then created and applied the migration (pressed **M**). #### Client - [ ] Added `client.auth.initializeGitHubSignIn(clientId: ..., redirectUri: ...)` after `client.auth.initialize()` inside `initializeClient()` in your Flutter app's `lib/client.dart`. - [ ] Both `clientId` and `redirectUri` match values registered on the GitHub App. - [ ] On **Android**, added the `flutter_web_auth_2` `CallbackActivity` to `AndroidManifest.xml` with the **exact** scheme used in your callback URL. - [ ] On **Web (Serverpod-hosted Flutter)**, registered `FlutterWebAuth2CallbackRoute` via `pod.webServer.addRoute(...)` in `server.dart` before `pod.start()`. On **Web (separately-hosted Flutter)**, created `web/auth.html` in your Flutter project. See [Web](https://docs.serverpod.dev/concepts/authentication/providers/github/setup.md#web) for both flows. - [ ] On **web**, ran Flutter on a fixed `--web-port` matching the port registered in the GitHub App's callback URL. ## Sign-in fails with redirect\_uri\_mismatch **Problem:** The OAuth flow fails with a `redirect_uri_mismatch` error, or GitHub shows a "The redirect\_uri MUST match the registered callback URL for this application" error page. **Cause:** The `redirectUri` your Flutter app sent to GitHub does not exactly match any of the **Callback URL** entries on your GitHub App. **Resolution:** Open your GitHub App's settings and verify the **Callback URL** entries match your app's `redirectUri` exactly. The match is strict: scheme, host, port, path, casing, and trailing slashes all count. Common mistakes: - Trailing slashes (`https://your-domain.com/auth/callback/`) or port differences. - Wrong scheme (`http` vs `https`, or mismatched custom scheme like `myapp:` vs `MyApp:`). - For Serverpod-hosted Flutter web, forgetting to register `FlutterWebAuth2CallbackRoute` via `pod.webServer.addRoute(...)` so the `/auth/callback` route is never registered. Hitting the URL returns 404. - For separately-hosted Flutter web, missing the `auth.html` file in the Flutter project's `web/` folder. - Flutter dev server running on a random port. Pass `--web-port=` to `flutter run` so the origin is stable across restarts. ## Callback never returns to the Flutter app **Problem:** The user authorizes the app on GitHub successfully, but the Flutter app never receives the result. The browser sits on a blank page or the OAuth window hangs. **Cause:** The browser was redirected to a URL that does not serve the callback page (web), or the callback custom scheme is not registered with the platform (mobile). **Resolution:** - **Web (Serverpod-hosted Flutter)**: Confirm `FlutterWebAuth2CallbackRoute` is registered on `pod.webServer` and the provider redirects to that route's URL. - **Web (separately-hosted Flutter)**: Confirm `web/auth.html` exists in your Flutter project and contains the callback script from [Web callback page (`auth.html`)](https://docs.serverpod.dev/concepts/authentication/setup.md#web-callback-page-authhtml). - **Android**: Verify the `` value in `AndroidManifest.xml` matches the scheme in your callback URL exactly. - **iOS / macOS**: Universal Links require HTTPS callback URLs and associated-domain entitlements. Standard custom-scheme callbacks work without extra configuration. ## Sign-in succeeds but the user has no email **Problem:** The user signs in successfully in the app, but the server-side `GitHubAccountDetails.email` value is `null`. **Cause:** GitHub users can keep their email private, and the OAuth response will return `null` for `email` in that case. Your app may have a custom validator that rejects accounts without an email and blocks the sign-in. **Resolution:** - If you do not strictly need an email, relax your validator. The default validator only checks that `userIdentifier` is non-empty, which works for private-email users. See [Custom account validation](https://docs.serverpod.dev/concepts/authentication/providers/github/customizations.md#custom-account-validation). - If you do need an email, confirm your GitHub App requests the **Account permissions > Email addresses** permission as **Read-only**. Even with permission, GitHub returns `null` if the user does not have a verified primary email. - Surface a helpful message to the user instead of failing silently. ## clientId or redirectUri missing at initialization **Problem:** The app throws on startup with an error about `clientId` or `redirectUri` being missing or empty. **Cause:** Unlike Google or Apple, GitHub does not have native platform-specific config files. The `clientId` and `redirectUri` must be passed explicitly to `initializeGitHubSignIn` (or read from `--dart-define`). **Resolution:** Either pass the values directly: ```dart await client.auth.initializeGitHubSignIn( clientId: 'your-github-client-id', redirectUri: 'myapp://auth', ); ``` Or read them from `--dart-define`: ```bash flutter run \ --dart-define=GITHUB_CLIENT_ID=your-github-client-id \ --dart-define=GITHUB_REDIRECT_URI=myapp://auth ``` See [Configuring client IDs on the app](https://docs.serverpod.dev/concepts/authentication/providers/github/customizations.md#configuring-client-ids-on-the-app). ## Sign-in works in dev but fails after deploy **Problem:** GitHub sign-in works locally but fails in production with `redirect_uri_mismatch` or a similar error. **Cause:** The production callback URL is not registered on the GitHub App, or the production Flutter build is using the dev `redirectUri`. **Resolution:** 1. Open your GitHub App's settings and confirm the production callback URL is listed under **Callback URL** alongside the development one. Both should remain registered so dev and prod work simultaneously. 2. Confirm your production Flutter build is initialized with the production `redirectUri`. The simplest way is to read it from `--dart-define` and pass the production value in your CI/CD or `flutter build` step. See [Publishing to production](https://docs.serverpod.dev/concepts/authentication/providers/github/setup.md#publishing-to-production). ## Sign-in works for you but not for other users **Problem:** Sign-in works for your own GitHub account but other users get an error from GitHub when they try to authorize the app. **Cause:** Your GitHub App's **Where can this GitHub App be installed?** is set to **Only on this account**. Users outside your account cannot install or authorize the app. **Resolution:** Open your GitHub App's settings and change **Where can this GitHub App be installed?** to **Any account**. The change takes effect immediately. ## Organization blocks the OAuth authorization **Problem:** A user tries to sign in but sees a GitHub message about the organization restricting access to third-party applications, or the sign-in flow returns with no authorization. **Cause:** The user's GitHub organization has **OAuth App access restrictions** enabled, and your app has not been approved for that organization. This is independent of your app's own settings. The organization controls it. **Resolution:** The user (or an organization owner) needs to request approval for your GitHub App in the organization's **Settings > Third-party Access** page on GitHub. There is nothing you can do server-side to bypass this. Surface a clear error message to the user explaining the org policy. ## Sign-in fails with bad\_verification\_code **Problem:** GitHub returns `bad_verification_code` when your server exchanges the authorization code for a token. **Cause:** The authorization code is single-use and short-lived (10 minutes). This error usually means the same code was sent twice (e.g., page refresh during the callback) or the code expired before the server received it. **Resolution:** - Make sure the OAuth callback fires only once. Refreshing the `auth.html` page or navigating back to it after authorization re-sends the now-spent code. - If the user genuinely took too long to complete sign-in, the code expired. Have them start the flow again from your sign-in button. - This is a transient error if it only happens occasionally. Investigate the app only if it reproduces consistently. ## GitHub API calls from getExtraGitHubInfoCallback fail or rate-limit **Problem:** Calls made inside `getExtraGitHubInfoCallback` start returning `401`, `403`, or `X-RateLimit-Remaining: 0`. **Cause:** Either the access token has been revoked (the user revoked the app's authorization, or **Expire user authorization tokens** caused it to expire), or you are exceeding GitHub's per-user rate limit (5,000 authenticated requests per hour by default). **Resolution:** - Cache the data you fetch instead of calling the API on every sign-in. The `getExtraGitHubInfoCallback` runs on **every** authentication attempt, so fetching the same data every time burns through rate limits quickly. - Serverpod does not keep the GitHub access token after sign-in, so fetch what you need inside `getExtraGitHubInfoCallback` and store the results, rather than re-fetching on every sign-in. - If a single user triggers many sign-ins (e.g., dev iteration), expect to hit the per-user limit. Wait an hour or test with a different account. ## Permission changes on the GitHub App do not take effect **Problem:** You changed permissions on your GitHub App (for example, added **Email addresses** read access), but existing users still see the old behavior or get errors about missing scopes. **Cause:** GitHub does not silently re-grant new permissions to users who authorized the app before the change. Each user must re-authorize the app for the new permissions to apply. **Resolution:** Have affected users sign out and sign in again. GitHub will prompt them to approve the updated permissions. For users who never signed in before the change, the new permissions apply immediately. ## Server crashes on startup with a missing password **Problem:** The server crashes on startup with an error about a missing `githubClientId` or `githubClientSecret` key. **Cause:** The keys are missing from `config/passwords.yaml`, are spelled differently, or are not present under the active environment section. **Resolution:** Confirm both keys exist under the section matching your run mode (`development:` when running locally with `serverpod start`, `production:` when deployed): ```yaml development: githubClientId: 'your-github-client-id' githubClientSecret: 'your-github-client-secret' ``` Quoting the values is a safeguard. YAML parses unquoted values that look like numbers or booleans as those types instead of strings. ## Server crashes on first GitHub sign-in with "no such table" **Problem:** The server builds and starts, but crashes when a user tries to sign in with GitHub. The error cites a missing table such as `serverpod_auth_idp_github_account`. **Cause:** The database migration that creates the provider's tables was never created or applied. **Resolution:** In the running `serverpod start` terminal, press **M** to create and apply the migration. ## Android sign-in opens GitHub but the callback never fires **Problem:** On Android, tapping the GitHub sign-in button opens the GitHub authorization page in a browser, but after authorizing, the browser stays open and the Flutter app never resumes. **Cause:** The `CallbackActivity` in `AndroidManifest.xml` is missing, has a wrong scheme, or `android:exported` is not set to `true`. **Resolution:** Open `android/app/src/main/AndroidManifest.xml` and confirm the `CallbackActivity` block exists with `android:exported="true"` and the `` value matches the scheme in your callback URL exactly. The block is shown in [Android setup](https://docs.serverpod.dev/concepts/authentication/providers/github/setup.md#android). After editing the manifest, run `flutter clean` and rebuild. ## Related - [Setup](https://docs.serverpod.dev/concepts/authentication/providers/github/setup.md): configure GitHub sign-in on the server and in your app. - [Customizations](https://docs.serverpod.dev/concepts/authentication/providers/github/customizations.md): configuration options and sign-in UI customization. - [UI components](https://docs.serverpod.dev/concepts/authentication/ui-components.md): the sign-in widgets and how to compose them. --- # Set up Microsoft sign-in https://docs.serverpod.dev/concepts/authentication/providers/microsoft/setup To set up **Sign in with Microsoft**, you must create an app registration on [Microsoft Entra ID (formerly Azure AD)](https://portal.azure.com/) and configure your Serverpod application accordingly. :::caution Install the authentication module before you continue. See [Setup](https://docs.serverpod.dev/concepts/authentication/setup.md). ::: ## Create your Microsoft Entra ID app 1. Go to [Microsoft Azure Portal](https://portal.azure.com/) and log in with your Microsoft account. 2. Navigate to **Microsoft Entra ID** from the portal menu. ![Register App](https://docs.serverpod.dev/img/authentication/providers/microsoft/1-register-app.png) 3. Go to **App registrations** and click **New registration**. ![New App Registration](https://docs.serverpod.dev/img/authentication/providers/microsoft/2-register-new-app.png) 4. Fill in the required fields: - **Name**: Enter a name for your application (e.g., "MyApp Authentication"). - **Supported account types**: Choose one of the following: - Single tenant only - Default Directory - Multiple Entra ID tenants - Any Entra ID Tenant + Personal Microsoft accounts - Personal accounts only - **Redirect URI (optional)**: Leave this blank for now. We'll add platform-specific URIs later. ![Microsoft App Setup](https://docs.serverpod.dev/img/authentication/providers/microsoft/3-ms.png) 5. Click **Register** to create your app. ## Get app credentials ### Get the client ID After registration, you'll be redirected to the app overview page where you can find your **Application (client) ID**. Copy this value - you'll need it for server configuration. ![Client ID](https://docs.serverpod.dev/img/authentication/providers/microsoft/4-client-id.png) ### Create a client secret 1. In your app's menu, navigate to **Certificates & secrets**. 2. Under **Client secrets**, click **New client secret**. ![Create Client Secret](https://docs.serverpod.dev/img/authentication/providers/microsoft/5-create-client-secret.png) 3. Add a description (e.g., "Serverpod Authentication") and choose an expiration period. 4. Click **Add** and immediately copy the **Value** (not the Secret ID). This is your **Client Secret**. :::warning The client secret value is only shown once. Store it securely immediately after creation. Never commit this value to version control. ::: ### Get the tenant ID (optional) If you're restricting authentication to a specific tenant, you'll need your **Directory (tenant) ID**, which is also shown on the app overview page. For most applications, you can use one of these common values: - `common`: Allows both personal Microsoft accounts and work/school accounts (default). - `organizations`: Allows only work/school accounts. - `consumers`: Allows only personal Microsoft accounts. ### Configure redirect URIs You need to configure redirect URIs for each platform you want to support. 1. In your app's menu, navigate to **Authentication**. 2. Click **Add a platform** to configure platform-specific settings. ![Authentication Redirect](https://docs.serverpod.dev/img/authentication/providers/microsoft/6-authentication-redirect-uri.png) #### Web 1. Select **Web** as the platform. 2. Add your redirect URI, typically in the format: `https://yourdomain.com/auth.html` ![Web Redirect URI](https://docs.serverpod.dev/img/authentication/providers/microsoft/7-web-redirect-uri.png) 3. Click **Configure**. #### iOS and macOS 1. Select **iOS / macOS** as the platform. 2. For the Bundle ID, enter your app's bundle identifier (e.g., `com.yourcompany.yourapp`). ![iOS/macOS Redirect URI](https://docs.serverpod.dev/img/authentication/providers/microsoft/8-ios-macos-redirect-uri.png) 3. Click **Configure**. :::note For iOS/macOS, Microsoft will automatically generate the redirect URI based on your bundle ID. You can also add custom redirect URIs as needed. ::: #### Android 1. Select **Android** as the platform. 2. Enter your Package name (e.g., `com.yourcompany.yourapp`). 3. Enter your Signature hash. You can get this by running: ```bash keytool -exportcert -alias SIGNATURE_ALIAS -keystore PATH_TO_KEYSTORE | openssl sha1 -binary | openssl base64 ``` ![Android Redirect URI](https://docs.serverpod.dev/img/authentication/providers/microsoft/9-android-redirect-uri.png) 4. Click **Configure**. :::tip For development, use your debug keystore signature hash. For production builds, use your release keystore signature hash. You can add both to Microsoft Entra ID during setup. ::: ## Server-side configuration ### Store the credentials Add your Microsoft credentials to the `config/passwords.yaml` file, or set them as environment variables `SERVERPOD_PASSWORD_microsoftClientId`, `SERVERPOD_PASSWORD_microsoftClientSecret`, and `SERVERPOD_PASSWORD_microsoftTenant`. ```yaml development: microsoftClientId: 'YOUR_MICROSOFT_CLIENT_ID' microsoftClientSecret: 'YOUR_MICROSOFT_CLIENT_SECRET' microsoftTenant: 'common' # or 'organizations', 'consumers', or your specific tenant ID ``` :::warning Keep your Client Secret confidential. Never commit this value to version control. Store it securely using environment variables or secret management. ::: ### Configure the Microsoft identity provider In your main `server.dart` file, configure the Microsoft identity provider: ```dart import 'package:serverpod_auth_idp_server/core.dart'; import 'package:serverpod_auth_idp_server/providers/microsoft.dart'; import 'src/generated/serverpod.dart'; void run(List args) async { final pod = Serverpod(args); pod.initializeAuthServices( tokenManagerBuilders: [ JwtConfigFromPasswords(), ], identityProviderBuilders: [ MicrosoftIdpConfig( clientId: pod.getPassword('microsoftClientId')!, clientSecret: pod.getPassword('microsoftClientSecret')!, tenant: pod.getPassword('microsoftTenant') ?? 'common', ), ], ); await pod.start(); } ``` :::tip You can use `MicrosoftIdpConfigFromPasswords()` to automatically load credentials from `config/passwords.yaml` or the `SERVERPOD_PASSWORD_microsoftClientId`, `SERVERPOD_PASSWORD_microsoftClientSecret`, and `SERVERPOD_PASSWORD_microsoftTenant` environment variables: ```dart identityProviderBuilders: [ MicrosoftIdpConfigFromPasswords(), ], ``` ::: ### Expose the endpoint Create an endpoint that extends `MicrosoftIdpBaseEndpoint` to expose the Microsoft authentication API: ```dart import 'package:serverpod_auth_idp_server/providers/microsoft.dart'; class MicrosoftIdpEndpoint extends MicrosoftIdpBaseEndpoint {} ``` ### Generate and migrate Finally, start the server with `serverpod start` to generate the client code, then create and apply the migration that initializes the database for the provider (in the `serverpod start` terminal, press **M**). More detailed instructions can be found in the general [identity providers setup section](https://docs.serverpod.dev/concepts/authentication/setup.md#identity-providers-configuration). ### Basic configuration options - `clientId`: Required. The Application (client) ID of your Microsoft Entra ID app. - `clientSecret`: Required. The Client Secret generated for your Microsoft Entra ID app. - `tenant`: Optional. Defaults to `'common'`. Can be `'common'`, `'organizations'`, `'consumers'`, or a specific tenant ID. For more details on configuration options, see the [customizations page](https://docs.serverpod.dev/concepts/authentication/providers/microsoft/customizations.md). ## Client-side configuration Add the `serverpod_auth_idp_flutter` package to your Flutter app. The Microsoft provider uses [`flutter_web_auth_2`](https://pub.dev/packages/flutter_web_auth_2) to handle the OAuth2 flow, so any documentation there should also apply to this setup. ### iOS and macOS There is no special configuration needed for iOS and macOS for "normal" authentication flows. However, if you are using **Universal Links** on iOS, they require redirect URIs to use **https**. Follow the instructions in the [flutter\_web\_auth\_2](https://pub.dev/packages/flutter_web_auth_2#ios) documentation. ### Android In order to capture the callback URL, add the following activity to your `AndroidManifest.xml`. Replace `YOUR_CALLBACK_URL_SCHEME_HERE` and `YOUR_CALLBACK_URL_HOST_HERE` with your actual callback URL scheme and host registered in your Microsoft Entra ID app. ```xml ``` ### Web Web sign-in needs the shared callback page that hands the OAuth2 result back to your app. Set it up once as described in [Web callback page (`auth.html`)](https://docs.serverpod.dev/concepts/authentication/setup.md#web-callback-page-authhtml), and point your redirect URI at it, for example `https://yourdomain.com/auth.html`. The same page serves every provider that uses the OAuth2 flow. ## Present the authentication UI ### Initializing the `MicrosoftSignInService` Before presenting any sign-in UI, initialize the Microsoft sign-in service. This step is necessary to configure the service with your Microsoft app credentials. ```dart await client.auth.initializeMicrosoftSignIn( clientId: 'YOUR_MICROSOFT_CLIENT_ID', redirectUri: 'YOUR_REGISTERED_REDIRECT_URI', ); ``` :::info For more information on configuration options and environment variables, see the [customizations page](https://docs.serverpod.dev/concepts/authentication/providers/microsoft/customizations.md). ::: ### Using the `MicrosoftSignInWidget` If you have configured the `SignInWidget` as described in the [setup section](https://docs.serverpod.dev/concepts/authentication/setup.md#present-the-authentication-ui), the Microsoft identity provider will be automatically detected and displayed in the sign-in widget. You can also use the `MicrosoftSignInWidget` to include the Microsoft authentication flow in your own custom UI. ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; MicrosoftSignInWidget( client: client, onAuthenticated: () { // Do something when the user is authenticated. // // NOTE: You should not navigate to the home screen here, otherwise // the user will have to sign in again every time they open the app. }, onError: (error) { // Handle errors ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: $error')), ); }, ) ``` The widget automatically handles: - Microsoft sign-in flow for iOS, Android, web, and macOS. - OAuth2 authentication flow. - Token management. - Underlying OAuth2 package error handling. For details on how to customize the Microsoft sign-in UI in your Flutter app, see the [customizations page](https://docs.serverpod.dev/concepts/authentication/providers/microsoft/customizations.md#customize-the-sign-in-button). --- # Customize Microsoft sign-in https://docs.serverpod.dev/concepts/authentication/providers/microsoft/customizations This page covers configuration options for the Microsoft identity provider beyond the basic setup, on both the server and the app. It also shows how to customize the sign-in UI. You can use the `MicrosoftSignInWidget` to display the Microsoft sign-in flow in your own custom UI, or the `MicrosoftAuthController` to build a completely custom authentication interface. ## Server configuration These options are set on the `MicrosoftIdpConfig` in your server code. ### Configuration options Below is a non-exhaustive list of some of the most common configuration options. For more details on all options, check the `MicrosoftIdpConfig` in-code documentation. #### Tenant configuration The `tenant` parameter determines which accounts can sign in to your application: - `'common'` (default) - Allows both personal Microsoft accounts and work/school accounts. - `'organizations'` - Allows only work/school accounts (any organization). - `'consumers'` - Allows only personal Microsoft accounts. - A specific tenant ID - Restricts to accounts from a specific Microsoft Entra ID tenant. ```dart final microsoftIdpConfig = MicrosoftIdpConfig( clientId: pod.getPassword('microsoftClientId')!, clientSecret: pod.getPassword('microsoftClientSecret')!, tenant: 'organizations', // Only allow work/school accounts ); ``` :::tip Use `'common'` for the widest user base. Use a specific tenant ID when building internal applications for a single organization. ::: #### Custom account validation You can customize the validation for Microsoft account details before allowing sign-in. By default, the validation checks that the received account details contain a non-empty userIdentifier. ```dart final microsoftIdpConfig = MicrosoftIdpConfigFromPasswords( // Optional: Custom validation for Microsoft account details microsoftAccountDetailsValidation: (MicrosoftAccountDetails accountDetails) { // Throw an exception if account doesn't meet custom requirements if (accountDetails.userIdentifier.isEmpty) { throw MicrosoftUserInfoMissingDataException(); } // Example: Require email to be present if (accountDetails.email == null || accountDetails.email!.isEmpty) { throw MicrosoftUserInfoMissingDataException(); } }, ); ``` :::note Users may choose not to share their email or other information during the Microsoft login flow. Adjust your validation function carefully to avoid blocking legitimate users. ::: #### MicrosoftAccountDetails The `microsoftAccountDetailsValidation` callback receives a `MicrosoftAccountDetails` record with the following properties: | Property | Type | Description | | ---------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `userIdentifier` | `String` | The Microsoft user's unique identifier (Object ID) | | `email` | `String?` | The user's email address (may be null) | | `name` | `String?` | The user's display name from Microsoft | | `imageBytes` | `Uint8List?` | The user's profile photo. Always `null` during validation, because the photo is fetched afterwards, and only when the `fetchProfilePhoto` option on `MicrosoftIdpConfig` is enabled (the default) | Example of accessing these properties: ```dart microsoftAccountDetailsValidation: (accountDetails) { print('Microsoft Object ID: ${accountDetails.userIdentifier}'); print('Email: ${accountDetails.email}'); print('Display name: ${accountDetails.name}'); // imageBytes is always null here. The photo is fetched after validation. // Custom validation logic if (accountDetails.email == null) { throw MicrosoftUserInfoMissingDataException(); } }, ``` :::info The properties available depend on the scopes requested and what the user consented to share. ::: #### Accessing Microsoft APIs on the server :::caution The `getExtraMicrosoftInfoCallback` below runs on **every** sign-in, not only the first. Cache what you fetch, and guard external calls with `try`/`catch` so a provider outage does not block sign-in. ::: On the server side, you can access Microsoft APIs using the access token. The `getExtraMicrosoftInfoCallback` in `MicrosoftIdpConfig` receives the access token and can be used to call Microsoft Graph APIs: ```dart import 'package:http/http.dart' as http; final microsoftIdpConfig = MicrosoftIdpConfigFromPasswords( // Optional: Extract additional info from Microsoft Graph APIs getExtraMicrosoftInfoCallback: (session, { required accountDetails, required accessToken, required transaction, }) async { // Use accessToken to call Microsoft Graph APIs and store additional info // Example: Access user's calendar final response = await http.get( Uri.https('graph.microsoft.com', '/v1.0/me/calendar'), headers: {'Authorization': 'Bearer $accessToken'}, ); // Process response and store additional info in the database }, ); ``` ### Reacting to account creation You can use the `onAfterMicrosoftAccountCreated` callback to run logic after a new Microsoft account has been created and linked to an auth user. This callback is only invoked for new accounts, not for returning users. This callback is complementary to the [core `onAfterAuthUserCreated` callback](https://docs.serverpod.dev/concepts/authentication/working-with-users.md#reacting-to-the-user-created-event). Use it for side effects specific to a Microsoft login, like storing analytics, sending a welcome email, or storing additional data. ```dart final microsoftIdpConfig = MicrosoftIdpConfigFromPasswords( onAfterMicrosoftAccountCreated: ( session, authUser, microsoftAccount, { required transaction, }) async { // e.g. store additional data, send a welcome email, or log for analytics }, ); ``` :::info This callback runs inside the same database transaction as the account creation. Throwing an exception inside this callback will abort the process. If you perform external side-effects, make sure to safeguard them with a try/catch to prevent unwanted failures. ::: :::caution Scopes you assign here with `AuthServices.instance.authUsers.update()` do not apply to the login that is already in progress, because token issuance uses the scopes loaded before this callback runs. They take effect the next time the user signs in. To assign scopes at creation time instead, use `onBeforeAuthUserCreated` together with `getExtraMicrosoftInfoCallback`, which runs before the auth user is created. ::: ## App configuration These options configure Microsoft sign-in in your Flutter app. ### Requesting additional Microsoft scopes The default setup allows access to basic user information, such as `name`, `email`. You may require additional access scopes to access other Microsoft APIs, such as accessing a user's calendar, mail, or OneDrive files. The default scopes requested are: - `openid`: Required for OpenID Connect authentication. - `profile`: Access to user's basic profile information. - `email`: Access to user's email address. - `offline_access`: Allows refresh tokens for long-lived sessions. - `https://graph.microsoft.com/User.Read`: Access to user's Microsoft Graph profile. To request additional scopes, you will need to: - Ensure the required API permissions are configured in your Microsoft Entra ID app registration (navigate to **API permissions** in the [Azure Portal](https://portal.azure.com/)). - Request access to the scopes when signing in. Do this by setting the `scopes` parameter of the `MicrosoftSignInWidget` or `MicrosoftAuthController`. A full list of available scopes and Microsoft Graph API permissions can be found in the [Microsoft Graph permissions reference](https://learn.microsoft.com/en-us/graph/permissions-reference). :::info Adding additional scopes may require admin consent depending on your tenant configuration and the sensitivity of the requested permissions. ::: To use the granted scopes from the server with the access token, see [Accessing Microsoft APIs on the server](#accessing-microsoft-apis-on-the-server). ### Configuring client IDs on the app #### Passing client IDs in code You can pass the `clientId`, `redirectUri`, and `tenant` directly when initializing the Microsoft sign-in service: ```dart await client.auth.initializeMicrosoftSignIn( clientId: 'YOUR_MICROSOFT_CLIENT_ID', redirectUri: 'yourapp://auth', tenant: 'common', // Optional, defaults to 'common' ); ``` This approach is useful when you need different client IDs per platform and want to manage them in your Dart code. #### Using environment variables Alternatively, you can pass client configuration during build time using the `--dart-define` option. The Microsoft sign-in provider supports the following environment variables: - `MICROSOFT_CLIENT_ID`: Your Microsoft Application (client) ID - `MICROSOFT_REDIRECT_URI`: The callback URI **Example usage:** ```bash flutter run -d \ --dart-define="MICROSOFT_CLIENT_ID=your_client_id" \ --dart-define="MICROSOFT_REDIRECT_URI=msauth://auth" ``` The tenant has no environment variable. Pass it as an argument when you initialize Microsoft sign-in. This approach is useful when you need to: - Manage separate client IDs for different platforms (Android, iOS, web, macOS) in a centralized way - Avoid committing client IDs to version control - Configure different credentials for different build environments (development, staging, production) :::tip You can also set these environment variables in your IDE's run configuration or CI/CD pipeline to avoid passing them manually each time. ::: ## Customize the sign-in button Inside `SignInWidget`, fields set on its `buttonStyle` take precedence over the settings below, as described in [Styling the buttons](https://docs.serverpod.dev/concepts/authentication/ui-components.md#styling-the-buttons). :::info The `SignInWidget` uses the `MicrosoftSignInWidget` internally to display the Microsoft sign-in flow. You can also supply a custom `MicrosoftSignInWidget` to the `SignInWidget` to override the default behavior. ```dart SignInWidget( client: client, microsoftSignInWidget: MicrosoftSignInWidget( client: client, // Shape and label survive inside SignInWidget, unless its buttonStyle // sets them. Brand colors do not. shape: SignInButtonShape.rounded, text: SignInButtonTextVariant.signInWith, // A custom widget replaces the built-in handling, so pass your own callbacks. onAuthenticated: () { /* ... */ }, onError: (error) { /* ... */ }, ), ) ``` ::: ### Using the `MicrosoftSignInWidget` The `MicrosoftSignInWidget` handles the complete Microsoft sign-in flow for your Flutter app. You can customize the widget's appearance and behavior: ```dart MicrosoftSignInWidget( client: client, // Button customization. The values shown are the defaults. style: MicrosoftButtonStyle.light, // or dark size: SignInButtonSize.large, // or medium, small text: SignInButtonTextVariant.continueWith, // or signInWith, signUpWith, signIn shape: SignInButtonShape.pill, // or rounded, rectangular logoAlignment: SignInButtonLogoAlignment.center, // or left minimumWidth: 240, // at most 400 textStyle: null, // TextStyle for the label // Scopes to request from Microsoft // These are the default scopes. scopes: const [ 'openid', 'profile', 'email', 'offline_access', 'https://graph.microsoft.com/User.Read', ], onAuthenticated: () { // Do something when the user is authenticated. // // NOTE: You should not navigate to the home screen here, otherwise // the user will have to sign in again every time they open the app. }, onError: (error) { // Handle errors }, ) ``` ## Build a custom UI with MicrosoftAuthController For more control over the UI, you can use the `MicrosoftAuthController` class, which provides all the authentication logic without any UI components. This allows you to build a completely custom authentication interface. ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; final controller = MicrosoftAuthController( client: client, onAuthenticated: () { // Do something when the user is authenticated. // // NOTE: You should not navigate to the home screen here, otherwise // the user will have to sign in again every time they open the app. }, onError: (error) { // Handle errors }, scopes: const [ 'openid', 'profile', 'email', 'offline_access', 'https://graph.microsoft.com/User.Read', ], ); // Initiate sign-in await controller.signIn(); ``` ### MicrosoftAuthController state management Your widget should render the appropriate UI based on the `state` property of the controller. You can also use the below state properties to build your UI: ```dart // Check current state final state = controller.state; // MicrosoftAuthState enum // Check if loading final isLoading = controller.isLoading; // Check if authenticated final isAuthenticated = controller.isAuthenticated; // Get error message final errorMessage = controller.errorMessage; // Listen to state changes controller.addListener(() { setState(() { // Rebuild UI when controller state changes }); }); ``` #### MicrosoftAuthController states - `MicrosoftAuthState.idle` - Ready for user interaction. - `MicrosoftAuthState.loading` - Processing a sign-in request. - `MicrosoftAuthState.error` - An error occurred. - `MicrosoftAuthState.authenticated` - Authentication was successful. ## Related - [Setup](https://docs.serverpod.dev/concepts/authentication/providers/microsoft/setup.md): set up Microsoft sign-in on the server and in your app. - [Troubleshooting](https://docs.serverpod.dev/concepts/authentication/providers/microsoft/troubleshooting.md): fix common Microsoft sign-in errors. - [UI components](https://docs.serverpod.dev/concepts/authentication/ui-components.md): use and style the built-in sign-in UI. - [Working with users](https://docs.serverpod.dev/concepts/authentication/working-with-users.md): manage auth users and react to user events. --- # Troubleshoot Microsoft sign-in https://docs.serverpod.dev/concepts/authentication/providers/microsoft/troubleshooting This page helps you identify common Sign in with Microsoft failures, explains why they occur, and shows how to resolve them. For underlying issues with the OAuth callback library, see the [flutter\_web\_auth\_2 documentation](https://pub.dev/packages/flutter_web_auth_2). ## Setup checklist Go through this before investigating a specific error. Most problems come from a missed step. #### Microsoft Entra ID portal - [ ] Created an app registration in [Microsoft Entra ID](https://portal.azure.com/). - [ ] Chose **Supported account types** that cover everyone who should sign in, and that match your `tenant` setting. - [ ] Registered every redirect URI you actually use under **Authentication**, each under the right platform: **Web** for `https://your-domain.com/auth.html`, **iOS / macOS** with your bundle ID, and **Android** with your package name and signature hash. - [ ] Created a client secret and copied its **Value** (not the Secret ID). Microsoft only shows it once. #### Server - [ ] Added `microsoftClientId` and `microsoftClientSecret` to `config/passwords.yaml` under the matching environment (`development:` for local, `production:` for prod), or set the matching `SERVERPOD_PASSWORD_microsoftClientId` and `SERVERPOD_PASSWORD_microsoftClientSecret` environment variables. The `microsoftTenant` key is optional and defaults to `common`. - [ ] Added `MicrosoftIdpConfigFromPasswords()` to `identityProviderBuilders` in `server.dart`. - [ ] Created an endpoint that extends `MicrosoftIdpBaseEndpoint`. - [ ] Started the server with `serverpod start`, then created and applied the migration (pressed **M**). #### Flutter app - [ ] Added `client.auth.initializeMicrosoftSignIn(clientId: ..., redirectUri: ...)` after `client.auth.initialize()` inside `initializeClient()` in your Flutter app's `lib/client.dart`. - [ ] Both `clientId` and `redirectUri` match values registered on the app registration. - [ ] The `tenant` passed on initialization matches the server's `tenant` setting. Both default to `common`. - [ ] On **Android**, added the `flutter_web_auth_2` `CallbackActivity` to `AndroidManifest.xml` with the **exact** scheme and host used in your callback URL. - [ ] On **Web**, created `web/auth.html` in your Flutter project with the callback script from [Web callback page (`auth.html`)](https://docs.serverpod.dev/concepts/authentication/setup.md#web-callback-page-authhtml). - [ ] On **web**, ran Flutter on a fixed `--web-port` matching the port in the registered redirect URI. ## Sign-in fails with a redirect URI error **Problem:** The browser opens the Microsoft sign-in page, but instead of completing, Microsoft shows an error page saying the redirect URI is not valid for the application. The app never receives a callback. **Cause:** The `redirectUri` your Flutter app sent to Microsoft does not exactly match any redirect URI registered on your app registration, or the URI is registered under the wrong platform. **Resolution:** Open your app registration's **Authentication** page and verify the registered redirect URIs match your app's `redirectUri` exactly. The match is strict: scheme, host, port, path, casing, and trailing slashes all count. Common mistakes: - Trailing slashes (`https://your-domain.com/auth.html/`) or port differences. - Wrong scheme (`http` vs `https`, or a mismatched custom scheme like `myapp:` vs `MyApp:`). - The URI registered under the wrong platform, for example a web URL added under a native platform. - Flutter dev server running on a random port. Pass `--web-port=` to `flutter run` so the origin is stable across restarts. ## Callback never returns to the Flutter app **Problem:** The user signs in on the Microsoft page successfully, but the Flutter app never receives the result. The browser sits on a blank page or the sign-in window hangs. **Cause:** The browser was redirected to a URL that does not serve the callback page (web), or the callback custom scheme is not registered with the platform (mobile). **Resolution:** - **Web**: Confirm `web/auth.html` exists in your Flutter project and contains the callback script from [Web callback page (`auth.html`)](https://docs.serverpod.dev/concepts/authentication/setup.md#web-callback-page-authhtml). The page posts the result back to its own origin, so your Flutter web app must be served from the same scheme, host, and port as the callback URL. - **Android**: Verify the `` values in `AndroidManifest.xml` match the scheme and host in your callback URL exactly. - **iOS / macOS**: Universal Links require HTTPS callback URLs and associated-domain entitlements. Standard custom-scheme callbacks work without extra configuration. ## Sign-in fails with an access token verification error **Problem:** The sign-in flow completes on the Microsoft page, but the app then reports "An error occurred while verifying the Microsoft access token. Please check your Microsoft account and try again. If the problem persists, please contact support." **Cause:** The server threw a `MicrosoftAccessTokenVerificationException`. This exception is deliberately generic so it does not leak details to potential attackers. It covers every server-side failure between receiving the authorization code and validating the account: - The token exchange with Microsoft was rejected. Typical reasons are a wrong or expired client secret (Microsoft Entra ID secrets always have an expiry date), a `tenant` mismatch between the app and the server, or a reused or expired authorization code. - The user info request to Microsoft Graph failed, usually because custom scopes dropped `https://graph.microsoft.com/User.Read`. See [Sign-in breaks after changing the scopes parameter](#sign-in-breaks-after-changing-the-scopes-parameter). - A custom `microsoftAccountDetailsValidation` callback threw. See [Custom account validation](https://docs.serverpod.dev/concepts/authentication/providers/microsoft/customizations.md#custom-account-validation). - A `getExtraMicrosoftInfoCallback` threw. See [Calls from getExtraMicrosoftInfoCallback fail or block sign-in](#calls-from-getextramicrosoftinfocallback-fail-or-block-sign-in). **Resolution:** The server logs the underlying cause at debug level for token exchange, user info, and callback failures. A throwing validation callback surfaces only as a generic invalid-user-info error. Check the server logs with debug logging visible, then work through the matching cause above. If the log shows a token exchange error, verify the client secret is current and that the `tenant` values on the server and in `initializeMicrosoftSignIn` are the same. ## clientId or redirectUri missing at initialization **Problem:** The app throws an `ArgumentError` on startup saying the Microsoft client ID or redirect URI is required. **Cause:** Microsoft has no native platform-specific config files. The `clientId` and `redirectUri` must be passed explicitly to `initializeMicrosoftSignIn`, or read from `--dart-define`. **Resolution:** Either pass the values directly: ```dart await client.auth.initializeMicrosoftSignIn( clientId: 'your-microsoft-client-id', redirectUri: 'myapp://auth', ); ``` Or read them from `--dart-define`: ```bash flutter run \ --dart-define=MICROSOFT_CLIENT_ID=your-microsoft-client-id \ --dart-define=MICROSOFT_REDIRECT_URI=myapp://auth ``` The tenant has no environment variable. Pass it as an argument when you initialize Microsoft sign-in. See [Configuring client IDs on the app](https://docs.serverpod.dev/concepts/authentication/providers/microsoft/customizations.md#configuring-client-ids-on-the-app). ## The sign-in button does nothing and onError never fires **Problem:** Tapping the Microsoft sign-in button appears to do nothing, or the flow opens and closes, and the `onError` callback is never called. **Cause:** The `onError` callback only receives errors that are safe to show to the user. Configuration mistakes and flow interruptions are not passed to it: - A `StateError` because `initializeMicrosoftSignIn` was never called before the button was used. - The user cancelled the sign-in window. - The OAuth flow failed before reaching the server, for example when Microsoft returned an error on the callback. **Resolution:** Check the debug console. The controller prints every failure as `[MicrosoftAuthController] Authentication error: ...` before deciding whether to surface it. If the log shows a `StateError`, move `initializeMicrosoftSignIn` so it runs during app startup, before any sign-in UI is shown. ## Changed initialization values do not take effect **Problem:** You changed the `clientId`, `redirectUri`, or `tenant` passed to `initializeMicrosoftSignIn`, but the app keeps using the old values. **Cause:** Initialization is idempotent. Only the first call in the app's lifetime stores the configuration, and later calls return without changing it. A hot reload keeps the old configuration alive. **Resolution:** After changing any initialization value, do a hot restart or fully restart the app. Either one re-runs initialization with the new values. ## Sign-in breaks after changing the scopes parameter **Problem:** Sign-in worked, then you set the `scopes` parameter on `MicrosoftSignInWidget` or `MicrosoftAuthController` to request extra permissions, and now every sign-in fails with the access token verification error. **Cause:** The `scopes` parameter replaces the default scopes instead of adding to them. The server fetches the user's details from Microsoft Graph during sign-in, which requires the `https://graph.microsoft.com/User.Read` scope. Dropping it breaks the account details fetch, and dropping the OpenID Connect scopes breaks the sign-in itself. **Resolution:** Include the defaults alongside your extra scopes: ```dart MicrosoftSignInWidget( client: client, scopes: [ ...MicrosoftAuthController.defaultScopes, 'https://graph.microsoft.com/Calendars.Read', ], ) ``` See [Requesting additional Microsoft scopes](https://docs.serverpod.dev/concepts/authentication/providers/microsoft/customizations.md#requesting-additional-microsoft-scopes) for the default scope list. ## Users see a "Need admin approval" screen **Problem:** A user signs in and Microsoft shows a screen saying the app needs admin approval instead of completing the flow. **Cause:** One of the requested scopes requires admin consent in the user's organization. Work and school tenants can require an administrator to approve permissions before any user can grant them. **Resolution:** Ask an administrator of that tenant to grant consent for the app's permissions, under **API permissions** in the app registration. Alternatively, remove the scope that triggers the requirement if you do not strictly need it. The default scopes normally do not require admin consent. ## Some account types cannot sign in **Problem:** Sign-in works for some users, but others get a Microsoft error page saying their account cannot be used with this application. **Cause:** Two settings restrict which accounts can sign in, and both must allow the user: - The **Supported account types** choice on the app registration. - The `tenant` value in your configuration. Use `common` for personal and work/school accounts, `organizations` for work/school accounts only, `consumers` for personal accounts only, or a tenant ID for a single organization. **Resolution:** Align both settings with your audience. Set the `tenant` in the server configuration and pass the same value to `initializeMicrosoftSignIn` in the Flutter app. Update **Supported account types** on the app registration to match. See [Tenant configuration](https://docs.serverpod.dev/concepts/authentication/providers/microsoft/customizations.md#tenant-configuration). ## Sign-in works on mobile but fails on web, or the reverse **Problem:** Microsoft sign-in works on Android and iOS, but fails on web with the access token verification error, or the other way around. **Cause:** Microsoft requires the client secret during token exchange for web apps and rejects it for native apps. Serverpod handles this through the `isWebPlatform` flag on the login endpoint, which the provided widgets pass automatically. A platform-specific failure usually means the redirect URI is registered under the wrong platform on the app registration, so Microsoft applies the wrong rules to the exchange. **Resolution:** On the app registration's **Authentication** page, confirm web redirect URIs sit under the **Web** platform and native ones under the **iOS / macOS** or **Android** platforms. If you built a custom flow that calls the login endpoint directly, pass `isWebPlatform: true` on web and `false` everywhere else. ## Sign-in succeeds but the user has no email, or the wrong one **Problem:** The user signs in successfully, but the server-side `MicrosoftAccountDetails.email` value is `null`, or the stored email is not an address the user recognizes. **Cause:** Microsoft Graph does not always return a mail address. The provider reads the `mail` field first and falls back to `userPrincipalName`, then stores the result in lowercase. The `mail` field can be empty for accounts without a mailbox, and `userPrincipalName` is a sign-in identifier that is not always a real address, especially for guest accounts. A custom validator that requires an email will block these users, and the app then shows the access token verification error. **Resolution:** - If you do not strictly need an email, relax your validator. The default validator only checks that `userIdentifier` is non-empty. See [Custom account validation](https://docs.serverpod.dev/concepts/authentication/providers/microsoft/customizations.md#custom-account-validation). - Treat the stored email as informational rather than as a verified mailbox, or collect an email in your own onboarding flow when you need a reliable one. ## Users have no profile photo **Problem:** Users sign in successfully, but their profiles have no photo. **Cause:** The provider fetches the photo from Microsoft Graph when `fetchProfilePhoto` on `MicrosoftIdpConfig` is enabled, which is the default. A failed photo fetch never fails the sign-in. It is logged and skipped. Microsoft also does not return a photo for every account. For returning users, the photo is only set when the profile does not already have one. **Resolution:** Confirm `fetchProfilePhoto` is not set to `false` if you expect photos. Accept that some accounts have none. If sign-in speed matters more than photos, disable the option. See [Profile photos](https://docs.serverpod.dev/concepts/authentication/profile-photos.md) for how photos are stored. ## Calls from getExtraMicrosoftInfoCallback fail or block sign-in **Problem:** After adding a `getExtraMicrosoftInfoCallback`, sign-ins start failing with the access token verification error, or external API calls inside the callback fail intermittently. **Cause:** The callback runs on **every** authentication attempt, before the system determines whether the user is new or returning. Any exception it throws aborts the whole sign-in. **Resolution:** - Wrap external calls in `try`/`catch` so a Microsoft Graph outage or rate limit does not block sign-in. - Cache what you fetch in your own tables, keyed by `MicrosoftAccountDetails.userIdentifier`, instead of re-fetching on every sign-in. - Do not create `MicrosoftAccount`, `UserProfile`, or `AuthUser` records inside the callback. That breaks new account detection and profile creation. See [Accessing Microsoft APIs on the server](https://docs.serverpod.dev/concepts/authentication/providers/microsoft/customizations.md#accessing-microsoft-apis-on-the-server). ## Sign-in works in dev but fails after deploy **Problem:** Microsoft sign-in works locally but fails in production with a redirect URI error or the access token verification error. **Cause:** The production redirect URI is not registered on the app registration, or the production Flutter build is using the dev `redirectUri`, or the production server has no Microsoft credentials configured. **Resolution:** 1. Confirm the production redirect URI is registered on the app registration alongside the development one. Both should remain registered so dev and prod work simultaneously. 2. Confirm your production Flutter build is initialized with the production `redirectUri`. The simplest way is to read it from `--dart-define` and pass the production value in your CI/CD or `flutter build` step. See [Using environment variables](https://docs.serverpod.dev/concepts/authentication/providers/microsoft/customizations.md#using-environment-variables). 3. Confirm the production environment provides `microsoftClientId` and `microsoftClientSecret`, in `config/passwords.yaml` under `production:` or through the `SERVERPOD_PASSWORD_` environment variables. ## Server fails to start with a missing password error **Problem:** The server crashes on startup with a `PasswordNotFoundException` naming `microsoftClientId` or `microsoftClientSecret`. **Cause:** You use `MicrosoftIdpConfigFromPasswords()`, and the named key is missing from `config/passwords.yaml` and from the environment. The exception message names the exact key and the environment variable it also looked for. **Resolution:** Confirm both keys exist under the section matching your run mode (`development:` when running locally with `serverpod start`, `production:` when deployed): ```yaml development: microsoftClientId: 'your-microsoft-client-id' microsoftClientSecret: 'your-microsoft-client-secret' microsoftTenant: 'common' # optional, defaults to common ``` Quoting the values is a safeguard. YAML parses unquoted values that look like numbers or booleans as those types instead of strings. ## Server crashes on first Microsoft sign-in with "no such table" **Problem:** The server builds and starts, but crashes when a user tries to sign in with Microsoft. The error cites a missing table such as `serverpod_auth_idp_microsoft_account`. **Cause:** The database migration that creates the provider's tables was never created or applied. **Resolution:** In the running `serverpod start` terminal, press **M** to create and apply the migration. ## Android sign-in opens Microsoft but the callback never fires **Problem:** On Android, tapping the Microsoft sign-in button opens the Microsoft authorization page in a browser, but after authorizing, the browser stays open and the Flutter app never resumes. **Cause:** The `CallbackActivity` in `AndroidManifest.xml` is missing, has a wrong scheme or host, or `android:exported` is not set to `true`. **Resolution:** Open `android/app/src/main/AndroidManifest.xml` and confirm the `CallbackActivity` block exists with `android:exported="true"` and the `` values matching your callback URL exactly. The block is shown in [Android setup](https://docs.serverpod.dev/concepts/authentication/providers/microsoft/setup.md#android). After editing the manifest, run `flutter clean` and rebuild. ## Related - [Setup](https://docs.serverpod.dev/concepts/authentication/providers/microsoft/setup.md): set up Microsoft sign-in on the server and in your app. - [Customizations](https://docs.serverpod.dev/concepts/authentication/providers/microsoft/customizations.md): configuration options and sign-in UI customization. - [UI components](https://docs.serverpod.dev/concepts/authentication/ui-components.md): the sign-in widgets and how to compose them. --- # Set up passkey sign-in https://docs.serverpod.dev/concepts/authentication/providers/passkey/setup Passkeys provide a passwordless authentication method using WebAuthn/FIDO2 standards. They offer a secure, phishing-resistant way for users to sign in using biometric authentication, security keys, or device PINs. :::warning The passkey identity provider is **experimental**. Flutter UI components for passkeys aren't available yet, so build your own UI with the generated client endpoints. ::: :::caution You need to install the auth module before you continue, see [Setup](https://docs.serverpod.dev/concepts/authentication/setup.md). ::: ## Server-side configuration Passkeys require minimal configuration. You will need only the hostname (relying party ID) that will be associated with your passkeys. :::info The hostname should match the domain where your application is hosted. For development, you can use `localhost` or your development domain. ::: ### Configure the Passkey identity provider In your main `server.dart` file, configure the Passkey identity provider by setting the `PasskeyIdpConfig` in your `pod.initializeAuthServices()` configuration: ```dart import 'package:serverpod_auth_idp_server/core.dart'; import 'package:serverpod_auth_idp_server/providers/passkey.dart'; import 'src/generated/serverpod.dart'; void run(List args) async { final pod = Serverpod(args); // Configure Passkey identity provider pod.initializeAuthServices( tokenManagerBuilders: [ JwtConfigFromPasswords(), ], identityProviderBuilders: [ PasskeyIdpConfig( hostname: pod.getPassword('passkeyHostname')!, ), ], ); await pod.start(); } ``` :::tip You can use the `PasskeyIdpConfigFromPasswords` constructor in replacement of the `PasskeyIdpConfig` above to automatically load the hostname from the `config/passwords.yaml` file or environment variables. It will expect the `passkeyHostname` key on the file or the `SERVERPOD_PASSWORD_passkeyHostname` environment variable. ::: Then, extend the abstract endpoint to expose it on the server: ```dart import 'package:serverpod_auth_idp_server/providers/passkey.dart'; class PasskeyIdpEndpoint extends PasskeyIdpBaseEndpoint {} ``` Finally, start the server with `serverpod start` to generate the client code, then create and apply the migration that initializes the database for the provider (in the `serverpod start` terminal, press **M**). More detailed instructions can be found in the general [identity providers setup section](https://docs.serverpod.dev/concepts/authentication/setup.md#identity-providers-configuration). ### Basic configuration options - `hostname`: Required. The hostname (relying party ID) to be used on the web and associated with any apps. This should match your application's domain. - `challengeLifetime`: Optional. Maximum time after which a challenge must have been solved. Default is 5 minutes. ## Client-side implementation :::info Flutter UI components for passkeys are not yet available. You'll need to implement the client-side passkey flow using the WebAuthn API directly or use a Flutter package that supports WebAuthn/FIDO2. ::: ### Using the generated endpoints The generated client endpoints provide the following methods: - `createChallenge()`: Creates a new challenge for registration or login. - `register()`: Registers a new passkey for the authenticated user. - `login()`: Authenticates a user using their passkey. --- # Custom providers https://docs.serverpod.dev/concepts/authentication/providers/custom-providers/overview A custom identity provider plugs your own sign-in method into the authentication module. It issues the same tokens, creates the same auth users, and appears to your app like any built-in provider. Build one when the provider you need is not among the built-in ones, or when you authenticate against your own user directory. A custom provider consists of a few parts: - **A provider class** that implements the `IdentityProvider` contract on the server. - **A config class** extending `IdentityProviderBuilder`, which you pass to `initializeAuthServices` like any built-in config. - **An endpoint** extending `IdpBaseEndpoint`, which your app calls to sign in. - **An account model** linking the provider's user identity to the Serverpod auth user. - **A controller** in your app that runs the sign-in flow and registers the returned session. How to build them depends on the kind of provider: - **OAuth2-based providers** (most third-party services): the module ships utilities that handle the PKCE flow, token exchange, and error handling on both sides. Start with the [OAuth2 utility setup](https://docs.serverpod.dev/concepts/authentication/providers/custom-providers/oauth2-utility/setup.md), then follow [creating an OAuth2-based identity provider](https://docs.serverpod.dev/concepts/authentication/providers/custom-providers/oauth2-utility/creating-an-oauth2-based-identity-provider.md) for the full walkthrough. - **Everything else** (your own credential store, an internal single sign-on system, an API-key exchange): the same parts apply, without the OAuth2 utilities. The walkthrough still shows the shape of each part. To replace the authentication module entirely rather than add a provider to it, see [custom overrides](https://docs.serverpod.dev/concepts/authentication/custom-overrides.md) instead. ## Related - [OAuth2 utility setup](https://docs.serverpod.dev/concepts/authentication/providers/custom-providers/oauth2-utility/setup.md): the client and server utilities for OAuth2 flows. - [Creating an OAuth2-based identity provider](https://docs.serverpod.dev/concepts/authentication/providers/custom-providers/oauth2-utility/creating-an-oauth2-based-identity-provider.md): the complete walkthrough. - [Custom overrides](https://docs.serverpod.dev/concepts/authentication/custom-overrides.md): replace the built-in authentication entirely. --- # Set up the OAuth2 utility https://docs.serverpod.dev/concepts/authentication/providers/custom-providers/oauth2-utility/setup The Serverpod Auth module provides generic OAuth2 utilities that simplify implementing custom identity providers. These utilities handle the complex OAuth2 authorization code flow with PKCE (Proof Key for Code Exchange), allowing you to integrate any OAuth2-compliant provider without dealing with low-level protocol details. The OAuth2 utility consists of client-side and server-side components that work together to securely authenticate users: - **Client-side (`OAuth2PkceUtil`)**: Manages the authorization flow in your Flutter app, handling browser redirects and PKCE challenge generation. - **Server-side (`OAuth2PkceUtil`)**: Exchanges authorization codes for access tokens on your backend. :::info The [GitHub provider](https://docs.serverpod.dev/concepts/authentication/providers/github/setup.md) is built using these utilities, serving as a reference implementation for developers creating custom providers. ::: ## Understanding OAuth2 with PKCE OAuth2 with PKCE is an authorization protocol that allows users to grant your application access to their data without sharing passwords. The PKCE extension adds an additional security layer, particularly important for mobile and public clients. ### The OAuth2 flow Here's how the complete flow works: 1. **Generate Code Verifier**: Client generates a random cryptographic string (code verifier). 2. **Generate Code Challenge**: Client creates a SHA-256 hash of the verifier (code challenge). 3. **Authorization Request**: Client redirects user to provider with the code challenge. 4. **User Authorizes**: User logs in and grants permissions. 5. **Receive Code**: Provider redirects back with an authorization code. 6. **Token Exchange**: Client sends code + verifier to your backend. 7. **Backend Exchange**: Backend exchanges code + verifier for access token. 8. **Access Protected Resources**: Use access token to fetch user information. PKCE ensures that even if an attacker intercepts the authorization code, they cannot exchange it for an access token without the original code verifier. ## Server-side implementation ### Configuration The OAuth2 utility requires a configuration object that defines how your server communicates with the OAuth2 provider's token endpoint. Create a server-side configuration for token exchange: ```dart import 'package:serverpod_auth_idp_server/core.dart'; final config = OAuth2PkceServerConfig( // Token endpoint URL for exchanging authorization codes tokenEndpointUrl: Uri.https('oauth.provider.com', '/oauth/token'), // OAuth client ID (must match client-side) clientId: pod.getPassword('myProviderClientId')!, // OAuth client secret (keep secure!) clientSecret: pod.getPassword('myProviderClientSecret')!, // Function to parse token response from provider parseTokenResponse: (data) { // Your parse logic here }, // Optional: Where to send credentials (default: header) credentialsLocation: OAuth2CredentialsLocation.header, // Optional: Custom parameter names for credentials clientIdKey: 'client_id', clientSecretKey: 'client_secret', // Optional: Custom headers for token requests tokenRequestHeaders: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', }, // Optional: Additional parameters for token exchange tokenRequestParams: { 'grant_type': 'authorization_code', }, ); ``` :::info The `credentialsLocation` parameter controls how your client credentials are sent to the OAuth2 provider: - **Header mode (recommended):** Credentials are placed in the `Authorization` header using HTTP Basic authentication. This follows RFC 6749 and is generally more secure, since sensitive values don't appear in the request body or logs. - **Body mode:** Credentials are sent as form parameters in the request body. Use this only if your provider doesn't support header-based authentication. ::: ### Exchanging tokens Using the previously created `config` object, create the `OAuth2PkceUtil` on your endpoint to exchange the authorization code: ```dart import 'package:serverpod/serverpod.dart'; import 'package:serverpod_auth_idp_server/core.dart'; import '../generated/protocol.dart'; class MyProviderIdpEndpoint extends IdpBaseEndpoint { final oauth2Util = OAuth2PkceUtil(config: config); /// Required by IdpBaseEndpoint: report whether the signed-in user already /// has an account with this provider. Query the account model your provider /// defines (see the full walkthrough for the model definition). @override Future hasAccount(Session session) async { final authUserId = session.authenticated?.authUserId; if (authUserId == null) return false; return await MyProviderAccount.db.findFirstRow( session, where: (t) => t.authUserId.equals(authUserId), ) != null; } Future authenticate( Session session, { required String code, required String codeVerifier, required String redirectUri, }) async { try { // Exchange authorization code for token response final tokenResponse = await oauth2Util.exchangeCodeForToken( code: code, codeVerifier: codeVerifier, redirectUri: redirectUri, ); // Fetch user information from provider using access token final userInfo = await _fetchUserInfo(session, tokenResponse.accessToken); // Authenticate (find existing account or create new user) final account = await _authenticate(session, userInfo); // Issue authentication token to client return await _issueToken( session, authUserId: account.authUserId, scopes: account.scopes, ); } on OAuth2InvalidResponseException catch (e) { session.log('Invalid token response: ${e.message}'); throw Exception('Authentication failed'); } on OAuth2MissingAccessTokenException catch (e) { session.log('Missing access token: ${e.message}'); throw Exception('Authentication failed'); } on OAuth2NetworkErrorException catch (e) { session.log('Network error: ${e.message}'); throw Exception('Network error during authentication'); } } Future> _fetchUserInfo( Session session, String accessToken, ) async { // Fetch user data from provider's API using the access token } Future<_AccountResult> _authenticate( Session session, Map userInfo, ) async { // Find the existing provider account or create a new auth user from the // provider's user info, and return its authUserId with the scopes to grant. } Future _issueToken( Session session, { required UuidValue authUserId, required Set scopes, }) async { // Issue Serverpod authentication token for the authenticated user } } /// What _authenticate resolves: the linked auth user and the scopes to grant. typedef _AccountResult = ({UuidValue authUserId, Set scopes}); ``` ### Exception handling The server-side utility throws these exceptions: | Exception | Description | Typical Cause | | ----------------------------------- | ------------------------------ | ---------------------------- | | `OAuth2InvalidResponseException` | Invalid response from provider | HTTP errors, malformed JSON | | `OAuth2MissingAccessTokenException` | Access token not in response | Provider didn't return token | | `OAuth2NetworkErrorException` | Network failure | Timeout, connection issues | | `OAuth2UnknownException` | Unexpected error | Unknown problems | ## Client-side implementation ### Configuration The client-side OAuth2 utility also requires a configuration object that defines your provider's OAuth2 endpoints and authorization parameters. Create a client-side configuration: ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; final config = OAuth2PkceProviderClientConfig( // Your provider's authorization endpoint authorizationEndpoint: Uri.https('oauth.provider.com', '/oauth/authorize'), // OAuth client ID from your provider clientId: 'your-client-id', // Callback URI registered with your provider redirectUri: 'myapp://auth-callback', // URL scheme for the callback callbackUrlScheme: 'myapp', // Default permission scopes to request defaultScopes: ['profile', 'email'], // Additional query parameters for authorization request additionalAuthParams: { 'response_mode': 'query', }, // Separator for joining scopes (default: ' ') scopeSeparator: ' ', // Enable state parameter for CSRF protection (default: true) enableState: true, // Enable PKCE for OAuth2 flow (default: true) enablePKCE: true, ); ``` ### Initiating authorization Using the previously created `config` object, create an `OAuth2PkceUtil` instance to start the authorization flow: ```dart final oauth2Util = OAuth2PkceUtil(config: config); try { final result = await oauth2Util.authorize( // Optional: override default scopes scopes: ['profile', 'email'], ); // The authorization code to exchange for an access token final code = result.code; // The PKCE code verifier (required for token exchange) final codeVerifier = result.codeVerifier; // Send both to your backend. codeVerifier is null when the provider does // not use PKCE, so guard it before calling an endpoint that requires it. if (codeVerifier == null) { throw StateError('The provider did not return a PKCE code verifier.'); } // The generated client drops the Endpoint suffix: // MyProviderIdpEndpoint becomes client.myProviderIdp. final authSuccess = await client.myProviderIdp.authenticate( code: code, codeVerifier: codeVerifier, redirectUri: config.redirectUri, ); // Register the session, otherwise the app is never actually signed in. await client.auth.updateSignedInUser(authSuccess); } on OAuth2PkceStateMismatchException catch (e) { // Possible CSRF attack detected print('Security error: ${e.message}'); } on OAuth2PkceMissingAuthorizationCodeException catch (e) { // No authorization code in callback print('Authorization failed: ${e.message}'); } on OAuth2PkceProviderErrorException catch (e) { // Provider returned an error print('Provider error: ${e.message}'); } on OAuth2PkceUnknownException catch (e) { // Unexpected error print('Unknown error: ${e.message}'); } ``` ### Exception handling The client-side utility throws specific exceptions to help you handle different error scenarios: | Exception | Description | Typical Cause | | --------------------------------------------- | -------------------------------- | --------------------------------------------------------- | | `OAuth2PkceStateMismatchException` | State validation failed | Possible CSRF attack or browser issue | | `OAuth2PkceMissingAuthorizationCodeException` | No authorization code received | Provider didn't return expected code | | `OAuth2PkceProviderErrorException` | Provider returned error response | Invalid credentials, rate limiting | | `OAuth2PkceUnknownException` | Unexpected error occurred | Network issues, unknown problems, and a cancelled sign-in | ### Platform-specific configuration The OAuth2 utility uses the [flutter\_web\_auth\_2](https://pub.dev/packages/flutter_web_auth_2) package under the hood, which requires platform-specific setup. #### iOS and macOS There is no special configuration needed for iOS and macOS for "normal" authentication flows. However, if you are using **Universal Links** on iOS, they require redirect URIs to use **https**. Follow the instructions in the [flutter\_web\_auth\_2](https://pub.dev/packages/flutter_web_auth_2#ios) documentation. #### Android Add the callback activity to your `AndroidManifest.xml`: ```xml ``` #### Web Web sign-in needs the shared callback page that hands the OAuth2 result back to your app. Set it up once as described in [Web callback page (`auth.html`)](https://docs.serverpod.dev/concepts/authentication/setup.md#web-callback-page-authhtml). The same page serves every provider built on the OAuth2 utility, as long as your redirect URIs point to it, for example `https://yourdomain.com/auth.html`. ## Complete example of a custom provider For a full end-to-end implementation of a custom OAuth2 provider (server configuration, client setup, and integration of all components), see the [Complete Example](https://docs.serverpod.dev/concepts/authentication/providers/custom-providers/oauth2-utility/creating-an-oauth2-based-identity-provider.md) page. ## Best practices ### Security considerations 1. **Always Use PKCE**: Keep `enablePKCE: true` in your client configuration. PKCE protects against authorization code interception attacks. 2. **Validate State Parameter**: Keep `enableState: true` to prevent CSRF attacks. The state parameter ensures the authorization response matches your request. 3. **Secure Client Secret**: Never expose your client secret in client-side code. Store it securely in `passwords.yaml` or environment variables on the server. 4. **Use HTTPS**: Always use HTTPS URLs for production endpoints. Only use HTTP for local development. 5. **Validate Redirect URIs**: Ensure redirect URIs in your code exactly match those registered with your OAuth2 provider. ### Error handling 1. **Catch Specific Exceptions**: Handle each exception type appropriately rather than using generic catch-all handlers. 2. **Log Securely**: Log errors for debugging but never log sensitive data like tokens or secrets. 3. **User-Friendly Messages**: Show clear, actionable error messages to users without exposing technical details. --- # Create an OAuth2-based identity provider https://docs.serverpod.dev/concepts/authentication/providers/custom-providers/oauth2-utility/creating-an-oauth2-based-identity-provider This page provides a complete, working implementation of a custom OAuth2 provider. The [GitHub provider](https://docs.serverpod.dev/concepts/authentication/providers/github/setup.md) is built the same way, using the same OAuth2 utility shown here, so this example illustrates the general pattern for your own provider. ## Overview This example implements authentication with a fictional OAuth2 provider called "MyProvider". The implementation includes: - Server-side token exchange and user management - Client-side authorization flow - Flutter UI integration - Error handling ## Server-side implementation ### 1. Data model First, create a data model to store provider accounts: ```yaml class: MyProviderAccount serverOnly: true table: my_provider_account fields: id: UuidValue?, defaultPersist=random_v7 # The AuthUser this account belongs to authUser: module:serverpod_auth_core:AuthUser?, relation(onDelete=Cascade) # Provider's user identifier providerId: String # User's email from provider (optional) email: String? # Creation timestamp created: DateTime, defaultModel=now indexes: my_provider_account_provider_id: fields: providerId unique: true ``` ### 2. Configuration Create the server configuration: ```dart import 'package:serverpod_auth_idp_server/core.dart'; import 'my_provider_idp.dart'; class MyProviderIdpConfig extends IdentityProviderBuilder { final String clientId; final String clientSecret; late final OAuth2PkceServerConfig oauth2Config; MyProviderIdpConfig({ required this.clientId, required this.clientSecret, }) : oauth2Config = OAuth2PkceServerConfig( tokenEndpointUrl: Uri.https('oauth.myprovider.com', '/oauth/token'), clientId: clientId, clientSecret: clientSecret, credentialsLocation: OAuth2CredentialsLocation.header, parseTokenResponse: parseTokenResponse, ); static OAuth2PkceTokenResponse parseTokenResponse(Map response) { final error = response['error'] as String?; if (error != null) { final description = response['error_description'] as String?; throw OAuth2InvalidResponseException( 'Provider error: $error${description != null ? ' - $description' : ''}', ); } final token = response['access_token'] as String?; if (token == null) { throw const OAuth2MissingAccessTokenException('No access token in response'); } return OAuth2PkceTokenResponse(accessToken: token); } @override MyProviderIdp build({ required TokenManager tokenManager, required AuthUsers authUsers, required UserProfiles userProfiles, }) { return MyProviderIdp( config: this, tokenIssuer: tokenManager, authUsers: authUsers, userProfiles: userProfiles, ); } } ``` ### 3. Provider class Create the main identity provider class: ```dart import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:serverpod/serverpod.dart'; import 'package:serverpod_auth_idp_server/core.dart'; import '../generated/protocol.dart'; import 'my_provider_idp_config.dart'; class MyProviderIdp implements IdentityProvider { /// Required by IdentityProvider: the stable method identifier for tokens. @override String get method => 'myprovider'; /// Required by IdentityProvider: carry this provider's rows over when two /// auth users are merged. @override Future mergeAuthUsers( Session session, { required UuidValue userToKeepId, required UuidValue userToRemoveId, required Transaction transaction, }) async { await MyProviderAccount.db.updateWhere( session, where: (t) => t.authUserId.equals(userToRemoveId), columnValues: (t) => [t.authUserId(userToKeepId)], transaction: transaction, ); } final MyProviderIdpConfig config; final TokenIssuer _tokenIssuer; final AuthUsers _authUsers; final UserProfiles _userProfiles; late final OAuth2PkceUtil _oauth2Util; MyProviderIdp({ required this.config, required TokenIssuer tokenIssuer, required AuthUsers authUsers, required UserProfiles userProfiles, }) : _tokenIssuer = tokenIssuer, _authUsers = authUsers, _userProfiles = userProfiles { _oauth2Util = OAuth2PkceUtil(config: config.oauth2Config); } Future login( Session session, { required String code, required String codeVerifier, required String redirectUri, }) async { return await DatabaseUtil.runInTransactionOrSavepoint( session.db, null, (transaction) async { // 1. Exchange authorization code for token response final tokenResponse = await _oauth2Util.exchangeCodeForToken( code: code, codeVerifier: codeVerifier, redirectUri: redirectUri, ); // 2. Fetch user information final userInfo = await _fetchUserInfo(session, tokenResponse.accessToken); // 3. Authenticate (find or create user) final account = await _authenticate(session, userInfo, transaction); // 4. Create user profile if new user if (account.newAccount) { await _createUserProfile( session, account.authUserId, userInfo, transaction, ); } // 5. Issue authentication token return await _tokenIssuer.issueToken( session, authUserId: account.authUserId, transaction: transaction, method: method, scopes: account.scopes, ); }, ); } Future> _fetchUserInfo( Session session, String accessToken, ) async { final response = await http.get( Uri.https('api.myprovider.com', '/v1/user'), headers: { 'Authorization': 'Bearer $accessToken', 'Accept': 'application/json', }, ); if (response.statusCode != 200) { session.log( 'Failed to fetch user info: ${response.statusCode}', level: LogLevel.error, ); throw MyProviderAuthException('Failed to fetch user information'); } try { return jsonDecode(response.body) as Map; } catch (e) { session.log( 'Failed to parse user info: $e', level: LogLevel.error, ); throw MyProviderAuthException('Invalid user information format'); } } Future<_AccountResult> _authenticate( Session session, Map userInfo, Transaction transaction, ) async { final providerId = userInfo['id']?.toString(); if (providerId == null || providerId.isEmpty) { throw MyProviderAuthException('Missing user ID from provider'); } // Check if account exists var account = await MyProviderAccount.db.findFirstRow( session, where: (t) => t.providerId.equals(providerId), transaction: transaction, ); final isNewAccount = account == null; if (isNewAccount) { // Create new auth user final authUser = await _authUsers.create( session, transaction: transaction, ); // Create provider account account = await MyProviderAccount.db.insertRow( session, MyProviderAccount( providerId: providerId, email: userInfo['email'] as String?, authUserId: authUser.id, ), transaction: transaction, ); return ( authUserId: authUser.id, newAccount: true, scopes: authUser.scopes, ); } else { // Get existing user final authUser = await _authUsers.get( session, authUserId: account.authUserId, transaction: transaction, ); return ( authUserId: authUser.id, newAccount: false, scopes: authUser.scopes, ); } } Future _createUserProfile( Session session, UuidValue authUserId, Map userInfo, Transaction transaction, ) async { try { await _userProfiles.createUserProfile( session, authUserId, UserProfileData( fullName: userInfo['name'] as String?, email: userInfo['email'] as String?, ), transaction: transaction, ); } catch (e, stackTrace) { session.log( 'Failed to create user profile', level: LogLevel.error, exception: e, stackTrace: stackTrace, ); // Don't fail the authentication if profile creation fails } } } typedef _AccountResult = ({ UuidValue authUserId, bool newAccount, Set scopes, }); class MyProviderAuthException implements Exception { final String message; const MyProviderAuthException(this.message); @override String toString() => 'MyProviderAuthException: $message'; } ``` ### 4. Endpoint Create the endpoint: ```dart import 'package:serverpod/serverpod.dart'; import 'package:serverpod_auth_idp_server/core.dart'; import '../generated/protocol.dart'; import 'my_provider_idp.dart'; class MyProviderIdpEndpoint extends IdpBaseEndpoint { MyProviderIdp get myProviderIdp => AuthServices.getIdentityProvider(); /// Required by IdpBaseEndpoint: report whether the signed-in user already /// has an account with this provider. @override Future hasAccount(Session session) async { final authUserId = session.authenticated?.authUserId; if (authUserId == null) return false; return await MyProviderAccount.db.findFirstRow( session, where: (t) => t.authUserId.equals(authUserId), ) != null; } Future login( Session session, { required String code, required String codeVerifier, required String redirectUri, }) async { try { return await myProviderIdp.login( session, code: code, codeVerifier: codeVerifier, redirectUri: redirectUri, ); } on OAuth2Exception catch (e) { session.log( 'OAuth2 error during authentication: ${e.message}', level: LogLevel.error, ); throw Exception('Authentication failed'); } on MyProviderAuthException catch (e) { session.log( 'MyProvider error: ${e.message}', level: LogLevel.error, ); throw Exception('Authentication failed'); } } } ``` ### 5. Server registration Register the provider in `server.dart`: ```dart import 'package:serverpod_auth_idp_server/core.dart'; import 'src/generated/serverpod.dart'; import 'my_provider_idp_config.dart'; void run(List args) async { final pod = Serverpod(args); final myProviderConfig = MyProviderIdpConfig( clientId: pod.getPassword('myProviderClientId')!, clientSecret: pod.getPassword('myProviderClientSecret')!, ); pod.initializeAuthServices( tokenManagerBuilders: [ JwtConfigFromPasswords(), ], identityProviderBuilders: [ myProviderConfig, ], ); await pod.start(); } ``` ## Client-side implementation ### 1. Configuration Create the client configuration: ```dart import 'package:flutter/foundation.dart'; import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; class MyProviderConfig { static const _clientIdEnvKey = 'MY_PROVIDER_CLIENT_ID'; static const _redirectUriEnvKey = 'MY_PROVIDER_REDIRECT_URI'; static OAuth2PkceProviderClientConfig get clientConfig { // Get credentials from environment or use defaults final clientId = _getClientId(); final redirectUri = _getRedirectUri(); return OAuth2PkceProviderClientConfig( authorizationEndpoint: Uri.https('oauth.myprovider.com', '/oauth/authorize'), clientId: clientId, redirectUri: redirectUri, callbackUrlScheme: Uri.parse(redirectUri).scheme, defaultScopes: ['profile', 'email'], ); } static String _getClientId() { const clientId = String.fromEnvironment(_clientIdEnvKey); if (clientId.isNotEmpty) return clientId; // Development fallback if (kDebugMode) { return 'dev-client-id'; } throw Exception('$_clientIdEnvKey not configured'); } static String _getRedirectUri() { const redirectUri = String.fromEnvironment(_redirectUriEnvKey); if (redirectUri.isNotEmpty) return redirectUri; // Platform-specific defaults for development if (kDebugMode) { if (kIsWeb) { return 'http://localhost:3000/auth.html'; } else { return 'myapp://auth-callback'; } } throw Exception('$_redirectUriEnvKey not configured'); } } ``` ### 2. Service Create the sign-in service: ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; import 'my_provider_config.dart'; class MyProviderService { static final instance = MyProviderService._(); MyProviderService._(); OAuth2PkceUtil? _oauth2Util; void initialize() { if (_oauth2Util != null) return; _oauth2Util = OAuth2PkceUtil( config: MyProviderConfig.clientConfig, ); } Future signIn({List? scopes}) async { if (_oauth2Util == null) { throw StateError( 'MyProviderService not initialized. Call initialize() first.', ); } return await _oauth2Util!.authorize(scopes: scopes); } } ``` ### 3. Controller Create an authentication controller: ```dart import 'package:flutter/foundation.dart'; import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; import 'package:your_client/your_client.dart'; import 'my_provider_config.dart'; import 'my_provider_service.dart'; enum MyProviderAuthState { idle, loading, authenticated, error, } class MyProviderAuthController extends ChangeNotifier { final Client client; final VoidCallback? onAuthenticated; final Function(Object error)? onError; MyProviderAuthController({ required this.client, this.onAuthenticated, this.onError, }); MyProviderAuthState _state = MyProviderAuthState.idle; Object? _error; MyProviderAuthState get state => _state; bool get isLoading => _state == MyProviderAuthState.loading; bool get isAuthenticated => client.auth.isAuthenticated; String? get errorMessage => _error?.toString(); Future signIn() async { if (_state == MyProviderAuthState.loading) return; _setState(MyProviderAuthState.loading); try { // Get authorization code from provider final result = await MyProviderService.instance.signIn(); // Exchange for tokens on the server. The generated client names the // endpoint after the class, with the Endpoint suffix dropped, so // MyProviderIdpEndpoint becomes client.myProviderIdp. final authSuccess = await client.myProviderIdp.login( code: result.code, // Safe to unwrap: this config keeps PKCE enabled, so the // verifier is always present. codeVerifier: result.codeVerifier!, redirectUri: MyProviderConfig.clientConfig.redirectUri, ); // Register the session, otherwise the app is never actually signed in. await client.auth.updateSignedInUser(authSuccess); _setState(MyProviderAuthState.authenticated); onAuthenticated?.call(); } catch (error) { // A cancelled sign-in arrives as OAuth2PkceUnknownException. _error = error; _setState(MyProviderAuthState.error); onError?.call(error); } } void _setState(MyProviderAuthState newState) { if (newState != MyProviderAuthState.error) { _error = null; } _state = newState; notifyListeners(); } } ``` ### 4. UI widget Create the sign-in button: ```dart import 'package:flutter/material.dart'; import 'package:your_client/your_client.dart'; import 'my_provider_auth_controller.dart'; class MyProviderSignInWidget extends StatefulWidget { final Client client; final VoidCallback? onAuthenticated; final Function(Object error)? onError; const MyProviderSignInWidget({ required this.client, this.onAuthenticated, this.onError, super.key, }); @override State createState() => _MyProviderSignInWidgetState(); } class _MyProviderSignInWidgetState extends State { late final MyProviderAuthController _controller; @override void initState() { super.initState(); _controller = MyProviderAuthController( client: widget.client, onAuthenticated: widget.onAuthenticated, onError: widget.onError, ); _controller.addListener(_onControllerStateChanged); } @override void dispose() { _controller.removeListener(_onControllerStateChanged); _controller.dispose(); super.dispose(); } void _onControllerStateChanged() => setState(() {}); @override Widget build(BuildContext context) { return ElevatedButton( onPressed: _controller.isLoading ? null : _controller.signIn, style: ElevatedButton.styleFrom( minimumSize: const Size(240, 48), ), child: _controller.isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Sign in with MyProvider'), ); } } ``` ### 5. Initialization Initialize in your app: ```dart import 'package:flutter/material.dart'; import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; import 'package:serverpod_flutter/serverpod_flutter.dart'; import 'package:your_client/your_client.dart'; import 'my_provider_service.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialize Serverpod client final client = Client('http://localhost:8080/') ..connectivityMonitor = FlutterConnectivityMonitor() ..authSessionManager = FlutterAuthSessionManager(); await client.auth.initialize(); // Initialize MyProvider service MyProviderService.instance.initialize(); runApp(MyApp(client: client)); } ``` ### 6. Usage in UI Use the widget in your sign-in page: ```dart import 'package:flutter/material.dart'; import 'package:your_client/your_client.dart'; import 'my_provider_sign_in_widget.dart'; class SignInPage extends StatelessWidget { final Client client; const SignInPage({required this.client, super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Sign In')), body: Center( child: MyProviderSignInWidget( client: client, onAuthenticated: () { // Navigate to home page after authentication Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (_) => HomePage(client: client), ), ); }, onError: (error) { // Show error message ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Sign in failed: $error'), backgroundColor: Colors.red, ), ); }, ), ), ); } } ``` This compact example provides a clear template you can adapt to integrate any OAuth2 provider into your Serverpod project. Use this as a template for implementing your own OAuth2 provider integration. --- # Managing tokens https://docs.serverpod.dev/concepts/authentication/token-managers/managing-tokens An authentication token is the credential the app sends with each request to prove who the signed-in user is. Token managers issue these tokens when a user signs in, and validate, revoke, and list them afterwards. ## Default token managers Serverpod provides two built-in token managers: - `JwtTokenManager` for JWT-based authentication. See [JWT token manager](https://docs.serverpod.dev/concepts/authentication/token-managers/jwt-token-manager.md) for details. - `ServerSideSessionsTokenManager` for session-based authentication. See [Server-side sessions token manager](https://docs.serverpod.dev/concepts/authentication/token-managers/server-side-sessions-token-manager.md) for details. The main trade-off between them: | | `JwtTokenManager` | `ServerSideSessionsTokenManager` | | ---------- | ------------------------------------------------------ | -------------------------------- | | Validation | Stateless, no database query | Database query per validation | | Revocation | Takes effect when the short-lived access token expires | Immediate | Pick JWT to avoid database load on every request. Pick server-side sessions when revocation must take effect immediately. ## Using the token managers After you configure at least one token manager with `pod.initializeAuthServices()` (see [Setup](https://docs.serverpod.dev/concepts/authentication/setup.md)), access the token manager through the `AuthServices.instance.tokenManager` property. ```dart final tokenManager = AuthServices.instance.tokenManager; ``` The property returns a `MultiTokenManager`, a wrapper that combines all configured token managers. Serverpod creates it automatically when you initialize the authentication services. The first builder you pass becomes the primary token manager. The `MultiTokenManager`: - Uses the primary token manager for issuing new tokens. - Validates tokens against all managers (primary and additional). - Delegates management operations to all managers. ### Token validation flow When validating a token, the `MultiTokenManager`: 1. Tries the primary token manager first. 2. If validation fails, tries each additional token manager in order. 3. Returns the first successful validation result. 4. Returns `null` if all managers fail to validate. This allows you to support multiple token types simultaneously, which is useful for: - Migrating from one token type to another. - Supporting legacy tokens alongside new tokens. - Using different token types for different use cases. ## Token lifecycle management ### Issuing tokens Identity providers issue tokens automatically when users authenticate. For your own sign-in flow, call `issueToken`: ```dart final authSuccess = await AuthServices.instance.tokenManager.issueToken( session, authUserId: userId, method: 'custom', scopes: {Scope.admin}, ); ``` Return the `AuthSuccess` to the app. It carries the token, its expiry, the granted scopes, and, for JWT, the refresh token. The `issueToken` method is built for sign-in: - If the caller is already signed in as a different user, it throws a `SignInWhileAuthenticatedException`. - On a web request with [cookie-based authentication](https://docs.serverpod.dev/concepts/authentication/web-authentication.md), it sends the refresh token (JWT) or the session token (server-side sessions) as an `httpOnly` cookie instead of in the response body. To create a token for another user, or a token the caller must receive in the response body, call `createToken` instead. For example, use it in an admin flow or for a personal access token. It skips the sign-in check and the cookie delivery, so the returned `AuthSuccess` always includes the token and, for JWT, the refresh token: ```dart final authSuccess = await AuthServices.instance.tokenManager.createToken( session, authUserId: targetUserId, method: 'pat', ); ``` #### Attaching metadata to tokens You can attach metadata to tokens in two ways. Configure a global callback on the token manager, or insert a metadata row right after issuing the token. For more details, see the specific configuration sections for [server-side sessions](https://docs.serverpod.dev/concepts/authentication/token-managers/server-side-sessions-token-manager.md#attaching-custom-metadata-to-sessions) and [JWT](https://docs.serverpod.dev/concepts/authentication/token-managers/jwt-token-manager.md#attaching-custom-metadata-to-tokens). ### Validating tokens Tokens are validated automatically by the authentication handler, the hook Serverpod runs for every request that carries an authentication token. You can also validate tokens manually: ```dart final authInfo = await AuthServices.instance.tokenManager.validateToken( session, token, ); if (authInfo != null) { // Token is valid final authUserId = authInfo.authUserId; } else { // Token is invalid or expired } ``` ### Revoking tokens Revoke specific tokens by token ID: ```dart await AuthServices.instance.tokenManager.revokeToken( session, tokenId: 'token-id-here', ); ``` If you attach custom metadata to [server-side sessions](https://docs.serverpod.dev/concepts/authentication/token-managers/server-side-sessions-token-manager.md#attaching-custom-metadata-to-sessions) or [JWT tokens](https://docs.serverpod.dev/concepts/authentication/token-managers/jwt-token-manager.md#attaching-custom-metadata-to-tokens), you can look up token IDs in your metadata tables, for example by device or user agent. Pass those IDs to `revokeToken` to revoke exactly those tokens. Revoke all tokens for a user: ```dart await AuthServices.instance.tokenManager.revokeAllTokens( session, authUserId: userId, ); ``` Revoke tokens by method: ```dart await AuthServices.instance.tokenManager.revokeAllTokens( session, authUserId: userId, method: 'email', ); ``` ### Listing tokens List all tokens for a user: ```dart final tokens = await AuthServices.instance.tokenManager.listTokens( session, authUserId: userId, ); for (final token in tokens) { print('Token: ${token.tokenId}, Method: ${token.method}'); } ``` List tokens by method (for example, `'google'`, `'email'`, or `'apple'`): ```dart final tokens = await AuthServices.instance.tokenManager.listTokens( session, authUserId: userId, method: 'google', ); ``` ## Accessing specific token managers If you configure more than one token manager, retrieve a specific one with the static `AuthServices.getTokenManager()` method. ```dart final jwtManager = AuthServices.getTokenManager(); final sessionManager = AuthServices.getTokenManager(); ``` ## Related - [Setup](https://docs.serverpod.dev/concepts/authentication/setup.md): configure token managers with `initializeAuthServices`. - [JWT token manager](https://docs.serverpod.dev/concepts/authentication/token-managers/jwt-token-manager.md): stateless tokens with automatic refresh. - [Server-side sessions token manager](https://docs.serverpod.dev/concepts/authentication/token-managers/server-side-sessions-token-manager.md): database-backed sessions with immediate revocation. --- # JWT token manager https://docs.serverpod.dev/concepts/authentication/token-managers/jwt-token-manager The `JwtTokenManager` uses JWT (JSON Web Tokens) for stateless authentication. It works with two kinds of tokens. The access token is short-lived and is sent with every request. The refresh token is long-lived and is used only to obtain a new access token when the old one expires. This token manager provides: - Stateless authentication (no database queries for validation). - Access tokens with short lifetimes. - Refresh tokens for long-term authentication. - Automatic token rotation. :::note Web apps With [cookie-based web authentication](https://docs.serverpod.dev/concepts/authentication/web-authentication.md) enabled, browsers keep the access token in memory only and receive the refresh token as an `httpOnly` cookie. ::: ## Server-side configuration The `JwtTokenManager` is created by passing a `JwtConfig` object in the `tokenManagerBuilders` list of `pod.initializeAuthServices()`: ```dart pod.initializeAuthServices( tokenManagerBuilders: [ JwtConfig( // The pepper used to hash the refresh token secret. refreshTokenHashPepper: pod.getPassword( 'jwtRefreshTokenHashPepper', )!, // The algorithm used to sign the tokens. algorithm: JwtAlgorithm.hmacSha512( SecretKey(pod.getPassword('jwtHmacSha512PrivateKey')!), ), ), ], ); ``` :::tip You can use the `JwtConfigFromPasswords` constructor instead of the `JwtConfig` above. It loads the credentials from the `config/passwords.yaml` file or environment variables. It expects either the following keys in the file: - `jwtRefreshTokenHashPepper` - `jwtHmacSha512PrivateKey` Or the following environment variables: - `SERVERPOD_PASSWORD_jwtRefreshTokenHashPepper` - `SERVERPOD_PASSWORD_jwtHmacSha512PrivateKey` ::: The app calls a refresh endpoint to renew expired access tokens without asking the user to sign in again. Extend the abstract endpoint to expose it on the server: ```dart import 'package:serverpod_auth_idp_server/core.dart' as core; class RefreshJwtTokensEndpoint extends core.RefreshJwtTokensEndpoint {} ``` Finally, run `serverpod generate` so the app can call the endpoint. If your server runs with `serverpod start`, this happens automatically. ### Basic configuration options - `algorithm`: Required. The algorithm to use for signing tokens (HMAC SHA-512, HMAC SHA-256 or ECDSA SHA-512). - `refreshTokenHashPepper`: Required. A secret pepper for hashing refresh tokens (see [storing secrets](https://docs.serverpod.dev/concepts/authentication/setup.md#storing-secrets) for what a pepper is). Must be at least 10 characters long, but [the recommended length is 32 bytes](https://www.ietf.org/archive/id/draft-ietf-kitten-password-storage-04.html#name-storage-2). #### Token algorithms There are three supported token algorithms: - **HMAC SHA-512**: Use HMAC SHA-512 for symmetric key signing. ```dart algorithm: JwtAlgorithm.hmacSha512( SecretKey(pod.getPassword('authenticationTokenPrivateKey')!), ), ``` - **HMAC SHA-256**: Use HMAC SHA-256 for symmetric key signing. ```dart algorithm: JwtAlgorithm.hmacSha256( SecretKey(pod.getPassword('authenticationTokenPrivateKey')!), ), ``` - **ECDSA SHA-512**: Use ECDSA SHA-512 for asymmetric key signing. ```dart algorithm: JwtAlgorithm.ecdsaSha512( privateKey: ecPrivateKey, publicKey: ecPublicKey, ), ``` As of now, the `JwtConfigFromPasswords` only supports HMAC SHA-512. To use HMAC SHA-256 or ECDSA SHA-512, use `JwtConfig` and pass the keys manually. ### Extra configuration options Common configuration options for the `JwtTokenManager`. For more details on all options, check the `JwtConfig` in-code documentation. ```dart final jwtConfig = JwtConfigFromPasswords( // Optional: Set fallback algorithms for token verification // This is useful for allowing old tokens to be validated after a rotation. fallbackVerificationAlgorithms: [ JwtAlgorithm.hmacSha512( SecretKey(pod.getPassword('fallbackJwtPrivateKey')!), ), ], // Optional: Configure token lifetimes accessTokenLifetime: Duration(minutes: 10), refreshTokenLifetime: Duration(days: 14), // Optional: Add custom claims to tokens. // Claims will be included in every access token and sent with requests. extraClaimsProvider: (session, context) async { return { 'userRole': 'admin', 'organizationId': 'org-123', }; }, // Check the [JwtConfig] documentation for more options. ); ``` ### Attaching custom metadata to tokens You can attach custom metadata to each JWT refresh token by providing an `onRefreshTokenCreated` callback. This is useful for storing device information, IP address, user agent, or any other data you need to query or use later (for example, to list or revoke tokens by device). The callback runs when a refresh token is created, within the same transaction as the token insert. Define a server-only table that relates to `RefreshToken` and store your metadata there. Example schema: ```yaml class: TokenMetadata serverOnly: true table: token_metadata fields: ### The [RefreshToken] this metadata belongs to refreshToken: module:serverpod_auth_core:RefreshToken?, relation(onDelete=Cascade) ### The name of the token name: String? ### Device information for the token deviceName: String? ### IP address from which the token was created ipAddress: String? ### User agent string userAgent: String? indexes: refresh_token_id_unique_idx: fields: refreshTokenId unique: true ``` Then configure the callback in your JWT config: ```dart JwtConfigFromPasswords( onRefreshTokenCreated: ( final session, { required final authUserId, required final refreshTokenId, required final transaction, }) async { await TokenMetadata.db.insertRow( session, TokenMetadata( refreshTokenId: refreshTokenId, name: 'general-token', ipAddress: session.request?.connectionInfo.remote.address.toString(), userAgent: session.request?.headers.userAgent, ), transaction: transaction, ); }, ), ``` To revoke tokens based on your custom metadata, query the metadata table for the token IDs you want to revoke and call `revokeToken` for each: ```dart final tokenMetadata = await TokenMetadata.db.find( session, where: (final row) => row.deviceName.equals('Old Device'), ); for (final row in tokenMetadata) { await AuthServices.instance.tokenManager.revokeToken( session, tokenId: row.refreshTokenId.toString(), ); } ``` #### Attaching metadata when creating tokens from an endpoint The `onRefreshTokenCreated` callback is global and runs for every new refresh token, including those created by identity providers. An endpoint that creates a personal access token (PAT) or CLI token often has its own parameters, such as a token name or label, which the callback can't see. In that case, create the token with `AuthServices.instance.tokenManager.createToken` rather than `issueToken`, which is for [sign-in flows](https://docs.serverpod.dev/concepts/authentication/token-managers/managing-tokens.md#issuing-tokens). Then use the returned `AuthSuccess.jwtRefreshTokenId` to insert your metadata with the endpoint's parameters: ```dart final authSuccess = await AuthServices.instance.tokenManager.createToken( session, authUserId: userId, method: 'pat', scopes: {Scope.admin}, ); await TokenMetadata.db.insertRow( session, TokenMetadata( refreshTokenId: authSuccess.jwtRefreshTokenId, name: tokenName, // from your endpoint parameter deviceName: deviceName, // from your endpoint parameter ipAddress: session.request?.connectionInfo.remote.address.toString(), userAgent: session.request?.headers.userAgent, ), ); ``` ## Client-side configuration The `JwtTokenManager` needs no extra configuration in your app. The client includes the access token in requests automatically and refreshes it when it is 30 seconds from expiring. If the refresh token itself expires, the client signs the user out the next time it validates the tokens. Your app decides what to show next, for example by listening to [authentication state changes](https://docs.serverpod.dev/concepts/authentication/basics.md#monitor-authentication-changes). ## Related - [Managing tokens](https://docs.serverpod.dev/concepts/authentication/token-managers/managing-tokens.md): issue, validate, revoke, and list tokens. - [Server-side sessions token manager](https://docs.serverpod.dev/concepts/authentication/token-managers/server-side-sessions-token-manager.md): the database-backed alternative. - [Setup](https://docs.serverpod.dev/concepts/authentication/setup.md): where token managers are configured. --- # Server-side sessions token manager https://docs.serverpod.dev/concepts/authentication/token-managers/server-side-sessions-token-manager The `ServerSideSessionsTokenManager` validates each token against a server-side session, a database record of who is signed in on which device. The database stores only a hash of the session key, never the token itself, which stays in the app. These auth sessions are unrelated to the `Session` object that endpoint methods receive, which is the context of a single request. This token manager provides: - Stateful authentication (database queries for validation). - Immediate session revocation. - Support for session expiration and inactivity timeouts. :::info Web apps With [cookie-based web authentication](https://docs.serverpod.dev/concepts/authentication/web-authentication.md) enabled, browsers receive the session token as an `httpOnly` cookie instead of in the response body. ::: ## Server-side configuration The `ServerSideSessionsTokenManager` is created by passing a `ServerSideSessionsConfig` object in the `tokenManagerBuilders` list of `pod.initializeAuthServices()`: ```dart pod.initializeAuthServices( tokenManagerBuilders: [ ServerSideSessionsConfig( // The pepper used to hash the session key. sessionKeyHashPepper: pod.getPassword('serverSideSessionKeyHashPepper')!, ), ], ); ``` :::tip You can use the `ServerSideSessionsConfigFromPasswords` constructor instead of the `ServerSideSessionsConfig` above. It loads the pepper from the `config/passwords.yaml` file or environment variables. It expects the `serverSideSessionKeyHashPepper` key in the file, or the `SERVERPOD_PASSWORD_serverSideSessionKeyHashPepper` environment variable. ::: ### Basic configuration options - `sessionKeyHashPepper`: Required. A secret pepper used for hashing session keys (see [storing secrets](https://docs.serverpod.dev/concepts/authentication/setup.md#storing-secrets) for what a pepper is). Must be at least 10 characters long, but [the recommended length is 32 bytes](https://www.ietf.org/archive/id/draft-ietf-kitten-password-storage-04.html#name-storage-2). ### Extra configuration options Common configuration options for the `ServerSideSessionsTokenManager`. For more details on all options, check the `ServerSideSessionsConfig` in-code documentation. ```dart final serverSideSessionsConfig = ServerSideSessionsConfigFromPasswords( // Optional: Fallback peppers for pepper rotation // This is useful for allowing old sessions to be validated after a rotation. fallbackSessionKeyHashPeppers: [ pod.getPassword('oldSessionKeyHashPepper')!, ], // Optional: Set default session lifetime (default is to never expire) defaultSessionLifetime: Duration(days: 30), // Optional: Set inactivity timeout (default is to never timeout) defaultSessionInactivityTimeout: Duration(days: 7), // Optional: Configure session key properties sessionKeySecretLength: 32, sessionKeyHashSaltLength: 16, // Check the [ServerSideSessionsConfig] documentation for more options. ); ``` ### Attaching custom metadata to sessions You can attach custom metadata to each server-side session by providing an `onSessionCreated` callback. This is useful for storing device information, IP address, user agent, or any other data you need to query or display later (for example, in a "sessions" or "devices" list). The callback runs when a session is created, within the same transaction as the session insert. Define a server-only table that relates to `ServerSideSession` and store your metadata there. Example schema: ```yaml class: SessionMetadata serverOnly: true table: session_metadata fields: ### The [ServerSideSession] this metadata belongs to serverSideSession: module:serverpod_auth_core:ServerSideSession?, relation(onDelete=Cascade) ### A display name for the session name: String? ### Device information for the session deviceName: String? ### IP address from which the session was created ipAddress: String? ### User agent string userAgent: String? indexes: server_side_session_id_unique_idx: fields: serverSideSessionId unique: true ``` Then configure the callback in your server-side sessions config: ```dart ServerSideSessionsConfigFromPasswords( onSessionCreated: ( final session, { required final authUserId, required final serverSideSessionId, required final transaction, }) async { await SessionMetadata.db.insertRow( session, SessionMetadata( serverSideSessionId: serverSideSessionId, name: 'general-session', ipAddress: session.request?.connectionInfo.remote.address.toString(), userAgent: session.request?.headers.userAgent, ), transaction: transaction, ); }, ), ``` To revoke sessions based on your custom metadata, query the metadata table for the session IDs you want to revoke and call `revokeToken` for each: ```dart final sessionMetadata = await SessionMetadata.db.find( session, where: (final row) => row.deviceName.equals('Old Device'), ); for (final row in sessionMetadata) { await AuthServices.instance.tokenManager.revokeToken( session, tokenId: row.serverSideSessionId.toString(), ); } ``` #### Attaching metadata when creating tokens from an endpoint The `onSessionCreated` callback is global and runs for every new session, including those created by identity providers. An endpoint that creates a personal access token (PAT) or CLI token often has its own parameters, such as a token name or label, which the callback can't see. In that case, create the token with `AuthServices.instance.tokenManager.createToken` rather than `issueToken`, which is for [sign-in flows](https://docs.serverpod.dev/concepts/authentication/token-managers/managing-tokens.md#issuing-tokens). Then use the returned `AuthSuccess.serverSideSessionId` to insert your metadata with the endpoint's parameters: ```dart final authSuccess = await AuthServices.instance.tokenManager.createToken( session, authUserId: userId, method: 'pat', scopes: {Scope.admin}, ); await SessionMetadata.db.insertRow( session, SessionMetadata( serverSideSessionId: authSuccess.serverSideSessionId, name: tokenName, // from your endpoint parameter deviceName: deviceName, // from your endpoint parameter ipAddress: session.request?.connectionInfo.remote.address.toString(), userAgent: session.request?.headers.userAgent, ), ); ``` ## Client-side configuration The `ServerSideSessionsTokenManager` needs no extra configuration in your app. The client includes the session token in requests automatically. If the session expires or is revoked, the client signs the user out the next time it validates the session. Your app decides what to show next, for example by listening to [authentication state changes](https://docs.serverpod.dev/concepts/authentication/basics.md#monitor-authentication-changes). ## Related - [Managing tokens](https://docs.serverpod.dev/concepts/authentication/token-managers/managing-tokens.md): issue, validate, revoke, and list tokens. - [JWT token manager](https://docs.serverpod.dev/concepts/authentication/token-managers/jwt-token-manager.md): the stateless alternative. - [Setup](https://docs.serverpod.dev/concepts/authentication/setup.md): where token managers are configured. --- # Authentication UI components https://docs.serverpod.dev/concepts/authentication/ui-components The authentication module ships with UI components and controllers for building sign-in interfaces. This page covers the all-in-one `SignInWidget`: how to control which providers it shows, how to style its buttons, and how to translate its texts. Each provider also has its own widgets and controllers for building a custom UI, covered on the provider pages. ## SignInWidget The `SignInWidget` is an all-in-one widget that automatically detects available authentication providers and displays the appropriate sign-in options. The [setup page](https://docs.serverpod.dev/concepts/authentication/setup.md#present-the-authentication-ui) shows the full wiring. In short: ```dart SignInWidget( client: client, onAuthenticated: () { // Runs after a successful sign-in. Do not navigate here; listen to // authentication state changes instead. }, onError: (error) { // Show the error to the user. }, ) ``` ### Disabling providers You can hide specific providers in the app while keeping them available on the server. This is useful when you want to phase out a provider but keep compatibility with older app versions. The reverse is not possible. A provider only appears when it is configured on the server, as described in [identity providers configuration](https://docs.serverpod.dev/concepts/authentication/setup.md#identity-providers-configuration). ```dart SignInWidget( client: client, disableEmailSignInWidget: false, disableGoogleSignInWidget: false, disableAppleSignInWidget: true, // Disable Apple sign-in onAuthenticated: () { // Do something when the user is authenticated. // // NOTE: You should not navigate to the home screen here, otherwise // the user will have to sign in again every time they open the app. }, ) ``` ### Customizing SignInWidget The `SignInWidget` builds a default widget for each identity provider it detects. Pass your own widget for a provider to replace the default one: ```dart final signInWidget = SignInWidget( client: client, emailSignInWidget: EmailSignInWidget( client: client, startScreen: EmailFlowScreen.login, // NOTE: When you opt-out of the internal widget, you need to provide your // own `onAuthenticated` and `onError` callbacks. onAuthenticated: _onAuthenticated, onError: _onError, // ... custom configuration ), googleSignInWidget: GoogleSignInWidget( client: client, scopes: const [ ...GoogleAuthController.defaultScopes, 'https://www.googleapis.com/auth/youtube', ], onAuthenticated: _onAuthenticated, onError: _onError, // ... custom configuration ), onAuthenticated: _onAuthenticated, onError: _onError, ); void _onAuthenticated() { // Handle successful authentication } void _onError(Object error) { // Handle errors } ``` ### Styling the buttons Inside `SignInWidget`, every provider button shares one neutral, theme-aware appearance. To change it, pass a `SignInButtonStyle` as `buttonStyle`: ```dart SignInWidget( client: client, buttonStyle: const SignInButtonStyle( shape: SignInButtonShape.rounded, text: SignInButtonTextVariant.signInWith, ), onAuthenticated: _onAuthenticated, onError: _onError, ) ``` Fields set on `buttonStyle` apply to the provider buttons, and they also override the same-named arguments on a custom provider widget you pass to `SignInWidget`. Fields left unset fall through to the widget's own arguments. Brand style presets, such as `GoogleButtonStyle.filledBlack`, only apply when a provider widget is used on its own, outside `SignInWidget`. For all options of each provider widget, see the provider's customizations page, which also covers building a custom UI with the provider's controller. For example, see [the Google provider](https://docs.serverpod.dev/concepts/authentication/providers/google/customizations.md#customize-the-sign-in-button) or [the email provider](https://docs.serverpod.dev/concepts/authentication/providers/email/customizing-the-ui.md). ## Localization All text shown by the authentication widgets is in English by default. To replace it, wrap `SignInWidget`, or any individual provider widget, in a `SignInLocalizationProvider` and pass the text objects for the parts you want to translate: - `BasicSignInTexts` for the shared messages and dividers. - `EmailSignInTexts` for the email flow: sign-in, registration, account verification, password reset, and the terms and privacy labels. - `PasswordRequirementTexts` for the labels of the built-in password requirement widgets. - One text object per provider for the sign-in buttons: `AppleSignInTexts`, `GoogleSignInTexts`, `GitHubSignInTexts`, `MicrosoftSignInTexts`, `FacebookSignInTexts`, and `AnonymousSignInTexts`. :::note The Firebase provider has no texts because it only supplies a controller rather than a finished UI. ::: The example below shows how to pass every text object explicitly: ```dart const basicTexts = BasicSignInTexts( noAuthenticationProvidersConfigured: 'No authentication providers configured', orContinueWith: 'or continue with', ); const emailTexts = EmailSignInTexts( title: 'Sign In with email', forgotPassword: 'Forgot password?', signIn: 'Sign in', dontHaveAnAccount: "Don't have an account?", signUp: 'Sign up', signUpTitle: 'Sign Up with email', continueAction: 'Continue', alreadyHaveAnAccount: 'Already have an account?', verifyAccountTitle: 'Verify account', verifyResetCodeTitle: 'Verify reset code', verificationMessage: 'A verification email has been sent. Please check your email and ' 'enter the verification code below.', verify: 'Verify', setAccountPasswordTitle: 'Set account password', passwordLabel: 'Password', backToSignUp: 'Back to sign up', setNewPasswordTitle: 'Set new password', newPasswordLabel: 'New Password', resetPasswordTitle: 'Reset password', resetPasswordDescription: 'Enter the email address to reset password.', requestPasswordReset: 'Request password reset', resetPassword: 'Reset password', backToSignIn: 'Back to sign in', emailLabel: 'Email', termsIntro: 'I have read and accept the ', termsAndConditions: 'Terms and Conditions', andText: ' and ', privacyPolicy: 'Privacy Policy', ); const passwordTexts = PasswordRequirementTexts( minLengthTemplate: 'At least {length} characters', maxLengthTemplate: 'At most {length} characters', containsLowercase: 'Contains at least one lowercase letter', containsUppercase: 'Contains at least one uppercase letter', containsNumber: 'Contains at least one number', containsSpecialCharacter: 'Contains at least one special character', ); SignInLocalizationProvider( basic: basicTexts, email: emailTexts, passwordRequirementTexts: passwordTexts, apple: const AppleSignInTexts(signInButton: 'Sign in with Apple'), google: const GoogleSignInTexts(signInButton: 'Sign in with Google'), github: const GitHubSignInTexts(signInButton: 'Sign in with GitHub'), microsoft: const MicrosoftSignInTexts(signInButton: 'Sign in with Microsoft'), facebook: const FacebookSignInTexts(signInButton: 'Sign in with Facebook'), anonymous: const AnonymousSignInTexts(signInButton: 'Sign in as guest'), child: SignInWidget( client: client, onAuthenticated: _onAuthenticated, onError: _onError, ), ); ``` The `{length}` placeholder in the two length templates is replaced with the configured limit, so keep it in the translated string. ### Translating part of the text Any text object you leave out keeps its built-in English text, so you only need to pass the ones you are translating. To change a few values inside an object, start from its `defaults` constant: ```dart SignInLocalizationProvider( email: EmailSignInTexts.defaults.copyWith( title: 'Sign in with email', signIn: 'Log in', ), child: signInWidget, ); ``` ### Switching locale The `SignInLocalizationProvider` widget hands the text objects to the widgets below it. It does not read Flutter's current `Locale` or pick a translation for you. In an app that supports several locales, select the text objects with your existing localization setup and rebuild `SignInLocalizationProvider` when the locale changes. ## Related - [Setup](https://docs.serverpod.dev/concepts/authentication/setup.md): wire the sign-in UI into your app. - [The basics](https://docs.serverpod.dev/concepts/authentication/basics.md): react to authentication state changes. - [Custom overrides](https://docs.serverpod.dev/concepts/authentication/custom-overrides.md): replace the built-in UI and endpoints entirely. --- # Manage user profile photos https://docs.serverpod.dev/concepts/authentication/profile-photos Add profile photo upload and display to your Serverpod app using the authentication module's built-in image handling. When you're done, signed-in users can upload a photo from Flutter, and your app displays it from a public URL. Profile photos are stored in **public** storage, square-cropped and resized on the server, and exposed as a public HTTP URL on `UserProfileModel.imageUrl`. The authentication module registers a read-only `userProfileInfo` endpoint by default. You expose the upload methods yourself. ## Before you start - [Authentication is set up](https://docs.serverpod.dev/concepts/authentication/setup.md) and users can sign in. - Users have a `UserProfile` (created automatically by most identity providers on first sign-in). - You have run `serverpod generate` at least once, or started the server with `serverpod start`. For the upload UI, add these Flutter dependencies: ```yaml title="pubspec.yaml" dependencies: image_picker: ^1.0.0 image: ^4.0.15 file_picker: '>=8.1.0 <11.0.0' # web image_cropper: '>=7.0.0 <13.0.0' # mobile, optional ``` ## Expose the profile edit endpoint The authentication module registers `userProfileInfo` (read-only `get()`). Upload methods live on `UserProfileEditBaseEndpoint`, which you must expose on your server. Create a concrete endpoint: ```dart title="lib/src/endpoints/user_profile_endpoint.dart" import 'package:serverpod/serverpod.dart'; import 'package:serverpod_auth_idp_server/core.dart'; /// Endpoint to view and edit the signed-in user's profile. class UserProfileEndpoint extends UserProfileEditBaseEndpoint {} ``` Run `serverpod generate`, or let `serverpod start` do it for you. Your app then calls the methods on `client.userProfile` (the accessor matches your endpoint class name): | Method | Returns | Auth required | | ---------------------------------- | ------------------ | ------------- | | `get()` | `UserProfileModel` | Yes | | `setUserImage(ByteData image)` | `UserProfileModel` | Yes | | `removeUserImage()` | `UserProfileModel` | Yes | | `changeUserName(String? userName)` | `UserProfileModel` | Yes | | `changeFullName(String? fullName)` | `UserProfileModel` | Yes | ```dart final profile = await client.userProfile.setUserImage(byteData); print(profile.imageUrl); // Uri?, the public URL of the new image ``` To fetch the profile without exposing edit methods, use the module endpoint: ```dart final profile = await client.modules.serverpod_auth_core.userProfileInfo.get(); ``` ### Restrict who can edit The built-in endpoint always acts on the signed-in user, so any signed-in user can edit their own photo. To add your own rules, override the method on your endpoint class. The legacy `serverpod_auth` module had a `userCanEditUserImage` flag for this; `serverpod_auth_idp` does not. ```dart class UserProfileEndpoint extends UserProfileEditBaseEndpoint { @override Future setUserImage(Session session, ByteData image) async { if (!session.authenticated!.scopes.contains(MyScope.canEditProfile)) { throw MyForbiddenException(); } return super.setUserImage(session, image); } } ``` ## Upload a profile photo from Flutter There is no built-in upload widget in the authentication module. Pick an image in the app, convert it to `ByteData`, and pass it to `setUserImage`. The server decodes the image, crops it to a square, resizes it, and stores it. The format you send does not affect the stored format. ```dart import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:image/image.dart' as img; import 'package:image_picker/image_picker.dart'; /// Picks an image, optionally resizes it, and returns [ByteData] for upload. /// The server crops to square and resizes again. Client-side prep is for UX and bandwidth. Future pickProfileImageBytes({int maxSize = 512}) async { Uint8List? rawBytes; if (kIsWeb) { final result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['jpg', 'jpeg', 'png'], ); rawBytes = result?.files.first.bytes; } else { final picked = await ImagePicker().pickImage(source: ImageSource.gallery); if (picked != null) rawBytes = await picked.readAsBytes(); } if (rawBytes == null) return null; final decoded = img.decodeImage(rawBytes); if (decoded == null) return null; // invalid image; show an error to the user final resized = decoded.width > maxSize || decoded.height > maxSize ? img.copyResizeCropSquare(decoded, size: maxSize) : decoded; final encoded = img.encodePng(resized); return ByteData.view(Uint8List.fromList(encoded).buffer); } ``` Call the endpoint after picking: ```dart Future uploadProfilePhoto(Client client) async { final bytes = await pickProfileImageBytes(); if (bytes == null) return null; return client.userProfile.setUserImage(bytes); } ``` On iOS and Android, use `image_cropper` with a circular crop before upload for better UX. Cropping is not available on web with this pattern. ### Error handling | Failure | What happens | | ------------------------------ | ------------------------------------------------------------------------------------- | | User not signed in | The call fails and surfaces as `ServerpodClientUnauthorized` in the app | | No profile exists | The server throws `UserProfileNotFoundException`. The app sees a generic server error | | Invalid or corrupt image bytes | Server throws when image decode fails | | User cancels picker | Return `null` in the app. No server call is made | ## Display the profile photo The `UserProfileModel.imageUrl` field is a public `Uri?`. When it is `null`, show a placeholder. ```dart final profile = await client.userProfile.get(); // Or: await client.modules.serverpod_auth_core.userProfileInfo.get(); final imageUrl = profile?.imageUrl?.toString(); CircleAvatar( backgroundImage: imageUrl != null ? NetworkImage(imageUrl) : null, child: imageUrl == null ? const Icon(Icons.person) : null, ) ``` For caching, use `cached_network_image` or `extended_image`. The built-in endpoint methods always act on the signed-in user, including the read-only `get`. To show another user's photo, fetch their `UserProfileModel` from your own endpoint and display its `imageUrl` the same way. The URL itself is not access-controlled, so anything holding it can load the image. ## Remove or replace a photo Remove the photo (`imageUrl` becomes `null`): ```dart final profile = await client.userProfile.removeUserImage(); ``` Unlike [legacy serverpod\_auth](https://docs.serverpod.dev/concepts/authentication/legacy/working-with-users.md), removing a photo does **not** restore a generated default avatar. Show a placeholder in your UI, or call `setDefaultUserImage` on signup (see below). Replace a photo by calling `setUserImage` again with new bytes. The server creates a new stored file with a new random suffix. ## Configure image size and format Pass `UserProfileConfig` to `initializeAuthServices` in `server.dart`: | Setting | Default | Description | | -------------------- | --------------------------- | ----------------------------------------------- | | `userImageSize` | `256` | Output width and height in pixels (square) | | `userImageFormat` | `UserProfileImageType.jpg` | Stored format (`.jpg` or `.png`) | | `userImageQuality` | `70` | JPG quality (ignored for PNG) | | `userImageGenerator` | `defaultUserImageGenerator` | Used by `setDefaultUserImage()` | | `imageFetchFunc` | `http.get` | Used when importing from a URL (social sign-in) | ```dart pod.initializeAuthServices( userProfileConfig: UserProfileConfig( userImageSize: 512, userImageFormat: UserProfileImageType.png, userImageQuality: 85, userImageGenerator: defaultUserImageGenerator, onAfterUserProfileCreated: (session, profile, {required transaction}) async { await AuthServices.instance.userProfiles.setDefaultUserImage( session, profile.authUserId, transaction: transaction, ); }, ), identityProviderBuilders: [...], tokenManagerBuilders: [...], ); ``` The `defaultUserImageGenerator` function produces a solid-colored square with the first letter of the user name. It looks circular when you display it in a circular widget such as `CircleAvatar`. The server automatically validates and optimizes uploads: it decodes bytes (must be a valid image), square-crops to `userImageSize`, re-encodes as JPG or PNG per config, and stores the file in public storage. Recommended checks in the app (not enforced by the server): - Allow only JPG and PNG extensions. - Reject files over roughly 5 to 10 MB before upload. - Verify `decodeImage` succeeds before calling `setUserImage`. Providers such as Google import a profile photo whenever the user signs in, the provider returns a photo, and the user has no image set. This is not limited to the first sign-in, so a photo the user removed can come back on their next sign-in with that provider. ## Configure storage for production In development, profile images use the default database-backed public storage. No extra setup is required. Public URLs look like: ```text http://localhost:8080/serverpod_cloud_storage?method=file&path=serverpod/user_images/{authUserId}-{id}.jpg ``` Set `publicHost`, `publicPort`, and `publicScheme` in `config/development.yaml` to match how clients reach your API server. For production, configure object storage (S3, Google Cloud Storage, or R2) for `storageId: 'public'`. See [Uploading files](https://docs.serverpod.dev/concepts/endpoints-and-apis/file-uploads.md). Profile images require publicly accessible URLs because clients load them directly over HTTP. ## Use server-side APIs for custom logic In endpoints or callbacks, use `AuthServices.instance.userProfiles`: ```dart import 'package:serverpod_auth_idp_server/core.dart'; // Set from bytes (same as the endpoint) await AuthServices.instance.userProfiles.setUserImageFromBytes( session, authUserId, imageBytes, ); // Import from a URL (e.g. after OAuth) await AuthServices.instance.userProfiles.setUserImageFromUrl( session, authUserId, Uri.parse('https://example.com/photo.jpg'), ); // Generate and store a default avatar await AuthServices.instance.userProfiles.setDefaultUserImage(session, authUserId); // Remove image await AuthServices.instance.userProfiles.removeUserImage(session, authUserId); // Read profile in any endpoint final profile = await session.authenticated!.userProfile(session); ``` ## Verify 1. Sign in to your app. 2. Call `client.userProfile.setUserImage(byteData)` with a test PNG or JPG. 3. Confirm the returned `imageUrl` is non-null and starts with your server's public host. 4. Open `imageUrl` in a browser. The image loads. 5. Confirm your UI updates after upload. 6. Call `removeUserImage()`. The `imageUrl` field becomes `null` and your placeholder appears. ## Troubleshooting ### `setUserImage` is missing on the client Create `UserProfileEndpoint extends UserProfileEditBaseEndpoint` and run `serverpod generate`. ### `UserProfileNotFoundException` in the server logs The signed-in user has no profile yet. Ensure your identity provider creates one on first sign-in. The app only sees a generic server error, so check the server logs for this exception. ### Image URL does not load Check that `publicHost`, `publicPort`, and `publicScheme` under `apiServer` in your server config match how apps reach the API server. ### Invalid image error The bytes are corrupt or not an image. Validate with `decodeImage` on the client before upload. ### Image looks low quality Default JPG quality is 70. Raise `userImageQuality` or use PNG format in `UserProfileConfig`. ### Web upload fails silently The `file_picker` package may return null bytes. Ensure you read bytes correctly from the picked file. ## Related - [Working with users](https://docs.serverpod.dev/concepts/authentication/working-with-users.md): profiles, names, and the edit endpoint overview - [Authentication setup](https://docs.serverpod.dev/concepts/authentication/setup.md): initial configuration of the authentication module - [Uploading files](https://docs.serverpod.dev/concepts/endpoints-and-apis/file-uploads.md): cloud storage for production - [Legacy: displaying or editing user images](https://docs.serverpod.dev/concepts/authentication/legacy/working-with-users.md): legacy `serverpod_auth` widgets --- # Set up authentication on macOS https://docs.serverpod.dev/concepts/authentication/macos-authentication Serverpod stores sign-in tokens in the macOS Keychain so sessions survive app restarts. Sandboxed macOS apps need a Keychain Sharing entitlement before they can read or write that data. This guide adds the entitlement to your Flutter project. It takes a few minutes and applies to every sign-in method (email, Google, Apple, and others). ## Before you start - A Serverpod project with the [authentication module](https://docs.serverpod.dev/concepts/authentication/setup.md) enabled (the default when you create a project with `serverpod create`). - A Flutter app that calls `client.auth.initialize()` on startup (included in the project template). - Xcode installed, if you prefer adding the entitlement through the Xcode UI. ## Add the Keychain Sharing entitlement Add the following to both entitlements files in your Flutter app: - `macos/Runner/DebugProfile.entitlements` - `macos/Runner/Release.entitlements` ```xml title="macos/Runner/DebugProfile.entitlements" keychain-access-groups ``` Copy the same block into `macos/Runner/Release.entitlements`. The empty array enables Keychain access without sharing keychain items with other apps. Leave the array empty unless you have a specific reason to add a group identifier. Rebuild and run your app: ```bash cd _flutter flutter run -d macos ``` ## Verify 1. Sign in through any configured provider (email is the quickest to test). 2. Confirm the app shows your signed-in content without an error. 3. Stop and restart the app. The user should still be signed in. If sign-in still fails, see [Troubleshooting](#troubleshooting). ## Advanced ### Add the entitlement in Xcode 1. Open `macos/Runner.xcworkspace` in Xcode. 2. Select the **Runner** target, then open **Signing & Capabilities**. 3. Click **+ Capability** and add **Keychain Sharing**. 4. Repeat for **Debug**, **Profile**, and **Release** build configurations if they use separate entitlements files. Xcode writes the same Keychain Sharing entry (`keychain-access-groups`) into your entitlements files. ### Use the login keychain for local development If you run unsigned macOS builds locally and want to avoid Keychain Sharing entitlements entirely, configure secure storage to use the login keychain instead of the Data Protection Keychain. Override the session manager storage where `initializeClient()` creates the client in your app's `lib/client.dart`: ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; client = Client(await serverUrl) ..connectivityMonitor = FlutterConnectivityMonitor() ..authSessionManager = FlutterAuthSessionManager( storage: SecureClientAuthSuccessStorage( secureStorage: const FlutterSecureStorage( mOptions: MacOsOptions( usesDataProtectionKeychain: false, ), ), ), ); ``` This path requires `flutter_secure_storage` 10.3.0 or later, where `usesDataProtectionKeychain: false` started taking effect. Projects created with `serverpod create` already resolve a compatible version. If your project was created earlier, run `flutter pub upgrade flutter_secure_storage` to update the lockfile. The login keychain works for local development. For production macOS builds, prefer the Keychain Sharing entitlement in [Add the Keychain Sharing entitlement](#add-the-keychain-sharing-entitlement) above. :::warning If you add a non-empty value to `keychain-access-groups` (for example, `$(AppIdentifierPrefix)com.example.app`), Xcode may require an Apple Developer account and a provisioning profile to sign the app. ::: ## Troubleshooting ### Sign-in fails with "A required entitlement isn't present" **Problem:** After entering valid credentials, sign-in fails with an error similar to: ```text PlatformException(-34018, A required entitlement isn't present, ...) ``` The server accepts the login, but the Flutter app cannot save the session. **Cause:** The macOS app is missing the Keychain Sharing entitlement (`keychain-access-groups`). When you create a project with `serverpod create`, the CLI adds network entitlements automatically, but not Keychain access. **Resolution:** Follow [Add the Keychain Sharing entitlement](#add-the-keychain-sharing-entitlement) above. Rebuild the app after editing the entitlements files. ### Session is lost after restarting the app **Problem:** Sign-in succeeds, but the user is signed out after closing and reopening the app. **Cause:** The app cannot read the stored session from the Keychain, usually because the entitlement is missing or the app was not rebuilt after adding it. **Resolution:** Confirm both entitlements files contain the Keychain Sharing entry (`keychain-access-groups`), rebuild with `flutter run -d macos`, then sign in again and restart the app. ## Related - [Get started with authentication](https://docs.serverpod.dev/concepts/authentication/get-started.md): turn on the sign-in screen and test your first login - [Authentication basics](https://docs.serverpod.dev/concepts/authentication/basics.md#client-side-authentication): check sign-in state and listen for auth changes on the client - [flutter\_secure\_storage macOS setup](https://pub.dev/packages/flutter_secure_storage#configure-macos-version): platform details for secure storage on macOS --- # Set up authentication on the web https://docs.serverpod.dev/concepts/authentication/web-authentication Browsers have no secure storage: anything kept in `localStorage`, `sessionStorage`, or IndexedDB is readable by any JavaScript running on the page, so a single XSS vulnerability can steal a signed-in user's token and replay it from anywhere. Serverpod's cookie mode keeps web tokens in `httpOnly` cookies instead, which scripts cannot read. Cookie mode is opt-in and recommended for any app with signed-in users on the web. Native and desktop apps are unaffected: they keep their tokens in secure OS storage (such as the Keychain) and need no changes. ## Before you start - A Serverpod project with the [authentication module](https://docs.serverpod.dev/concepts/authentication/setup.md) enabled. - Your app served over `https` in production. Auth cookies are marked `Secure` by default; relax this only for `http://localhost` during development. ## Configure the server Enable cookie auth by adding an `authCookie` section to your server configuration (`config/development.yaml`, `config/production.yaml`, and so on), together with the list of origins your web app is served from: ```yaml authCookie: sameSite: lax allowedOrigins: - https://app.example.com ``` For local development, use `secure: false` in `config/development.yaml` so the cookie is sent over `http://localhost`. The section must contain at least one field: an `authCookie:` key with no children is read as unset, and cookie auth stays off. `allowedOrigins` is required when `authCookie` is set: it backs the CSRF origin checks and credentialed CORS, which cannot use a wildcard origin. List every browser origin that calls your server. With cookie auth enabled, browsers on origins that are not in the list lose cross-origin access, including to public endpoints. Each field and its default: | Field | Default | Purpose | | ------------- | ---------------- | ----------------------------------------------------------- | | `name` | `serverpod_auth` | Name of the auth cookie. | | `refreshName` | `_refresh` | Name of the JWT refresh cookie. | | `domain` | host-only | Set to `example.com` to share the cookie across subdomains. | | `path` | `/` | Cookie path; also the base path behind a reverse proxy. | | `secure` | `true` | Set to `false` only for `http://localhost` development. | | `sameSite` | `lax` | `lax`, `strict`, or `none` (`none` requires `secure`). | Each field can also be set through environment variables (`SERVERPOD_AUTH_COOKIE_NAME`, `SERVERPOD_AUTH_COOKIE_REFRESH_NAME`, `SERVERPOD_AUTH_COOKIE_DOMAIN`, `SERVERPOD_AUTH_COOKIE_PATH`, `SERVERPOD_AUTH_COOKIE_SECURE`, `SERVERPOD_AUTH_COOKIE_SAME_SITE`, and `SERVERPOD_ALLOWED_ORIGINS`), which override the YAML values. ## Configure the client Turn on cookie transport when the app runs on the web. Set `cookieAuth` where `initializeClient()` creates the client in your app's `lib/client.dart`, before the app makes any calls: ```dart import 'package:flutter/foundation.dart'; client = Client(await serverUrl) ..cookieAuth = kIsWeb ..connectivityMonitor = FlutterConnectivityMonitor() ..authSessionManager = FlutterAuthSessionManager(); ``` Everything else is unchanged: sign-in flows, the `client.auth` session manager, and endpoint calls work as on other platforms. Setting `cookieAuth` to `true` on a non-web platform throws, since those transports have no browser cookie jar. ## Verify 1. Sign in from your web app with the browser's developer tools open. 2. In the **Network** tab, inspect the sign-in response. It carries an `HttpOnly` `Set-Cookie` header: `serverpod_auth` holding the session token with server-side sessions, or `serverpod_auth_refresh` holding the refresh token with JWT. The cookie's token no longer appears in the response body. With JWT, the body still carries the short-lived access token, which the app keeps in memory. 3. Confirm no token appears in **Local Storage** for your app's origin. 4. Reload the page. The user is still signed in. If sign-in fails, check that every browser origin is listed in `allowedOrigins`, and on `http://localhost` that `authCookie.secure` is `false`. If the app throws `StateError: cookieAuth is enabled but the server returned the auth token in the response body`, the server's `authCookie` section is missing or has no fields; add at least one. ## How it works - With **server-side sessions**, the session token is delivered as an `httpOnly` cookie and never appears in the response body. - With **JWT**, the access token is kept in memory only, and the refresh token is delivered as an `httpOnly` cookie scoped to the refresh endpoint's path. On page load, the session is restored by refreshing from the cookie. Multiple tabs coordinate their refreshes through the browser's Web Locks API, so a shared refresh token is only rotated by one tab at a time. - **Signing out** clears the cookies and revokes the session on the server. - **Method streams** authenticate from the cookie at the WebSocket handshake. When the signed-in user changes (sign-in or sign-out), open method streams are closed gracefully (subscriptions receive `onDone` without an error) and new streams connect with the current identity. This applies on every platform, not only the web. - **Switching users requires a sign-out first.** Signing in as a different user from an already-authenticated session is rejected with a `SignInWhileAuthenticatedException` on all platforms. ## Cross-site request protection Cookie auth is protected against CSRF in layers: cookies default to `SameSite=Lax`, the server validates the request `Origin` against `allowedOrigins`, and the cookie only authenticates requests carrying a marker header set by the client, which a cross-site form cannot add without a CORS preflight. Nothing needs configuring beyond `allowedOrigins`. Set `sameSite: none` only if your app is embedded cross-site, and keep `secure: true` with it. ## Cross-subdomain cookies To share the signed-in session between `app.example.com` and other subdomains, set `authCookie.domain` to the registrable domain: ```yaml authCookie: domain: example.com ``` By default the cookie is host-only. ## Local development - On `http://localhost`, set `authCookie.secure: false` so the browser accepts the cookies. - On a plain-`http` LAN address (testing from another device), browsers disable the Web Locks API outside secure contexts, so cross-tab refresh coordination is off. The client logs a warning, and refreshing in several tabs at the same moment can occasionally sign the user out. This is an artifact of the insecure test origin; production `https` deployments are unaffected. ## Related - [Setup](https://docs.serverpod.dev/concepts/authentication/setup.md): install and configure the authentication module. - [Token managers](https://docs.serverpod.dev/concepts/authentication/token-managers/managing-tokens.md): choose between JWT and server-side sessions. - [Streaming](https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming.md): how method streams work. --- # Custom authentication overrides https://docs.serverpod.dev/concepts/authentication/custom-overrides The `serverpod_auth_idp` module covers most authentication needs, but you can replace it when you have requirements it does not meet. A custom override means two things: a server-side handler that turns a token into a signed-in user, and an app-side provider that sends that token with every request. This page covers both. ## Server setup With a custom override you decide how users are stored and how tokens are issued and validated. ### Token validation The token validation is performed by providing a custom `AuthenticationHandler` callback when initializing Serverpod. The callback should return an `AuthenticationInfo` object if the token is valid, otherwise `null`. ```dart final pod = Serverpod( args, authenticationHandler: (Session session, String token) async { /// Custom validation handler if (token != 'valid') return null; return AuthenticationInfo('1', {}, authId: 'device-1'); }, ); ``` In the above example, the `authenticationHandler` callback is overridden with a custom validation method. The method returns an `AuthenticationInfo` object with `userIdentifier` `"1"` and no scopes if the token is the literal "valid", otherwise `null`. The `AuthenticationInfo` constructor takes the user's identifier as a `String` (an empty string throws). The required `authId` field links the authentication to a specific device or token, so it can be revoked individually later. :::note Inside the `authenticationHandler` callback, the `authenticated` field on the session is always `null`, since it is the handler itself that figures out who the user is. ::: #### Scopes The scopes returned from the `authenticationHandler` are used to grant access to [scope-restricted endpoints](https://docs.serverpod.dev/concepts/authentication/basics.md#authorization-on-endpoints). The `Scope` class is a simple wrapper around a nullable `String`, so you can format your scopes however you want. A JWT (JSON Web Token) implementation would normally store the scopes inside the token. After extracting them, convert each string into a `Scope` object by calling the constructor: ```dart List scopes = extractScopes(token); Set userScopes = scopes.map((scope) => Scope(scope)).toSet(); ``` ### Handling revoked authentication When a user's authentication is revoked, the server must be told so it can act on the change, for example by closing method streams. Call `session.messages.authenticationRevoked` with the message type that matches the extent of the revocation. ```dart var userIdentifier = '1'; var revokedScopes = ['write']; var message = RevokedAuthenticationScope( scopes: revokedScopes, ); await session.messages.authenticationRevoked( userIdentifier, message, ); ``` #### Parameters - `userIdentifier` - The `userIdentifier` of the `AuthenticationInfo` object to be revoked. - `message` - The revoked authentication event message. See below for the different type of messages. #### Revoked authentication messages There are three types of `RevokedAuthentication` messages that are used to specify the extent of the authentication revocation: | Message type | Description | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `RevokedAuthenticationUser` | All authentication is revoked for a user. | | `RevokedAuthenticationAuthId` | A single authentication id is revoked for the user. This should match the `authId` field in the `AuthenticationInfo` object. | | `RevokedAuthenticationScope` | List of scopes that have been revoked for a user. | ### Send the token to the app You are responsible for implementing the endpoints that authenticate the user. The example below shows the shape of such an endpoint. The `authenticateUser` and `issueMyToken` functions are placeholders for your own logic. ```dart class UserEndpoint extends Endpoint { Future login( Session session, String username, String password, ) async { var identifier = await authenticateUser(session, username, password); if (identifier == null) return null; return issueMyToken(identifier, scopes: {}); } } ``` In the above example, the `login` method authenticates the user and creates an auth token. The token is then returned to the app. ## Client setup To authenticate from your app, configure an auth key provider on the client. The client then asks the provider for an authentication header value and includes it in every request to the server. ### Configure auth key provider Auth key providers need to implement the `ClientAuthKeyProvider` interface. The provider is assigned to the client's `authKeyProvider` field after construction, as the example below does. If no provider is configured, the client will not include authentication headers in requests to the server. ```dart import 'package:serverpod_client/serverpod_client.dart'; class SimpleAuthKeyProvider implements ClientAuthKeyProvider { String? _key; @override Future get authHeaderValue async { if (_key == null) return null; return wrapAsBearerAuthHeaderValue(_key!); } Future put(String key) async { _key = key; } Future remove() async { _key = null; } } var client = Client('http://localhost:8080/') ..authKeyProvider = SimpleAuthKeyProvider() ..connectivityMonitor = FlutterConnectivityMonitor(); ``` In the above example, the `SimpleAuthKeyProvider` is configured as the client's auth key provider. The `SimpleAuthKeyProvider` stores the token in memory and wraps it as a Bearer auth header value using the `wrapAsBearerAuthHeaderValue` utility function. :::info The `SimpleAuthKeyProvider` is not practical and should only be used for testing. A secure implementation of the auth key provider is available in the `serverpod_auth_core_flutter` package. It provides safe, persistent storage for the auth token. ::: Your app is responsible for storing the token in the auth key provider. Reach the provider through the client's `authKeyProvider` field, which you have to cast back to your own type. A getter saves you from repeating the cast: ```dart SimpleAuthKeyProvider get authProvider => client.authKeyProvider as SimpleAuthKeyProvider; ``` ## Authentication schemes The token travels from the app to the server in the HTTP `authorization` header. Anyone who reads that header can use the token, so serve your production API over HTTPS. See [Security and TLS](https://docs.serverpod.dev/concepts/operations/security-and-tls.md). Serverpod does not pick a scheme for you. Whatever your auth key provider returns from `authHeaderValue` is sent as the header value. The server accepts three schemes: `Bearer`, `Basic`, and `Digest`. The scheme name is matched case-sensitively, and a header in any other scheme is rejected with a 400 response before your `AuthenticationHandler` runs. Set `validateHeaders` to `false` in your server configuration to turn that check off and receive the raw header value instead. Before calling your handler, the server unwraps the value. A `Bearer` value has its scheme prefix stripped, and a `Basic` value is base64-decoded, so your handler receives the plain token either way. Use `Bearer` for a token with no internal structure, which is what the built-in authentication module does and what the example above shows. The `Basic` scheme is only suitable for a key shaped like `username:password`, because it is rejected when the decoded value has no colon or either part is empty. ### Implementing your own authentication scheme Return the header value you want from the `authHeaderValue` getter of your `ClientAuthKeyProvider` implementation. The utility functions `wrapAsBearerAuthHeaderValue` and `wrapAsBasicAuthHeaderValue` format the token for the two common schemes. The header value must be compliant with the HTTP header format defined in RFC 9110 HTTP Semantics, 11.6.2. Authorization. See: - [HTTP Authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) - [RFC 9110, 11.6.2. Authorization](https://httpwg.org/specs/rfc9110.html#field.authorization) The example below adapts the earlier code to bearer tokens, the scheme OAuth2 uses. Client side: ```dart import 'package:serverpod_client/serverpod_client.dart'; class MyOAuthKeyProvider implements ClientAuthKeyProvider { String? _key; @override Future get authHeaderValue async { if (_key == null) return null; return wrapAsBearerAuthHeaderValue(myBearerTokenObtainer(_key!)); } Future put(String key) async { _key = key; } Future remove() async { _key = null; } } var client = Client('http://localhost:8080/') ..authKeyProvider = MyOAuthKeyProvider() ..connectivityMonitor = FlutterConnectivityMonitor(); ``` Server side: ```dart final pod = Serverpod( args, authenticationHandler: (Session session, String token) async { /// Bearer token validation handler var (uid, scopes) = myBearerTokenValidator(token); if (uid == null) return null; return AuthenticationInfo(uid, scopes, authId: token); }, ); ``` ## Related - [Setup](https://docs.serverpod.dev/concepts/authentication/setup.md): the built-in authentication module, which these overrides replace. - [The basics](https://docs.serverpod.dev/concepts/authentication/basics.md): how `requireLogin` and scopes restrict endpoints. - [Token managers](https://docs.serverpod.dev/concepts/authentication/token-managers/managing-tokens.md): the built-in token machinery. --- # Set up the legacy auth module https://docs.serverpod.dev/concepts/authentication/legacy/setup The legacy `serverpod_auth` module adds user management and sign-in with email, Google, Apple, and Firebase to existing Serverpod projects. New projects should use the [current authentication module](https://docs.serverpod.dev/concepts/authentication/setup.md) instead. To move an existing app off this module, see [Migrate from legacy auth](https://docs.serverpod.dev/upgrading/migrate-from-legacy-auth.md). It is also possible to build a [custom authentication implementation](https://docs.serverpod.dev/concepts/authentication/legacy/custom-overrides.md). ![Sign-in with Serverpod](https://github.com/serverpod/serverpod/raw/main/misc/images/sign-in.png) ## Installing the auth module Install the module on the server, client, and app as shown in the sections below. Beyond sign-in, the module handles basic user information, such as user names and profile pictures. Make sure to use the same version numbers as for Serverpod itself for all dependencies. ## Server setup Add the module as a dependency to the server project's `pubspec.yaml`. ```sh $ dart pub add serverpod_auth_server ``` Add the authentication handler to the Serverpod instance. ```dart import 'package:serverpod_auth_server/serverpod_auth_server.dart' as auth; void run(List args) async { var pod = Serverpod( args, authenticationHandler: auth.authenticationHandler, // Add this line ); ... } ``` Optionally, add a nickname for the module in the `config/generator.yaml` file. This nickname will be used as the name of the module in the code. ```yaml modules: serverpod_auth: nickname: auth ``` While still in the server project, generate the client code and endpoint methods for the auth module by running the `serverpod generate` command line tool. ```bash $ serverpod generate ``` ### Initialize the auth database After adding the module to the server project, you need to initialize the database. First you have to create a new migration that includes the auth module tables. This is done by running the `serverpod create-migration` command line tool in the server project. ```bash $ serverpod create-migration ``` Start your database container from the server project. ```bash $ docker compose up --build --detach ``` Then apply the migration by starting the server with the `apply-migrations` flag. ```bash $ dart run bin/main.dart --role maintenance --apply-migrations ``` The full migration instructions can be found in the [migration guide](https://docs.serverpod.dev/concepts/data-and-the-database/database/migrations.md). ### Configure Authentication The module comes with a default authentication configuration. To customize it, go to your main `server.dart` file, import the `serverpod_auth_server` module and set up the authentication configuration: ```dart import 'package:serverpod_auth_server/serverpod_auth_server.dart' as auth; void run(List args) async { auth.AuthConfig.set(auth.AuthConfig( minPasswordLength: 12, )); // Start the Serverpod server. await pod.start(); } ``` | **Property** | **Description** | **Default** | | :--------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------: | | **allowUnsecureRandom** | True if unsecure random number generation is allowed. If set to false, an error will be thrown if the platform does not support secure random number generation. | false | | **appleClientIds** | The client identifiers Sign in with Apple accepts identity tokens for: your app's bundle IDs and any services IDs. Sign in with Apple is disabled until this is set. | `{}` | | **appleIdentityTokenClockSkewTolerance** | How much clock drift to tolerate when checking the expiry and issue time of a Sign in with Apple identity token. | 1min | | **emailSignInFailureResetTime** | The reset period for email sign in attempts. Defaults to 5 minutes. | 5min | | **enableUserImages** | True if user images are enabled. | true | | **extraSaltyHash** | True if the server should use the accounts email address as part of the salt when storing password hashes (strongly recommended). | true | | **firebaseServiceAccountKeyJson** | Path to the Firebase service account key JSON file. Generate and download from the Firebase console. | `config/firebase_service_account_key.json` | | **importUserImagesFromGoogleSignIn** | True if user images should be imported when signing in with Google. | true | | **maxAllowedEmailSignInAttempts** | Max allowed failed email sign in attempts within the reset period. | 5 | | **maxPasswordLength** | The maximum length of passwords when signing up with email. | 128 | | **minPasswordLength** | The minimum length of passwords when signing up with email. | 8 | | **onUserCreated** | Called after a user has been created. Listen to this callback if you need to do additional setup. | - | | **onUserUpdated** | Called whenever a user has been updated. This can be when the user name is changed or if the user uploads a new profile picture. | - | | **onUserWillBeCreated** | Called when a user is about to be created, gives a chance to abort the creation by returning false. | - | | **passwordResetExpirationTime** | The time for password resets to be valid. Defaults to 15 minutes. | 15min | | **sendPasswordResetEmail** | Called when a user should be sent a reset code by email. | - | | **sendValidationEmail** | Called when a user should be sent a validation code on account setup. | - | | **userCanEditFullName** | True if users can edit their full name. | false | | **userCanEditUserImage** | True if users can update their profile images. | true | | **userCanEditUserName** | True if users can edit their user names. | true | | **userCanSeeFullName** | True if users can view their full name. | true | | **userCanSeeUserName** | True if users can view their user name. | true | | **userImageFormat** | The format used to store user images. | jpg | | **userImageGenerator** | Generator used to produce default user images. | `defaultUserImageGenerator` | | **userImageQuality** | The quality setting for images if JPG format is used. | 70 | | **userImageSize** | The size of user images. | 256 | | **userInfoCacheLifetime** | The duration which user infos are cached locally in the server. | 1min | | **validationCodeLength** | The length of the validation code used in the authentication process. This value determines the number of digits in the validation code. Values below 8 log a security warning, and values below 4 throw an `ArgumentError`. | 8 | ## Client setup Add the auth client in your client project's `pubspec.yaml`. ```yaml dependencies: ... serverpod_auth_client: 4.0.0 ``` ## App setup First, add dependencies to your app's `pubspec.yaml` file for the methods of signing in that you want to support. ```yaml dependencies: flutter: sdk: flutter serverpod_flutter: 4.0.0 auth_example_client: path: ../auth_example_client serverpod_auth_shared_flutter: 4.0.0 ``` Next, you need to set up a `SessionManager`, which keeps track of the user's state. It will also handle the authentication keys passed to the client from the server, upload user profile images, etc. ```dart late SessionManager sessionManager; late Client client; void main() async { // Need to call this as we are using Flutter bindings before runApp is called. WidgetsFlutterBinding.ensureInitialized(); // The android emulator does not have access to the localhost of the machine. // const ipAddress = '10.0.2.2'; // Android emulator ip for the host // On a real device replace the ipAddress with the IP address of your computer. const ipAddress = 'localhost'; // Sets up a singleton client object that can be used to talk to the server from // anywhere in our app. The client is generated from your server code. // The client is set up to connect to a Serverpod running on a local server on // the default port. You will need to modify this to connect to staging or // production servers. client = Client('http://$ipAddress:8080/') ..authKeyProvider = FlutterAuthenticationKeyManager() ..connectivityMonitor = FlutterConnectivityMonitor(); // The session manager keeps track of the signed-in state of the user. You // can query it to see if the user is currently signed in and get information // about the user. sessionManager = SessionManager( caller: client.modules.auth, ); await sessionManager.initialize(); runApp(MyApp()); } ``` The `SessionManager` has useful methods for viewing and monitoring the user's current state. #### Check authentication state To check if the user is signed in: ```dart sessionManager.isSignedIn; ``` Returns `true` if the user is signed in, or `false` otherwise. #### Access current user To retrieve information about the current user: ```dart sessionManager.signedInUser; ``` Returns a `UserInfo` object if the user is currently signed in, or `null` if the user is not. #### Register authentication To register a signed in user in the session manager: ```dart await sessionManager.registerSignedInUser( userInfo, keyId, authKey, ); ``` This will persist the user information and store the auth key in the client's key manager. For more details, see [Custom Providers - Client Setup](https://docs.serverpod.dev/concepts/authentication/legacy/providers/custom-providers.md#client-setup). #### Monitor authentication changes To add a listener that tracks changes in the user's authentication state, useful for updating the UI: ```dart @override void initState() { super.initState(); // Rebuild the page if authentication state changes. sessionManager.addListener(() { setState(() {}); }); } ``` The listener is triggered whenever the user's sign-in state changes. #### Sign out current device To sign the user out on from the current device: ```dart await sessionManager.signOutDevice(); ``` Returns `true` if the sign-out is successful, or `false` if it fails. #### Sign out all devices To sign the user out across all devices: ```dart await sessionManager.signOutAllDevices(); ``` Returns `true` if the user is successfully signed out from all devices, or `false` if it fails. :::info The `signOut` method was removed in the 3.0 release of the module. Use `signOutDevice` or `signOutAllDevices` instead. ::: --- # Legacy authentication basics https://docs.serverpod.dev/concepts/authentication/legacy/basics :::info This page documents the legacy `serverpod_auth` module. To move an existing app to the current authentication framework, see [Migrate from legacy auth](https://docs.serverpod.dev/upgrading/migrate-from-legacy-auth.md). ::: Serverpod automatically checks if the user is logged in and if the user has the right privileges to access the endpoint. When using the `serverpod_auth` module you will not have to worry about keeping track of tokens, refreshing them or, even including them in requests as this all happens automatically under the hood. The `Session` object provides information about the current user. A unique `userIdentifier` identifies a user. You should use this id whenever you are referring to a user. Access the id of a signed-in user through the `authenticated` getter of the `Session` object. Since the default implementation of `serverpod_auth` uses numeric IDs for the users, there is a convenience getter `userId` on the `AuthenticationInfo`, which returns the integer value. ```dart Future myMethod(Session session) async { final authenticationInfo = session.authenticated; final userIdentifier = authenticationInfo?.userIdentifier; final userId = authenticationInfo?.userId; ... } ``` You can also use the Session object to check if a user is authenticated: ```dart Future myMethod(Session session) async { var isSignedIn = session.isUserSignedIn; ... } ``` ## Requiring authentication on endpoints It is common to want to restrict access to an endpoint to users that have signed in. You can do this by overriding the `requireLogin` property of the `Endpoint` class. ```dart class MyEndpoint extends Endpoint { @override bool get requireLogin => true; Future myMethod(Session session) async { ... } ... } ``` ## Explicitly allowing unauthenticated access In some cases, you may want to explicitly allow certain endpoints or methods to be accessed without authentication. Serverpod provides the `@unauthenticatedClientCall` annotation for this purpose. When an endpoint or method is annotated with `@unauthenticatedClientCall`: - No authentication will be added to the header on the client when calling it. - The server will receive calls as if there is no user signed in. :::info Under the hood, the `@unauthenticatedClientCall` annotation makes the client omit authentication headers for calls to the annotated endpoint or method. On the server side, it ensures that the session is treated as unauthenticated for those calls, regardless of any existing authentication state. ::: You can use this annotation in two ways: 1. On the entire endpoint class to make all methods unauthenticated: ```dart @unauthenticatedClientCall class UnauthenticatedEndpoint extends Endpoint { Future someMethod(Session session) async { return session.isUserSignedIn; // Will always return false } Stream someStream(Session session) async* { yield session.isUserSignedIn; // Will always return false } } ``` 2. On specific methods to make only those methods unauthenticated: ```dart class PartiallyUnauthenticatedEndpoint extends Endpoint { @unauthenticatedClientCall Future publicMethod(Session session) async { return session.isUserSignedIn; // Will always return false } Future authenticatedMethod(Session session) async { return session.isUserSignedIn; } } ``` This is particularly useful for endpoints that must not receive authentication, such as JWT refresh endpoints. :::warning Using `@unauthenticatedClientCall` on an endpoint or method that also has `requireLogin` set to true will lead to a conflict. Since the client will suppress sending authentication information, but the server will expect it, calls to such endpoints or methods will always fail with an authentication error. ::: ## Authorization on endpoints Serverpod also supports scopes for restricting access. One or more scopes can be associated with a user. For instance, this can be used to give admin access to a specific user. To restrict access for an endpoint, override the `requiredScopes` property. Note that setting `requiredScopes` implicitly sets `requireLogin` to true. ```dart class MyEndpoint extends Endpoint { @override bool get requireLogin => true; @override Set get requiredScopes => {Scope.admin}; Future myMethod(Session session) async { ... } ... } ``` ### Managing scopes New users are created without any scopes. To update a user's scopes, use the `Users` class's `updateUserScopes` method (requires the `serverpod_auth_server` package). This method replaces all previously stored scopes. ```dart await Users.updateUserScopes(session, userId, {Scope.admin}); ``` ### Custom scopes You may need more granular access control for specific endpoints. To create custom scopes, extend the Scope class, as shown below: ```dart class CustomScope extends Scope { const CustomScope(String name) : super(name); static const userRead = CustomScope('userRead'); static const userWrite = CustomScope('userWrite'); } ``` Then use the custom scopes like this: ```dart class MyEndpoint extends Endpoint { @override bool get requireLogin => true; @override Set get requiredScopes => {CustomScope.userRead, CustomScope.userWrite}; Future myMethod(Session session) async { ... } ... } ``` :::caution Keep in mind that a scope is merely an arbitrary string and can be written in any format you prefer. However, it's crucial to use unique strings for each scope, as duplicated scope strings may lead to unintentional data exposure. ::: ## User authentication The `StatusEndpoint` class includes methods for handling user sign-outs, whether from a single device or all devices. :::info In addition to the `StatusEndpoint` methods, Serverpod provides more comprehensive tools for managing user authentication and sign-out processes across multiple devices. For more detailed information on managing and revoking authentication keys, please refer to the [Revoking authentication keys](https://docs.serverpod.dev/concepts/authentication/legacy/providers/custom-providers.md#revoking-authentication-keys) section. ::: #### Sign out device To sign out a single device: ```dart await client.modules.auth.status.signOutDevice(); ``` This status endpoint method obtains the authentication key from session's authentication information, then revokes that key. #### Sign out all devices To sign the user out across all devices: ```dart await client.modules.auth.status.signOutAllDevices(); ``` This status endpoint retrieves the user ID from session's authentication information, then revokes all authentication keys related to that user. :::info The `signOut` status endpoint was removed in the 3.0 release of the module. Use `signOutDevice` or `signOutAllDevices` instead. ::: --- # Working with users https://docs.serverpod.dev/concepts/authentication/legacy/working-with-users :::info This page documents the legacy `serverpod_auth` module. To move an existing app to the current authentication framework, see [Migrate from legacy auth](https://docs.serverpod.dev/upgrading/migrate-from-legacy-auth.md). ::: It's a common task to read or update user information on your server. You can always retrieve the id of a signed-in user through the session object. ```dart var userId = session.authenticated?.userId; ``` If you sign in users through the auth module, you will be able to retrieve more information through the static methods of the `Users` class. ```dart var userInfo = await Users.findUserByUserId(session, userId!); ``` The `UserInfo` is automatically populated when the user signs in. Different data may be available depending on which method was used for authentication. :::tip The `Users` class contains many other convenient methods for working with users. You can find the full documentation [here](https://pub.dev/documentation/serverpod_auth_server/latest/serverpod_auth_server/Users-class.html). ::: ## Displaying or editing user images The module has built-in methods for handling a user's basic settings, including uploading new profile pictures. ![UserImageButton](https://github.com/serverpod/serverpod/raw/main/misc/images/user-image-button.png) To display a user's profile picture, use the `CircularUserImage` widget and pass a `UserInfo` retrieved from the `SessionManager`. To edit a user profile image, use the `UserImageButton` widget. It will automatically fetch the signed-in user's profile picture and communicate with the server. --- # Email sign-in https://docs.serverpod.dev/concepts/authentication/legacy/providers/email :::info This page documents the legacy `serverpod_auth` module. To move an existing app to the current authentication framework, see [Migrate from legacy auth](https://docs.serverpod.dev/upgrading/migrate-from-legacy-auth.md). ::: To properly configure Sign in with Email, you must connect your Serverpod to an external service that can send the emails. One convenient option is the [mailer](https://pub.dev/packages/mailer) package, which can send emails through any SMTP service. Most email providers, such as Sendgrid or Mandrill, support SMTP. A comprehensive tutorial covering email/password sign-in complete with sending the validation code via email is available [here](https://medium.com/serverpod/getting-started-with-serverpod-authentication-part-1-72c25280e6e9). :::caution You need to install the auth module before you continue, see [Setup](https://docs.serverpod.dev/concepts/authentication/legacy/setup.md). ::: ## Server-side configuration In your main `server.dart` file, import the `serverpod_auth_server` module, and set up the authentication configuration: ```dart import 'package:serverpod_auth_server/serverpod_auth_server.dart' as auth; auth.AuthConfig.set(auth.AuthConfig( sendValidationEmail: (session, email, validationCode) async { // Send the validation email to the user. // Return `true` if the email was successfully sent, otherwise `false`. return true; }, sendPasswordResetEmail: (session, userInfo, validationCode) async { // Send the password reset email to the user. // Return `true` if the email was successfully sent, otherwise `false`. return true; }, )); // Start the Serverpod server. await pod.start(); ``` :::info For debugging purposes, you can print the validation code to the console. The chat module example does just this. You can view that code [here](https://github.com/serverpod/serverpod/blob/main/examples/legacy/chat/chat_server/lib/server.dart). ::: ## Client-side configuration Add the dependencies to your `pubspec.yaml` in your **client** project. ```yaml dependencies: ... serverpod_auth_client: 4.0.0 ``` Add the dependencies to your `pubspec.yaml` in your **Flutter** project. ```yaml dependencies: ... serverpod_auth_email_flutter: 4.0.0 serverpod_auth_shared_flutter: 4.0.0 ``` ### Prebuilt sign in button The package includes both methods for creating a custom email sign-in form and a pre-made `SignInWithEmailButton` widget. The widget is easy to use, all you have to do is supply the auth client. It handles everything from user signups, login, and password resets for you. ```dart SignInWithEmailButton( caller: client.modules.auth, onSignedIn: () { // Optional callback when user successfully signs in }, ), ``` ![SignInWithEmailButton](https://docs.serverpod.dev/img/authentication/providers/email/1-sign-in-with-email-button.png) ### Modal example The triggered modal will look like this: ![SignInWithEmailDialog](https://docs.serverpod.dev/img/authentication/providers/email/2-auth-email-dialog.png) ## Custom UI with EmailAuthController The `serverpod_auth_email_flutter` module provides the `EmailAuthController` class, which encapsulates the functionality for email/password authentication. You can use this class and create a custom UI for user registration, login, and password management. ```dart import 'package:serverpod_auth_email_flutter/serverpod_auth_email_flutter.dart'; final authController = EmailAuthController(client.modules.auth); ``` To let a user signup first call the `createAccountRequest` method which will trigger the backend to send an email to the user with the validation code. ```dart await authController.createAccountRequest(userName, email, password); ``` Then let the user type in the code and send it to the backend with the `validateAccount` method. This method creates the user and returns the new `UserInfo` if the code is valid. Call `signIn` afterwards to sign the user in. ```dart await authController.validateAccount(email, verificationCode); ``` To let users log in if they already have an account you can use the `signIn` method. ```dart await authController.signIn(email, password); ``` Finally to let a user reset their password you first initiate a password reset with the `initiatePasswordReset` this will trigger the backend to send a verification email to the user. ```dart await authController.initiatePasswordReset(email); ``` Let the user type in the verification code along with the new password and send it to the backend with the `resetPassword` method. ```dart await authController.resetPassword(email, verificationCode, password); ``` After the password has been reset you have to call the `signIn` method to log in. This can be achieved by either letting the user type in the details again or chaining the `resetPassword` and `signIn` methods for a seamless UX. ## Password storage security Serverpod provides some additional configurable options to provide extra layers of security for stored password hashes. :::info By default, the minimum password length is set to 8 characters. If you wish to modify this requirement, you can utilize the properties within AuthConfig. ::: ### Peppering For an additional layer of security, it is possible to configure a password hash pepper. A pepper is a server-side secret that is added, along with a unique salt, to a password before it is hashed and stored. The pepper makes it harder for an attacker to crack password hashes if they have only gained access to the database. The [recommended pepper length](https://www.ietf.org/archive/id/draft-ietf-kitten-password-storage-04.html#name-storage-2) is 32 bytes. To configure a pepper, set the `emailPasswordPepper` property in the `config/passwords.yaml` file. ```yaml development: emailPasswordPepper: 'your-pepper' ``` It is essential to keep the pepper secret and never expose it to the client. :::warning If the pepper is changed, all passwords in the database will need to be re-hashed with the new pepper. ::: ### Secure random Serverpod uses the `dart:math` library to generate random salts for password hashing. By default, the server throws an exception if no secure random number generator is available (the `allowUnsecureRandom` property in the `AuthConfig` defaults to `false`). To fall back to a cryptographically unsecure random number instead, set the `allowUnsecureRandom` property to `true`. ```dart auth.AuthConfig.set(auth.AuthConfig( allowUnsecureRandom: true, )); ``` ## Custom password hash generator It is possible to override the default password hash generator. The `AuthConfig` class allows you to provide a custom hash generator using the field `passwordHashGenerator` and a custom hash validator through the field `passwordHashValidator`. The validator receives named parameters and must return a `PasswordValidationResult`, either `PasswordValidationSuccess` or `PasswordValidationFailed`. ```dart AuthConfig( passwordHashValidator: ({ required password, required email, required hash, }) async { // Custom hash validation. Return PasswordValidationSuccess() if the // password matches the hash. return const PasswordValidationSuccess(); }, passwordHashGenerator: (password) async { // Custom hash generation. Return the generated hash. return myHashFunction(password); }, ) ``` It could be useful if you already have stored passwords that should be preserved or migrated. :::warning Using a custom hash generator will permanently disrupt compatibility with the default hash generator. ::: --- # Google sign-in https://docs.serverpod.dev/concepts/authentication/legacy/providers/google :::info This page documents the legacy `serverpod_auth` module. To move an existing app to the current authentication framework, see [Migrate from legacy auth](https://docs.serverpod.dev/upgrading/migrate-from-legacy-auth.md). ::: To set up Sign in with Google, you will need a Google account for your organization and set up a new project. For the project, you need to set up *Credentials* and *Oauth consent screen*. You will also need to add the `serverpod_auth_google_flutter` package to your app and do some additional setup depending on each platform. A comprehensive tutorial covering everything about google sign in is available [here](https://medium.com/serverpod/integrating-google-sign-in-with-serverpod-authentication-part-2-6fade3099baf). :::note Right now, we have official support for iOS, Android, and web for Google Sign In. ::: :::caution You need to install the auth module before you continue, see [Setup](https://docs.serverpod.dev/concepts/authentication/legacy/setup.md). ::: ## Create your credentials To implement Google Sign In, you need a google cloud project. You can create one in the [Google Cloud Console](https://console.cloud.google.com/). ### Enable Peoples API To be allowed to access user data and use the authentication method in Serverpod we have to enable the Peoples API in our project. [Enable it here](https://console.cloud.google.com/apis/library/people.googleapis.com) or find it yourself by, navigating to the *Library* section under *APIs & Services*. Search for *Google People API*, select it, and click on *Enable*. ### Setup OAuth consent screen The setup for the OAuth consent screen can be found [here](https://console.cloud.google.com/apis/credentials/consent) or under *APIs & Services* > *OAuth consent screen*. 1. Fill in all the required information, for production use you need a domain that adds under `Authorized` domains. 2. Add the scopes `.../auth/userinfo.email` and `.../auth/userinfo.profile`. 3. Add your email to the test users so that you can test your integration in development mode. ![Scopes](https://docs.serverpod.dev/img/authentication/providers/google/1-scopes.png) ## Server-side configuration Create the server credentials in the google cloud console. Navigate to *Credentials* under *APIs & Services*. Click *Create Credentials* and select *OAuth client ID*. Configure the OAuth client as a ***Web application***. If you have a domain add it to the `Authorized JavaScript origins` and `Authorized redirect URIs`. For development purposes we can add `http://localhost:8082` to both fields, this is the address to the web server. Download the JSON file for your web application OAuth client. This file contains both the client id and the client secret. Rename the file to `google_client_secret.json` and place it in your server's `config` directory. Alternatively, you can provide the contents of the JSON file through the environment variable `SERVERPOD_PASSWORD_serverpod_auth_googleClientSecret`. :::warning The `google_client_secret.json` contains a private key and should not be version controlled. ::: ![Google credentials](https://docs.serverpod.dev/img/6-google-credentials.jpg) ## Client-side configuration For our client-side configurations, we have to first create client-side credentials and include the credentials files in our projects. The Android and iOS integrations use the [google\_sign\_in](https://pub.dev/packages/google_sign_in) package under the hood, any documentation there should also apply to this setup. :::info Rather than using the credentails file for iOS and Android, you can pass the `clientId` and the `serverClientId` to the `signInWithGoogle` method or the `SignInWithGoogleButton` widget. The `serverClientId` is the client ID from the server credentials. ::: ### iOS Create the client credentials. Navigate to *Credentials* under *APIs & Services*. Click *Create Credentials* and select *OAuth client ID*. Configure the OAuth client as Application type ***iOS***. Fill in all the required information, and create the credentials. Then download the `plist` file rename it to `GoogleService-Info.plist` and put it inside your ios project folder. Then drag and drop it into your XCode project to include the file in your build. Open the `GoogleService-Info.plist` in your editor and add the SERVER\_CLIENT\_ID if it does not exist: ```xml ... SERVER_CLIENT_ID your_server_client_id ``` Replace `your_server_client_id` with the client id from the JSON file you put inside the config folder in the server. #### Add the URL scheme To allow us to navigate back to the app after the user has signed in we have to add the URL Scheme, the scheme is the reversed client ID of your iOS app. You can find it inside the `GoogleService-Info.plist` file. Open the `info.plist` file in your editor and add the following to register the URL Scheme. ```xml ... CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLSchemes your_reversed_client_id ``` Replace `your_reversed_client_id` with your reversed client ID. :::info If you have any social logins in your app you also need to integrate "Sign in with Apple" to publish your app to the app store. ([Read more](https://developer.apple.com/sign-in-with-apple/get-started/)). ::: ### Android Create the client credentials. Navigate to *Credentials* under *APIs & Services*. Click *Create Credentials* and select *OAuth client ID*. Configure the OAuth client as Application type ***Android***. Fill in all required information, you can get the debug SHA-1 hash by running `./gradlew signingReport` in your Android project directory. Create the credentials and download the JSON file. Put the file inside the `android/app/` directory and rename it to `google-services.json`. :::info For a production app you need to get the SHA-1 key from your production keystore! This can be done by running this command: ([Read more](https://support.google.com/cloud/answer/6158849#installedapplications\&android\&zippy=%2Cnative-applications%2Candroid)). ```bash $ keytool -list -v -keystore /path/to/keystore ``` ::: ### Web There is no need to create any client credentials for the web we will simply pass the `serverClientId` to the sign-in button. However, we have to modify the server credentials inside the google cloud console. Navigate to *Credentials* under *APIs & Services* and select the server credentials. Under `Authorized JavaScript origins` and `Authorized redirect URIs` add the domain for your Flutter app, for development, this is `http://localhost:port` where the port is the port you are using. :::info Force flutter to run on a specific port by running. ```bash $ flutter run -d chrome --web-port=49660 ``` ::: Set up the actual redirect URI where the user will navigate after the sign-in. You can choose any path you want but it has to be the same in the credentials, your server, and Flutter configurations. For example, using the path `/googlesignin`. For development inside `Authorized redirect URIs` add `http://localhost:8082/googlesignin`, in production use `https://example.com/googlesignin`. ![Google credentials](https://docs.serverpod.dev/img/authentication/providers/google/2-credentials.png) #### Serve the redirect page Register the Google Sign In route inside `server.dart`. ```dart import 'package:serverpod_auth_server/serverpod_auth_server.dart' as auth; void run(List args) async { ... pod.webServer.addRoute(auth.RouteGoogleSignIn(), '/googlesignin'); ... } ``` This page is needed for the web app to receive the authentication code given by Google. ### Flutter implementation ![Scopes](https://docs.serverpod.dev/img/authentication/providers/google/3-button.png) Add the `SignInWithGoogleButton` to your widget. ```dart import 'package:serverpod_auth_google_flutter/serverpod_auth_google_flutter.dart'; SignInWithGoogleButton( caller: client.modules.auth, serverClientId: _googleServerClientId, // needs to be supplied for the web integration redirectUri: Uri.parse('http://localhost:8082/googlesignin'), ) ``` As an alternative to adding the JSON files in your client projects, you can supply the client and server ID on iOS and Android. ```dart import 'package:serverpod_auth_google_flutter/serverpod_auth_google_flutter.dart'; SignInWithGoogleButton( caller: client.modules.auth, clientId: _googleClientId, // Client ID of the client (null on web) serverClientId: _googleServerClientId, // Client ID from the server (required on web) redirectUri: Uri.parse('http://localhost:8082/googlesignin'), ) ``` ## Calling Google APIs The default setup allows access to basic user information, such as email, profile image, and name. You may require additional access scopes, such as accessing a user's calendar, contacts, or files. To do this, you will need to: - Add the required scopes to the OAuth consent screen. - Request access to the scopes when signing in. Do this by setting the `additionalScopes` parameter of the `signInWithGoogle` method or the `SignInWithGoogleButton` widget. A full list of available scopes can be found [here](https://developers.google.com/identity/protocols/oauth2/scopes). :::info Adding additional scopes may require approval by Google. On the OAuth consent screen, you can see which of your scopes are considered sensitive. ::: On the server side, you can now access these Google APIs. If a user has signed in with Google, use the `GoogleAuth.authClientForUser` method from the `serverpod_auth_server` package to request an `AutoRefreshingAuthClient`. The `AutoRefreshingAuthClient` can be used to access Google's APIs on the user's behalf. For instance, to access the Youtube APIs, add the scope to your `SignInWithGoogleButton` in your app: ```dart SignInWithGoogleButton( ... additionalScopes: const ['https://www.googleapis.com/auth/youtube'], ) ``` On the server, you can utilize the [googleapis](https://pub.dev/packages/googleapis) package to access the Youtube API by first creating a client, then calling the API. ```dart import 'package:serverpod_auth_server/serverpod_auth_server.dart'; import 'package:googleapis/youtube/v3.dart'; final googleClient = await GoogleAuth.authClientForUser(session, userId); if (googleClient != null) { var youTubeApi = YouTubeApi(googleClient); var favorites = await youTubeApi.playlistItems.list( ['snippet'], playlistId: 'LL', // Liked List ); } else { // The user hasn't signed in with Google. } ``` --- # Apple sign-in https://docs.serverpod.dev/concepts/authentication/legacy/providers/apple :::info This page documents the legacy `serverpod_auth` module. To move an existing app to the current authentication framework, see [Migrate from legacy auth](https://docs.serverpod.dev/upgrading/migrate-from-legacy-auth.md). ::: Sign-in with Apple, requires that you have a subscription to the [Apple developer program](https://developer.apple.com/programs/), even if you only want to test the feature in development mode. A comprehensive tutorial covering Sign in with Apple is available [here](https://medium.com/serverpod/integrating-apple-sign-in-with-serverpod-authentication-part-3-f5a49d006800). :::note Right now, we have official support for iOS and MacOS for Sign in with Apple. ::: :::caution You need to install the auth module before you continue, see [Setup](https://docs.serverpod.dev/concepts/authentication/legacy/setup.md). ::: ## Server-side configuration Set `appleClientIds` on `AuthConfig` to the client identifiers your server accepts Sign in with Apple tokens for: your app's bundle IDs and any services IDs. Sign in with Apple is disabled until this is set, so every sign-in request fails. ```dart auth.AuthConfig.set(auth.AuthConfig( appleClientIds: {'com.example.myapp'}, )); ``` If you already call `AuthConfig.set`, add `appleClientIds` to that call, because `set` replaces the whole configuration. See [Setup](https://docs.serverpod.dev/concepts/authentication/legacy/setup.md) for where to set `AuthConfig`. ## Client-side configuration Add the dependency to your `pubspec.yaml` in your flutter project. ```yaml dependencies: ... serverpod_auth_apple_flutter: 4.0.0 ``` ### Config Enable the sign-in with Apple capability in your Xcode project, this is the same type of configuration for your iOS and MacOS projects respectively. ![Add capabilities](https://docs.serverpod.dev/img/authentication/providers/apple/1-xcode-add.png) ![Sign in with Apple](https://docs.serverpod.dev/img/authentication/providers/apple/2-xcode-sign-in-with-apple.png) ### Sign in button `serverpod_auth_apple_flutter` package comes with the widget `SignInWithAppleButton` that renders a nice Sign in with Apple button and triggers the native sign-in UI. ```dart import 'package:serverpod_auth_apple_flutter/serverpod_auth_apple_flutter.dart'; SignInWithAppleButton( caller: client.modules.auth, ); ``` The SignInWithAppleButton widget takes a caller parameter that you pass in the authentication module from your Serverpod client, in this case, client.modules.auth. ![Sign-in button](https://docs.serverpod.dev/img/authentication/providers/apple/3-button.png) ## Extra The `serverpod_auth_apple_flutter` implements the sign-in logic using [sign\_in\_with\_apple](https://pub.dev/packages/sign_in_with_apple). The documentation for this package should in most cases also apply to the Serverpod integration. *Note that Sign in with Apple may not work on some versions of the Simulator (iOS 13.5 works). This issue doesn't affect real devices.* --- # Firebase authentication https://docs.serverpod.dev/concepts/authentication/legacy/providers/firebase :::info This page documents the legacy `serverpod_auth` module. To move an existing app to the current authentication framework, see [Migrate from legacy auth](https://docs.serverpod.dev/upgrading/migrate-from-legacy-auth.md). ::: Serverpod uses [Firebase UI auth](https://pub.dev/packages/firebase_ui_auth) to handle authentication through Firebase. It allows you to add social sign-in types that Serverpod doesn't directly support. :::warning Serverpod automatically merges accounts that are using the same email addresses, so make sure only to allow sign-ins where the email has been verified. ::: ## Server-side configuration The server needs the service account credentials for access to your Firebase project. To create a new key go to the [Firebase console](https://console.firebase.google.com/) then navigate to `project settings > service accounts` click on `Generate new private key` and then `Generate key`. ![Service account](https://docs.serverpod.dev/img/authentication/providers/firebase/1-server-key.png) This will download the JSON file, rename it to `firebase_service_account_key.json` and place it in the `config` folder in your server project. Note that if this file is corrupt or if the name does not follow the convention here the authentication with firebase will fail. Alternatively, you can provide the contents of the JSON file through the environment variable `SERVERPOD_PASSWORD_serverpod_auth_firebaseServiceAccountKey`. :::danger The firebase\_service\_account\_key.json file gives admin access to your Firebase project, never store it in version control. ::: ## Client-side configuration To add authentication with Firebase, you must first install and initialize the Firebase CLI tools and Flutter fire. Follow the instructions [here](https://firebase.google.com/docs/flutter/setup?platform=web) for your Flutter project. ## Firebase config The short version: ```bash $ flutter pub add firebase_core firebase_auth firebase_ui_auth $ flutterfire configure ``` In the Firebase console, configure the different social sign-ins you plan to use, under `Authentication > Sign-in method`. ![Auth provider](https://docs.serverpod.dev/img/authentication/providers/firebase/2-auth-provider.png) In your `main.dart` in your flutter project add: ```dart import 'package:firebase_ui_auth/firebase_ui_auth.dart' as firebase; import 'package:firebase_core/firebase_core.dart'; import 'firebase_options.dart'; ... void main() async { ... await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); firebase.FirebaseUIAuth.configureProviders([ firebase.PhoneAuthProvider(), ]); ... runApp(const MyApp()); } ``` ## Trigger the auth UI with Serverpod Add the [serverpod\_auth\_firebase\_flutter](https://pub.dev/packages/serverpod_auth_firebase_flutter) package. ```bash $ flutter pub add serverpod_auth_firebase_flutter ``` The `SignInWithFirebaseButton` is a convenient button that triggers the sign-in flow and can be used like this: ```dart SignInWithFirebaseButton( caller: client.modules.auth, authProviders: [ firebase.PhoneAuthProvider(), ], onFailure: () => print('Failed to sign in with Firebase.'), onSignedIn: () => print('Signed in with Firebase.'), ) ``` Where `caller` is the Serverpod client you use to talk with the server and `authProviders` a list with the firebase auth providers you want to enable in the UI. You can also trigger the Firebase auth UI by calling the method `signInWithFirebase` like so: ```dart await signInWithFirebase( context: context, caller: client.modules.auth, authProviders: [ firebase.PhoneAuthProvider(), ], ); ``` Where `context` is your `BuildContext`, `caller` and `authProviders` are the same as for the button. The method returns a nullable [UserInfo](https://docs.serverpod.dev/concepts/authentication/legacy/working-with-users.md) object, if the object is null the Sign-in failed, if not the Sign-in was successful. --- # Custom providers https://docs.serverpod.dev/concepts/authentication/legacy/providers/custom-providers :::info This page documents the legacy `serverpod_auth` module. To move an existing app to the current authentication framework, see [Migrate from legacy auth](https://docs.serverpod.dev/upgrading/migrate-from-legacy-auth.md). ::: The legacy `serverpod_auth` module makes it easy to implement custom authentication providers. This allows you to leverage all the existing providers supplied by the module along with the specific providers your project requires. ## Server setup After successfully authenticating a user through a custom provider, an auth token can be created and connected to the user to preserve the authenticated user's permissions. This token is used to identify the user and facilitate endpoint authorization validation. The token can be removed when the user signs out to prevent further access. ### Connect user The authentication module provides methods to find or create users. This ensures that all authentication tokens from the same user are connected. Users can be identified either by their email through the `Users.findUserByEmail(...)` method or by a unique identifier through the `Users.findUserByIdentifier(...)` method. If no user is found, a new user can be created through the `Users.createUser(...)` method. ```dart UserInfo? userInfo; userInfo = await Users.findUserByEmail(session, email); userInfo ??= await Users.findUserByIdentifier(session, userIdentifier); if (userInfo == null) { userInfo = UserInfo( userIdentifier: userIdentifier, userName: name, email: email, blocked: false, created: DateTime.now().toUtc(), scopeNames: [], ); userInfo = await Users.createUser(session, userInfo, _authMethod); } ``` The example above tries to find a user by email and user identifier. If no user is found, a new user is created with the provided information. :::note For many authentication platforms the `userIdentifier` is the user's email, but it can also be another unique identifier such as a phone number or a social security number. ::: ### Custom identification methods If other identification methods are required you can easily implement them by accessing the database directly. The `UserInfo` model can be interacted with in the same way as any other model with a database in Serverpod. ```dart var userInfo = await UserInfo.db.findFirstRow( session, where: (t) => t.fullName.equals(name), ); ``` The example above shows how to find a user by name using the `UserInfo` model. ### Create auth token When a user has been found or created, an auth token that is connected to the user should be created. To create an auth token, call the `signInUser` method in the `UserAuthentication` class, accessible as a static method, e.g. `UserAuthentication.signInUser`. The `signInUser` method takes four arguments: the first is the session object, the second is the user ID, the third is information about the method of authentication, and the fourth is a set of scopes granted to the auth token. ```dart var authToken = await UserAuthentication.signInUser(session, userInfo.id!, 'myAuthMethod', scopes: { Scope('delete'), Scope('create'), }); ``` The example above creates an auth token for a user with the unique identifier taken from the `userInfo`. The auth token preserves that it was created using the method `myAuthMethod` and has the scopes `delete` and `create`. :::info The unique identifier for the user should uniquely identify the user regardless of authentication method. The information allows authentication tokens associated with the same user to be grouped. ::: ### Send auth token to client Once the auth token is created, it should be sent to the client. We recommend doing this using an `AuthenticationResponse`. This ensures compatibility with the client-side authentication module. ```dart class MyAuthenticationEndpoint extends Endpoint { Future login( Session session, String username, String password, ) async { // Authenticates a user with email and password. if (!authenticateUser(session, username, password)) { return AuthenticationResponse(success: false); } // Finds or creates a user in the database using the User methods. var userInfo = findOrCreateUser(session, username); // Creates an authentication key for the user. var authToken = await UserAuthentication.signInUser( session, userInfo.id!, 'myAuth', scopes: {}, ); // Returns the authentication response. return AuthenticationResponse( success: true, keyId: authToken.id, key: authToken.key, userInfo: userInfo, ); } } ``` The example above shows how to create an `AuthenticationResponse` with the auth token and user information. ### Revoking authentication keys Serverpod provides built-in methods for managing user authentication across multiple devices. These methods handle several critical security and state management processes automatically, ensuring consistent and secure authentication state across your servers. When using the authentication management methods (`signOutUser` or `revokeAuthKey`), the following key actions are automatically handled: - Closing all affected method streaming connections to maintain connection integrity. - Synchronizing authentication state across all connected servers. - Updating the session's authentication state with `session.updateAuthenticated(null)` if the affected user is currently authenticated. #### Revoking specific keys To revoke specific authentication keys, use the `revokeAuthKey` method: ```dart await UserAuthentication.revokeAuthKey( session, authKeyId: 'auth-key-id-here', ); ``` ##### Fetching and revoking an authentication key using AuthenticationInfo To revoke a specific authentication key for the current session, you can directly access the session's authentication information and call the `revokeAuthKey` method: ```dart // Fetch the authentication information for the current session var authId = session.authenticated?.authId; // Revoke the authentication key if the session is authenticated and has an authId if (authId != null) { await UserAuthentication.revokeAuthKey( session, authKeyId: authId, ); } ``` ##### Fetching and revoking a specific authentication key for a user To revoke a specific authentication key associated with a user, you can retrieve all authentication keys for that user and select the key you wish to revoke: ```dart // Fetch all authentication keys for the user var authKeys = await AuthKey.db.find( session, where: (t) => t.userId.equals(userId), ); // Revoke a specific key (for example, the last one) if (authKeys.isNotEmpty) { var authKeyId = authKeys.last.id.toString(); // Convert the ID to string await UserAuthentication.revokeAuthKey( session, authKeyId: authKeyId, ); } ``` ##### Removing specific tokens (direct deletion) ```dart await AuthKey.db.deleteWhere( session, where: (t) => t.userId.equals(userId) & t.method.equals('username'), ); ``` :::warning Directly removing authentication tokens from the `AuthKey` table bypasses necessary processes such as closing method streaming connections and synchronizing servers state. It is strongly recommended to use `UserAuthentication.revokeAuthKey` to ensure a complete and consistent sign-out. ::: #### Signing out all devices The `signOutUser` method signs a user out from all devices: ```dart await UserAuthentication.signOutUser( session, userId: 123, // Optional: If omitted, the currently authenticated user will be signed out ); ``` This method deletes all authentication keys associated with the user. ##### Signing out a specific user In this example, a specific `userId` is provided to sign out that user from all their devices: ```dart // Sign out the user with ID 123 from all devices await UserAuthentication.signOutUser( session, userId: 123, ); ``` ##### Signing out the currently authenticated user If no `userId` is provided, `signOutUser` will automatically sign out the user who is currently authenticated in the session: ```dart // Sign out the currently authenticated user await UserAuthentication.signOutUser( session, // No userId provided, signs out the current user ); ``` #### Creating a logout endpoint To sign out a user on all devices using an endpoint, the `signOutUser` method in the `UserAuthentication` class can be used: ```dart class AuthenticatedEndpoint extends Endpoint { @override bool get requireLogin => true; Future logout(Session session) async { await UserAuthentication.signOutUser(session); } } ``` ## Client setup The client must store and include the auth token in communication with the server. Luckily, the client-side authentication module handles this for you through the `SessionManager`. The session manager is responsible for storing the auth token and user information. It is initialized on client startup and will restore any existing user session from local storage. After a successful authentication where an authentication response is returned from the server, the user should be registered in the session manager through the `sessionManager.registerSignedInUser(...)` method. The session manager singleton is accessible by calling `SessionManager.instance`. ```dart var serverResponse = await caller.myAuthentication.login(username, password); if (serverResponse.success) { // Store the user info in the session manager. SessionManager sessionManager = await SessionManager.instance; await sessionManager.registerSignedInUser( serverResponse.userInfo!, serverResponse.keyId!, serverResponse.key!, ); } ``` The example above shows how to register a signed-in user in the session manager. --- # Custom authentication overrides https://docs.serverpod.dev/concepts/authentication/legacy/custom-overrides :::info This page documents the legacy `serverpod_auth` module. To move an existing app to the current authentication framework, see [Migrate from legacy auth](https://docs.serverpod.dev/upgrading/migrate-from-legacy-auth.md). ::: If the legacy `serverpod_auth` module does not fulfill your requirements, you can implement your own authentication handling. Serverpod is designed to make it easy to add custom authentication overrides. ## Server setup When running a custom auth integration it is up to you to build the authentication model and issuing auth tokens. ### Token validation The token validation is performed by providing a custom `AuthenticationHandler` callback when initializing Serverpod. The callback should return an `AuthenticationInfo` object if the token is valid, otherwise `null`. ```dart final pod = Serverpod( args, authenticationHandler: (Session session, String token) async { /// Custom validation handler if (token != 'valid') return null; return AuthenticationInfo('1', {}, authId: 'valid'); }, ); ``` In the above example, the `authenticationHandler` callback is overridden with a custom validation method. The method returns an `AuthenticationInfo` object with `userIdentifier` `"1"`, no scopes, and `authId` `"valid"` if the token is the literal "valid", otherwise `null`. :::note The `userIdentifier` passed to the `AuthenticationInfo` constructor, as the first parameter, is a `String` and must not be empty. Since the default implementation of `serverpod_auth` uses numeric IDs for the users, the legacy module provides a convenience getter `userId`, which parses the identifier and returns the integer value. ::: :::note In the authenticationHandler callback the `authenticated` field on the session will always be `null` as it is the authenticationHandler that figures out who the user is. ::: :::info The required `authId` field in the `AuthenticationInfo` object links the user to a specific authentication id, for example a device session. It is what `RevokedAuthenticationAuthId` matches against when revoking authentication for a specific device. ::: #### Scopes The scopes returned from the `authenticationHandler` is used to grant access to scope restricted endpoints. The `Scope` class is a simple wrapper around a nullable `String` in dart. This means that you can format your scopes however you want as long as they are in a String format. Normally if you implement a JWT you would store the scopes inside the token. When extracting them all you have to do is convert the String stored in the token into a Scope object by calling the constructor. ```dart List scopes = extractScopes(token); Set userScopes = scopes.map((scope) => Scope(scope)).toSet(); ``` ### Handling revoked authentication When a user's authentication is revoked, the server must be notified to respect the changes (e.g. to close method streams). Invoke the `session.messages.authenticationRevoked` method and raise the appropriate event to notify the server. ```dart var userIdentifier = '1'; var revokedScopes = ['write']; var message = RevokedAuthenticationScope( scopes: revokedScopes, ); await session.messages.authenticationRevoked( userIdentifier, message, ); ``` ##### Parameters - `userIdentifier` - The `userIdentifier` of the `AuthenticationInfo` object to be revoked. - `message` - The revoked authentication event message. See below for the different type of messages. #### Revoked authentication messages There are three types of `RevokedAuthentication` messages that are used to specify the extent of the authentication revocation: | Message type | Description | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `RevokedAuthenticationUser` | All authentication is revoked for a user. | | `RevokedAuthenticationAuthId` | A single authentication id is revoked for the user. This should match the `authId` field in the `AuthenticationInfo` object. | | `RevokedAuthenticationScope` | List of scopes that have been revoked for a user. | Each message type provides a tailored approach to revoke authentication based on different needs. ### Send token to client You are responsible for implementing the endpoints to authenticate/authorize the user. But as an example such an endpoint could look like the following. ```dart class UserEndpoint extends Endpoint { Future login( Session session, String username, String password, ) async { var identifier = authenticateUser(session, username, password); if (identifier == null) return null; return issueMyToken(identifier, scopes: {}); } } ``` In the above example, the `login` method authenticates the user and creates an auth token. The token is then returned to the client. ## Client setup Enabling authentication in the client is as simple as configuring a key manager and placing any token in it. If a key manager is configured, the client will automatically query the manager for a token and include it in communication with the server. ### Configure key manager Key managers need to implement the `AuthenticationKeyManager` interface. The interface's abstract `toHeaderValue` method formats the token for the HTTP authorization header. The class is deprecated in favor of `ClientAuthKeyProvider`, so the analyzer warns on it. It keeps working with the legacy module. The key manager is assigned to the client's `authKeyProvider` field. If no key manager is configured, the client will not include tokens in requests to the server. ```dart class SimpleAuthKeyManager extends AuthenticationKeyManager { String? _key; @override Future get() async { return _key; } @override Future put(String key) async { _key = key; } @override Future remove() async { _key = null; } @override Future toHeaderValue(String? key) async { if (key == null) return null; return wrapAsBasicAuthHeaderValue(key); } } var keyManager = SimpleAuthKeyManager(); var client = Client('http://localhost:8080/') ..authKeyProvider = keyManager ..connectivityMonitor = FlutterConnectivityMonitor(); ``` In the above example, the `SimpleAuthKeyManager` is configured as the client's authentication key manager. The `SimpleAuthKeyManager` stores the token in memory. :::info The `SimpleAuthKeyManager` is not practical and should only be used for testing. A secure implementation of the key manager is available in the `serverpod_auth_shared_flutter` package named `FlutterAuthenticationKeyManager`. It provides safe, persistent storage for the auth token. ::: The sections below use the `keyManager` reference created above to interact with the stored token. ### Store token When the client receives a token from the server, it is responsible for storing it in the key manager using the `put` method. The key manager will then include the token in all requests to the server. ```dart await keyManager.put(token); ``` In the above example, the `token` is placed in the key manager. It will now be included in communication with the server. ### Remove token To remove the token from the key manager, call the `remove` method. ```dart await keyManager.remove(); ``` The above example removes any token from the key manager. ### Retrieve token To retrieve the token from the key manager, call the `get` method. ```dart var token = await keyManager.get(); ``` The above example retrieves the token from the key manager and stores it in the `token` variable. ## Authentication schemes The module's shipped key manager, `FlutterAuthenticationKeyManager`, passes the authentication token from client to server in accordance with the HTTP `authorization` header standard, with the `basic` scheme name and encoding. This is securely transferred as the connection is TLS encrypted. The legacy module's `FlutterAuthenticationKeyManager` encodes and wraps the user-provided token in a `basic` scheme with the `wrapAsBasicAuthHeaderValue` helper. The value is automatically unwrapped on the server side before being handed to the user-provided authentication handler described above. For apps using `FlutterAuthenticationKeyManager`, this wrapping and unwrapping is invisible to application code. ### Implementing your own authentication scheme If you are implementing your own authentication and are using the `basic` scheme, note that this is supported but will be automatically unwrapped i.e. decoded on the server side before being handed to your `AuthenticationHandler` implementation. It will in this case receive the decoded auth key value after the `basic` scheme name. If you are implementing a different authentication scheme, for example OAuth 2 using bearer tokens, implement the `toHeaderValue` method of your key manager accordingly. This client-side method converts the authentication key to the format that is sent as a transport header to the server. On the server side, both `basic` and `bearer` values are automatically unwrapped before the `AuthenticationHandler` is invoked. The handler receives the bare token without the scheme prefix. Other schemes are passed through unchanged and need manual parsing in the handler. The header value must be compliant with the HTTP header format defined in RFC 9110 HTTP Semantics, 11.6.2. Authorization. See: - [HTTP Authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) - [RFC 9110, 11.6.2. Authorization](https://httpwg.org/specs/rfc9110.html#field.authorization) An approach to adding OAuth handling might make changes to the above code akin to the following. Client side: ```dart class MyOAuthKeyManager extends AuthenticationKeyManager { String? _key; @override Future get() async { return _key; } @override Future put(String key) async { _key = key; } @override Future remove() async { _key = null; } @override Future toHeaderValue(String? key) async { if (key == null) return null; return 'Bearer ${myBearerTokenObtainer(key)}'; } } var client = Client('http://localhost:8080/') ..authKeyProvider = MyOAuthKeyManager() ..connectivityMonitor = FlutterConnectivityMonitor(); ``` Server side: ```dart final pod = Serverpod( args, authenticationHandler: (Session session, String token) async { /// Bearer token validation handler var (uid, scopes, authId) = myBearerTokenValidator(token); if (uid == null) return null; return AuthenticationInfo(uid, scopes, authId: authId); }, ); ``` --- # Overview https://docs.serverpod.dev/concepts/web-server/overview Serverpod comes with a built-in web server that runs beside the API server. It serves anything that speaks plain HTTP: REST APIs and webhooks, static files, server-rendered HTML with templates or [Jaspr](https://jaspr.site), and single-page apps including Flutter web. Web requests get the same [`Session`](https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions.md) your endpoint methods receive, with full access to your database and business logic. The web server is built on the [Relic](https://github.com/serverpod/relic) framework, and its routing engine, middleware system, and typed headers are available directly. The web server and the API server answer different callers. [Endpoints](https://docs.serverpod.dev/concepts/endpoints-and-apis.md) are the typed methods your own app calls through the generated client. Web server **routes** serve everyone else: browsers, webhooks, third-party services, and anything that needs a URL. If you are building a feature for your Flutter app, write an endpoint. If something outside your app needs to reach your server over HTTP, write a route. ## Your first route New Serverpod projects set up the web server by default, with working web code in `lib/src/web/` and assets in `web/`. Here's how to add a simple JSON route: ```dart import 'dart:convert'; import 'package:serverpod/serverpod.dart'; class HelloRoute extends Route { @override Future handleCall(Session session, Request request) async { return Response.ok( body: Body.fromString( jsonEncode({'message': 'Hello from Serverpod!'}), mimeType: MimeType.json, ), ); } } ``` Register the route in your `server.dart` file before starting the server: ```dart pod.webServer.addRoute(HelloRoute(), '/api/hello'); await pod.start(); ``` Visit `http://localhost:8082/api/hello` to see your response. Port 8082 is the web server's default development port, configured per run mode in the [server configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md). :::info If your project was created with the "None" web server option, the first use of `pod.webServer` throws `Bad state: Web server is disabled`. To enable it, add the `webServer` section to your `config/.yaml` files and register at least one route before `pod.start()`. With a configuration but no routes, the web server simply does not start. ::: ## Core concepts ### Routes and handlers A **route** is a destination in your web server that handles requests and generates responses. Routes extend the `Route` base class and implement the `handleCall()` method: ```dart class ApiRoute extends Route { @override Future handleCall(Session session, Request request) async { // Your logic here return Response.ok(); } } ``` The `handleCall()` method receives: - **Session** - Access to your database, logging, and authenticated user. The web server creates a `WebCallSession` for each request and closes it when the response is sent. - **Request** - The HTTP request with headers, body, and URL information. By default, a route answers GET requests only. Pass `methods:` to the constructor to accept others: ```dart class FormRoute extends Route { FormRoute() : super(methods: {Method.get, Method.post}); // ... } ``` A request with a method the route does not accept gets an automatic `405 Method Not Allowed` response. ### Response types Each named `Response` constructor maps to an HTTP status code: | Constructor | Status | | ------------------------------ | ------ | | `Response.ok` | 200 | | `Response.noContent` | 204 | | `Response.movedPermanently` | 301 | | `Response.found` | 302 | | `Response.seeOther` | 303 | | `Response.notModified` | 304 | | `Response.badRequest` | 400 | | `Response.unauthorized` | 401 | | `Response.forbidden` | 403 | | `Response.notFound` | 404 | | `Response.contentTooLarge` | 413 | | `Response.internalServerError` | 500 | | `Response.notImplemented` | 501 | Status codes without a named constructor, such as `201 Created`, use the generic form: `Response(201, body: ...)`. ### Adding routes Routes are added with a path pattern: ```dart // Exact path pod.webServer.addRoute(UserRoute(), '/api/users'); // Serve a directory (tail matching is automatic) pod.webServer.addRoute(StaticRoute.directory(Directory('web')), '/static/'); ``` Paths can contain parameters and wildcards, and requests are matched by specificity rather than registration order. See [Routing](https://docs.serverpod.dev/concepts/web-server/routing.md) for the matching rules. ### Built-in routes Every web server automatically answers the health probe paths `/livez`, `/readyz`, and `/startupz`. A healthy server responds with an empty `200 OK`, and deployment platforms use these paths to check that the server is alive. These paths are reserved, so avoid registering your own routes on them. ## When to use what | You want to serve | Route type | Page | | --------------------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------- | | REST APIs, webhooks, custom HTTP handlers | Your own `Route` subclass | [Routing](https://docs.serverpod.dev/concepts/web-server/routing.md) | | Static assets: CSS, JavaScript, images | `StaticRoute` | [Static files](https://docs.serverpod.dev/concepts/web-server/static-files.md) | | Server-rendered HTML, with templates or Jaspr | `WidgetRoute`, or a route rendering Jaspr | [Server-side HTML](https://docs.serverpod.dev/concepts/web-server/server-side-html.md) | | A single-page app with client-side routing | `SpaRoute` | [Single-page apps](https://docs.serverpod.dev/concepts/web-server/single-page-apps.md) | | Your Flutter app compiled for the web | `FlutterRoute` | [Flutter web](https://docs.serverpod.dev/concepts/web-server/flutter-web.md) | ## Database access The `Session` parameter gives you full access to your Serverpod database: ```dart class UserRoute extends Route { @override Future handleCall(Session session, Request request) async { // Query database final users = await User.db.find(session); // Use logging session.log('Retrieved ${users.length} users'); return Response.ok( body: Body.fromString( jsonEncode(users.map((u) => u.toJson()).toList()), mimeType: MimeType.json, ), ); } } ``` ## Going to production When you deploy, the web server ships with the rest of your project. On [Serverpod Cloud](https://docs.serverpod.dev/deployments/deploy-to-serverpod-cloud.md), it is served through a CDN that honors the cache headers your routes set. See [Content delivery and caching](https://docs.serverpod.dev/cloud/concepts/cdn.md) for how the two interact. ## Next steps - **[Routing](https://docs.serverpod.dev/concepts/web-server/routing.md)** - Match requests to handlers by method and URL pattern - **[Request data](https://docs.serverpod.dev/concepts/web-server/request-data.md)** - Access path parameters, query parameters, headers, and body - **[Web server middleware](https://docs.serverpod.dev/concepts/web-server/web-server-middleware.md)** - Intercept and transform requests and responses - **[Static files](https://docs.serverpod.dev/concepts/web-server/static-files.md)** - Serve static assets - **[Server-side HTML](https://docs.serverpod.dev/concepts/web-server/server-side-html.md)** - Render HTML on the server with templates or Jaspr - **[Single-page apps](https://docs.serverpod.dev/concepts/web-server/single-page-apps.md)** - Serve SPAs with client-side routing - **[Flutter web](https://docs.serverpod.dev/concepts/web-server/flutter-web.md)** - Serve Flutter web applications --- # Routing https://docs.serverpod.dev/concepts/web-server/routing Routing decides which of your `Route` classes answers an incoming HTTP request. This page covers the matching rules, HTTP method filtering, path parameters, wildcards, fallback handling, and virtual hosts. ## Route classes The `Route` base class gives you complete control over request handling. By extending `Route` and implementing `handleCall()`, you can build REST APIs, serve files, or handle any custom HTTP interaction. This is ideal when you need to work directly with request bodies, headers, and response formats. ```dart class ApiRoute extends Route { ApiRoute() : super(methods: {Method.get, Method.post}); @override Future handleCall(Session session, Request request) async { // Access request method if (request.method == Method.post) { // Read request body final body = await request.readAsString(); final data = jsonDecode(body); // Process and return JSON response return Response.ok( body: Body.fromString( jsonEncode({'status': 'success', 'data': data}), mimeType: MimeType.json, ), ); } // Return data for GET requests return Response.ok( body: Body.fromString( jsonEncode({'message': 'Hello from API'}), mimeType: MimeType.json, ), ); } } ``` You need to register your custom routes with the built-in router under a given path: ```dart // Register the route pod.webServer.addRoute(ApiRoute(), '/api/data'); ``` :::info The examples in this documentation omit error handling for brevity. ::: ## How requests are matched Requests are matched by specificity, not by the order routes were registered. A literal path segment such as `/users/list` wins over a dynamic one such as `/users/:id` or a wildcard. Registering two routes with the same method and path throws at startup with `Invalid argument(s): Conflicting values`, so conflicts surface immediately rather than silently shadowing each other. ## HTTP methods Routes answer GET requests only by default. Use the `methods` parameter to specify which HTTP methods a route responds to: ```dart class UserRoute extends Route { UserRoute() : super( methods: {Method.get, Method.post, Method.delete}, ); // ... } ``` A request whose method is not in the set gets an automatic `405 Method Not Allowed` response with an `Allow` header listing the accepted methods. ## Path parameters Define path parameters in your route pattern using the `:paramName` syntax: ```dart pod.webServer.addRoute(UserRoute(), '/api/users/:id'); // Matches: /api/users/123, /api/users/456, etc. ``` You can use multiple path parameters in a single route: ```dart pod.webServer.addRoute(route, '/:userId/posts/:postId'); // Matches: /123/posts/456 ``` ## Wildcards Routes also support wildcard matching for catching all paths: ```dart // Single-level wildcard - matches /item/foo but not /item/foo/bar pod.webServer.addRoute(ItemRoute(), '/item/*'); // Tail-match wildcard - matches /item/foo and /item/foo/bar/baz pod.webServer.addRoute(ItemRoute(), '/item/**'); ``` :::info Tail matches The `/**` wildcard is a **tail-match** pattern and can only appear at the end of a route path (e.g., `/static/**`). Patterns like `/a/**/b` are not supported. ::: Access the matched path information through the `Request` object: ```dart @override Future handleCall(Session session, Request request) async { // Get the remaining path after the route prefix final remainingPath = request.remainingPath; // Access query parameters final query = request.queryParameters.raw['query']; return Response.ok( body: Body.fromString('Path: $remainingPath, Query: $query'), ); } ``` ## Fallback routes You can set a fallback route that handles requests when no other route matches: ```dart class NotFoundRoute extends Route { @override Future handleCall(Session session, Request request) async { return Response.notFound( body: Body.fromString('Page not found: ${request.url.path}'), ); } } // Set as fallback pod.webServer.fallbackRoute = NotFoundRoute(); ``` The fallback runs only when no route matches the request. If a route matches but its handler returns a 404, the fallback is not invoked. To rewrite those responses, use [`FallbackMiddleware`](https://docs.serverpod.dev/concepts/web-server/single-page-apps.md#using-fallbackmiddleware-directly) instead. :::tip Advanced: Grouping routes into modules As your web server grows, managing dozens of individual route registrations can become unwieldy. You can group related endpoints into reusable modules by overriding the `injectIn()` method. This lets you register multiple handler functions instead of implementing a single `handleCall()` method. Here's an example: ```dart class UserCrudModule extends Route { @override void injectIn(RelicRouter router) { // Register multiple routes with path parameters router ..get('/', _list) ..get('/:id', _get); } // Not used when injectIn registers its own handlers. @override Future handleCall(Session session, Request request) => throw UnimplementedError(); // Handler methods Future _list(Request request) async { final session = await request.session; final users = await User.db.find(session); return Response.ok( body: Body.fromString( jsonEncode(users.map((u) => u.toJson()).toList()), mimeType: MimeType.json, ), ); } static const _idParam = IntPathParam(#id); Future _get(Request request) async { int userId = request.pathParameters.get(_idParam); final session = await request.session; final user = await User.db.findById(session, userId); if (user == null) { return Response.notFound( body: Body.fromString('User not found'), ); } return Response.ok( body: Body.fromString( jsonEncode(user.toJson()), mimeType: MimeType.json, ), ); } } // Register the entire CRUD module under /api/users pod.webServer.addRoute(UserCrudModule(), '/api/users'); ``` This creates `GET /api/users` and `GET /api/users/:id` routes. Handlers receive only a `Request` parameter. To access the `Session`, use `request.session`, which returns a `Future` that must be awaited. The `handleCall()` override is still required by the base class, but it is never called when `injectIn` registers its own handlers, so throwing `UnimplementedError` is the expected pattern. Typed path parameter accessors such as `IntPathParam` are covered in [Request data](https://docs.serverpod.dev/concepts/web-server/request-data.md). ::: ## Virtual host routing Virtual host routing allows you to serve different content based on the `Host` header of incoming requests. This is useful for multi-tenant applications, or for serving different front-ends from distinct subdomains with a single server instance, such as an API on `api.example.com`, a web app on `www.example.com`, and an admin panel on `admin.example.com`. ### How it works By default, routes match requests from any host. ```dart // Route that responds to any host (default behavior) class PublicRoute extends Route { PublicRoute() : super(); // host defaults to null @override Future handleCall(Session session, Request request) async { return Response.ok( body: Body.fromString('Public endpoint'), ); } } ``` You can restrict a route to a specific host by providing the `host` parameter when creating the route: ```dart // Route that only responds to api.example.com class ApiRoute extends Route { ApiRoute() : super(host: 'api.example.com'); @override Future handleCall(Session session, Request request) async { return Response.ok( body: Body.fromString( jsonEncode({'message': 'API endpoint'}), mimeType: MimeType.json, ), ); } } ``` ### Registering host-specific routes Register routes with host restrictions just like regular routes: ```dart // Route that only responds to api.example.com pod.webServer.addRoute(ApiRoute(), '/v1'); // Route that only responds to www.example.com pod.webServer.addRoute( SpaRoute( Directory('web/app'), fallback: File('web/app/index.html'), host: 'www.example.com', ), '/', ); // Route that responds to any host (default behavior) pod.webServer.addRoute(HealthRoute(), '/health'); ``` ### Supported route types All route types support virtual host routing: - **Custom routes** - Pass `host` to the `Route` constructor - **StaticRoute** - Use `host` parameter in `StaticRoute.directory()` or `StaticRoute.file()` - **[SpaRoute](https://docs.serverpod.dev/concepts/web-server/single-page-apps.md)** - Use `host` parameter in the `SpaRoute` constructor - **[FlutterRoute](https://docs.serverpod.dev/concepts/web-server/flutter-web.md)** - Use `host` parameter in the `FlutterRoute` constructor ```dart // Static files for a specific host pod.webServer.addRoute( StaticRoute.directory( Directory('web/admin'), host: 'admin.example.com', ), '/static/', ); // SPA for a specific host pod.webServer.addRoute( SpaRoute( Directory('web/app'), fallback: File('web/app/index.html'), host: 'app.example.com', ), '/', ); ``` ## Next steps - **[Request data](https://docs.serverpod.dev/concepts/web-server/request-data.md)** - Access path parameters, query parameters, headers, and body - **[Middleware](https://docs.serverpod.dev/concepts/web-server/web-server-middleware.md)** - Intercept and transform requests and responses - **[Static files](https://docs.serverpod.dev/concepts/web-server/static-files.md)** - Serve static assets - **[Server-side HTML](https://docs.serverpod.dev/concepts/web-server/server-side-html.md)** - Render HTML on the server --- # Request data https://docs.serverpod.dev/concepts/web-server/request-data Once a route matches, you'll need to extract data from the request. Relic provides type-safe accessors for path parameters, query parameters, headers, and the request body. ## Path parameters Access path parameters using typed `PathParam` accessors: ```dart class UserRoute extends Route { static const _idParam = IntPathParam(#id); @override Future handleCall(Session session, Request request) async { int userId = request.pathParameters.get(_idParam); final user = await User.db.findById(session, userId); if (user == null) { return Response.notFound(); } return Response.ok( body: Body.fromString( jsonEncode(user.toJson()), mimeType: MimeType.json, ), ); } } ``` The `IntPathParam` combines a symbol with a parser, throwing if the parameter is missing or not a valid integer. The `#id` is a Dart Symbol literal naming the `:id` segment from the route's path pattern. You can also access raw unparsed values with `request.pathParameters.raw[#id]`. ## Query parameters Query parameters (`?key=value`) use the same typed accessor pattern: ```dart class SearchRoute extends Route { static const _pageParam = IntQueryParam('page'); @override Future handleCall(Session session, Request request) async { int page = request.queryParameters.get(_pageParam); // typed access String? query = request.queryParameters.raw['query']; // raw access // ... } } ``` Like the path accessors, `get()` throws when the parameter is missing. Query parameters are often optional, so use raw access with a default for those, e.g. `int.tryParse(request.queryParameters.raw['page'] ?? '') ?? 1`. ## Headers Access headers through `request.headers` with type-safe getters for standard HTTP headers: ```dart @override Future handleCall(Session session, Request request) async { // Type-safe accessors for standard headers final userAgent = request.headers.userAgent; final contentLength = request.headers.contentLength; final auth = request.headers.authorization; // Raw access for custom headers final apiKey = request.headers['X-API-Key']?.first; // ... } ``` The accessors above read headers from the incoming request. To set headers on the response, pass a `headers:` value built with `Headers.build` when you construct the `Response`: ```dart return Response.ok( body: Body.fromString('ok'), headers: Headers.build((h) { h['X-Custom-Header'] = ['value']; }), ); ``` Relic also exposes typed setters for standard headers. See the [Relic documentation](https://docs.dartrelic.dev/) for the full header reference. ## Body Read the request body as a string for JSON or form data: ```dart @override Future handleCall(Session session, Request request) async { final body = await request.readAsString(); final data = jsonDecode(body); // ... } ``` :::warning The body can only be read once. Attempting to read it again will throw a `StateError`. ::: ### Streaming bodies For large payloads, stream instead of buffering the whole body in memory. On the request side, `request.read()` gives you the body as a stream of chunks, which suits large uploads: ```dart final stream = request.read(); await for (final chunk in stream) { // Process chunk } ``` On the response side, `Body.fromDataStream` streams data to the client as it is produced, for example when serving a large file: ```dart Stream dataStream = getFileStream(); return Response.ok( body: Body.fromDataStream( dataStream, contentLength: fileSize, mimeType: MimeType.octetStream, ), ); ``` For more details on typed parameters and headers, see the [Relic documentation](https://docs.dartrelic.dev/). ## Next steps - **[Middleware](https://docs.serverpod.dev/concepts/web-server/web-server-middleware.md)** - Intercept and transform requests and responses - **[Static files](https://docs.serverpod.dev/concepts/web-server/static-files.md)** - Serve static assets - **[Server-side HTML](https://docs.serverpod.dev/concepts/web-server/server-side-html.md)** - Render HTML on the server --- # Web server middleware https://docs.serverpod.dev/concepts/web-server/web-server-middleware Routes handle the core logic of your application, but many concerns cut across multiple routes: logging every request, validating API keys, handling CORS headers, or catching errors. Rather than duplicating this code in each route, middleware lets you apply it globally or to specific path prefixes. Middleware functions are wrappers that sit between the incoming request and your route handler. They can inspect or modify requests before they reach your routes, and transform responses before they're sent back to the client. The middleware on this page applies only to web server routes. For middleware around your endpoint methods, see [Endpoint middleware](https://docs.serverpod.dev/concepts/endpoints-and-apis/endpoint-middleware.md). ## Adding middleware Use the `addMiddleware` method to apply middleware to specific path prefixes: ```dart // Apply to all routes below `/path` pod.webServer.addMiddleware(myMiddleware, '/path'); ``` ### Host-specific middleware You can restrict middleware to a specific virtual host by providing the optional `host` parameter: ```dart // Middleware for a specific host pod.webServer.addMiddleware( apiKeyMiddleware, '/api', host: 'api.example.com', ); // Middleware for all hosts (default behavior) pod.webServer.addMiddleware(loggingMiddleware, '/'); ``` When `host` is `null` (the default), the middleware applies to all hosts. When set, the middleware only applies to requests with a matching `Host` header. ## Creating custom middleware Middleware is a function that takes a `Handler` and returns a new `Handler`. Here's a simple example that validates API keys for protected routes: ```dart Handler apiKeyMiddleware(Handler next) { return (Request request) async { // Check for API key in header final apiKey = request.headers['X-API-Key']?.firstOrNull; if (apiKey == null) { return Response.unauthorized( body: Body.fromString('API key required'), ); } // Verify API key if (!await isValidApiKey(apiKey)) { return Response.forbidden( body: Body.fromString('Invalid API key'), ); } // Continue to the next handler return await next(request); }; } // Apply to protected routes pod.webServer.addMiddleware(apiKeyMiddleware, '/api'); ``` :::info For user authentication, use Serverpod's built-in [authentication](https://docs.serverpod.dev/concepts/authentication/basics.md) system, which integrates with the `Session` object your route's `handleCall` receives. The middleware examples here are for additional web-specific validations like API keys, rate limiting, or request validation. ::: ## Middleware execution order Middleware wraps your routes in layers based on path hierarchy. Middleware registered on a broader path runs first, and middleware on a more specific path runs closer to the route handler. Within the same path, middleware executes in the order it was registered: ```dart pod.webServer.addMiddleware(rateLimitMiddleware, '/api/users'); // Runs second for /api/users/list pod.webServer.addMiddleware(apiKeyMiddleware, '/api'); // Runs first for /api/users/list ``` For a request to `/api/users/list`, the execution order is: ```mermaid sequenceDiagram participant Client participant Auth as apiKeyMiddleware participant RateLimit as rateLimitMiddleware participant Handler as Route Handler Client->>Auth: activate Auth Note over Auth: Before logic Auth->>RateLimit: activate RateLimit Note over RateLimit: Before logic RateLimit->>Handler: activate Handler Note over Handler: Execute route logic Handler-->>RateLimit: Response deactivate Handler Note over RateLimit: After logic RateLimit-->>Auth: deactivate RateLimit Note over Auth: After logic Auth-->>Client: Response ``` ## Request-scoped data Middleware often needs to pass computed data to downstream handlers. For example, a tenant identification middleware might extract the tenant ID from a subdomain, or a logging middleware might generate a request ID for tracing. Since `Request` objects are immutable, you can't just add properties to them. This is where `ContextProperty` comes in. A `ContextProperty` provides a type-safe way to attach data to a `Request` object without modifying it. Think of it as a side channel for request-scoped data that middleware can write to and routes can read from. The data is automatically cleaned up when the request completes. For more details, see the [Relic documentation](https://docs.dartrelic.dev/). :::info Serverpod's `Route.handleCall()` already receives a `Session` parameter which includes authenticated user information if available. Use `ContextProperty` for web-specific request data that isn't part of the standard Session, such as request IDs, feature flags, or API version information extracted from headers. ::: ### Creating a `ContextProperty` Define a `ContextProperty` as a top-level static field: ```dart // Define a private context property. final _tenantProperty = ContextProperty('tenant'); // Create a public getter extension to allow handlers and other middleware to // read, but not modify the context property. extension TenantRequestEx on Request { String get tenant => _tenantProperty.get(this); // get() throws when unset; [] returns null instead. } ``` ### Setting values in middleware Middleware can set values on the context property, making them available to all downstream handlers: ```dart // Tenant identification middleware (extracts from subdomain) Handler tenantMiddleware(Handler next) { return (Request request) async { final host = request.headers.host; // Validate tenant exists (implement your own logic) final session = await request.session; final tenant = await extractAndValidateTenant(session, host); if (tenant == null) { return Response.notFound( body: Body.fromString('Tenant not found'), ); } // Attach tenant to context _tenantProperty[request] = tenant; return await next(request); }; } ``` ### Accessing values in routes Route handlers can retrieve the value from the context property: ```dart // Routes automatically have access to the tenant class TenantDataRoute extends Route { @override Future handleCall(Session session, Request request) async { final tenant = request.tenant; // using the previously defined extension // Fetch tenant-specific data final data = await Product.db.find( session, where: (t) => t.tenantId.equals(tenant), ); return Response.ok( body: Body.fromString( jsonEncode(data.map((p) => p.toJson()).toList()), mimeType: MimeType.json, ), ); } } ``` ## Next steps - **[Request data](https://docs.serverpod.dev/concepts/web-server/request-data.md)** - Access path parameters, query parameters, headers, and body - **[Static files](https://docs.serverpod.dev/concepts/web-server/static-files.md)** - Serve static assets - **[Server-side HTML](https://docs.serverpod.dev/concepts/web-server/server-side-html.md)** - Render HTML on the server --- # Static files https://docs.serverpod.dev/concepts/web-server/static-files The `StaticRoute` class serves files from disk: images, CSS, JavaScript, fonts, or any other asset, with automatic content-type detection for common file formats. This page covers serving directories and single files, cache headers, and cache busting. ## Serving static files The simplest way to serve static files is to use `StaticRoute.directory()`: ```dart final staticDir = Directory('web/static'); pod.webServer.addRoute( StaticRoute.directory(staticDir), '/static/', ); ``` This serves all files from the `web/static` directory at the `/static` path. For example, `web/static/logo.png` becomes accessible at `/static/logo.png`. To serve one specific file instead of a directory, use `StaticRoute.file(File('web/pages/landing.html'))`. :::info The `StaticRoute.directory()` factory automatically handles tail matching, so you don't need to add `**` to the path. The route will serve all files under the given prefix. ::: ## Cache control By default, `StaticRoute` sends no `Cache-Control` header at all. Responses still carry `ETag` and `Last-Modified` headers, so browsers revalidate with [conditional requests](#conditional-requests-etags-and-last-modified) and receive a `304 Not Modified` when the file is unchanged. To set a caching policy, pass one of the cache-control helpers as the `cacheControlFactory` parameter: | Helper | Resulting header | Use when | | ------------------------------------------ | ------------------------------ | --------------------------------------------------------------------------- | | `StaticRoute.public(maxAge: ...)` | `public, max-age=N` | Assets that change occasionally. Caches keep them for the given lifetime | | `StaticRoute.publicImmutable(maxAge: ...)` | `immutable, public, max-age=N` | Cache-busted assets whose URL changes with the content. Use a long lifetime | | `StaticRoute.privateNoCache()` | `no-cache, private` | Per-user content that must be revalidated on every request | | `StaticRoute.noStore()` | `no-store` | Content that must never be cached | ```dart pod.webServer.addRoute( StaticRoute.directory( staticDir, cacheControlFactory: StaticRoute.public(maxAge: const Duration(minutes: 5)), ), '/static/', ); ``` To set the header per environment instead of in code, set `SERVERPOD_WEB_SERVER_STATIC_CACHE_CONTROL` to the `Cache-Control` value the route should send: ```bash SERVERPOD_WEB_SERVER_STATIC_CACHE_CONTROL="public, max-age=3600" ``` The environment variable applies to every `StaticRoute` that does not pass a `cacheControlFactory` of its own. Reserve `publicImmutable` for [cache-busted](#static-file-cache-busting) assets: `immutable` tells caches to never revalidate, which is only safe when a changed file also gets a new URL. On [Serverpod Cloud](https://docs.serverpod.dev/cloud/concepts/cdn.md), a CDN sits in front of the web server and honors these headers, and its cache is cleared on every deploy. ## Static file cache-busting When deploying static assets, browsers and CDNs cache files aggressively for performance. This means updated files may not be served to users unless you implement a cache-busting strategy. Relic provides `CacheBustingConfig` to automatically version your static files. For more details, see the [Relic documentation](https://docs.dartrelic.dev/). ```dart final staticDir = Directory('web/static'); final cacheBustingConfig = CacheBustingConfig( mountPrefix: '/static', fileSystemRoot: staticDir, separator: '@', // or use custom separator like '___' ); pod.webServer.addRoute( StaticRoute.directory( staticDir, cacheBustingConfig: cacheBustingConfig, cacheControlFactory: StaticRoute.publicImmutable(maxAge: const Duration(days: 365)), ), '/static/', ); ``` With cache busting, a changed file gets a new URL, so caches can safely keep each version for a long time. This is the setup where `publicImmutable` with a long lifetime belongs. ### Generating versioned URLs Use the `assetPath()` method to generate cache-busted URLs for your assets: ```dart // In your route handler final imageUrl = await cacheBustingConfig.assetPath('/static/logo.png'); // Returns: /static/logo@.png // Pass to your template return MyPageWidget(logoUrl: imageUrl); ``` The cache-busting system: - Automatically generates content-based hashes for asset versioning - Allows custom separators (default `@`, but you can use `___` or any other) - Preserves file extensions - Works transparently - requesting `/static/logo@abc123.png` serves `/static/logo.png` ## Conditional requests (ETags and Last-Modified) Every `StaticRoute` automatically supports HTTP conditional requests through Relic's `StaticHandler`, along with `Range` requests for partial downloads. This provides efficient caching without transferring file content when unchanged: **Supported features:** - **ETag headers** - Content-based fingerprinting for cache validation - **Last-Modified headers** - Timestamp-based cache validation - **If-None-Match** - Client sends ETag, server returns 304 Not Modified if unchanged - **If-Modified-Since** - Client sends timestamp, server returns 304 if not modified These work automatically without configuration: **Initial request:** ```http GET /static/logo.png HTTP/1.1 Host: example.com HTTP/1.1 200 OK ETag: "abc123" Last-Modified: Tue, 15 Nov 2024 12:00:00 GMT Content-Length: 12345 [file content] ``` **Subsequent request with ETag:** ```http GET /static/logo.png HTTP/1.1 Host: example.com If-None-Match: "abc123" HTTP/1.1 304 Not Modified ETag: "abc123" [no body - saves bandwidth] ``` Conditional requests do the caching work in the default setup and with `public(maxAge:)`, where clients revalidate once a file's lifetime expires. With cache-busted `immutable` assets, clients skip revalidation entirely by design, since a changed file gets a new URL instead. For dynamic content that changes per request, see [Server-side HTML](https://docs.serverpod.dev/concepts/web-server/server-side-html.md). --- # Server-side HTML https://docs.serverpod.dev/concepts/web-server/server-side-html Server-rendered pages generate their HTML on the server for each request, with full access to your database through the `Session`. Choose this for content pages, dashboards, and landing pages. For client-rendered apps, see [Single-page apps](https://docs.serverpod.dev/concepts/web-server/single-page-apps.md) instead. Serverpod's built-in mechanism for this is `WidgetRoute` with [Mustache](https://mustache.github.io/) templates. Beyond it, any route can return HTML that you produce with a tool of your choice. This page covers both, using [Jaspr](https://jaspr.site), which brings a Flutter-like component model to the web, as the worked example of the second approach. ## Creating a WidgetRoute Create custom routes by extending the `WidgetRoute` class and implementing the `build` method: ```dart class MyRoute extends WidgetRoute { @override Future build(Session session, Request request) async { return MyPageWidget(title: 'Home page'); } } // Register the route pod.webServer.addRoute(MyRoute(), '/my/page/address'); ``` The `build` method returns the widget that renders the page. In this example, that widget is `MyPageWidget`, which the [next section](#creating-a-templatewidget) defines. Return `null` to respond with 404, for example when the requested content does not exist. Like other routes, a `WidgetRoute` answers GET requests by default. Pass `methods:` to handle form submissions too, e.g. `super(methods: {Method.get, Method.post})`. Rendered responses are sent with `Cache-Control: no-cache, private`, so browsers and CDNs revalidate them on every request. The exception is `RedirectWidget`, whose 303 response carries no cache header. ## Creating a TemplateWidget A `TemplateWidget` renders a Mustache template. It is a subclass of `WebWidget`, which is what allows `MyRoute`'s `build` method above to return one. A template widget consists of a Dart class and a corresponding HTML template file. Create one by extending `TemplateWidget`: ```dart class MyPageWidget extends TemplateWidget { MyPageWidget({required String title}) : super(name: 'my_page') { values = { 'title': title, }; } } ``` Place the corresponding HTML template in the `web/templates` directory. The HTML file uses the [Mustache](https://mustache.github.io/) template language: ```html {{title}}

{{title}}

Welcome to my page!

``` Template values are converted to `String` objects before being passed to the template. Mustache escapes HTML in `{{value}}` tags, so to embed HTML produced by a nested widget, use the unescaped `{{{value}}}` form. ### How templates load Templates are loaded from `web/templates` when the server starts. Constructing a `TemplateWidget` whose template file is missing throws a `StateError` naming the widget class, e.g. `Template my_page.html missing for MyPageWidget`, and the request fails with a 500. While `serverpod start` runs, templates reload on save and the browser refreshes automatically. With a plain `dart run bin/main.dart`, restart the server to pick up template changes. ### Cache-busted asset URLs A template can refer to a stylesheet or an image by its [cache-busted](https://docs.serverpod.dev/concepts/web-server/static-files.md#static-file-cache-busting) URL with the `{{{@/path}}}` tag, so the browser picks up a new version of the file as soon as it changes. Write the asset's served path after the `@`: ```html title="web/templates/my_page.html" ``` Pass the same `CacheBustingConfig` as the `StaticRoute` serving the directory to the route that renders the template: ```dart class MyRoute extends WidgetRoute { MyRoute() : super(cacheBustingConfig: cacheBustingConfig); @override Future build(Session session, Request request) async { return MyPageWidget(title: 'Home page'); } } ``` To build the versioned URL in Dart instead and pass it in as a template value, use [`assetPath()`](https://docs.serverpod.dev/concepts/web-server/static-files.md#generating-versioned-urls). ## Built-in widgets Serverpod provides several built-in widgets for common use cases: - **`ListWidget`** - Concatenates multiple widgets into a single response ```dart return ListWidget(widgets: [ HeaderWidget(), ContentWidget(), FooterWidget(), ]); ``` - **`JsonWidget`** - Renders JSON documents from serializable data structures ```dart return JsonWidget(object: {'status': 'success', 'data': myData}); ``` - **`RedirectWidget`** - Redirects the browser to another URL with a `303 See Other` response ```dart return RedirectWidget(url: '/new/location'); ``` ## Database access and logging The web server passes a `Session` object to the `WidgetRoute` class' `build` method. This gives you access to all the features you typically get from a standard method call to an endpoint. Use the database, logging, or caching the same way you would in a method call: ```dart class DataRoute extends WidgetRoute { @override Future build(Session session, Request request) async { // Access the database final users = await User.db.find(session); // Logging session.log('Rendering user list page'); return UserListWidget(users: users); } } ``` ## Rendering with Jaspr [Jaspr](https://jaspr.site) lets you build server-rendered pages from components, with a `build` method and composition model much like Flutter's. It works with the web server through a plain `Route`: your route renders a component to HTML and returns it as the response. Add the dependency to your server package (the example on this page uses jaspr 0.23): ```bash $ dart pub add jaspr ``` Then define a component and a route that renders it: ```dart import 'dart:convert'; import 'package:jaspr/dom.dart' as jaspr; import 'package:jaspr/server.dart' as jaspr; import 'package:serverpod/serverpod.dart'; class HelloPage extends jaspr.StatelessComponent { const HelloPage({required this.name, super.key}); final String name; @override jaspr.Component build(jaspr.BuildContext context) { return jaspr.div([ jaspr.h1([jaspr.Component.text('Hello from Jaspr!')]), jaspr.p([jaspr.Component.text('Rendered server-side for $name.')]), ]); } } class JasprHelloRoute extends Route { @override Future handleCall(Session session, Request request) async { final result = await jaspr.renderComponent(HelloPage(name: 'Serverpod')); return Response.ok( body: Body.fromString( utf8.decode(result.body), mimeType: MimeType.html, ), ); } } ``` Register the route as usual, and initialize Jaspr once in your `run()` function before the server starts: ```dart jaspr.Jaspr.initializeApp(); pod.webServer.addRoute(JasprHelloRoute(), '/jaspr'); ``` :::warning The `initializeApp()` call is required. Without it, the first render kills the whole server process instead of failing the single request. ::: Two details make this example work. Both `package:jaspr/server.dart` and `package:jaspr/dom.dart` are imported under one alias, because `server.dart` exports names that collide with the web server's `Request` and `Response`, and the HTML components (`div`, `h1`, `p`) live in `dom.dart`. And `renderComponent` returns a result record whose `body` is bytes, which `utf8.decode` turns into the HTML string. ## Next steps - Use [custom routes](https://docs.serverpod.dev/concepts/web-server/routing.md) for REST APIs and custom request handling - Serve [static files](https://docs.serverpod.dev/concepts/web-server/static-files.md) for CSS, JavaScript, and images - Add [middleware](https://docs.serverpod.dev/concepts/web-server/web-server-middleware.md) for cross-cutting concerns like logging and error handling --- # Single-page apps https://docs.serverpod.dev/concepts/web-server/single-page-apps Single-page applications (SPAs) handle routing on the client side, which requires special server configuration. When users navigate to a route like `/dashboard` or `/settings`, the browser requests that path from the server. Since these aren't real files, the server needs to return the main `index.html` file so the client-side router can handle the route. Serverpod provides `SpaRoute` to handle this pattern automatically. ## Basic setup Use `SpaRoute` to serve your SPA with automatic fallback to `index.html`: ```dart final webDir = Directory('web/app'); pod.webServer.addRoute( SpaRoute( webDir, fallback: File('web/app/index.html'), ), ); ``` :::info The route path defaults to `'/'` (root). See [Serving from a sub-path](#serving-from-a-sub-path) to mount the SPA at a different location. ::: This configuration: - Serves static files from `web/app` when they exist - Falls back to `index.html` for any path that doesn't match a file - Enables client-side routing frameworks (React Router, Vue Router, etc.) to work correctly ## How it works When a request comes in: 1. The `SpaRoute` first tries to serve a matching static file from the directory 2. If no file exists (404 response), it serves the fallback file instead 3. The client-side JavaScript then handles routing based on the URL This is implemented using `FallbackMiddleware` internally, which you can also use directly for custom fallback behavior. ## Cache control Configure caching for your static assets: ```dart pod.webServer.addRoute( SpaRoute( webDir, fallback: File('web/app/index.html'), cacheControlFactory: StaticRoute.public( maxAge: const Duration(minutes: 5), ), ), ); ``` To set the header per environment instead of in code, set `SERVERPOD_WEB_SERVER_SPA_CACHE_CONTROL` to the `Cache-Control` value the route should send: ```bash SERVERPOD_WEB_SERVER_SPA_CACHE_CONTROL="public, max-age=3600" ``` The environment variable applies to every `SpaRoute` that does not pass a `cacheControlFactory` of its own. Like the factory, it never applies to the fallback file, which is always served without a `Cache-Control` header so a deploy is never masked by a cached app shell. The `SERVERPOD_WEB_SERVER_STATIC_CACHE_CONTROL` variable used by `StaticRoute` does not apply here. See [Static files](https://docs.serverpod.dev/concepts/web-server/static-files.md#cache-control) for more on cache control, including why `publicImmutable` belongs only with cache-busted assets. ## Cache busting Enable cache-busted URLs for your assets: ```dart final webDir = Directory('web/app'); final cacheBustingConfig = CacheBustingConfig( mountPrefix: '/', fileSystemRoot: webDir, ); pod.webServer.addRoute( SpaRoute( webDir, fallback: File('web/app/index.html'), cacheBustingConfig: cacheBustingConfig, cacheControlFactory: StaticRoute.publicImmutable( maxAge: const Duration(days: 365), ), ), ); ``` See [Static files](https://docs.serverpod.dev/concepts/web-server/static-files.md#static-file-cache-busting) for more on cache busting. ## Using FallbackMiddleware directly For more control, use `FallbackMiddleware` with `StaticRoute`: ```dart final webDir = Directory('web/app'); final indexFile = File('web/app/index.html'); pod.webServer.addMiddleware( FallbackMiddleware( fallback: StaticRoute.file(indexFile), on: (response) => response.statusCode == 404, ).call, '/', ); pod.webServer.addRoute(StaticRoute.directory(webDir), '/'); ``` This gives you flexibility to customize the fallback condition. For example, you could fall back on any 4xx error: ```dart FallbackMiddleware( fallback: StaticRoute.file(indexFile), on: (response) => response.statusCode >= 400 && response.statusCode < 500, ).call ``` ## Serving from a sub-path To serve your SPA from a sub-path instead of the root, pass the path as the second argument to `addRoute`: ```dart final webDir = Directory('web/app'); pod.webServer.addRoute( SpaRoute( webDir, fallback: File('web/app/index.html'), ), '/app', ); ``` This serves the SPA at `/app`, so users would access it at `http://localhost:8082/app`. :::note When using cache busting with a sub-path, update the `mountPrefix` to match: ```dart final cacheBustingConfig = CacheBustingConfig( mountPrefix: '/app', fileSystemRoot: webDir, ); ``` ::: ## Serving Flutter web applications For serving Flutter web applications specifically, see [Flutter web apps](https://docs.serverpod.dev/concepts/web-server/flutter-web.md). --- # Flutter web apps https://docs.serverpod.dev/concepts/web-server/flutter-web Serverpod can serve your Flutter web application directly, allowing you to host both your API and web frontend from the same server. The `FlutterRoute` class handles the specifics: serving the build output, SPA-style fallback to `index.html` for client-side routing, and cache headers that make redeploys take effect right away. New fullstack projects come with this already set up. The scaffolded `server.dart` serves your Flutter app at the root URL when a build exists in `web/app`, and shows a build-instructions page when it does not. ## Basic setup Use `FlutterRoute` to serve your Flutter web build: ```dart pod.webServer.addRoute( FlutterRoute(Directory('web/app')), ); ``` :::info The route path defaults to `'/'` (root). See [Serving from a sub-path](https://docs.serverpod.dev/concepts/web-server/single-page-apps.md#serving-from-a-sub-path) to mount the app at a different location. ::: This configuration: - Serves all static files from the Flutter build - Falls back to `index.html` for client-side routing - Serves every file with revalidation-based cache headers, so users get new versions on the next load after a deploy ## Building Flutter for web New fullstack projects ship a build task. Run it from the server directory: ```bash $ serverpod run flutter_build ``` It runs the underlying Flutter build and places the output where the server serves it: ```bash cd my_project_flutter flutter build web --base-href / --output ../my_project_server/web/app ``` If you mount the app at a sub-path such as `/app`, pass that path as `--base-href /app/` so the app's own asset URLs resolve correctly. The build output ends up next to your server code: ```text my_project/ ├── my_project_server/ │ ├── lib/ │ │ └── server.dart │ └── web/ │ └── app/ # Flutter web build output │ ├── index.html │ ├── main.dart.js │ ├── flutter.js │ └── ... └── my_project_flutter/ # Build source ``` ## Cache control By default, `FlutterRoute` serves every file with `Cache-Control: no-cache, private`. The browser keeps a local copy but revalidates it on each load using `ETag` headers, and unchanged files answer with an empty `304 Not Modified`. The cost is one small round-trip per asset rather than a re-download, and the payoff is that a redeploy takes effect on the next page load with no stale-version tricks needed. This design fits Flutter's build output, which uses fixed file names (such as `main.dart.js`) without content hashing, so longer-lived caching would risk serving a stale app. ### Custom cache control Override the default behavior using `cacheControlFactory`: ```dart pod.webServer.addRoute( FlutterRoute( Directory('web/app'), cacheControlFactory: StaticRoute.public( maxAge: const Duration(hours: 1), ), ), ); ``` :::warning A custom factory applies to all files served from the directory. The fallback-served `index.html` (a client-side route like `/dashboard`) always gets no-cache headers, but a direct request to `/index.html` follows your factory, so exclude `index.html` from long-lived caching in a custom strategy. ::: To set the header per environment instead of in code, set `SERVERPOD_WEB_SERVER_FLUTTER_CACHE_CONTROL` to the `Cache-Control` value the route should send: ```bash SERVERPOD_WEB_SERVER_FLUTTER_CACHE_CONTROL="public, max-age=3600" ``` The environment variable applies to every `FlutterRoute` that does not pass a `cacheControlFactory` of its own, and the same warning applies to it. See [Static files](https://docs.serverpod.dev/concepts/web-server/static-files.md#cache-control) for more on cache control. ### Cache busting For long-lived caching without stale versions, enable cache-busted URLs: ```dart final webDir = Directory('web/app'); final cacheBustingConfig = CacheBustingConfig( mountPrefix: '/', fileSystemRoot: webDir, ); pod.webServer.addRoute( FlutterRoute( webDir, cacheBustingConfig: cacheBustingConfig, cacheControlFactory: StaticRoute.publicImmutable( maxAge: const Duration(days: 365), ), ), ); ``` See [Static files](https://docs.serverpod.dev/concepts/web-server/static-files.md#static-file-cache-busting) for more on cache busting. ## WASM builds Flutter can compile to WebAssembly for improved performance, with multi-threaded rendering that requires `SharedArrayBuffer`. Browsers only enable `SharedArrayBuffer` on pages with cross-origin isolation headers: - `Cross-Origin-Opener-Policy: same-origin` - `Cross-Origin-Embedder-Policy: require-corp` The `enableWasmHeaders` parameter controls whether `FlutterRoute` sends them. The constructor defaults it to true, but generated projects pass `enableWasmHeaders: false` because the plain JavaScript build is the scaffolded default and cross-origin isolation has side effects: it blocks embedding cross-origin resources, such as images or iframes from other domains, unless they opt in. To serve a WASM build: 1. Build with the WASM target: ```bash cd my_project_flutter flutter build web --wasm --base-href / --output ../my_project_server/web/app ``` 2. Turn the headers on: ```dart pod.webServer.addRoute( FlutterRoute(Directory('web/app'), enableWasmHeaders: true), ); ``` :::info WASM builds automatically fall back to JavaScript in browsers that don't support WebAssembly Garbage Collection (WasmGC). Your app works everywhere while taking advantage of WASM performance where available. ::: ### Using WasmHeadersMiddleware directly If you're using `SpaRoute` or custom routes instead of `FlutterRoute`, add the headers with `WasmHeadersMiddleware`: ```dart pod.webServer.addMiddleware(const WasmHeadersMiddleware().call, '/'); pod.webServer.addRoute( SpaRoute( Directory('web/app'), fallback: File('web/app/index.html'), ), '/', ); ``` ## Complete example Here's a `server.dart` excerpt serving a Flutter web app, following the pattern generated projects use: ```dart import 'dart:io'; import 'src/generated/serverpod.dart'; void run(List args) async { final pod = Serverpod(args); final flutterAppDir = Directory('web/app'); if (flutterAppDir.existsSync()) { pod.webServer.addRoute(FlutterRoute(flutterAppDir, enableWasmHeaders: false)); } else { // No build yet: show instructions instead of a 404. pod.webServer.addRoute( StaticRoute.file(File('web/pages/build_flutter_app.html')), '/**', ); } await pod.start(); } ``` With this configuration, your Flutter web app is served at the root URL of your web server (typically `http://localhost:8082` in development). --- # Overview https://docs.serverpod.dev/concepts/scheduling/overview A future call is a piece of server work you schedule to run later instead of right now. You pick a method, say when it should run, and Serverpod invokes it at that time, even if the request that scheduled it has long since finished. Common uses are a welcome email an hour after sign-up, a reminder a day before an appointment, or a cleanup job that runs every night. Future calls are stored in the database, so they survive a server restart and are shared across every server instance you run. This makes them different from an in-memory `Timer`: a scheduled call is not lost if the process that created it stops. ## What you can schedule You write a future call as a method on a class, generate the type-safe code, and then schedule it in one of three ways: - **After a delay**, for example one hour from now. - **At a specific time**, for example a fixed date and time in the future. - **On a repeating schedule**, either a fixed interval (every 20 minutes) or a [cron](https://en.wikipedia.org/wiki/Cron) expression (every day at 02:00 UTC). Cron is a standard text format for describing repeating schedules. ## Execution guarantees A scheduled call runs **at least once**. Across all your running server instances, one instance claims each call and normally runs it a single time. If that instance crashes partway through, the call is picked up and run again by another instance, so a call can occasionally run more than once. Write work that is safe to repeat (for example, check whether the email was already sent before sending it). If the method itself throws an exception, Serverpod logs the error and does not retry the call. You are responsible for retrying failed work: schedule a new call if the work needs to happen. Recurring calls are the exception, since the next run is always scheduled regardless of whether the current one succeeds. ## Where future calls run Which servers run future calls depends on their role, the mode a server is started in. Servers in the default `monolith` role run them, and future calls need a database. A server started in the `serverless` role does not run them. If you host in a serverless environment, run a separate scheduled process in the `maintenance` role to execute due calls. See [server roles](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md#choose-a-server-role) and [hosting elsewhere](https://docs.serverpod.dev/deployments/custom-hosting/hosting-elsewhere.md). ## What each page covers - **[Future calls](https://docs.serverpod.dev/concepts/scheduling/future-calls.md)**: define a future call, schedule it after a delay or at a time, and cancel scheduled calls. - **[Recurring tasks](https://docs.serverpod.dev/concepts/scheduling/recurring-tasks.md)**: run a call on a repeating cron or interval schedule. - **[Inheritance](https://docs.serverpod.dev/concepts/scheduling/inheritance.md)**: extend future calls from other classes and modules. - **[Configuration](https://docs.serverpod.dev/concepts/scheduling/configuration.md)**: set concurrency, the scan interval, and broken-call handling. ## Related - [Sessions](https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions.md): the session a future call receives when it runs. - [Server roles](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md#choose-a-server-role): which roles run scheduled work. - [Configuration reference](https://docs.serverpod.dev/concepts/lookups/configuration-reference.md): every future-call key with its environment variable. --- # Future calls https://docs.serverpod.dev/concepts/scheduling/future-calls A future call is a method that Serverpod runs later. You define it on a class, generate the type-safe code, and schedule it from anywhere you have access to the server. This page covers the one-off cases: run once after a delay, or once at a specific time. For repeating schedules, see [Recurring tasks](https://docs.serverpod.dev/concepts/scheduling/recurring-tasks.md). ## Define a future call Extend the `FutureCall` class and add the methods you want to invoke later. ```dart import 'package:serverpod/serverpod.dart'; class ExampleFutureCall extends FutureCall { Future doWork(Session session) async { // Do something interesting in the future here. } Future doOtherWork(Session session, String data) async { // Do something interesting in the future here. } } ``` Each method must: - Return `Future`. - Take a [`Session`](https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions.md) as its first parameter. - Use only [serializable types](https://docs.serverpod.dev/concepts/data-and-the-database/models.md) for any other parameters. Typed `List`, `Map`, `Set`, and Dart records are allowed, but streams are not. Generate the code for your future calls. With `serverpod start` running, saving the file regenerates the code. Outside a session, run `serverpod generate`. This creates a type-safe interface in your server's `lib/src/generated/future_calls.dart` file, reachable from the `Serverpod` object as `pod.futureCalls`. You do not register these calls yourself. Serverpod does it for you when the server starts, so your `server.dart` needs no future-call setup: ```dart import 'src/generated/serverpod.dart'; void run(List args) async { final pod = Serverpod(args); await pod.start(); } ``` :::note The `pod.futureCalls` accessor exists only once you have defined at least one future call and generated the code. ::: :::warning Scheduling a call before the server has started throws an exception, so schedule from an endpoint or after `pod.start()` rather than while building the server. ::: ## Schedule a call Schedule a call by chaining the timing method, the accessor for your class, and the method to invoke. The accessor is your class name with the first letter lowercased and a trailing `FutureCall` removed, so `ExampleFutureCall` becomes `example`. Run a call one hour from now with `callWithDelay`: ```dart await pod.futureCalls .callWithDelay(const Duration(hours: 1)) .example .doWork(); ``` Run a call at a specific time with `callAtTime`: ```dart await pod.futureCalls .callAtTime(DateTime(2030, 1, 1)) .example .doOtherWork('1'); ``` The call runs at the exact instant you pass. A `DateTime` denotes an absolute moment, so `DateTime(2030, 1, 1)` schedules the call for midnight in the server's local time. Pass a UTC value, such as `DateTime.utc(2030, 1, 1)`, if you want the numbers to be read as UTC. ### Schedule from an endpoint Inside an endpoint you reach the server through `session.serverpod`, so you can schedule a call in response to a request: ```dart class OrderEndpoint extends Endpoint { Future placeOrder(Session session, Order order) async { // ... store the order ... // Send a follow-up an hour from now. await session.serverpod.futureCalls .callWithDelay(const Duration(hours: 1)) .example .doWork(); } } ``` ## Cancel scheduled calls Give a call an `identifier` when you schedule it so you can cancel it later. The same identifier can be applied to several calls. ```dart await pod.futureCalls .callWithDelay( const Duration(hours: 1), identifier: 'an-identifying-string', ) .example .doWork(); ``` Cancel every not-yet-run call that shares an identifier: ```dart await pod.futureCalls.cancel('an-identifying-string'); ``` Calls that share an identifier run independently. The identifier only groups them so you can cancel them together, and cancelling affects only calls that have not run yet. ## Inspect scheduled calls Scheduled calls are stored in the `serverpod_future_call` database table. You can query it to see what is pending, which helps when debugging why a call did or did not run. ## Execution timeout Future calls have no execution timeout: a call runs until its method returns. If a call needs a time limit, enforce it inside the method yourself, for example with `Future.timeout` around the work. ## Related - [Recurring tasks](https://docs.serverpod.dev/concepts/scheduling/recurring-tasks.md): run a call on a repeating schedule. - [Configuration](https://docs.serverpod.dev/concepts/scheduling/configuration.md): concurrency, scan interval, and broken-call handling. - [Sessions](https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions.md): the session a future call receives when it runs. --- # Recurring tasks https://docs.serverpod.dev/concepts/scheduling/recurring-tasks A recurring task runs the same future call again and again on a schedule, such as a nightly cleanup or an hourly sync. Schedule one with `callRecurring`, choosing either a fixed interval or a [cron](https://en.wikipedia.org/wiki/Cron) expression. Serverpod schedules the next run automatically, so you set it up once. Define the future call the same way as any other, then schedule it with `callRecurring`. Give recurring calls an `identifier` so you can stop them later. ```dart import 'package:serverpod/serverpod.dart'; class ExampleFutureCall extends FutureCall { Future cleanUp(Session session) async { session.log('Running the recurring cleanup.'); } } ``` ## Repeat on a fixed interval Use `every` to run the call once per interval. The first run happens one interval from now, unless you pass a `start` time. ```dart await pod.futureCalls .callRecurring(identifier: 'nightly-cleanup') .every(const Duration(hours: 24)) .example .cleanUp(); ``` Pass `start` to set when the first run happens. Later runs follow one interval apart from it: ```dart await pod.futureCalls .callRecurring(identifier: 'nightly-cleanup') .every(const Duration(hours: 24), start: DateTime.now().toUtc()) .example .cleanUp(); ``` A `start` in the past runs the call at the next scan, then continues on the interval from there. To pin a task to a wall-clock time such as 02:00 every day, use a cron expression instead. If the server is down when a run is due, that run happens once when the server is back, then the schedule jumps ahead to the next interval boundary. The intervals missed while the server was down do not pile up and fire all at once. ## Repeat on a cron schedule Use `cron` for calendar-based schedules that a fixed interval cannot express, such as "every day at 02:00" or "every Monday". A cron expression is a short text format of five fields, `minute hour day-of-month month day-of-week`, where `*` means "every". Serverpod also accepts a sixth field, written first, for seconds. ```dart await pod.futureCalls .callRecurring(identifier: 'daily-report') .cron('0 2 * * *') // Every day at 02:00 UTC. .example .cleanUp(); ``` Cron times are interpreted in UTC, so `'0 2 * * *'` runs at 02:00 UTC, not in the server's local time. The `Cron` parser is available from `package:serverpod/serverpod.dart` if you want to work with expressions directly. An invalid expression throws a `CronFormatException` when you schedule the call. ## Stop a recurring task A recurring task keeps rescheduling until you cancel it by its identifier: ```dart await pod.futureCalls.cancel('nightly-cleanup'); ``` This is why recurring calls should always be given an identifier when scheduled. ## Failure behavior Serverpod schedules the next run before it invokes the current one, so a recurring task keeps going even if one run throws. A failed run is logged and skipped, and the schedule continues. This is the opposite of a one-off call, which is not retried after a failure. ## Reschedule manually For an interval that changes from run to run, schedule the next call from inside the current one instead of using `callRecurring`. The call reschedules itself with whatever delay you compute: ```dart class ExampleFutureCall extends FutureCall { Future poll(Session session, int backoffSeconds) async { await _poll(session); // Schedule the next run with a delay computed from this run. await session.serverpod.futureCalls .callWithDelay(Duration(seconds: backoffSeconds * 2)) .example .poll(backoffSeconds * 2); } Future _poll(Session session) async { // ... the work ... } } ``` Because this pattern reschedules from inside the method, a run that throws before it schedules the next one stops the loop. Prefer `callRecurring` for fixed schedules. ## Related - [Future calls](https://docs.serverpod.dev/concepts/scheduling/future-calls.md): defining a call and scheduling one-off runs. - [Configuration](https://docs.serverpod.dev/concepts/scheduling/configuration.md): how often the server checks for due calls. - [Server roles](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md#choose-a-server-role): which roles run scheduled work. --- # Inheritance https://docs.serverpod.dev/concepts/scheduling/inheritance A `FutureCall` class can extend another and inherit its future-call methods. You can use this to share common logic across your own classes, or to run future calls that a [module](https://docs.serverpod.dev/concepts/server-fundamentals/modules.md) exposes to the projects that depend on it. A subclass keeps every method it inherits and can add new methods or override inherited ones. ## Extend a class When one `FutureCall` extends another, the generated code exposes both the inherited methods and the new ones. ```dart import 'package:serverpod/serverpod.dart'; class Greeter extends FutureCall { Future hello(Session session, String name) async { session.log('Hello $name'); } } class MyGreeter extends Greeter { Future bye(Session session, String name) async { session.log('Bye $name'); } } ``` The `Greeter` class exposes `hello`, and `MyGreeter` exposes both the inherited `hello` and its own `bye`. ## Expose future calls from a module Mark the parent `abstract` when you want it to define methods without being scheduled on its own. Serverpod generates no accessor for an abstract `FutureCall`, so the methods become available only through a concrete subclass. ```dart import 'package:serverpod/serverpod.dart'; abstract class Greeter extends FutureCall { Future hello(Session session, String name) async { session.log('Hello $name'); } } class MyGreeter extends Greeter {} ``` Here `Greeter` is not scheduled directly, and `MyGreeter` exposes the inherited `hello`. This is the pattern a module uses to hand future calls to the projects that depend on it. The module ships an abstract `Greeter`, and the consuming project defines a concrete subclass such as `MyGreeter`. The code is generated in the consuming project, so `pod.futureCalls` there exposes the module's `hello` method through the local subclass. A concrete subclass can still add its own methods on top of the inherited ones: ```dart class MyGreeter extends Greeter { Future bye(Session session, String name) async { session.log('Bye $name'); } } ``` This `MyGreeter` exposes both `hello` and `bye`. ## Override an inherited method A subclass can override an inherited method to change its behavior. An override replaces the parent's implementation: ```dart import 'package:serverpod/serverpod.dart'; abstract class Greeter extends FutureCall { Future hello(Session session, String name) async { session.log('Hello $name'); } } class ExcitedGreeter extends Greeter { @override Future hello(Session session, String name) async { session.log('Hello $name!!!'); } } ``` Here `ExcitedGreeter` exposes a single `hello` that logs `Hello $name!!!`. To build on the parent's behavior instead of replacing it, call `super.hello(session, name)` from inside the override. The override must keep a signature compatible with the base method, following Dart's own rules: you can add optional parameters, but you cannot add required parameters or change the return type. ## Related - [Future calls](https://docs.serverpod.dev/concepts/scheduling/future-calls.md): defining and scheduling a call. - [Modules](https://docs.serverpod.dev/concepts/server-fundamentals/modules.md): sharing code between projects. --- # Configuration https://docs.serverpod.dev/concepts/scheduling/configuration You configure future calls in your Serverpod config files or through environment variables. The options and their environment-variable names are listed in full in the [Configuration reference](https://docs.serverpod.dev/concepts/lookups/configuration-reference.md). | Option | Default | Controls | | ------------------------------ | ------- | ------------------------------------------------------------ | | `futureCallExecutionEnabled` | `true` | Whether this server runs future calls at all. | | `futureCall.concurrencyLimit` | `1` | How many calls may run at once. | | `futureCall.scanInterval` | `5000` | How often, in milliseconds, the server checks for due calls. | | `futureCall.checkBrokenCalls` | unset | Whether to scan for broken calls on startup. | | `futureCall.deleteBrokenCalls` | `false` | Whether to delete broken calls that are found. | ```yaml futureCallExecutionEnabled: true futureCall: concurrencyLimit: 1 # default scanInterval: 5000 # default, in milliseconds ``` ## Execution options ### Enable or disable execution The `futureCallExecutionEnabled` option turns future call execution on or off for a server. It is `true` by default. Set it to `false` in environments where background tasks should not run, such as a staging server where you want to test API behavior without triggering scheduled work. ```yaml futureCallExecutionEnabled: false ``` ### Concurrency limit The `concurrencyLimit` option sets how many future calls may run at the same time. The default is `1`, meaning calls run one after another. Raise it to run more calls in parallel, or keep it small so future calls do not crowd out other work on a busy server. Set it to `0` or a negative number to remove the limit entirely, allowing unlimited concurrent calls. Use this with care, since a burst of due calls can then exhaust the server's resources. ```yaml futureCall: concurrencyLimit: 5 ``` ### Scan interval The `scanInterval` option sets how often, in milliseconds, the server checks the database for calls that are due. The default is `5000` (5 seconds). A shorter interval runs calls closer to their scheduled time. A longer one reduces database load. ```yaml futureCall: scanInterval: 2000 ``` ## Broken future calls A scheduled call becomes broken when the code needed to run it no longer matches what was stored. This happens if, before the call runs, you restart the server after: - Removing the method the call points to. - Changing a future call method's signature so the stored parameters no longer deserialize. - Changing a model used as a parameter so the stored data no longer deserializes. A broken call fails at runtime when the server tries to execute it. You are responsible for keeping calls backward compatible, but Serverpod can detect and remove broken calls for you. ### Check for broken calls On startup the server can scan for broken calls. Whether it does depends on `checkBrokenCalls`: - **Unset (the default):** the check runs only when the table holds fewer than 1000 future calls, so a large backlog does not slow startup. - **`true`:** the check always runs, regardless of how many calls are scheduled. - **`false`:** the check never runs. ```yaml futureCall: checkBrokenCalls: true ``` :::note The `maintenance` role also runs this check. Started in that role, the server checks for broken calls, runs every currently-due future call once, and then exits. This is how future calls get processed in environments that do not run a persistent server, such as serverless hosting. See [server roles](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md#choose-a-server-role). ```bash dart run bin/main.dart --role maintenance ``` The process exits normally whether or not broken calls are found, so read the result from the logs. ::: :::warning This role executes every due future call, so it is not a dry check. Do not run it against a production database to see what would happen. ::: ### Delete broken calls When the check finds broken calls it logs a warning but does not delete them. Set `deleteBrokenCalls` to `true` to delete them instead. Deletion only happens when the check actually runs. ```yaml futureCall: deleteBrokenCalls: true ``` ## Related - [Future calls](https://docs.serverpod.dev/concepts/scheduling/future-calls.md): defining and scheduling a call. - [Configuration reference](https://docs.serverpod.dev/concepts/lookups/configuration-reference.md): every key with its environment variable. - [Server roles](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md#choose-a-server-role): the maintenance role used above. --- # Overview https://docs.serverpod.dev/concepts/operations/overview Once your server is written, the work shifts from building features to running them. This section covers what you need after deployment: seeing what the server is doing, proving to a host that it is healthy, securing the traffic it accepts, finding out when something breaks, scaling past a single process, and measuring it under load. Everything here works the same whether you deploy to [Serverpod Cloud](https://docs.serverpod.dev/deployments/deploy-to-serverpod-cloud.md) or [host it yourself](https://docs.serverpod.dev/deployments/custom-hosting/choosing-a-strategy.md), though the two differ in how much is set up for you. ## What each page covers - **[Logging](https://docs.serverpod.dev/concepts/operations/logging.md)**: what the server records for every call, where those records go, and how to keep the log tables from growing without bound. - **[Health checks](https://docs.serverpod.dev/concepts/operations/health-checks.md)**: the HTTP endpoints a host calls to decide whether your server is alive and ready for traffic, plus the metrics Serverpod collects about itself. - **[Security and TLS](https://docs.serverpod.dev/concepts/operations/security-and-tls.md)**: how traffic to your server is encrypted, and when you need to configure that yourself. - **[Exception monitoring](https://docs.serverpod.dev/concepts/operations/exception-monitoring.md)**: reporting exceptions to a monitoring service as they happen. This one is an experimental API. - **[Scalability](https://docs.serverpod.dev/concepts/operations/scalability.md)**: roles for scale-out, isolates for CPU work, Postgres and connection pools, Redis, JWT auth, and streaming tradeoffs. - **[Redis](https://docs.serverpod.dev/concepts/operations/redis.md)**: the optional service that lets several server instances share a cache, messages, and auth revocation, plus how to enable it, set its password, run it locally in Docker, and connect a GUI. - **[Load testing](https://docs.serverpod.dev/concepts/operations/load-testing.md)**: measuring how a production-like server behaves under concurrent traffic, with Locust or the generated client. ## Related - [Configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md): the config files and environment variables every setting on these pages is read from. - [Sessions](https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions.md): the object that produces most of what ends up in your logs. - [Caching](https://docs.serverpod.dev/concepts/endpoints-and-apis/caching.md): storing values in server memory or Redis. - [Run code on shutdown](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md#run-code-on-shutdown): cleanup work when the server stops. - [Insights](https://docs.serverpod.dev/tools/insights.md): the companion app for reading logs and metrics. --- # Logging https://docs.serverpod.dev/concepts/operations/logging Logging is how you find out what your server did after it did it: which calls ran, which queries were slow, and what failed. Serverpod records this for you, and you add your own messages on top. There are three kinds of record, and the difference matters for everything below: - **Session records** describe one unit of work, such as an endpoint call: how long it took, whether it failed, and which endpoint it hit. - **Query records** describe individual database queries run during that session. - **Log messages** are the entries you write yourself with `session.log`. ## Write your own messages Call `log` on the [session](https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions.md) you were given: ```dart session.log('This is working well'); ``` Pass a level, an exception, and a stack trace when something goes wrong: ```dart session.log( 'Oops, something went wrong', level: LogLevel.warning, exception: e, stackTrace: stackTrace, ); ``` Messages are collected while the session runs and written when it closes, whether it finished normally or threw. ## Where logs go Records are written to the database, to the console, to both, or to neither. In the database they land in three tables: | Table | Holds | | ----------------------- | ------------------------------ | | `serverpod_session_log` | One row per completed session. | | `serverpod_log` | Your `session.log` messages. | | `serverpod_query_log` | Database queries. | The last two reference the session row, so deleting a session row removes its queries with it. :::info The companion app [Serverpod Insights](https://docs.serverpod.dev/tools/insights.md) reads and searches these tables, and can change the runtime settings described below. ::: ## Which sessions get recorded Not every session produces a row, and the default depends on the run mode. In `development`, every completed session is recorded. In `staging`, `production`, and `test`, a session is recorded only when it ran longer than one second, it failed, or it produced a log or query entry. Ordinary fast calls leave no row, which keeps the table to the sessions worth looking at. These thresholds are runtime settings stored in the `serverpod_runtime_settings` table, so you can change them on a running server through Insights without redeploying. They control whether all sessions are logged, whether all queries are logged, what counts as slow, and the minimum level a message must have to be kept. You can also override them per endpoint and per method. ## Configure logging Session logging is configured under `sessionLogs:` in your config file for the run mode, or through environment variables. Environment variables win over the config file, key by key. | Setting | Environment variable | Default | | ------------------- | ------------------------------------------ | ----------------------------------------------------------------------- | | `persistentEnabled` | `SERVERPOD_SESSION_PERSISTENT_LOG_ENABLED` | `true` when a database is configured | | `consoleEnabled` | `SERVERPOD_SESSION_CONSOLE_LOG_ENABLED` | `true` in `development` or when there is no database, otherwise `false` | | `consoleLogFormat` | `SERVERPOD_SESSION_CONSOLE_LOG_FORMAT` | `text` in `development`, otherwise `json` | Three more settings control [purging](#purge-old-records), which behaves differently enough to be worth reading before you rely on it. ```yaml sessionLogs: persistentEnabled: true # Store records in the database consoleEnabled: true # Also print them cleanupInterval: 6h # Purge every 6 hours retentionPeriod: 30d # Keep 30 days retentionCount: 5000 # Keep at most 5,000 sessions ``` Durations use the same format as [model default values](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#supported-default-values), such as `30d`, `6h`, or `1d 2h 30min`. :::warning Setting `persistentEnabled` to `true` without a configured database throws a `StateError` on startup. Persistent logging needs somewhere to persist to. ::: :::warning Persistent logging is unavailable on SQLite, which cannot handle the concurrent writes it needs. The server warns and skips it, and nothing takes its place, so enable `consoleEnabled` if you want records in the run modes where it defaults to off. ::: :::info Every environment variable in the table takes a real value. Setting one to an empty string is not a way to unset it: the server fails to start. To turn a policy off, set the key to `null` in the config file instead. ::: ## Purge old records Log tables grow with every call your server handles, so Serverpod can delete old records for you. Cleanup runs on the `cleanupInterval`, and removes session rows that are either older than `retentionPeriod` or beyond the newest `retentionCount`, whichever applies first. Deleting a session row takes its query and log rows with it. | Setting | Environment variable | Default | | ----------------- | ---------------------------------------- | ------------------------ | | `cleanupInterval` | `SERVERPOD_SESSION_LOG_CLEANUP_INTERVAL` | Unset, so no purging | | `retentionPeriod` | `SERVERPOD_SESSION_LOG_RETENTION_PERIOD` | Unset, so no age limit | | `retentionCount` | `SERVERPOD_SESSION_LOG_RETENTION_COUNT` | Unset, so no count limit | :::warning These three fall back to `24h`, `90d`, and `100000` only when `sessionLogs` is absent from your config entirely. Set any one session-log key, in the file or through an environment variable, and the ones you did not set resolve to unset rather than to those values, switching that policy off with no warning. Generated projects ship a `sessionLogs` block in the `development`, `test`, and `production` configs, so purging is off in those run modes until you set all three explicitly. Set `cleanupInterval`, `retentionPeriod`, and `retentionCount` together whenever you configure any part of `sessionLogs`. ::: The `cleanupInterval` setting is the switch for the whole job. With no interval set, nothing is purged whatever the two retention values say. Purging also requires `persistentEnabled`, since there is nothing to delete otherwise. Cleanup is triggered by log writes rather than by a timer, so a server that is not logging anything does not purge. A single pass gives up after an hour, and the next interval starts a fresh one. ## Related - [Configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md): the config files these settings live in. - [Configuration reference](https://docs.serverpod.dev/concepts/lookups/configuration-reference.md): every session-log key with its environment variable. - [Sessions](https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions.md): what a session is, and the `log` method. - [Insights](https://docs.serverpod.dev/tools/insights.md): reading logs and editing runtime settings. - [Load testing](https://docs.serverpod.dev/concepts/operations/load-testing.md): why persisting every session during a run becomes the bottleneck. --- # Health checks https://docs.serverpod.dev/concepts/operations/health-checks When your server runs behind a host that can restart it or route traffic away from it, that host needs a way to ask how the server is doing. Serverpod answers three such questions over HTTP, at URLs that match what container platforms like Kubernetes expect. This page covers two separate things that share the word "health": - **Health probes**, the HTTP endpoints something else calls to decide whether to send you traffic or restart you. - **Health metrics**, numbers Serverpod records about itself into your database for you to look at later. ## Health probes The three endpoints are always available, with no setup. Every server your Serverpod instance runs answers them, so in a default development configuration they respond on the API server's port `8080`, the Insights server's `8081`, and the web server's `8082` when your project has one. Point your host at whichever port it can reach. These three paths are reserved. A [web server route](https://docs.serverpod.dev/concepts/web-server/routing.md) registered on them would never be reached. | Endpoint | Question it answers | Returns | | ----------- | -------------------------------------------------------- | ------------------------------------------------------------- | | `/livez` | Is the process still working, or should it be restarted? | `200` if the server can respond at all. | | `/readyz` | Should traffic be sent here right now? | `200` when dependencies are healthy, `503` when they are not. | | `/startupz` | Has the server finished starting? | `200` once startup is complete. | ```bash curl http://localhost:8080/readyz ``` **Liveness** is deliberately permissive. It only reports failure when the process is broken beyond recovery, because failing it means a restart. A slow database or a brief network problem should not restart your server, so `/livez` does not check dependencies at all. **Readiness** is the one that controls traffic. It checks the database, Redis when it is enabled, and any custom indicators you add. Failing readiness stops new traffic without restarting the process, which gives a struggling server room to recover. **Startup** exists so the other two are not consulted too early. While it is failing, a platform holds off its liveness and readiness probes. In practice Serverpod opens its HTTP listeners as the last step of starting, so a probe sent during startup gets a refused connection rather than a `503`. ### Response format The probes follow the [draft standard for health check responses](https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-06). Requests without valid authentication get the status code and an empty body, so nothing about your dependencies is exposed publicly. An authenticated request gets the same status code plus a body. Any credential your server's authentication handler accepts unlocks the body, with no particular scope required. ```json { "status": "pass", "time": "2026-01-14T10:30:00Z", "checks": { "database:connection": [ { "componentType": "datastore", "status": "pass", "observedValue": 12, "observedUnit": "ms", "time": "2026-01-14T10:30:00Z" } ], "redis:connection": [ { "componentType": "datastore", "status": "pass", "time": "2026-01-14T10:30:00Z" } ] } } ``` When a check fails, the response also carries `notes` listing which ones. The `checks` object is left out when there is nothing to report, which is the normal case for `/livez`. ### Built-in indicators Serverpod registers these based on your configuration: - `serverpod:startup` records that the server has begun starting. - `database:connection` checks the database, when one is configured. - `redis:connection` checks Redis, when it is [enabled](https://docs.serverpod.dev/concepts/operations/redis.md). ### Add your own indicator Extend `HealthIndicator` to check something your server depends on, such as an external API: ```dart import 'package:serverpod/serverpod.dart'; class StripeApiIndicator extends HealthIndicator { @override String get name => 'stripe:api'; @override String get componentType => HealthComponentType.component.name; @override String get observedUnit => 'ms'; @override Duration get timeout => const Duration(seconds: 3); @override Future check() async { final stopwatch = Stopwatch()..start(); try { await stripeClient.ping(); stopwatch.stop(); return pass(observedValue: stopwatch.elapsedMilliseconds.toDouble()); } catch (e) { return fail(output: 'Stripe API unavailable: $e'); } } } ``` The type parameter is the type of `observedValue`, which is what the check reports alongside pass or fail. Use `output` to attach a message, as the failure branch above does. Override `componentId` when several instances of the same component exist, such as `primary-db` and `replica-db`, and the response should say which one answered. The built-in indicators leave it unset. Register it through `healthConfig` when you create the server, choosing the list by which probe should run it: ```dart final pod = Serverpod( args, healthConfig: HealthConfig( cacheTtl: Duration(seconds: 2), additionalReadinessIndicators: [StripeApiIndicator()], additionalStartupIndicators: [CacheWarmupIndicator()], ), ); ``` - `additionalReadinessIndicators` are checked by `/readyz`, so use them for dependencies that must be available to serve traffic. - `additionalStartupIndicators` are checked by `/startupz`, so use them for work that has to finish before the server is ready at all, such as warming a cache. The `cacheTtl` option sets how long a result is reused before the check runs again, which keeps frequent probing from hammering your dependencies. It defaults to one second. Each indicator can set its own `timeout`, five seconds by default, so one slow check cannot hold up the whole response. ## Health metrics Separately from the probes, Serverpod records numbers about itself: CPU, memory, and how long the database takes to respond. These go into the `serverpod_health_metric` and `serverpod_health_connection_info` tables, and Insights charts them. Collection runs once per `healthCheckInterval`, one minute by default. Setting the interval to zero turns collection off. A few conditions apply: a cycle writes nothing when the database has not been used since the last one, collection only runs in the `monolith` and `maintenance` [server roles](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md#choose-a-server-role), and it does not run on Windows. Older rows are folded up rather than kept forever: minute rows become hourly after two days, and hourly rows become daily after about a month. ### Record your own metric A `HealthCheckHandler` runs on the same schedule as the built-in metrics and returns whatever you want recorded: ```dart Future> myHealthCheckHandler( Serverpod pod, DateTime timestamp, ) async { return [ ServerHealthMetric( name: 'MyMetric', serverId: pod.serverId, timestamp: timestamp, isHealthy: true, value: 1.0, granularity: 1, ), ]; } ``` The `granularity` field is the period the value covers, in minutes. Use `1` for values recorded on the normal cycle, since Serverpod produces the hourly and daily rows itself. Register the handler on the server: ```dart final pod = Serverpod( args, healthCheckHandler: myHealthCheckHandler, ); ``` :::note The two APIs read alike but are not related. Use `healthConfig` with `HealthIndicator` for the HTTP probes, and `healthCheckHandler` with `ServerHealthMetric` for the recorded metrics. ::: ## Related - [Logging](https://docs.serverpod.dev/concepts/operations/logging.md): the other half of knowing what your server is doing. - [Insights](https://docs.serverpod.dev/tools/insights.md): charts for the collected metrics. - [Load testing](https://docs.serverpod.dev/concepts/operations/load-testing.md): reading these probes and metrics while generating concurrent traffic. - [Custom hosting](https://docs.serverpod.dev/deployments/custom-hosting/choosing-a-strategy.md): wiring the probes up to your host. --- # Security and TLS https://docs.serverpod.dev/concepts/operations/security-and-tls Traffic between your app and your server should be encrypted, which on the web means HTTPS. HTTPS is HTTP wrapped in TLS, and somewhere in the chain a piece of software has to hold your certificate and do the encrypting. That job is called terminating TLS. Most of the time it is not your server doing it. On Serverpod Cloud, TLS is handled for you and there is nothing to configure. On your own infrastructure it is normally handled in front of the server, by a load balancer or reverse proxy such as Nginx, a cloud load balancer, or Cloudflare, which forwards plain HTTP to Serverpod on an internal network. Serverpod can also terminate TLS itself, which is useful when there is nothing in front of it to do the job. ## Serve HTTPS from the server Pass a `SecurityContextConfig` when you create the server, with a certificate chain and private key for each server you want to secure: ```dart final securityContext = SecurityContext() ..useCertificateChain('path/to/server_cert.pem') ..usePrivateKey('path/to/server_key.pem', password: 'password'); Serverpod( args, securityContextConfig: SecurityContextConfig( apiServer: securityContext, webServer: securityContext, insightsServer: securityContext, ), ); ``` A Serverpod instance runs [three servers](https://docs.serverpod.dev/concepts/server-fundamentals/your-serverpod-project.md#the-three-servers), and each takes its own context, so you can secure them independently. ## Trust the server's certificate from your app Your app only needs configuring when it cannot verify your certificate on its own. Certificates from a public authority, including the ones Serverpod Cloud provisions, are trusted automatically and need nothing here. Self-signed certificates and private certificate authorities are the exception. There, name the certificate you want trusted by passing a `SecurityContext` to the generated `Client`: ```dart final securityContext = SecurityContext() ..setTrustedCertificates('path/to/server_cert.pem'); final client = Client( 'https://yourserver.com', securityContext: securityContext, ); ``` ### With an HTTP client override The `securityContext` and [`httpClientOverride`](https://docs.serverpod.dev/concepts/endpoints-and-apis/configure-http-calls.md) parameters cannot both be set on the same `Client`, since the override replaces the HTTP client the security context would have configured. Supply the certificates through the client you pass in instead. On `dart:io` platforms, build an `HttpClient` with your trusted certificates and wrap it: ```dart import 'dart:io'; import 'package:http/io_client.dart'; final securityContext = SecurityContext() ..setTrustedCertificates('path/to/server_cert.pem'); final client = Client( 'https://yourserver.com', httpClientOverride: IOClient( HttpClient(context: securityContext), ), ); ``` ## Related - [Configure HTTP calls](https://docs.serverpod.dev/concepts/endpoints-and-apis/configure-http-calls.md): certificates and HTTP client overrides on the app side. - [Configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md): request size limits and header settings. - [Custom hosting](https://docs.serverpod.dev/deployments/custom-hosting/choosing-a-strategy.md): where a proxy fits when you host it yourself. --- # Exception monitoring https://docs.serverpod.dev/concepts/operations/exception-monitoring When something throws in production you want to hear about it without reading log tables. Serverpod can hand every exception to code you provide, so you can forward it to a monitoring service such as [Sentry](https://sentry.io/), [Highlight](https://www.highlight.io/), or [Datadog](https://www.datadoghq.com/). This works for exceptions thrown in your own code and for exceptions the framework raises itself, including failures during startup and shutdown. :::warning This is an experimental feature, reached through `serverpod.experimental`. Its API can change in a breaking way in any minor release, so pin your Serverpod version if you depend on it and re-check this page when you upgrade. Experimental APIs move to `Serverpod` proper once they settle. ::: ## Add a handler Handlers are registered through the `experimentalFeatures` argument on the `Serverpod` constructor. A handler receives the event, the space it came from, and its context: ```dart var pod = Serverpod( args, experimentalFeatures: ExperimentalFeatures( diagnosticEventHandlers: [ AsEventHandler((event, {required space, required context}) { print('$event Origin is $space\n Context is ${context.toJson()}'); }), ], ), ); ``` The `AsEventHandler` class wraps a plain function as a handler, which is the shortest way to get started. For anything longer, implement `DiagnosticEventHandler` yourself. The `space` tells you whether the event came from your application code or from the framework, and the `context` carries details such as the server ID and run mode. Add as many handlers as you like. Serverpod runs them concurrently and does not wait for them, so a slow handler does not slow down the request that triggered it. Each invocation is abandoned after `experimentalDiagnosticHandlerTimeout`, which defaults to 30 seconds. See the [Configuration reference](https://docs.serverpod.dev/concepts/lookups/configuration-reference.md). Handlers are for observation only. They cannot suppress an exception, change it, or alter the response. If a handler throws, the error is logged and otherwise ignored. ## Report an exception yourself Call `submitDiagnosticEvent` on the `experimental` member of the server to report something you caught: ```dart class OrderEndpoint extends Endpoint { Future placeOrder(Session session) async { try { throw Exception('An exception is thrown'); } catch (e, stackTrace) { session.serverpod.experimental.submitDiagnosticEvent( ExceptionEvent(e, stackTrace), session: session, ); } return 'success'; } } ``` The `ExceptionEvent` class is the built-in event for a thrown exception and a stack trace. Passing the session attaches its context to the event, so the handler knows which call the exception came from. This works anywhere you have a session, including endpoint methods, web calls, and future calls. ## Related - [Logging](https://docs.serverpod.dev/concepts/operations/logging.md): what the server records without any handler. - [Error handling and exceptions](https://docs.serverpod.dev/concepts/endpoints-and-apis/error-handling-and-exceptions.md): how exceptions reach your app. - [Run code on shutdown](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md#run-code-on-shutdown): the other experimental API Serverpod exposes. - [Configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md#experimental-features): opting in to experimental features. - [Testing](https://docs.serverpod.dev/concepts/testing/advanced-examples.md): testing that your handlers receive the events you expect. --- # Scalability https://docs.serverpod.dev/concepts/operations/scalability When traffic jumps, the first bottlenecks are usually a blocked Dart event loop, an undersized database pool across many nodes, or shared state that only lives in one process. This page covers how Serverpod scales out request-serving instances, and what to change so CPU work, Postgres, auth, caching, and long-lived connections keep up. Elastic scale-out of request handlers is already covered by [server roles](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md#choose-a-server-role). The sections below cover what still bites at high daily active users. Built-in read replicas and a Serverpod-owned Postgres tuning recipe are not available today; use cloud provider guides and the escape hatches noted here. ## How Serverpod scales Each Serverpod process runs on one Dart isolate and one event loop. CPU-heavy work on that isolate delays request handling for every concurrent call on the same process. HTTPS APIs with JWT auth are largely stateless and scale behind a load balancer. Features that keep in-process state (local caches, local server events, long-lived streams without a shared bus) need either sticky routing, Redis, or a redesign so state lives outside the process. For serverless or auto-scaled fleets, run request nodes in the `serverless` role (or `monolith` if you accept in-process maintenance), and run `maintenance` on a schedule for future calls and health metric collection. See [Hosting elsewhere](https://docs.serverpod.dev/deployments/custom-hosting/hosting-elsewhere.md#server-roles) and [Scheduling overview](https://docs.serverpod.dev/concepts/scheduling/overview.md). ## Split request and maintenance work Use roles so request capacity and background work do not share the same scaling curve: | Role | Serves requests | Runs future calls and health metrics | | ------------- | --------------- | ------------------------------------ | | `monolith` | Yes | Yes (continuous) | | `serverless` | Yes | No | | `maintenance` | No | Once, then exit | ```bash dart run bin/main.dart --mode production --role serverless ``` Schedule a separate process in the `maintenance` role (for example once per minute) when request nodes run as `serverless`. On pure request nodes you can also set `futureCallExecutionEnabled` to `false`; see [Future call configuration](https://docs.serverpod.dev/concepts/scheduling/configuration.md). ## Offload CPU work to isolates Image decoding, password hashing, and other CPU-bound work should not run on the request isolate. Use Dart's `Isolate.run` with plain data only: ```dart Future> buildThumbnail(List bytes) async { return Isolate.run(() { // CPU-heavy decode, resize, and encode. // Pass only serializable data in and out. return processImage(bytes); }); } ``` Do not pass a `Session`, database connection, or Redis client into the isolate. Do I/O on the main isolate after the isolate returns. There is no first-party Serverpod helper for this pattern. For sustained background workloads, raise `futureCall.concurrencyLimit` carefully (default `1`), or plan for a dedicated worker model as your product grows. ## Optimize Postgres queries Declare indexes in your models for columns you filter and sort on. Without an index that covers an `ORDER BY`, Postgres may sort the full result set before applying `OFFSET` / `LIMIT`. See [Indexing](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md). Prefer [cursor-based pagination](https://docs.serverpod.dev/concepts/data-and-the-database/database/pagination.md#cursor-based-pagination) over deep `offset` pages on large tables: filter with `where: (t) => t.id > lastId`, `orderBy` on `id`, and a `limit`. At high scale, treat wide `OR` / `inSet` filters carefully, especially expressions like `(A OR B) AND (C OR D)`. Those shapes can explode into many indexed paths. Prefer a denormalized flag, a `UNION` of simpler indexed queries, or a narrower filter design when those queries dominate load. ## Size the connection pool Each Serverpod process opens its own pool. The default `database.maxConnectionCount` is `10` (override with `SERVERPOD_DATABASE_MAX_CONNECTION_COUNT`). Across a fleet: `(number of nodes × maxConnectionCount) + headroom ≤ Postgres max_connections` Leave headroom for Insights, maintenance jobs, migrations, and admin tools. Unlimited pools (a negative value, or omitting the key) are easy to misconfigure under auto-scaling. See [Database connection](https://docs.serverpod.dev/concepts/data-and-the-database/database/connection.md#configure-connection-pool-size). ## Scale Postgres itself Assigning CPU, memory, and storage to Postgres is owned by your cloud provider (RDS instance class, Cloud SQL machine type, and similar). Point your operators at those guides rather than copying example parameters into Serverpod config. Serverpod does not ship a read-only query API (there is no `MyTable.dbro`) and does not auto-route reads to replicas. Practical options today: - Put a proxy or cloud read endpoint in front of the database (PgBouncer, RDS Proxy, or your provider's reader endpoint) when the proxy can classify traffic. - Or wrap `session.db` with a [database interceptor](https://docs.serverpod.dev/concepts/data-and-the-database/database/database-interceptors.md) that routes selected reads yourself. Treat third-party Postgres tuning examples as illustrative for your workload and instance size, not as Serverpod defaults. ## Prefer JWT for auth at scale Modern projects use the [JWT token manager](https://docs.serverpod.dev/concepts/authentication/token-managers/jwt-token-manager.md). Access-token verification is local and does not query the database on every request. Refresh still hits the database; that path is far less frequent than ordinary API calls. [Server-side sessions](https://docs.serverpod.dev/concepts/authentication/token-managers/managing-tokens.md) are the opposite tradeoff: a database lookup on validation, with immediate revocation. Prefer JWT when request volume is the constraint. Legacy auth key handlers that load a row per request do not belong on a high-QPS fleet. ## Share state across instances with Redis Local caches (`session.caches.local` and `localPrio`) stay on one process. For values every instance must see, use the [global cache](https://docs.serverpod.dev/concepts/endpoints-and-apis/caching.md#the-global-cache-and-redis) with Redis enabled. Cross-instance [server events](https://docs.serverpod.dev/concepts/endpoints-and-apis/server-events.md) need Redis as well. `MessageScope.global` throws a `StateError` without Redis, and the default `MessageScope.auto` falls back to local delivery, so without Redis events silently stop reaching other instances. [Enable Redis in production config](https://docs.serverpod.dev/concepts/operations/redis.md#test-staging-and-production) when you run more than one request node that shares cache entries or broadcast events. ## Choose streams carefully [Streaming endpoints](https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming.md) keep a WebSocket session open for the life of the stream. That is the right model for chat, multiplayer, and live dashboards. At very high concurrent clients per node, long-lived connections consume file descriptors and complicate load balancing and autoscaling. When fan-out volume is the problem, prefer short HTTPS RPCs for client-to-server work and an external push service (FCM, APNs, or similar) for server-to-client wakeups. Firebase in Serverpod is an auth identity provider, not a push delivery product; wire push as an integration outside the framework. Keep `websocketPingInterval` (default 30 seconds) in mind under high connection counts: each open socket pays keepalive cost. ## Configuration cheat sheet | Setting | Default | Why it matters at scale | | -------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------- | | `role` / `SERVERPOD_SERVER_ROLE` | `monolith` | Split request nodes from maintenance work. | | `database.maxConnectionCount` | `10` | Pool size times node count must fit Postgres. | | `redis.enabled` | `true` when a `redis` section is present | Required for shared cache and global events. | | `maxRequestSize` | `524288` | Large uploads increase memory pressure. | | `websocketPingInterval` | `30` (seconds) | Keepalive cost under many open streams. | | `futureCall.concurrencyLimit` | `1` | Caps background CPU and database load. | | `sessionLogs.persistentEnabled` | on when a database is configured | Extra database writes per request when on. | | `healthCheckInterval` | 1 minute | Metrics write load; aggressive liveness elsewhere can cascade restarts. | Full keys and environment variables: [Configuration reference](https://docs.serverpod.dev/concepts/lookups/configuration-reference.md). ## Limits - No first-class read-replica routing or `dbro` API; use a proxy or a database interceptor. - No Serverpod-owned production Postgres tuning profile; follow your cloud database guide. - Isolate offloading is a Dart convention in your code, not a framework API. - Terraform and sample AWS layouts in community examples are starting points, not production scale blueprints. ## Related - [Server roles](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md#choose-a-server-role): which processes serve traffic versus maintenance. - [Hosting elsewhere](https://docs.serverpod.dev/deployments/custom-hosting/hosting-elsewhere.md): roles and Docker on your own host. - [Indexing](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md): declare indexes for filters and sorts. - [Caching](https://docs.serverpod.dev/concepts/endpoints-and-apis/caching.md): local versus Redis-backed global cache. - [Redis](https://docs.serverpod.dev/concepts/operations/redis.md): enabling Redis, the password, and connecting a GUI. - [Health checks](https://docs.serverpod.dev/concepts/operations/health-checks.md): probes and metric collection under load. - [Configuration reference](https://docs.serverpod.dev/concepts/lookups/configuration-reference.md): every scale-related env var. --- # Redis https://docs.serverpod.dev/concepts/operations/redis Turn Redis on when more than one Serverpod instance must share a cache, pass messages, or revoke authentication together. Redis is optional. Without it, those features stay local to each process, so a second instance does not see the first instance's cache or events. This page covers enabling Redis, the password, the development Docker instance, and connecting a client such as RedisInsight. For using the cache and messages once Redis is up, see [Caching](https://docs.serverpod.dev/concepts/endpoints-and-apis/caching.md) and [Server events](https://docs.serverpod.dev/concepts/endpoints-and-apis/server-events.md). Whether a new project includes Redis configuration depends on how you created it: - **`serverpod create` with the setup screen:** only if you select Redis. - **`serverpod create` without the setup screen** (for example with `--no-interactive`): included by default. - **`serverpod quickstart`:** always included. Including it copies the config, the Compose service, and a generated password. It doesn't turn Redis on. ## Connection details Each run-mode file can contain a `redis` block. For local development, that file is `config/development.yaml`. ```yaml title="config/development.yaml" redis: enabled: false host: localhost port: 8091 # user: # defaults to empty # requireSsl: true # defaults to false ``` The `host` is the domain name or IP address of your Redis instance, and `port` is the port Redis is listening on. The `user` and `requireSsl` keys are optional. Leave `user` unset unless your Redis server uses ACL usernames. The `requireSsl` key defaults to `false`. The scaffold writes `enabled: false` in every run mode. If the `redis` block exists and you omit `enabled`, Serverpod treats Redis as enabled. When Redis is disabled, Serverpod does not load it, and the password is optional. | File | `enabled` | Host | Port | | ------------------------- | --------- | ----------------------------------------- | ---- | | `config/development.yaml` | `false` | `localhost` | 8091 | | `config/test.yaml` | `false` | `localhost` | 9091 | | `config/staging.yaml` | `false` | `redis.private-staging.examplepod.com` | 6379 | | `config/production.yaml` | `false` | `redis.private-production.examplepod.com` | 6379 | Development and test use 8091 and 9091 so they do not clash with a Redis installed on the machine's default port 6379. The staging and production templates use 6379 and placeholder private hostnames you replace with your own. If your project has no `redis` block, add the snippet above, a password (see [Redis password](#redis-password)), and a Redis service to `docker-compose.yaml`. The `host`, `port`, `user`, `enabled`, and `requireSsl` keys can each be set with environment variables instead of YAML. See the [configuration reference](https://docs.serverpod.dev/concepts/lookups/configuration-reference.md) for the full list. You can enable Redis with environment variables alone, with no `redis` block in YAML, if host, port, and password are set. ## Redis password The Redis password is stored in `config/passwords.yaml` under the run-mode key, as a sibling of `database`. It is not nested under `database`. ```yaml title="config/passwords.yaml" development: database: '' redis: '' ``` The create command generates a password for `development` and `test` only. Staging and production templates do not include a `redis` password. Add one under that run mode before you enable Redis there, or set it through an environment variable. ```bash export SERVERPOD_PASSWORD_redis='...' ``` ```bash export SERVERPOD_REDIS_PASSWORD='...' ``` Both variables set the same secret. If both are set, `SERVERPOD_PASSWORD_redis` wins. Environment variables override the passwords file. See [Manage secrets](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md#manage-secrets) for how secrets are loaded. If Redis is enabled and the password is missing, the server refuses to start: ```text Missing password for "redis". Please check your config/passwords.yaml file or the `SERVERPOD_PASSWORD_redis` environment variable. ``` ## Enable Redis Set `enabled` to `true` in the run-mode file you will start: ```yaml title="config/development.yaml" redis: enabled: true host: localhost port: 8091 ``` When Redis is enabled, `host`, `port`, and a password are required. ## The development Redis The scaffolded development Redis is a Docker container from the server package's `docker-compose.yaml`. It uses the `redis:6.2.6` image, publishes host port **8091** to container port **6379**, and starts Redis with password-only authentication (`--requirepass`). There is no username. New projects use an [embedded PostgreSQL](https://docs.serverpod.dev/concepts/data-and-the-database/database/embedded-postgres.md) (`database.dataPath`), so `serverpod start` does not bring Compose up on its own. Start Redis yourself from the server package: ```bash docker compose up --detach ``` To start Compose whenever the server starts, pass `--docker`: ```bash serverpod start --docker ``` See [Running your server](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md) for when Compose is started automatically. To stop Redis: ```bash docker compose stop ``` To remove the containers and **delete** all associated data: ```bash docker compose down -v ``` ## Connect a GUI Use these values from your machine for RedisInsight, `redis-cli`, or any other Redis client. :::caution Redis itself listens on port 6379. Serverpod's development setup publishes **8091** on the host to avoid conflicts. A GUI running on your machine must use 8091, not 6379. ::: **Development** (Docker from the template) | Field | Value | | -------- | ---------------------------------------------- | | Host | `localhost` | | Port | `8091` (not 6379) | | Username | empty | | Password | `development.redis` in `config/passwords.yaml` | | TLS | off (`requireSsl: false`) | ```bash redis-cli -h localhost -p 8091 -a '' ``` **Test** (Docker from the template) | Field | Value | | -------- | --------------------------------------- | | Host | `localhost` | | Port | `9091` | | Username | empty | | Password | `test.redis` in `config/passwords.yaml` | The default Docker Redis authenticates with a password only. Leave the username empty. If you set `user` in YAML, Serverpod sends Redis ACL authentication with that username and the password. Inside the Compose network, Redis listens on **6379** with the same password. Port 8091 is only the mapping published to the host. ## Test, staging, and production The test instance is a second Compose service on host port **9091**, with its password under `test.redis` in `passwords.yaml`. Enable it in `config/test.yaml` the same way as development. Staging and production templates listen on **6379** and ship with placeholder hosts. They do not include a generated Redis password. Before you set `enabled: true` in those files: 1. Point `host` at your Redis instance. 2. Add a `redis` password under that run mode in `passwords.yaml`, or set `SERVERPOD_PASSWORD_redis` / `SERVERPOD_REDIS_PASSWORD`. 3. Set `requireSsl` and `user` if your provider requires them. On [Serverpod Cloud](https://docs.serverpod.dev/cloud.md), set the same values with `serverpod cloud password set` and `serverpod cloud variable set`. See [Use Redis for PubSub and caching](https://docs.serverpod.dev/cloud/guides/redis.md). ## SSL and username Managed Redis (Upstash, ElastiCache, Memorystore, and similar) often requires TLS, and some providers require a username. ```yaml title="config/production.yaml" redis: enabled: true host: your-redis-host.example.com port: 6379 user: default requireSsl: true ``` The `requireSsl` key defaults to `false`. Set it to `true` when the provider requires TLS. Set `user` only when the provider uses Redis ACL usernames. The default Docker Redis in development has neither. For Serverpod Cloud with a third-party Redis such as Upstash, follow [Use Redis for PubSub and caching](https://docs.serverpod.dev/cloud/guides/redis.md). ## What Redis is used for Local caches (`session.caches.local`, `localPrio`, and `query`) never use Redis. | Feature | With Redis | Without Redis | | ------------------------------ | --------------------------------- | --------------------------------------- | | `session.caches.global` | Shared across instances | Isolated in-memory fallback, not shared | | `session.messages` (default) | Pub/sub across the cluster | Local to this process | | `MessageScope.global` | Cluster-wide | Throws `StateError` | | Auth revocation across servers | Propagated through Redis | Posted locally only | | `/readyz` | Includes a Redis connection check | Redis check omitted | The in-memory fallback for `session.caches.global` is a separate cache, not `local` or `localPrio`. See [The global cache and Redis](https://docs.serverpod.dev/concepts/endpoints-and-apis/caching.md#the-global-cache-and-redis). Cluster messaging is best effort. Failed publishes are not retried. The `MessageScope.global` scope requires Redis and throws `StateError('Redis needs to be enabled to use this method')` without it. See [Message scope](https://docs.serverpod.dev/concepts/endpoints-and-apis/server-events.md#message-scope). The [`/readyz`](https://docs.serverpod.dev/concepts/operations/health-checks.md) probe checks Redis only when Redis is enabled. The `/livez` probe does not. In every run mode except production, a failed Redis connection at startup logs that Serverpod is falling back to a local cache, and the server still starts. In production there is no fallback: the global cache stays Redis-backed even if Redis is down. To send Redis commands beyond the cache and pub/sub APIs, borrow the connection Serverpod manages. See [Send Redis commands directly](https://docs.serverpod.dev/concepts/endpoints-and-apis/caching.md#send-redis-commands-directly). ## Troubleshooting ### Redis is in my project but nothing connects The templates write `enabled: false`. Opting into Redis at create only includes the config, Compose service, and password. Set `enabled: true` in the run-mode file, then start Redis. ### A GUI or `redis-cli` cannot authenticate The password is `development.redis` in `config/passwords.yaml`, not nested under `database`. The default Docker Redis has no username: leave that field empty. From the host, connect to port **8091**, not 6379. ### The server started but instances do not share state In every run mode except production, a down Redis does not stop the server. Confirm the container is running and that [`/readyz`](https://docs.serverpod.dev/concepts/operations/health-checks.md) reports a passing Redis check. Production does not fall back. ### Staging or production fails at startup with a missing Redis password Those templates do not generate a `redis` password. Add `redis:` under that run mode in `passwords.yaml`, or set `SERVERPOD_PASSWORD_redis` / `SERVERPOD_REDIS_PASSWORD`, before enabling Redis. ## Related - [Configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md): run modes, YAML files, and secrets. - [Configuration reference](https://docs.serverpod.dev/concepts/lookups/configuration-reference.md): every Redis option and environment variable. - [Caching](https://docs.serverpod.dev/concepts/endpoints-and-apis/caching.md): the global cache Redis backs. - [Server events](https://docs.serverpod.dev/concepts/endpoints-and-apis/server-events.md): cluster messaging through Redis. - [Use Redis for PubSub and caching](https://docs.serverpod.dev/cloud/guides/redis.md): connecting a third-party Redis service to Serverpod Cloud. --- # Load testing https://docs.serverpod.dev/concepts/operations/load-testing You need to know what happens when many clients call your backend at once: whether latency climbs, whether the database pool saturates, and whether errors appear before you ship. Serverpod does not ship a load-testing tool, so you drive the same HTTP API your generated client uses (with Locust, or with that client itself) against a production-like server, then read Insights, health probes, session logs, and Postgres. The [test tools](https://docs.serverpod.dev/concepts/testing/get-started.md) are a different job. They boot an in-process server in `test` mode and roll the database back after each test, which is how you check correctness, not throughput. ## Before you start - A Serverpod project with the [endpoints](https://docs.serverpod.dev/concepts/endpoints-and-apis.md) you want to measure, and `serverpod generate` already run. - [Locust installed](https://docs.locust.io/en/stable/installation.html). - A PostgreSQL instance you run yourself. The [embedded PostgreSQL](https://docs.serverpod.dev/concepts/data-and-the-database/database/embedded-postgres.md) that `serverpod start` manages is for local development and tests, not load. ## Run a production-like server A run under `serverpod start` logs every session, uses JIT, and hot reloads. Start the same kind of process you would deploy: [production (or staging) run mode](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md#run-modes), the project's Dockerfile (it builds with `dart build cli`), and a real Postgres with no `dataPath`. ```bash dart run bin/main.dart --mode production ``` That command is enough to match production logging and config. It is still JIT. For numbers you would trust in production, run the compiled binary from your Dockerfile instead. Leave `--logging` at `normal`. Each Serverpod process runs your Dart code on a single isolate, which is one thread of execution. Synchronous CPU work on that isolate delays every other request on the same process. To use more cores, run more server processes (or pods), not more threads inside one process. The production config template leaves Redis off. Turn it on if the scenario uses the [global cache](https://docs.serverpod.dev/concepts/endpoints-and-apis/caching.md) or messaging across instances. ## Call an endpoint over HTTP The API server listens on port `8080` by default. Endpoint methods are `POST` requests with a JSON body and `Content-Type: application/json`. An `Authorization` header is optional, and is how you exercise authenticated methods. The generated client posts to `/{endpoint}/{method}` with a JSON object of named arguments: ```bash curl -X POST http://localhost:8080/example/hello \ -H 'Content-Type: application/json' \ -d '{"name":"World"}' ``` A method on a module uses `{module}.{endpoint}/{method}`. [Streaming methods](https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming.md) use WebSockets, not HTTP POST, so they need a separate scenario. ## Write a Locust scenario Locust is the default path: it already speaks this POST-and-JSON shape, and a [community benchmark](https://github.com/abdelaziz-mahdy/backend-benchmark/tree/main/backends/dart/server-pod/tests) uses it against Serverpod. Point each task at one method. Give database reads, database writes, and static methods their own files so you can tell them apart. ```python from locust import FastHttpUser, task class ExampleUser(FastHttpUser): @task def hello(self): self.client.post("/example/hello", json={"name": "World"}) ``` Run it against the API server: ```bash locust -f locustfile.py --host http://localhost:8080 ``` Locust opens a web UI (port `8089` by default, separate from Serverpod's `8080`/`8081`/`8082`). Start with a small user count and raise it until something saturates. For think time between calls, set `wait_time` on the user class; omit it when you want a raw request ceiling. See [writing a locustfile](https://docs.locust.io/en/stable/writing-a-locustfile.html) for spawning, headless runs, and percentiles. Generic HTTP tools such as k6 and wrk work for the same POST-and-JSON calls. You encode the argument map yourself, and you lose the typed client. ## Measure the server Locust reports client-side requests per second, latency percentiles (p50, p95, p99), error rate, and timeouts. That is only half the picture. Watch the server at the same time. **Health probes**: `/livez` (process alive), `/readyz` (database, Redis, and custom indicators), `/startupz` (boot complete). Every server answers them; probe the API port (`8080` by default), since that is the server under load. During a run, `/readyz` is the one that tells you the server should still receive traffic. See [Health checks](https://docs.serverpod.dev/concepts/operations/health-checks.md). **Health metrics** in [Insights](https://docs.serverpod.dev/tools/insights.md): CPU load average, memory, database `SELECT 1` latency, and HTTP connection counts (`active`, `closing`, `idle`). Collection runs once a minute. Leave that interval alone; a shorter one writes extra database rows and can distort the run. CPU and memory metrics are not collected on Windows. **Session and query logs** in Insights: endpoint, method, duration, query count, and whether the call or query was slow or failed. In `production`, a session is persisted only if it was slower than one second, it failed, or it produced a log or query entry. Fast successful calls leave no row, which is what you want. :::warning Turning on logging for every session or every query during a load test writes a database row per request and becomes the bottleneck. Keep the production defaults, or disable persistent session logs for the run (`sessionLogs.persistentEnabled: false`, or `SERVERPOD_SESSION_PERSISTENT_LOG_ENABLED=false`). If you need request traces, console logs with `consoleLogFormat: json` are cheaper than the log tables. ::: **Postgres**: watch connection count against your pool size (`database.maxConnectionCount`, default **10**, env `SERVERPOD_DATABASE_MAX_CONNECTION_COUNT`). Postgres `max_connections` must be at least the pool size times the number of server processes. See [Configure connection pool size](https://docs.serverpod.dev/concepts/data-and-the-database/database/connection.md#configure-connection-pool-size). A valid run has a near-zero error rate (or the failures you meant to provoke), `/readyz` returning `200`, and session-log tables that are not growing by one row per request. ## Check these settings before you run | Setting | Why it matters | | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | | `--mode development` (the default) | Logs every session and runs with JIT and hot reload. | | JIT (`dart run` / `serverpod start`) | Understates production throughput. Use the compiled binary from the Dockerfile. | | `database.maxConnectionCount` (default 10) | Saturates first under database load. | | `sessionLogs.persistentEnabled` | A row per request will cap throughput. See the warning above. | | `--logging verbose` | Extra work on every call. | | `futureCall.concurrencyLimit` (default 1) | Only if the scenario [schedules future calls](https://docs.serverpod.dev/concepts/scheduling/future-calls.md). | | `healthCheckInterval` below one minute | Extra metric rows in Postgres. | | Embedded Postgres (`dataPath`) | Not representative of a hosted database. | Purge or cap the log tables between runs (`cleanupInterval`, `retentionPeriod`, `retentionCount` on [Logging](https://docs.serverpod.dev/concepts/operations/logging.md)). Unbounded log tables distort later measurements. ## Drive load with the generated client Use the generated Dart client when the scenario needs type-safe models, authentication, or [streaming](https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming.md). Locust will not speak WebSockets for you. ```dart final client = Client('http://localhost:8080/'); final result = await client.example.hello('World'); ``` Give each virtual user its own `Client` (or a small pool), drive the generated stubs, and time calls with `Stopwatch`. The client's default timeout is 20 seconds. Spawn many isolates or processes on the client; a single isolate will under-generate load. ## Scenario types Treat these as separate runs. They stress different parts of the stack: - Static methods with no database - Create, read, update, and delete against Postgres - Authenticated methods, using the same `Authorization` header your app sends ([Authentication basics](https://docs.serverpod.dev/concepts/authentication/basics.md)) - Streaming over WebSocket - [File uploads](https://docs.serverpod.dev/concepts/endpoints-and-apis/file-uploads.md) - Mixed user behavior (Locust tasks with `wait_time`) ## Troubleshooting ### Why do the numbers look better than production? The server was started with `serverpod start` or without `--mode production`. Development logs every session and runs with JIT and hot reload. Rebuild with the project's Dockerfile and start in production or staging mode. ### Why is latency high while CPU is idle? The database pool is full. The default `maxConnectionCount` is 10. Raise it, and confirm Postgres `max_connections` is large enough for every replica. ### Why did Postgres become the bottleneck immediately? Persistent session or query logging is writing a row per request. Keep production log defaults, or set `sessionLogs.persistentEnabled` to `false` for the run. ### Why can't a Dart driver generate enough load? The driver is running in one isolate. Spawn many isolates or processes on the client, and scale the server with more processes as well. ## Related - [Get started with testing](https://docs.serverpod.dev/concepts/testing/get-started.md): correctness tests with `withServerpod`, not a load-test path. - [Working with endpoints](https://docs.serverpod.dev/concepts/endpoints-and-apis.md): how the generated client calls your server. - [Insights](https://docs.serverpod.dev/tools/insights.md): live health, session logs, and runtime log settings. - [Logging](https://docs.serverpod.dev/concepts/operations/logging.md): which sessions are persisted, and how to turn persistent logs off. - [Health checks](https://docs.serverpod.dev/concepts/operations/health-checks.md): `/livez`, `/readyz`, `/startupz`, and collected CPU, memory, and database metrics. - [Configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md): run modes, YAML, and environment variables. - [Configure connection pool size](https://docs.serverpod.dev/concepts/data-and-the-database/database/connection.md#configure-connection-pool-size): the pool that saturates first under database load. - [Community Locust scripts](https://github.com/abdelaziz-mahdy/backend-benchmark/tree/main/backends/dart/server-pod/tests): worked examples that split database and static scenarios. - [Community benchmark discussion](https://github.com/serverpod/serverpod/discussions/1791): an earlier Locust run against Serverpod, not a current baseline. --- # Get started with testing https://docs.serverpod.dev/concepts/testing/get-started Serverpod generates test tools that let you call your endpoints from a test the same way your app calls them, as ordinary Dart functions. Behind that call the tools run a real server against a real database, so a test exercises your endpoint, your models, and your queries together. That means no separate server to start, no HTTP client to write, and no mocked database. It also means these are integration tests: they check your code end to end rather than one class in isolation. Plain unit tests still work as they always do, and they need none of this. Two things follow from that: - **Tests stay independent.** Anything a test writes to the database is rolled back once that test finishes, so tests never see each other's data and you do not clean up after yourself. By default, each `withServerpod` group also runs against its own temporary database, which is dropped when the group finishes. - **Each test sets its own scene.** A session builder decides what the server state looks like for a call, such as who is signed in. ## Set up New projects are ready to test. If you have `test/integration/test_tools/serverpod_test_tools.dart` in your server package, skip to [writing a test](#write-a-test). :::info Projects created with Serverpod 2.1 or earlier need a few one-time setup steps first. See [Upgrade to 2.2](https://docs.serverpod.dev/upgrading/archive/upgrade-to-two-point-two.md). ::: The test tools are generated along with the rest of your code. With `serverpod start` running, saving a file regenerates them. Outside a session, run `serverpod generate` from your server directory. Either way you get `test/integration/test_tools/serverpod_test_tools.dart`. The `test/integration` folder keeps these apart from your unit tests, which makes a project easier to navigate. To generate the file somewhere else, change `server_test_tools_path` in `config/generator.yaml`. ## Write a test Import the generated file rather than the `serverpod_test` package: it carries the helpers and your endpoints together. ```dart import 'package:test/test.dart'; import 'test_tools/serverpod_test_tools.dart'; void main() { withServerpod('Given Greeting endpoint', (sessionBuilder, endpoints) { test('when calling `hello` then it returns a greeting', () async { final greeting = await endpoints.greeting.hello(sessionBuilder, 'Bob'); expect(greeting.message, 'Hello Bob'); }); }); } ``` The `withServerpod` callback gives you the two things every test uses. The `sessionBuilder` describes the server state a call runs under, and `endpoints` holds your endpoints. Run it: ```bash dart test ``` New projects set `dataPath` under `database` in `config/test.yaml`, which makes the test server start and manage its own PostgreSQL. There is nothing to launch first. If your `test.yaml` connects to a PostgreSQL server without a `dataPath`, that server has to be running. Each test group gets its own database on it. ## Next - [Writing tests](https://docs.serverpod.dev/concepts/testing/writing-tests.md): the session builder, authentication, and seeding data. - [Configuration](https://docs.serverpod.dev/concepts/testing/configuration.md): the options `withServerpod` accepts. - [Advanced examples](https://docs.serverpod.dev/concepts/testing/advanced-examples.md): streams, future calls, and business logic. - [Best practices](https://docs.serverpod.dev/concepts/testing/best-practices.md): conventions worth following. - [Load testing](https://docs.serverpod.dev/concepts/operations/load-testing.md): measuring a live server under concurrent traffic. --- # Writing tests https://docs.serverpod.dev/concepts/testing/writing-tests Every test you write with the test tools uses the same few pieces. The `withServerpod` callback hands you two of them: - **`sessionBuilder`** describes the server state a call runs under, such as who is signed in. You pass it to endpoint calls, and you can build it into a `Session` when you need one directly. - **`endpoints`** is your generated endpoints, called the same way your app calls them. ```dart withServerpod('Given Greeting endpoint', (sessionBuilder, endpoints) { test('then hello returns a greeting', () async { var greeting = await endpoints.greeting.hello(sessionBuilder, 'Bob'); expect(greeting.message, 'Hello Bob'); }); }); ``` The generated file carries the test helpers and your endpoints. Serverpod's own types, such as `Session` and `Scope`, and your models come from their own packages: ```dart import 'package:serverpod/serverpod.dart'; import 'package:test/test.dart'; import 'package:my_project_server/src/generated/protocol.dart'; import 'test_tools/serverpod_test_tools.dart'; ``` ## Set up a scenario Call `copyWith` on the session builder to change the state a call runs under. It returns a new builder, leaving the original untouched, so each scenario in a test file can have its own. | Property | What it does | If you omit it | | ---------------- | -------------------------------------------------- | --------------------------------------------------- | | `authentication` | Who is signed in for calls made with this builder. | Inherits from the builder you called `copyWith` on. | | `enableLogging` | Whether the session writes log entries. | Inherits from the builder you called `copyWith` on. | A fresh builder from `withServerpod` starts out unauthenticated, and logs only if you passed [`enableSessionLogging`](https://docs.serverpod.dev/concepts/testing/configuration.md#options). When you need a `Session` rather than a builder, for a database call or to pass into [your own business logic](https://docs.serverpod.dev/concepts/testing/advanced-examples.md#test-business-logic-that-depends-on-session), build one: ```dart Session session = sessionBuilder.build(); ``` ## Simulate authentication The `AuthenticationOverride` class sets who the call runs as. It offers two: ```dart static AuthenticationOverride unauthenticated(); static AuthenticationOverride authenticationInfo( String userIdentifier, Set scopes, { String? authId, }); ``` The `userIdentifier` is the signed-in user's id, which is a UUID string when you use Serverpod's auth module. Scopes are the permissions the call should carry. See [Authentication](https://docs.serverpod.dev/concepts/authentication/basics.md) for both. Pass an override through `copyWith` to test each case: ```dart withServerpod('Given AuthenticatedExample endpoint', (sessionBuilder, endpoints) { final userId = '550e8400-e29b-41d4-a716-446655440000'; group('when authenticated', () { var authenticated = sessionBuilder.copyWith( authentication: AuthenticationOverride.authenticationInfo(userId, {Scope('user')}), ); test('then calling `hello` returns a greeting', () async { final greeting = await endpoints.authenticatedExample.hello(authenticated, 'Michael'); expect(greeting, 'Hello, Michael!'); }); }); group('when unauthenticated', () { var unauthenticated = sessionBuilder.copyWith( authentication: AuthenticationOverride.unauthenticated(), ); test('then calling `hello` throws', () async { final future = endpoints.authenticatedExample.hello(unauthenticated, 'Michael'); await expectLater( future, throwsA(isA())); }); }); }); ``` ## Seed the database Build a session and use it for database calls exactly as your server code would: ```dart withServerpod('Given Products endpoint', (sessionBuilder, endpoints) { var session = sessionBuilder.build(); setUp(() async { await Product.db.insert(session, [ Product(name: 'Apple', price: 10), Product(name: 'Banana', price: 10), ]); }); test('then calling `all` returns all products', () async { final products = await endpoints.products.all(sessionBuilder); expect(products, hasLength(2)); expect(products.map((p) => p.name), containsAll(['Apple', 'Banana'])); }); }); ``` Everything a test writes to the database is rolled back when the test ends, so each test starts from the same state and you do not have to clean up after yourself. You can change when that happens, or turn it off, with [`rollbackDatabase`](https://docs.serverpod.dev/concepts/testing/configuration.md#rollbackdatabase). ## Exceptions the test tools throw The generated file exports these, so you can name them in a test without importing anything else. | Exception | Thrown when | | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ServerpodUnauthenticatedException` | An endpoint call was made without authentication and the endpoint requires it. | | `ServerpodInsufficientAccessException` | The authentication used did not carry the scopes the endpoint requires. | | `ConnectionClosedException` | A stream connection closed with an error, for example because authentication was revoked. | | `InvalidConfigurationException` | The endpoint ran database calls or transactions in parallel, which the automatic rollback cannot wrap. See [`rollbackDatabase`](https://docs.serverpod.dev/concepts/testing/configuration.md#rollbackdatabase). | The test server also throws when it fails to start or takes longer than the start timeout. That error type is not exported, so you cannot catch it by name, but the message says which of the two happened. ## Wait for async work The `flushEventQueue` helper lets already-queued async work run before the test carries on. Reach for it when a call has to get as far as its first `yield` before you assert on it: ```dart var stream = endpoints.someEndpoint.generatorFunction(sessionBuilder); await flushEventQueue(); ``` For a complete case, see [multiple users interacting with a shared stream](https://docs.serverpod.dev/concepts/testing/advanced-examples.md#multiple-users-interacting-with-a-shared-stream). ## Related - [Configuration](https://docs.serverpod.dev/concepts/testing/configuration.md): every option `withServerpod` accepts. - [Advanced examples](https://docs.serverpod.dev/concepts/testing/advanced-examples.md): streams, future calls, and business logic. - [Sessions](https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions.md): what a session carries in production. --- # Configuration https://docs.serverpod.dev/concepts/testing/configuration The `withServerpod` helper takes optional named arguments that control how the test server starts and how tests are isolated from each other. Some of the values below, such as `ServerpodRunMode` and `Constant`, come from `package:serverpod/serverpod.dart` rather than the generated test tools, so import it when you use them. The `serverDirectory` option takes a `Directory` from `dart:io`. ```dart withServerpod( 'Given Products endpoint', (sessionBuilder, endpoints) { /* test code */ }, rollbackDatabase: RollbackDatabase.afterAll, ); ``` ## Options | Option | What it does | Default | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | | `applyMigrations` | Whether pending migrations are applied when the test server starts. | `true` | | `configOverride` | A function that changes values in the loaded config before the server starts. | `null` | | `databaseInterceptor` | A [database interceptor](https://docs.serverpod.dev/concepts/data-and-the-database/database/database-interceptors.md) for the test server to use. | `null` | | `enableSessionLogging` | Whether session logging is on. | `false` | | `ephemeralDatabase` | Whether the group gets its own empty database. See [below](#ephemeraldatabase). | `true` | | `experimentalFeatures` | Experimental features to enable, such as [diagnostic event handlers](https://docs.serverpod.dev/concepts/operations/exception-monitoring.md). | `null` | | `rollbackDatabase` | When database changes are rolled back. See [below](#rollbackdatabase). | `RollbackDatabase.afterEach` | | `runMode` | The run mode the server starts in. | `test` | | `runtimeParametersBuilder` | Runtime parameters to apply before startup. See [runtime parameters](https://docs.serverpod.dev/concepts/data-and-the-database/database/runtime-parameters.md). | `null` | | `serverDirectory` | The directory that `config/.yaml`, `config/passwords.yaml`, and `migrations/` are resolved against. | The directory the test process runs in | | `serverpodLoggingMode` | The logging mode used when creating Serverpod. | `ServerpodLoggingMode.normal` | | `serverpodStartTimeout` | How long to wait for the server to start. | 120 seconds | | `testGroupTagsOverride` | The tags applied to the generated test group. See [below](#test-tags). | `['integration']` | | `testServerOutputMode` | How much server output reaches your terminal. See [below](#server-output). | `TestServerOutputMode.normal` | :::note When the run mode files configure no database, for example in a project created with `--no-database`, the generated `withServerpod` has no `applyMigrations`, `databaseInterceptor`, `ephemeralDatabase`, `rollbackDatabase`, or `runtimeParametersBuilder` parameter, so passing one is a compile error. Rollback is off in that case and there is nothing to migrate. ::: Set `serverDirectory` when tests run from somewhere other than the server package, such as the workspace root. Without it the server resolves those paths against the current directory, misses your config, and falls over later on whatever it needed from it. ## Run mode Tests run in the `test` run mode, so the server reads `config/test.yaml`. Point them at a different mode when you need its configuration instead: ```dart withServerpod( 'Given Products endpoint', (sessionBuilder, endpoints) { /* test code */ }, runMode: ServerpodRunMode.development, ); ``` ## Use an existing database {#ephemeraldatabase} Each `withServerpod` group runs against its own empty database, created when the group starts and dropped when it finishes. To run against the database configured for the run mode instead, for example one you seeded beforehand, pass `ephemeralDatabase: false`: ```dart withServerpod( 'Given a seeded database', (sessionBuilder, endpoints) { /* test code */ }, ephemeralDatabase: false, ); ``` Serverpod then neither creates nor drops that database, and [`rollbackDatabase`](#rollbackdatabase) still decides whether the group's writes are rolled back. Groups that share the database can't run in parallel, so run their test files one at a time with `dart test --concurrency=1`. ## Database rollback {#rollbackdatabase} By default each test runs inside a transaction that is rolled back when that test ends, so tests cannot see each other's data. Three values control this: | Value | Rolls back | | ---------------------------- | ----------------------------------------------------------------------- | | `RollbackDatabase.afterEach` | After every test. The default. | | `RollbackDatabase.afterAll` | Once the whole `withServerpod` group finishes. | | `RollbackDatabase.disabled` | Never. Data stays until the group finishes and its database is dropped. | Three situations call for changing it. **Tests that build on each other.** When one test depends on what the previous one left behind, `afterAll` keeps state across the group and still rolls everything back at the end. Tests that share state are usually worth avoiding, but this helps when the setup is expensive. **Concurrent database work in an endpoint.** The rollback wraps each test in one transaction, and concurrent calls cannot be nested inside it. An endpoint that runs database calls or transactions in parallel throws `InvalidConfigurationException` unless you pass `RollbackDatabase.disabled`: ```dart Future concurrentTransactionCalls(Session session) async { await Future.wait([ session.db.transaction((tx) => /*...*/), // Throws InvalidConfigurationException unless rollbackDatabase // is RollbackDatabase.disabled. session.db.transaction((tx) => /*...*/), ]); } ``` With rollback disabled, later tests in the same group see what earlier tests committed. To start each test from a clean state, clean up in `tearDown`: ```dart withServerpod( 'Given ProductsEndpoint when calling concurrentTransactionCalls', (sessionBuilder, endpoints) { tearDown(() async { var session = sessionBuilder.build(); await Product.db.deleteWhere(session, where: (_) => Constant.bool(true)); }); test('then it commits all transactions', () async { var result = await endpoints.products.concurrentTransactionCalls(sessionBuilder); // ... }); }, rollbackDatabase: RollbackDatabase.disabled, ); ``` **Caught database exceptions.** This is the one case where the test tools behave differently from production: ```dart var transactionFuture = session.db.transaction((tx) async { var data = UniqueData(number: 1, email: 'test@test.com'); try { await UniqueData.db.insertRow(session, data, transaction: tx); await UniqueData.db.insertRow(session, data, transaction: tx); } on DatabaseException catch (_) { // Ignored. } }); // Throws in production, but not under the test tools. await transactionFuture; ``` In production a transaction fails if any database exception happened inside it, even one your code caught. The test tools emulate nested transactions and do not, so code that swallows database exceptions passes a test it would fail in production. Set `RollbackDatabase.disabled` to get production behavior. ## Server output The `testServerOutputMode` option controls what the test server prints, which is the setting to reach for when you cannot see why a server failed: | Value | Shows | | ------------------------------ | ----------------------------- | | `TestServerOutputMode.normal` | Errors only. The default. | | `TestServerOutputMode.verbose` | Everything the server prints. | | `TestServerOutputMode.silent` | Nothing. | ## Test tags Test groups created by `withServerpod` are tagged `integration`, which is what lets you run integration and unit tests separately. Change the tag with `testGroupTagsOverride`. See [running tests separately](https://docs.serverpod.dev/concepts/testing/advanced-examples.md#run-unit-and-integration-tests-separately). ## Related - [Writing tests](https://docs.serverpod.dev/concepts/testing/writing-tests.md): the pieces every test uses. - [Advanced examples](https://docs.serverpod.dev/concepts/testing/advanced-examples.md): the options above put to work. - [Server configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md): the config files the test server reads. --- # Advanced examples https://docs.serverpod.dev/concepts/testing/advanced-examples These examples build on [Writing tests](https://docs.serverpod.dev/concepts/testing/writing-tests.md) and cover less common needs: separating unit and integration tests, testing business logic directly, multi-user stream interactions, future calls, connection limits, and exception monitoring. ## Run unit and integration tests separately To run unit and integration tests separately, the `"integration"` tag can be used as a filter. See the following examples: ```bash # All tests (unit and integration) dart test # Only integration tests: add --tags (-t) flag dart test -t integration # Only unit tests: add --exclude-tags (-x) flag dart test -x integration ``` To change the name of this tag, see the [`testGroupTagsOverride`](https://docs.serverpod.dev/concepts/testing/configuration.md#test-tags) configuration option. ## Test business logic that depends on `Session` It is common to break out business logic into modules and keep it separate from the endpoints. If such a module depends on a `Session` object (e.g to interact with the database), then the `withServerpod` helper can still be used and the second `endpoint` argument can be ignored: ```dart withServerpod('Given decreasing product quantity when quantity is zero', ( sessionBuilder, _, ) { var session = sessionBuilder.build(); setUp(() async { await Product.db.insertRow(session, Product( id: 123, name: 'Apple', quantity: 0, ), ); }); test('then should throw `InvalidOperationException`', () async { var future = ProductsBusinessLogic.updateQuantity( session, id: 123, decrease: 1, ); await expectLater(future, throwsA(isA())); }); }); ``` ## Multiple users interacting with a shared stream For cases where there are multiple users reading from or writing to a stream, such as real-time communication, it can be helpful to validate this behavior in tests. Given the following simplified endpoint: ```dart class CommunicationExampleEndpoint { static const sharedStreamName = 'shared-stream'; Future postNumberToSharedStream(Session session, int number) async { await session.messages .postMessage(sharedStreamName, SimpleData(num: number)); } Stream listenForNumbersOnSharedStream(Session session) async* { var sharedStream = session.messages.createStream(sharedStreamName); await for (var message in sharedStream) { yield message.num; } } } ``` Then a test to verify this behavior can be written as below. Note the call to the `flushEventQueue` helper (exported by the test tools), which ensures that `listenForNumbersOnSharedStream` executes up to its first `yield` statement before continuing with the test. This guarantees that the stream was registered by Serverpod before messages are posted to it. ```dart withServerpod('Given CommunicationExampleEndpoint', (sessionBuilder, endpoints) { final userId1 = '550e8400-e29b-41d4-a716-446655440000'; final userId2 = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; test( 'when calling postNumberToSharedStream and listenForNumbersOnSharedStream ' 'with different sessions then number should be echoed', () async { var userSession1 = sessionBuilder.copyWith( authentication: AuthenticationOverride.authenticationInfo( userId1, {}, ), ); var userSession2 = sessionBuilder.copyWith( authentication: AuthenticationOverride.authenticationInfo( userId2, {}, ), ); var stream = endpoints.communicationExample.listenForNumbersOnSharedStream(userSession1); // Wait for `listenForNumbersOnSharedStream` to execute up to its // `yield` statement before continuing await flushEventQueue(); await endpoints.communicationExample.postNumberToSharedStream(userSession2, 111); await endpoints.communicationExample.postNumberToSharedStream(userSession2, 222); await expectLater(stream.take(2), emitsInOrder([111, 222])); }); }); ``` ## Run a future call The generated test tools expose your [future calls](https://docs.serverpod.dev/concepts/scheduling/future-calls.md) alongside your endpoints, so a test can invoke one immediately instead of waiting for its scheduled time. Given a future call class named `ReminderFutureCall` with a `send` method, the accessor is `reminder`, following the same [naming rule](https://docs.serverpod.dev/concepts/scheduling/future-calls.md#schedule-a-call) as scheduling: ```dart withServerpod('Given the reminder future call', (sessionBuilder, endpoints) { late Session session; setUp(() { session = sessionBuilder.build(); }); test('when invoked then it records a reminder', () async { await endpoints.futureCalls.reminder.send(sessionBuilder, 'user-42'); final reminders = await Reminder.db.find(session); expect(reminders, hasLength(1)); }); }); ``` This runs the future call's method directly. It does not exercise scheduling, so use it to test what the call does rather than when it runs. ## Too many database connections Dart's test runner runs test files in parallel, and each `withServerpod` group starts its own server with its own connection pool. On a machine with many cores, enough files running at once can exceed the database's connection limit and fail the run. This is uncommon, and worth addressing only once you hit it. When you do, raise the limit on the database or cap how many files run at once: ```bash dart test -t integration --concurrency=4 ``` ## Testing exception monitoring The `withServerpod` helper accepts the same `experimentalFeatures` argument as the server, so you can register a [diagnostic event handler](https://docs.serverpod.dev/concepts/operations/exception-monitoring.md) in a test and assert that your code reports the exceptions you expect. Write a handler that records what it receives, so the test can wait for an event: ```dart import 'dart:async'; import 'package:serverpod/serverpod.dart'; class TestExceptionHandler extends ExceptionHandler { final eventsStreamController = StreamController>(); Stream> get events => eventsStreamController.stream; @override Future handleTypedEvent( ExceptionEvent event, { required OriginSpace space, required DiagnosticEventContext context, }) async { eventsStreamController.add(DiagnosticEventRecord(event, space, context)); } } ``` Then register it for the test run: ```dart void main() { var exceptionHandler = TestExceptionHandler(); withServerpod( 'Given withServerpod with a diagnostic event handler', experimentalFeatures: ExperimentalFeatures( diagnosticEventHandlers: [exceptionHandler], ), (sessionBuilder, endpoints) { test( 'when calling an endpoint method that submits an exception event ' 'then the diagnostic event handler gets called', () async { final result = await endpoints.order.placeOrder(sessionBuilder); expect(result, 'success'); final record = await exceptionHandler.events.first.timeout(Duration(seconds: 1)); expect(record.event.exception, isA()); expect(record.space, equals(OriginSpace.application)); expect(record.context, isA()); expect( record.context.toJson(), allOf([ containsPair('serverId', 'default'), containsPair('serverRunMode', 'test'), containsPair('serverName', 'Server default'), ]), ); }); }, ); } ``` Handlers run asynchronously and are not awaited by the code that triggers them, so wait for the event rather than asserting immediately after the call. ## Related - [Writing tests](https://docs.serverpod.dev/concepts/testing/writing-tests.md): the pieces these examples build on. - [Configuration](https://docs.serverpod.dev/concepts/testing/configuration.md): the options used above, such as `experimentalFeatures` and `rollbackDatabase`. - [Best practices](https://docs.serverpod.dev/concepts/testing/best-practices.md): conventions worth following. --- # Best practices https://docs.serverpod.dev/concepts/testing/best-practices ## Imports The generated test tools file re-exports the test helpers, so importing `serverpod_test` as well brings in the same names twice and adds noise for no gain. ### Don't ```dart import 'test_tools/serverpod_test_tools.dart'; // Don't import `serverpod_test` directly. import 'package:serverpod_test/serverpod_test.dart'; ❌ ``` ### Do ```dart // The generated file carries the test helpers and your endpoints. import 'test_tools/serverpod_test_tools.dart'; ✅ ``` Serverpod's own types are a separate matter. The generated file does not re-export them, so a test that uses `Session`, `Scope`, `Constant`, `ServerpodRunMode`, `ExperimentalFeatures`, or a Serverpod exception type needs `package:serverpod/serverpod.dart` as well. Your own models come from your project's generated protocol. ## Database clean up Unless configured otherwise, by default `withServerpod` does all database operations inside a transaction that is rolled back after each `test` (see [the configuration options](https://docs.serverpod.dev/concepts/testing/configuration.md#rollbackdatabase) for more info on this behavior). ### Don't ```dart withServerpod('Given ProductsEndpoint', (sessionBuilder, endpoints) { var session = sessionBuilder.build(); setUp(() async { await Product.db.insertRow(session, Product(name: 'Apple', price: 10)); }); tearDown(() async { await Product.db.deleteWhere( ❌ // Unnecessary clean up session, where: (_) => Constant.bool(true), ); }); // ... }); ``` ### Do ```dart withServerpod('Given ProductsEndpoint', (sessionBuilder, endpoints) { var session = sessionBuilder.build(); setUp(() async { await Product.db.insertRow(session, Product(name: 'Apple', price: 10)); }); ✅ // Clean up can be omitted since the transaction is rolled back after each by default // ... }); ``` ## Calling endpoints While it's technically possible to instantiate an endpoint class and call its methods directly with a Serverpod `Session`, it's advised that you do not. The reason is that lifecycle events and validation that should happen before or after an endpoint method is called is taken care of by the framework. Calling endpoint methods directly would circumvent that and the code would not behave like production code. Using the test tools guarantees that the way endpoints behave during tests is the same as in production. ### Don't ```dart void main() { // ❌ Don't instantiate endpoints directly var greetingEndpoint = GreetingEndpoint(); withServerpod('Given Greeting endpoint', ( sessionBuilder, _ /* not using the provided endpoints */, ) { var session = sessionBuilder.build(); test('when calling `hello` then should return greeting', () async { // ❌ Don't call an endpoint method directly on the endpoint class. final greeting = await greetingEndpoint.hello(session, 'Bob'); expect(greeting.message, 'Hello Bob'); }); }); } ``` ### Do ```dart void main() { withServerpod('Given Greeting endpoint', (sessionBuilder, endpoints) { test('when calling `hello` then should return greeting', () async { // ✅ Use the provided `endpoints` to call the endpoint that should be tested. final greeting = await endpoints.greeting.hello(sessionBuilder, 'Bob'); expect(greeting.message, 'Hello Bob'); }); }); } ``` ## Unit and integration tests It is significantly easier to navigate a project if the different types of tests are clearly separated. ### Don't ❌ Mix different types of tests together. ### Do ✅ Have a clear structure for the different types of test. Serverpod recommends the following two folders in the `server`: - `test/unit`: Unit tests. - `test/integration`: Tests for endpoints or business logic modules using the `withServerpod` helper. ## Related - [Advanced examples](https://docs.serverpod.dev/concepts/testing/advanced-examples.md): patterns for streams, future calls, and business logic. - [Configuration](https://docs.serverpod.dev/concepts/testing/configuration.md): the options `withServerpod` accepts. - [Deploy to Serverpod Cloud](https://docs.serverpod.dev/deployments/deploy-to-serverpod-cloud.md): ship the server your tests cover. --- # Introduction to the Serverpod CLI https://docs.serverpod.dev/concepts/cli/introduction The Serverpod CLI (`serverpod`) creates projects, generates server and client code, and manages your database migrations from the command line. This reference covers every command, option, and flag. ## Install the CLI Install the CLI with Dart before running any command: ```bash dart install serverpod_cli ``` See [Installation](https://docs.serverpod.dev/installation.md) for the full setup, including the prerequisites. ## Command syntax Every `serverpod` invocation follows the same shape: ```text serverpod [global options] [] [arguments] [options] ``` Run `serverpod help` to list the commands, or `serverpod help ` for the options of a single command. ## Global options A small set of options applies to every command: output verbosity (`-q` / `--quiet`, `-v` / `--verbose`), analytics (`-a` / `--analytics`), interactive prompts (`--interactive`), and experimental features (`--experimental-features`). The full list lives on the [Global options](https://docs.serverpod.dev/concepts/cli/global_options.md) page. ## Environment variables The CLI reads a single environment variable, described on the [Environment variables](https://docs.serverpod.dev/concepts/cli/env_vars.md) page. ## How this reference is organized Most command pages combine a hand-written introduction with the auto-generated subcommand and option listings drawn from `serverpod help`. The sidebar lists commands alphabetically. Use the search box for fast lookup by flag or argument name. --- # Global options https://docs.serverpod.dev/concepts/cli/global_options The global options of `serverpod` can be specified anywhere on the command line, in conjunction with any command. They control output verbosity, analytics, interactive prompts, and experimental features. ## Usage ```console Manage your serverpod app development Usage: serverpod [arguments] Global options: -h, --help Print this usage information. -q, --quiet Suppress all cli output. Is overridden by -v, --verbose. -v, --verbose Prints additional information useful for development. Overrides --q, --quiet. -a, --[no-]analytics Toggles if analytics data is sent. (defaults to on) --[no-]interactive Enable interactive prompts. Automatically disabled in CI environments. --version Prints the active version of the Serverpod CLI. --experimental-features Enable experimental features. Experimental features might be removed at any time. Available commands: clean Delete the cached server build so the next `serverpod start` recompiles from scratch. cloud Manage Serverpod Cloud projects through the Serverpod Cloud. completion Command line completion commands create Creates a new Serverpod project, specify project name (must be lowercase with no special characters). create-migration Creates a migration from the last migration to the current state of the database. create-repair-migration Repairs the database by comparing the target state to what is in the live database instead of comparing to the latest migration. database Manage the embedded PostgreSQL database used by a Serverpod project. generate Generate code from yaml files for server and clients. language-server Launches a serverpod language server communicating with JSON-RPC-2 intended to be used with a client integrated in an IDE. mcp-server Start an MCP bridge to the `serverpod start` runner of one server project. quickstart Creates a new Serverpod project with basic options. run Run a script defined in the "serverpod/scripts" section of pubspec.yaml. start Start the full development stack with hot reload: generates code, runs the server, and launches the companion Flutter apps in an interactive terminal UI. upgrade Upgrade the Serverpod CLI to the latest version. version Prints the active version of the Serverpod CLI. Run "serverpod help " for more information about a command. ``` --- # CLI environment variables https://docs.serverpod.dev/concepts/cli/env_vars These environment variables affect the `serverpod` command: - **`SERVERPOD_HOME`** points to the Serverpod framework directory. It is only required when running the CLI in development mode, from a checkout of the Serverpod repository. A normal installation does not need it. - **Server configuration variables**, the `SERVERPOD_*` and `SERVERPOD_PASSWORD_*` variables, apply to commands that load your project's config, such as `serverpod start` and `serverpod database`. See the [Configuration reference](https://docs.serverpod.dev/concepts/lookups/configuration-reference.md). --- # serverpod clean https://docs.serverpod.dev/concepts/cli/commands/clean The `serverpod clean` command deletes the cached server build of a server project, so the next `serverpod start` compiles the server from scratch. Run it when `serverpod start` keeps reporting compilation errors you already fixed. The command needs Serverpod 4.0.4 or later. Stop `serverpod start` before you run the command, then start it again to rebuild. ## Usage ```console Delete the cached server build so the next `serverpod start` recompiles from scratch. Only needed when `serverpod start` keeps reporting compilation errors that have already been fixed, even after restarting it. Usage: serverpod clean [arguments] -h, --help Print this usage information. -d, --directory The server directory (defaults to auto-detect from current directory). Run "serverpod help" to see global options. ``` --- # serverpod cloud https://docs.serverpod.dev/concepts/cli/commands/cloud `serverpod cloud` forwards everything after it to the Serverpod Cloud CLI (`scloud`), so you can manage Serverpod Cloud projects without installing `scloud` separately. For example, `serverpod cloud deploy` runs `scloud deploy`. - **Install:** If `scloud` is missing, `serverpod cloud` installs it with `dart install serverpod_cloud_cli`. An existing `scloud` from `dart install` or `dart pub global activate` is reused. - **Updates:** If the Cloud CLI updates itself, `serverpod cloud` runs your command again with the new version. If the Cloud CLI needs a required update it can't install on its own, `serverpod cloud` installs the update and then runs your command again. - **Terminal:** The Cloud CLI shares your terminal, so prompts and browser sign-in work as usual. Its messages show `serverpod cloud` as the command name. - **Exit code:** The command exits with the Cloud CLI's exit code, so scripts can check the result. For the full set of Cloud commands and options, see the [Serverpod Cloud CLI reference](https://docs.serverpod.dev/cloud/reference/cli/introduction.md). --- # serverpod completion https://docs.serverpod.dev/concepts/cli/commands/completion `serverpod completion` sets up shell tab-completion for the `serverpod` command. Use the `install` subcommand to add completion to your shell, or `generate` to print a completion specification you can wire up yourself. Both subcommands target a completion tool with `--tool`, either [`completely`](https://github.com/bashly-framework/completely) or [`carapace`](https://carapace.sh/). For a walkthrough, see [Shell completion](https://docs.serverpod.dev/tools/shell-completion.md). ## Usage ```console Command line completion commands Usage: serverpod completion [arguments] -h, --help Print this usage information. Available subcommands: generate Generate a command line completion specification install Install a command line completion script Run "serverpod help" to see global options. ``` ### Sub commands #### `generate` ```console Generate a command line completion specification Usage: serverpod completion generate [arguments] -h, --help Print this usage information. -t, --tool (mandatory) The completion tool to target [completely] Use the `completely` tool (https://github.com/bashly-framework/completely) [carapace] Use the `carapace` tool (https://carapace.sh/) -e, --exec-name Override the name of the executable -f, --file Write the specification to a file instead of stdout Run "serverpod help" to see global options. ``` #### `install` ```console Install a command line completion script Usage: serverpod completion install [arguments] -h, --help Print this usage information. -t, --tool (mandatory) The completion tool to target [completely] Use the `completely` tool (https://github.com/bashly-framework/completely) [carapace] Use the `carapace` tool (https://carapace.sh/) -e, --exec-name Override the name of the executable -d, --write-dir Override the directory to write the script to Run "serverpod help" to see global options. ``` --- # serverpod create https://docs.serverpod.dev/concepts/cli/commands/create `serverpod create` scaffolds a new Serverpod project. By default it generates a full server project with a database, a server package, a client package, and a Flutter app. In an interactive terminal, `serverpod create` opens a setup screen where you choose the project's features. Pass `--template server` for a server without a Flutter app, or `--template module` to create a shareable module. To set up the prerequisites first, see [Installation](https://docs.serverpod.dev/installation.md). The `--database`, `--redis`, `--auth`, `--webapp`, `--website`, and `--ide` flags apply only when the setup screen does not open, for example with `--no-interactive` or in CI. Without the setup screen, the project also gets Redis configuration and editor setup for Claude, Cursor, and VS Code, unless you pass `--no-redis` or choose editors with `--ide`. To create a project without a database, deselect **Database (recommended)** under **Database & caching** on the setup screen, or pass `--no-interactive --no-database`. The new project's run mode files then have no `database` section, which is what turns off database code generation. See [Database backends](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md#database-backends). ## Usage ```console Creates a new Serverpod project, specify project name (must be lowercase with no special characters). Usage: serverpod create [arguments] -h, --help Print this usage information. -f, --force Create the project even if there are issues that prevent it from running out of the box. --[no-]database Include a database in the project. --[no-]redis Include Redis caching in the project. --[no-]auth Include authentication in the project. Requires a database. --[no-]webapp Configure the server to host a Flutter web app. --[no-]website Configure the server to host a website. --ide Configure agent skills and MCP servers for one or more IDEs. Use "none" to disable all IDE configuration. [none] Do not configure agent skills or MCP servers [antigravity] Configure agent skills and MCP for Antigravity [codex] Configure agent skills and MCP for Codex [claude] (default) Configure agent skills and MCP for Claude [cursor] (default) Configure agent skills and MCP for Cursor [opencode] Configure agent skills and MCP for OpenCode [vscode] (default) Configure agent skills and MCP for VS Code -n, --name (mandatory) The name of the project to create. Can also be specified as the first argument. Project Template -t, --template Template to use when creating a new project [fullstack] (default) Fullstack project including a server and a companion Flutter app [server] Server project with standard features including database [module] Serverpod Module project Run "serverpod help" to see global options. ``` --- # serverpod create-migration https://docs.serverpod.dev/concepts/cli/commands/create-migration The `serverpod create-migration` command compares your current models and database definition to the last migration and writes a new migration for the difference. Run it after changing your model files. Use `--force` to proceed when a change may drop data, `--tag` to label the revision (lowercase letters, numbers, and dashes only), or `--empty` to create a migration even when nothing has changed. For the full workflow, see [Migrations](https://docs.serverpod.dev/concepts/data-and-the-database/database/migrations.md). ## Usage ```console Creates a migration from the last migration to the current state of the database. Usage: serverpod create-migration [arguments] -h, --help Print this usage information. --empty Creates the migration even if there are no database changes. -f, --force Creates the migration even if there are warnings or information that may be destroyed. -t, --tag Add a tag to the revision to easier identify it. Run "serverpod help" to see global options. ``` --- # serverpod create-repair-migration https://docs.serverpod.dev/concepts/cli/commands/create-repair-migration The `serverpod create-repair-migration` command builds a migration by comparing the target state to what is actually in the live database, rather than to the latest migration. Use it to bring a database back in sync when it has drifted from your migration history. Select which database to inspect with `--mode` (`development`, `staging`, or `production`), and target a specific version with `--version`. See [Migrations](https://docs.serverpod.dev/concepts/data-and-the-database/database/migrations.md). ## Usage ```console Repairs the database by comparing the target state to what is in the live database instead of comparing to the latest migration. Usage: serverpod create-repair-migration [arguments] -h, --help Print this usage information. -f, --force Creates the migration even if there are warnings or information that may be destroyed. -t, --tag Add a tag to the revision to easier identify it. -v, --version The target version for the repair. If not specified, the latest migration version will be repaired. -m, --mode Used to specify which database configuration to use when fetching the live database definition. [development (default), staging, production] Run "serverpod help" to see global options. ``` --- # serverpod database https://docs.serverpod.dev/concepts/cli/commands/database The `serverpod database` command manages the embedded PostgreSQL database of a Serverpod project. Use `serverpod database start` to boot the database on its own, without starting the server, and keep it running until you stop it with Ctrl+C. This is useful for connecting external database tools while the server is stopped. The database configuration comes from the run mode selected with `--mode`, which must have `database.dataPath` set. For that option and its default, see the [Configuration reference](https://docs.serverpod.dev/concepts/lookups/configuration-reference.md). ## Usage ```console Manage the embedded PostgreSQL database used by a Serverpod project. Usage: serverpod database [arguments] -h, --help Print this usage information. Available subcommands: start Start the configured embedded PostgreSQL database over TCP. Run "serverpod help" to see global options. ``` ### Sub commands #### `start` ```console Start the configured embedded PostgreSQL database over TCP. Usage: serverpod database start [arguments] -h, --help Print this usage information. -s, --server-dir= Server project directory. Defaults to auto-detection. -m, --mode Serverpod run mode whose database config should be used. (defaults to "development") -p, --port= TCP port override. Defaults to the configured database port. Run "serverpod help" to see global options. ``` --- # serverpod generate https://docs.serverpod.dev/concepts/cli/commands/generate `serverpod generate` reads your model and endpoint definitions and generates the server and client Dart code that connects them. Run it whenever you add or change a model or an endpoint. Pass `--watch` to regenerate automatically as you edit, or `--force` to rebuild after hand-editing generated files. For what gets generated, see [Working with endpoints](https://docs.serverpod.dev/concepts/endpoints-and-apis.md) and [Models](https://docs.serverpod.dev/concepts/data-and-the-database/models.md). ## Usage ```console Generate code from yaml files for server and clients. Usage: serverpod generate [arguments] -h, --help Print this usage information. -w, --watch Watch for changes and continuously generate code. -d, --directory The directory to generate code for (defaults to current directory). (defaults to "") -f, --force Regenerate even when the output looks up to date. Use this to rebuild after hand-editing or reverting generated files. Run "serverpod help" to see global options. ``` --- # serverpod language-server https://docs.serverpod.dev/concepts/cli/commands/language-server `serverpod language-server` starts a Serverpod language server that communicates over JSON-RPC 2.0. It is meant to be launched by an IDE integration rather than run directly, and it powers features like diagnostics in your model files (`.spy.yaml`, `.spy.yml`, and `.spy`). See [Language server](https://docs.serverpod.dev/tools/lsp.md). ## Usage ```console Launches a serverpod language server communicating with JSON-RPC-2 intended to be used with a client integrated in an IDE. Usage: serverpod language-server [arguments] -h, --help Print this usage information. --[no-]stdio (defaults to on) Run "serverpod help" to see global options. ``` --- # serverpod mcp-server https://docs.serverpod.dev/concepts/cli/commands/mcp-server `serverpod mcp-server` starts a Model Context Protocol bridge to the `serverpod start` runner of a server project. An agent can use it to create and apply migrations, reload the server and Flutter app, and read logs. The server directory is auto-detected from the current working directory. Pass `--server-dir` explicitly in monorepos that contain more than one server project. The bridge drives a running session, so start one with `serverpod start` before asking an agent to use these tools. The bridge itself can stay connected while the session stops and starts. ## Set up your editor The `serverpod create` command writes the MCP configuration for the editors you select, so a new project needs no manual setup. Run `serverpod create .` in an existing project to add the same files. | Editor | File | | ----------- | ------------------------------------------------- | | Claude | `.mcp.json` | | Cursor | `.cursor/mcp.json` | | VS Code | `.vscode/mcp.json` | | Antigravity | `.agents/plugins/serverpod-local/mcp_config.json` | | Codex | `.codex/config.toml` | | OpenCode | `opencode.json` | In Cursor, once the file is created, open **Customize** in the sidebar and click **MCPs**. There, make sure both the `serverpod` and `dart` servers are turned on. ### Set up any other MCP client Serverpod registers two servers, its own bridge and the Dart MCP server. Any client that speaks MCP over stdio can run them, so write these two commands in whatever format your client expects. ```json { "mcpServers": { "serverpod": { "command": "serverpod", "args": ["mcp-server", "--server-dir", "_server"] }, "dart": { "command": "dart", "args": ["mcp-server"] } } } ``` The shape differs per editor. VS Code names the block `servers` rather than `mcpServers`. Antigravity and OpenCode name the Dart entry `dart-mcp-server`, and OpenCode puts both under an `mcp` block. Codex uses TOML. :::note Module projects get only the Dart MCP server. A module has no runnable server, so there is no `serverpod start` session for the bridge to connect to. ::: ## Migrations and your data The bridge exposes three migration tools. The `create_migration` and `create_repair_migration` tools write migration files without changing your database. Both accept a `force` parameter that proceeds past Serverpod's warning that a change may destroy data. The `apply_migrations` tool then applies the pending migrations to your database. :::warning The `apply_migrations` tool takes no parameters and applies every pending migration, including one an agent created with `force`. Keep `serverpod start` in its default `development` run mode, because another mode loads that environment's database. Review each `apply_migrations` call before you approve it. ::: ## Usage ```console Start an MCP bridge to the `serverpod start` runner of one server project. Usage: serverpod mcp-server -h, --help Print this usage information. -s, --server-dir Path to the server project directory (the package that contains a `serverpod` dependency). Auto-detected from the current working directory if omitted. Pass this flag explicitly in monorepos with multiple server projects. Run "serverpod help" to see global options. ``` --- # serverpod quickstart https://docs.serverpod.dev/concepts/cli/commands/quickstart `serverpod quickstart` creates a new Serverpod project with a small set of options, suited to a quick experiment or a simple app. It is a streamlined version of `serverpod create`. Choose a template with `--template`: - `fullstack` (default): a server and a Flutter app. - `server`: a server only. - `module`: a shareable module. For a guided first project, see [Quickstart](https://docs.serverpod.dev/quickstart.md). ## Usage ```console Creates a new Serverpod project with basic options. Suitable for a simple project or quick experiment. Specify project name (must be lowercase with no special characters). Usage: serverpod quickstart [arguments] -h, --help Print this usage information. -f, --force Create the project even if there are issues that prevent it from running out of the box. -t, --template Template to use when creating a new project [fullstack] (default) Fullstack project including a server and a companion Flutter app [server] Server project with standard features including database [module] Serverpod Module project -n, --name (mandatory) The name of the project to create. Can also be specified as the first argument. Run "serverpod help" to see global options. ``` --- # serverpod run https://docs.serverpod.dev/concepts/cli/commands/run `serverpod run` runs a script defined in the `serverpod/scripts` section of your `pubspec.yaml`. Pass `--list` to see the available scripts. See [Run scripts](https://docs.serverpod.dev/tools/run-scripts.md). ## Usage ```console Run a script defined in the "serverpod/scripts" section of pubspec.yaml. Usage: serverpod run -h, --help Print this usage information. --script The name of the script to run. -l, --[no-]list List all available scripts. Run "serverpod help" to see global options. ``` --- # serverpod start https://docs.serverpod.dev/concepts/cli/commands/start The `serverpod start` command runs your project in development mode. It generates the latest code, starts the server with hot reload, and launches your companion Flutter apps, all in a single interactive terminal. ```bash serverpod start ``` Run it from anywhere inside your project folder. ## Usage ```console Start the full development stack with hot reload: generates code, runs the server, and launches the companion Flutter apps in an interactive terminal UI. Usage: serverpod start [-- ] -h, --help Print this usage information. -w, --[no-]watch Watch files and use the Frontend Server for fast incremental compilation. With --no-watch, the server is started via `dart run`. (defaults to on) -d, --directory The server directory (defaults to auto-detect from current directory). (defaults to "") --[no-]docker Start Docker Compose services if a Docker Compose file exists. Defaults to on if the project has a Docker Compose file and the database is configured to PostgreSQL on localhost without a dataPath. Otherwise, defaults to off. Pass --docker or --no-docker to override the default behavior. --[no-]tui Show interactive terminal UI. (defaults to on) --[no-]flutter Auto-launch the companion Flutter apps as configured on the server pubspec.yaml with `auto_launch: true`. Use --no-flutter to disable auto-launch. Apps can still be started on demand from the TUI. (defaults to on) Run "serverpod help" to see global options. ``` --- # serverpod upgrade https://docs.serverpod.dev/concepts/cli/commands/upgrade `serverpod upgrade` updates the Serverpod CLI to the latest version. For the version-specific steps when moving between major releases, see the [Upgrade to 4.0](https://docs.serverpod.dev/upgrading/upgrade-to-four.md) guide. ## Usage ```console Upgrade the Serverpod CLI to the latest version. Usage: serverpod upgrade [arguments] -h, --help Print this usage information. --channel Which published versions to consider. Defaults to following prereleases when a prerelease is installed, and stable releases otherwise. [stable] Only install stable releases [any] Install the newest version, including prereleases --version= Install a specific version instead of the newest one in the channel. This is the only way to move the installation backwards. -f, --force Install even when the installed version is already the target, or when the CLI being run is not the one that would be replaced. Run "serverpod help" to see global options. ``` --- # serverpod version https://docs.serverpod.dev/concepts/cli/commands/version `serverpod version` prints the version of the Serverpod CLI you have installed. Include it when reporting a bug or asking for help, so the response can target your build. To update to the latest version, run `serverpod upgrade`. ## Usage ```console Prints the active version of the Serverpod CLI. Usage: serverpod version [arguments] -h, --help Print this usage information. Run "serverpod help" to see global options. ``` --- # Configuration reference https://docs.serverpod.dev/concepts/lookups/configuration-reference Every configuration option Serverpod's core library reads. Options come from three sources: the `config/.yaml` files, environment variables, and the `ServerpodConfig` Dart object. Environment variables override the YAML files. If you pass a `ServerpodConfig` to the `Serverpod` constructor, the server ignores the YAML files and environment variables. Command-line options such as `--mode` still override the values in your `ServerpodConfig`. For how to choose between them, see [Configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md). ## Run options Set the run mode, server role, and boot behavior. Declare each per run mode in the matching `config/.yaml`, or as an environment variable. | Environment variable | Command line option | Config file option | Default | Description | | ----------------------------------- | -------------------------- | -------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------- | | SERVERPOD\_RUN\_MODE | `--mode` | N/A | development | Configures the mode of the server instance. Valid options are `development`, `staging`, `production` and `test`. | | SERVERPOD\_SERVER\_ID | `--server-id` | serverId | default | Configures the id of the server instance. | | SERVERPOD\_SERVER\_ROLE | `--role` | role | monolith | Configures the role of the server instance. Valid options are `monolith`, `serverless` and `maintenance`. | | SERVERPOD\_LOGGING\_MODE | `--logging` | logging | normal | Configures the logging level. Valid options are `normal`, and `verbose`. | | SERVERPOD\_APPLY\_MIGRATIONS | `--apply-migrations` | applyMigrations | false | Configures if migrations should be applied when the server starts. | | SERVERPOD\_APPLY\_REPAIR\_MIGRATION | `--apply-repair-migration` | applyRepairMigration | false | Configures if repair migrations should be applied when the server starts. | ## Server and services Ports, hosts, and connection settings for the API, Insights, and web servers, the database, Redis, session logs, and future calls, plus a few options that exist only on the Dart config object or as environment variables. | Environment variable | Config file | Default | Description | | ----------------------------------------------------- | ----------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | SERVERPOD\_API\_SERVER\_PORT | apiServer.port | 8080 | The port number for the API server | | SERVERPOD\_API\_SERVER\_PUBLIC\_HOST | apiServer.publicHost | localhost | The public host address of the API server | | SERVERPOD\_API\_SERVER\_PUBLIC\_PORT | apiServer.publicPort | 8080 | The public port number for the API server | | SERVERPOD\_API\_SERVER\_PUBLIC\_SCHEME | apiServer.publicScheme | http | The public scheme (http/https) for the API server | | SERVERPOD\_INSIGHTS\_SERVER\_PORT | insightsServer.port | - | The port number for the Insights server | | SERVERPOD\_INSIGHTS\_SERVER\_PUBLIC\_HOST | insightsServer.publicHost | - | The public host address of the Insights server | | SERVERPOD\_INSIGHTS\_SERVER\_PUBLIC\_PORT | insightsServer.publicPort | - | The public port number for the Insights server | | SERVERPOD\_INSIGHTS\_SERVER\_PUBLIC\_SCHEME | insightsServer.publicScheme | - | The public scheme (http/https) for the Insights server | | SERVERPOD\_INSIGHTS\_SERVER\_ENABLE\_DATABASE\_ACCESS | insightsServer.enableDatabaseAccess | false | Whether the Insights server serves the direct database access endpoints (used by custom service client tooling, not the Insights app) | | SERVERPOD\_WEB\_SERVER\_PORT | webServer.port | - | The port number for the Web server | | SERVERPOD\_WEB\_SERVER\_PUBLIC\_HOST | webServer.publicHost | - | The public host address of the Web server | | SERVERPOD\_WEB\_SERVER\_PUBLIC\_PORT | webServer.publicPort | - | The public port number for the Web server | | SERVERPOD\_WEB\_SERVER\_PUBLIC\_SCHEME | webServer.publicScheme | - | The public scheme (http/https) for the Web server | | SERVERPOD\_WEB\_SERVER\_STATIC\_CACHE\_CONTROL | N/A | - | The `Cache-Control` header for `StaticRoute` responses without a `cacheControlFactory`. See [Static files](https://docs.serverpod.dev/concepts/web-server/static-files.md#cache-control). | | SERVERPOD\_WEB\_SERVER\_SPA\_CACHE\_CONTROL | N/A | - | The `Cache-Control` header for `SpaRoute` responses without a `cacheControlFactory`. Never applies to the fallback file. See [Single-page apps](https://docs.serverpod.dev/concepts/web-server/single-page-apps.md#cache-control). | | SERVERPOD\_WEB\_SERVER\_FLUTTER\_CACHE\_CONTROL | N/A | - | The `Cache-Control` header for `FlutterRoute` responses without a `cacheControlFactory`. See [Flutter web](https://docs.serverpod.dev/concepts/web-server/flutter-web.md#cache-control). | | SERVERPOD\_DATABASE\_HOST | database.host | - | The host address of the database | | SERVERPOD\_DATABASE\_PORT | database.port | - | The port number for the database connection | | SERVERPOD\_DATABASE\_NAME | database.name | - | The name of the database | | SERVERPOD\_DATABASE\_USER | database.user | - | The user name for database authentication | | SERVERPOD\_DATABASE\_SEARCH\_PATHS | database.searchPaths | - | The search paths used for all database connections | | SERVERPOD\_DATABASE\_REQUIRE\_SSL | database.requireSsl | false | Indicates if SSL is required for the database | | SERVERPOD\_DATABASE\_IS\_UNIX\_SOCKET | database.isUnixSocket | false | Specifies if the database connection is a Unix socket | | SERVERPOD\_DATABASE\_MAX\_CONNECTION\_COUNT | database.maxConnectionCount | 10 | The maximum number of connections in the database pool. Set to 0 or a negative value for unlimited connections. | | N/A | database.filePath | - | The SQLite database file path. Set this instead of host/port/name/user when using SQLite. Config file only. | | N/A | database.dialect | Inferred | The database dialect. Valid options are `postgres` and `sqlite`. When unset, a `filePath` selects SQLite, and host/port/name/user select PostgreSQL. Config file only. | | SERVERPOD\_DATABASE\_DATA\_PATH | database.dataPath | - | Directory for the embedded PostgreSQL cluster, relative to the server package unless absolute. When set, Serverpod starts or attaches to the cluster before connecting. PostgreSQL only; ignored for SQLite. | | SERVERPOD\_REDIS\_HOST | redis.host | - | The host address of the Redis server | | SERVERPOD\_REDIS\_PORT | redis.port | - | The port number for the Redis server | | SERVERPOD\_REDIS\_USER | redis.user | - | The user name for Redis authentication | | SERVERPOD\_REDIS\_ENABLED | redis.enabled | true | Indicates if Redis is enabled. Defaults to `true` when a `redis` section is present; Redis is off when the section is absent or `enabled` is `false`. | | SERVERPOD\_REDIS\_REQUIRE\_SSL | redis.requireSsl | false | Indicates if SSL is required for the Redis connection | | SERVERPOD\_MAX\_REQUEST\_SIZE | maxRequestSize | 524288 | The maximum size of API requests in bytes. Also caps file uploads that post to the API server. | | SERVERPOD\_VALIDATE\_HEADERS | validateHeaders | true | Validate HTTP headers using the typed API. Set to `false` to accept headers without the required formatting, for example an unwrapped token in the Authorization header. | | SERVERPOD\_ALLOWED\_ORIGINS | allowedOrigins | - | Browser origins allowed to make credentialed cross-origin calls, as `scheme://host[:port]`. A YAML list, or comma-separated in the environment variable. Required when `authCookie` is set. When set, browser WebSocket connections for [streaming](https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming.md) from other origins are also rejected, with or without `authCookie`. See [Web authentication](https://docs.serverpod.dev/concepts/authentication/web-authentication.md). | | SERVERPOD\_AUTH\_COOKIE\_NAME | authCookie.name | - | Name of the auth cookie. Defaults to `serverpod_auth`. Setting any `authCookie` field enables cookie auth for web clients; an empty `authCookie` section is read as unset. | | SERVERPOD\_AUTH\_COOKIE\_REFRESH\_NAME | authCookie.refreshName | - | Name of the JWT refresh cookie. Defaults to `_refresh`. | | SERVERPOD\_AUTH\_COOKIE\_DOMAIN | authCookie.domain | - | Cookie `Domain` attribute. Defaults to host-only; set to share the cookie across subdomains. | | SERVERPOD\_AUTH\_COOKIE\_PATH | authCookie.path | / | Cookie `Path` attribute. | | SERVERPOD\_AUTH\_COOKIE\_SECURE | authCookie.secure | true | Whether the cookie is only sent over https. Set to `false` only for `http://localhost` development. | | SERVERPOD\_AUTH\_COOKIE\_SAME\_SITE | authCookie.sameSite | lax | Cookie `SameSite` attribute. Valid options are `lax`, `strict`, and `none` (`none` requires `secure`). | | SERVERPOD\_SESSION\_PERSISTENT\_LOG\_ENABLED | sessionLogs.persistentEnabled | - | Enables or disables logging session data to the database. Defaults to `true` if a database is configured, otherwise `false`. | | SERVERPOD\_SESSION\_LOG\_CLEANUP\_INTERVAL | sessionLogs.cleanupInterval | - | How often to run the log cleanup job. Duration string, for example `24h` or `2d`. Set to null to disable automated purging. Defaults to `24h` only when `sessionLogs` is not configured at all. See [Purge old records](https://docs.serverpod.dev/concepts/operations/logging.md#purge-old-records). | | SERVERPOD\_SESSION\_LOG\_RETENTION\_PERIOD | sessionLogs.retentionPeriod | - | How long to keep session log entries. Duration string, for example `30d` or `60d`. Set to null to disable time-based cleanup. Defaults to `90d` only when `sessionLogs` is not configured at all. See [Purge old records](https://docs.serverpod.dev/concepts/operations/logging.md#purge-old-records). | | SERVERPOD\_SESSION\_LOG\_RETENTION\_COUNT | sessionLogs.retentionCount | - | Maximum number of session log entries to keep. Set to null to disable count-based cleanup. Defaults to `100000` only when `sessionLogs` is not configured at all. See [Purge old records](https://docs.serverpod.dev/concepts/operations/logging.md#purge-old-records). | | SERVERPOD\_SESSION\_CONSOLE\_LOG\_ENABLED | sessionLogs.consoleEnabled | - | Enables or disables logging session data to the console. Defaults to `true` if no database is configured or the run mode is `development`, otherwise `false`. | | SERVERPOD\_SESSION\_CONSOLE\_LOG\_FORMAT | sessionLogs.consoleLogFormat | - | The format for console logging of session data. Valid options are `text` and `json`. Defaults to `text` for run mode `development`, otherwise `json`. | | SERVERPOD\_FUTURE\_CALL\_EXECUTION\_ENABLED | futureCallExecutionEnabled | true | Enables or disables the execution of future calls. | | SERVERPOD\_FUTURE\_CALL\_CONCURRENCY\_LIMIT | futureCall.concurrencyLimit | 1 | The maximum number of concurrent future calls allowed. If the value is negative or null, no limit is applied. | | SERVERPOD\_FUTURE\_CALL\_SCAN\_INTERVAL | futureCall.scanInterval | 5000 | The interval in milliseconds for scanning future calls | | SERVERPOD\_FUTURE\_CALL\_CHECK\_BROKEN\_CALLS | futureCall.checkBrokenCalls | - | Enables or disables the automatic check for broken future calls on startup. By default, the server performs an automatic check if there are less than 1000 calls in the database. | | SERVERPOD\_FUTURE\_CALL\_DELETE\_BROKEN\_CALLS | futureCall.deleteBrokenCalls | false | Enables or disables the deletion of broken future calls when running the check on startup. | | SERVERPOD\_WEBSOCKET\_PING\_INTERVAL | websocketPingInterval | 30 | The interval in seconds between WebSocket ping messages sent to keep streaming connections alive. Must be a positive integer. | ### Dart-only options These options have no environment variable or config-file key. Set them on the `ServerpodConfig` Dart object passed to the `Serverpod` constructor. | ServerpodConfig field | Default | Description | | ------------------------------------ | ------- | -------------------------------------------------------------------------------------------------------------------- | | healthCheckInterval | 1m | How often the server collects health metrics. Set to zero to disable health checks. | | experimentalDiagnosticHandlerTimeout | 30s | The timeout for [diagnostic event handlers](https://docs.serverpod.dev/concepts/operations/exception-monitoring.md). | ### Password environment variables Secrets are read from `config/passwords.yaml` and can be overridden per secret through environment variables; see [Manage secrets](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md#manage-secrets). Two forms exist, and when both are set for the same secret, the `SERVERPOD_PASSWORD_*` form wins: | Environment variable | Overrides passwords-file key | | ----------------------------- | -------------------------------------------------------------------------------- | | `SERVERPOD_PASSWORD_` | Any secret; the prefix is stripped (`SERVERPOD_PASSWORD_database` → `database`). | | SERVERPOD\_DATABASE\_PASSWORD | database | | SERVERPOD\_SERVICE\_SECRET | serviceSecret | | SERVERPOD\_REDIS\_PASSWORD | redis | ## Code generation Options for `config/generator.yaml`, which configures `serverpod generate`. | Option | Type | Default | Description | | --------------------------------- | ------ | ---------------------------- | ------------------------------------------------------------------------------------------- | | type | string | server | The package type. Valid options are `server`, `module`, or `internal`. | | nickname | string | - | For modules only. Defines how the module is referenced in code. | | client\_package\_path | string | ../\[name]\_client | Path to the client package relative to the server. | | server\_test\_tools\_path | string | test/integration/test\_tools | Path where test tools are generated. Remove this to disable test tools generation. | | shared\_packages | list | - | Paths to shared packages containing models usable by both server and client. | | modules | map | - | Module dependencies with optional nicknames. | | extraClasses | list | - | List of custom serializable classes to include in code generation. | | serialize\_as\_jsonb\_by\_default | bool | false | When true, all serializable fields default to `jsonb` storage instead of `json`. | | experimental\_features | map | - | Experimental features. Available keys: `databaseSync` (enables `database: sync`) and `all`. | ## Related - [Configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration.md): how the three configuration sources work, run modes, secrets, and package types. - [Redis](https://docs.serverpod.dev/concepts/operations/redis.md): enabling Redis, the password, and connecting a GUI. - [Running your server](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md): the run mode and files the server loads on start. --- # Model reference https://docs.serverpod.dev/concepts/lookups/model-reference Every keyword available in a Serverpod model file, and whether it applies to a `class`, `exception`, or `enum`. For how each one works, see [Working with models](https://docs.serverpod.dev/concepts/data-and-the-database/models.md). | **Keyword** | Note | [class](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#class) | [exception](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#exception) | [enum](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#enum) | | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------: | | [**values**](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#enum) | A special key for enums with a list of all enum values. | | | ✅ | | [**serialized**](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#enum) | Sets the mode enums are serialized in. | | | ✅ | | [**properties**](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#enhanced-enums-with-properties) | Defines custom properties for enhanced enums. | | | ✅ | | [**immutable**](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#immutable-classes) | Boolean flag to generate an immutable class with final fields, equals operator, and hashCode. | ✅ | | | | [**serverOnly**](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#limiting-visibility-of-a-generated-class) | Boolean flag if code generator only should create the code for the server. | ✅ | ✅ | ✅ | | [**table**](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md) | A name for the database table, enables generation of database code. | ✅ | | | | [**database**](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md#choosing-where-a-table-lives) | Where a table lives: `server` (default), `client`, `all`, or the experimental `sync`. | ✅ | | | | [**managedMigration**](https://docs.serverpod.dev/concepts/data-and-the-database/database/migrations.md#opt-out-of-migrations) | A boolean flag to opt out of the database migration system. | ✅ | | | | [**serializationDataType**](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md#storing-serializable-fields-as-jsonb) | Stores serializable fields as `json` or `jsonb`. Valid on classes and on individual fields. | ✅ | | | | [**fields**](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#class) | All fields in the generated class should be listed here. | ✅ | ✅ | | | [**type (fields)**](https://docs.serverpod.dev/concepts/data-and-the-database/models/field-types.md) | Denotes the data type for a field. | ✅ | ✅ | | | [**required**](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#required-fields) | Makes the field required. This keyword can only be used for **nullable** fields. | ✅ | ✅ | | | [**tail**](https://docs.serverpod.dev/concepts/data-and-the-database/models/inheritance-and-polymorphism.md#ordering-inherited-fields) | Places the field after all normal fields of an inheritance hierarchy. Child tail fields come before parent tail fields. Not allowed on the `id` field. | ✅ | ✅ | | | [**scope**](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#limiting-visibility-of-a-generated-class) | Denotes the scope for a field. | ✅ | | | | [**jsonKey**](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#json-key-aliasing) | Sets a custom key name for JSON serialization and deserialization. | ✅ | | | | [**column**](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md#column-name-override) | Overrides the database column name for a field. | ✅ | | | | [**persist**](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md) | A boolean flag if the data should be stored in the database or not can be negated with `!persist` | ✅ | | | | [**relation**](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations.md) | Sets a relation between model files, requires a table name to be set. | ✅ | | | | [**name**](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one.md#bidirectional-relations) | Give a name to a relation to pair them. | ✅ | | | | [**parent**](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one.md#with-an-id-field) | Sets the parent table on a relation. | ✅ | | | | [**field**](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one.md#custom-foreign-key-field) | Names the foreign key field of a relation. Generated if the field is not declared on the model. | ✅ | | | | [**fk**](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one.md#bidirectional-relations) | Boolean flag marking the side of a bidirectional relation that holds the foreign key, keeping the generated field name. Mutually exclusive with **field**. | ✅ | | | | [**onUpdate**](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/referential-actions.md) | Set the referential actions when updating data in the database. | ✅ | | | | [**onDelete**](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/referential-actions.md) | Set the referential actions when deleting data in the database. | ✅ | | | | [**optional**](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/one-to-one.md#optional-relation) | A boolean flag to make a relation optional. | ✅ | | | | [**deferrable**](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/deferrable-constraints.md) | Make the foreign key constraint deferrable, checked after each statement unless a transaction defers it. | ✅ | | | | [**deferred**](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations/deferrable-constraints.md) | Make the foreign key constraint deferrable and checked at commit. Mutually exclusive with **deferrable**. | ✅ | | | | [**indexes**](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md) | Create indexes on your fields / columns. | ✅ | | | | [**fields (index)**](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md) | List the fields to create the indexes on. | ✅ | | | | [**type (index)**](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md) | The type of index to create. | ✅ | | | | [**parameters (index)**](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md#vector-indexes) | Parameters for specialized index types like HNSW and IVFFLAT vector indexes. | ✅ | | | | [**distanceFunction (index)**](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md#vector-indexes) | Distance function for vector indexes. Allowed values depend on the field's vector type and index type, see [vector indexes](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md#vector-indexes). | ✅ | | | | [**operatorClass (index)**](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md#operator-classes) | Operator class for GIN indexes. | ✅ | | | | [**unique**](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md#make-fields-unique) | Creates a unique index. Also valid directly on a field, optionally scoped with `unique(per=...)`. | ✅ | | | | [**nulls\_distinct (index)**](https://docs.serverpod.dev/concepts/data-and-the-database/database/indexing.md#null-values-in-unique-indexes) | Whether null values collide in a unique index. Only allowed on unique indexes. | ✅ | | | | [**default**](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#default-values) | Sets the default value for both the model and the database. This keyword cannot be used with **relation**. On an enum, sets the [fallback for unknown values](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#handling-unknown-enum-values). | ✅ | ✅ | ✅ | | [**defaultModel**](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#default-values) | Sets the default value for the model side. This keyword cannot be used with **relation**. | ✅ | ✅ | | | [**defaultPersist**](https://docs.serverpod.dev/concepts/data-and-the-database/models.md#default-values) | Sets the default value for the database side. This keyword cannot be used with **relation** and **!persist**. | ✅ | | | | [**extends**](https://docs.serverpod.dev/concepts/data-and-the-database/models/inheritance-and-polymorphism.md#extending-a-class) | Specifies a parent class or exception of the same kind to inherit from. | ✅ | ✅ | | | [**sealed**](https://docs.serverpod.dev/concepts/data-and-the-database/models/inheritance-and-polymorphism.md#sealed-classes) | Boolean flag to create a sealed class or exception hierarchy, enabling exhaustive type checking. | ✅ | ✅ | | ## Related - [Working with models](https://docs.serverpod.dev/concepts/data-and-the-database/models.md): how each keyword works, with examples. - [Field types](https://docs.serverpod.dev/concepts/data-and-the-database/models/field-types.md): YAML types mapped to Dart, Postgres, SQLite, and JSON. - [Dynamic fields](https://docs.serverpod.dev/concepts/data-and-the-database/models/dynamic-fields.md): store and transfer values whose type varies at runtime. - [Serializable exceptions](https://docs.serverpod.dev/concepts/endpoints-and-apis/error-handling-and-exceptions.md#serializable-exceptions): define, inherit, throw, and catch application errors. - [Database tables](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md): the database-specific keywords (`table`, `relation`, `persist`, and indexing). --- # Deploy to Serverpod Cloud https://docs.serverpod.dev/deployments/deploy-to-serverpod-cloud [Serverpod Cloud](https://docs.serverpod.dev/cloud.md) is the recommended way to host a Serverpod server. It is a fully managed platform built and maintained by the Serverpod team, designed specifically for Serverpod apps. You deploy in minutes with zero configuration, and there is no VM setup, container configuration, or infrastructure to orchestrate yourself. :::info Your first project includes a one-month free trial, with no credit card required. ::: Cloud manages the production infrastructure for you: - An optional production-grade Postgres database, with migrations applied automatically on each deploy. - Networking, load balancing, and custom domains with TLS certificates. - Server instances (podlets) with predictable pricing. See [Cloud plans](https://serverpod.dev/cloud) for what each plan includes. - A secure key manager for your app secrets. ## Deploy from the terminal You deploy with the Serverpod CLI: 1. Run `serverpod cloud launch`. It installs the Cloud CLI if needed. If you aren't signed in, it opens your browser so you can sign in. 2. For a new project, the Cloud Console opens so you can create it. The database setting is pre-filled from your server's config. 3. When prompted, select the custom passwords from `config/passwords.yaml` to copy to Cloud. The command then deploys your server. 4. Set any other secrets (API keys, OAuth credentials) with `serverpod cloud password set`. Then run `serverpod cloud deploy` so the server picks them up. See the [Serverpod Cloud documentation](https://docs.serverpod.dev/cloud.md) for the full walkthrough, including secrets, custom domains, and logs. To run the server yourself instead, see [Custom hosting](https://docs.serverpod.dev/deployments/custom-hosting/choosing-a-strategy.md). --- # Choosing a strategy https://docs.serverpod.dev/deployments/custom-hosting/choosing-a-strategy You can run a Serverpod server anywhere that supports a Dart process or a Docker container: a managed server cluster, a serverless platform, or your own machine. This page covers the trade-offs. For the managed path, see [Serverpod Cloud](https://docs.serverpod.dev/cloud.md). ## Server cluster or serverless? The two main options are running on a cluster of servers or on a serverless platform. Run on a **cluster** (such as Google Cloud Engine) if your server holds state. Run **serverless** (such as Google Cloud Run) if your server is stateless. An example of a stateful server is [Pixorama](https://pixorama.live), where the server keeps live state in memory. If you only make API calls that read and write a database, serverless may be the better fit. | | Server cluster | Serverless | | :--- | :-------------------------------------------------------------------------------------- | :---------------------------------------------------------------- | | Pros | All features are supported. Great for real-time communication. Cost-efficient at scale. | Minimal starting cost. Easier configuration. Minimal maintenance. | | Cons | Slightly higher starting cost. More complex to set up. | Limited feature set. The server cannot hold state. | Serverless does not support: - Future calls. (Possible to configure, but requires a more advanced setup.) - Health metrics. - On-server caching. Caching can still happen while a serverless instance stays warm, but it can be lost at any time. Caching with Redis is supported. - In-memory state. Store shared state in an external service such as Postgres, Redis, or another API instead. ## Pick a guide - [Hosting elsewhere](https://docs.serverpod.dev/deployments/custom-hosting/hosting-elsewhere.md): run Serverpod on any Dart or Docker host. - [Community-supported deployments](https://docs.serverpod.dev/deployments/custom-hosting/community-supported.md): tools built by the community. ## Approximate costs A minimal **server cluster** on Google Cloud Platform: | Service | Min cost | | :----------------------- | :------- | | Compute Engine Instance | $7 / mo | | Cloud Load Balancing | $19 / mo | | Cloud SQL for PostgreSQL | $10 / mo | A minimal **serverless** setup on Google Cloud Run, where you only pay for the traffic you serve and no load balancer is required: | Service | Min cost | | :----------------------- | :------- | | Cloud Run | $0 / mo | | Cloud SQL for PostgreSQL | $10 / mo | :::info These prices are rough approximations to give you a sense of hosting costs. Actual costs vary and change over time. Do your own research before provisioning infrastructure. ::: --- # Hosting elsewhere https://docs.serverpod.dev/deployments/custom-hosting/hosting-elsewhere You can host Serverpod anywhere, running Dart directly or through a Docker container. ## Required services Serverpod needs a database with the correct tables added, unless the project was created without a database, for example with `serverpod create --no-interactive --no-database`. Serverpod can also optionally use Redis. You enable Redis in your configuration files. ## Configuration files Serverpod has three main configuration files, one for each mode the server can run in: `development`, `staging`, and `production`. The files are located in the `config/` directory. Two options control how the server starts: - `--mode` selects the configuration file. By default, the server starts in development mode. - `--server-id` sets the id of each server when you run multiple servers in a cluster. By default, the server runs as id `default`. For example, to start the server in production mode with id `2`, run the following command: ```bash $ dart bin/main.dart --mode production --server-id 2 ``` Running all servers with the same id can also be valid. This helps on platforms such as AWS Fargate, where individual server ids are hard to configure. By default, Serverpod will listen on ports 8080, 8081, and 8082. The ports are used by the API server, Serverpod Insights, and the [web server](https://docs.serverpod.dev/concepts/web-server/overview.md). You can configure the ports in the configuration files. Most often, you will want to place your server or servers behind a load balancer that handles the SSL certificates for your server and maps the traffic to different domain addresses and ports (typically 443 for HTTPS). ## Server roles Serverpod can assume different roles depending on your configuration. If you run Serverpod on a single server or a cluster of servers, you typically will want to use the default `monolith` role. If you use a serverless service, use the `serverless` role. When Serverpod runs as a monolith, it will handle all maintenance tasks, such as health checks and future calls. If you run it serverless, you will need to schedule a cron job to start Serverpod in the `maintenance` role once every minute if you need support for future calls and health checks. | Role | Function | | :------------ | :------------------------------------------------------------------------------------------------------ | | `monolith` | Handles incoming connections and maintenance tasks. Allows the server to contain a state. Default role. | | `serverless` | Only handles incoming connections. | | `maintenance` | Runs the maintenance tasks once, then exits. | You can specify the role of your server when you launch it by setting the `--role` argument. ```bash $ dart bin/main.dart --role serverless ``` For connection pools, Redis, isolates, and other production scale concerns beyond roles, see [Scalability](https://docs.serverpod.dev/concepts/operations/scalability.md). ## Docker container Running Serverpod through a Docker container is often the best option as it provides a well-defined environment. It's also easy to integrate into your build and deployment process and runs well on most platforms. You will get a `Dockerfile` created in your server directory when you set up a new project. Build it from the project root, because it also copies the `pubspec.lock` file stored there: ```bash $ docker build -f _server/Dockerfile . ``` The file works out of the box but can be tailored to your needs. It has no build options, but you can define environment variables when running it. The following variables are supported. | Environment variable | Meaning | | :------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------ | | `runmode` | The run mode to start the server in, possible values are `development`, `staging`, or `production` (the `Dockerfile` default). | | `serverid` | Identifier of your server, default is `default` | | `logging` | Logging mode at startup, default is `normal`, but you can specify `verbose` to get more information during startup which can help with debugging. | | `role` | The role that the server will assume, possible values are `monolith` (default), `serverless`, or `maintenance`. | --- # Community-supported deployments https://docs.serverpod.dev/deployments/custom-hosting/community-supported Serverpod can be deployed in multiple ways, and the community has developed tools to simplify the process. While these tools are not officially maintained by the Serverpod team, they can be a great starting point for getting your server up and running quickly. ## Virtual Private Server (VPS) [serverpod\_vps](https://pub.dev/packages/serverpod_vps) provides a simple way to deploy Serverpod on a Virtual Private Server using Docker-Compose. The CLI creates all required files and the detailed step-by-step guide will help you get your server up and running in no time. --- # Serverpod Insights https://docs.serverpod.dev/tools/insights Serverpod has a companion app. It is currently available for Mac and Windows, but Linux is coming soon. The app has support for viewing your server's logs and health metrics, but we are adding many more features in version 1.2. You must use a version of the app that matches the version of Serverpod you use in your project, or not all features may work correctly. ![Serverpod Insights](https://docs.serverpod.dev/img/serverpod-insights.webp) ## Downloads | App version | MacOS | Windows | | :------------------- | :-------------------------------------------------------------------- | :---------------------------------------------------------------------- | | Serverpod 4 (latest) | [Download](https://downloads.serverpod.dev/macos/Serverpod-4.0.0.zip) | [Download](https://downloads.serverpod.dev/windows/serverpod-4.0.0.zip) | | Serverpod 3 | [Download](https://downloads.serverpod.dev/macos/Serverpod-3.0.0.zip) | [Download](https://downloads.serverpod.dev/windows/serverpod-3.0.0.zip) | | Serverpod 2 | [Download](https://downloads.serverpod.dev/macos/Serverpod-2.9.1.zip) | [Download](https://downloads.serverpod.dev/windows/serverpod-2.9.1.zip) | | Serverpod 1.2.0 | [Download](https://downloads.serverpod.dev/macos/Serverpod-1.2.0.zip) | [Download](https://downloads.serverpod.dev/windows/serverpod-1.2.0.zip) | | Serverpod 1.1.0 | [Download](https://downloads.serverpod.dev/macos/Serverpod-1.1.0.zip) | [Download](https://downloads.serverpod.dev/windows/serverpod-1.1.0.zip) | | Serverpod 1.0.0 | [Download](https://serverpod.dev/insights/Serverpod-1.0.0.zip) | n/a | ## Database access The Insights server also exposes endpoints that read and write your database directly (`fetchDatabaseBulkData`, `runQueries`, `getDatabaseRowCount`, and `executeSql`). The Insights app doesn't use them; they exist only to support custom tooling built on the `serverpod_service_client` package. They are disabled by default and throw an `AccessDeniedException` unless you explicitly enable them in the server configuration: ```yaml insightsServer: port: 8081 publicHost: localhost publicPort: 8081 publicScheme: http enableDatabaseAccess: true ``` The same setting is available as the `SERVERPOD_INSIGHTS_SERVER_ENABLE_DATABASE_ACCESS` environment variable. Since the flag gives anyone holding the service secret full read and write access to the database, leave it off unless you have custom service client tooling that needs it. --- # LSP server https://docs.serverpod.dev/tools/lsp The [Language Server Protocol (LSP)](https://microsoft.github.io/language-server-protocol/) is a standardized protocol designed to provide development environments with language-specific functionalities. Serverpod's language server understands your model files (`.spy.yaml`, `.spy.yml`, and `.spy`) and gives your editor: - **Diagnostics.** Errors in model files are reported as you type, and refreshed when files change on disk, for example after a `git checkout`. - **Go to definition.** In a model file, Ctrl+Click (Cmd+Click on macOS) on a name jumps to the model, enum, or field it refers to, including models from modules and tables referenced through `relation(parent=...)`. - **Find references.** Lists where a model, table, or field is used in other model files. Usages in Dart code are reported by the Dart language server instead. To start the Serverpod LSP server for an editor that speaks LSP, use the following command: ```bash $ serverpod language-server ``` :::info If you use [VS Code](https://code.visualstudio.com/) you can instead use the [Serverpod extension](https://marketplace.visualstudio.com/items?itemName=serverpod.serverpod). It starts the language server for you and needs the `serverpod` command on your `PATH`. ::: ## Navigate from Dart code The VS Code extension also links generated Dart classes back to their model files. Go to definition on a model class in Dart code offers its model file alongside the generated class, and the **Serverpod: Go to Model Definition** command (Ctrl+Alt+F12, Cmd+Alt+F12 on macOS) jumps straight to it. The extension attaches only to the `.spy.yaml`, `.spy.yml`, and `.spy` extensions, which are the only model file names Serverpod 4 accepts. Plain `.yaml` files get no diagnostics or navigation. --- # Run scripts https://docs.serverpod.dev/tools/run-scripts The `serverpod run` command allows you to define and execute custom scripts from your `pubspec.yaml`. This is useful for common tasks like starting the server, running tests, or building executables. To define scripts, add them to your server's `pubspec.yaml` under the `serverpod/scripts` section: ```yaml name: my_server version: 1.0.0 serverpod: scripts: start: dart bin/main.dart --apply-migrations maintenance: dart bin/main.dart --apply-migrations --role maintenance test: dart test build: dart compile exe bin/main.dart ``` To run a script: ```bash $ serverpod run start ``` To list all available scripts, use the `--list` flag or omit the script name: ```bash $ serverpod run --list ``` Scripts run in a shell environment (`bash` on Linux/macOS, `cmd` on Windows), so you can use pipes, conditionals, and environment variables. The CLI forwards signals like `Ctrl+C` to the running script and propagates exit codes. :::note Scripts use `bash` on Linux/macOS and `cmd` on Windows. If you need cross-platform compatibility, use Dart commands (e.g., `dart run`, `dart test`) or write platform-specific scripts. ::: --- # Command line completion https://docs.serverpod.dev/tools/shell-completion As a `serverpod` user, you can enable command line completion in most shells. The Serverpod CLI supports completion through [Carapace](https://carapace.sh/), which provides tab-completion for commands, options, and their values in bash, zsh, fish, PowerShell, and more. ## Installing completion ### Prerequisites Install [Carapace](https://carapace.sh/) for your platform: **macOS:** ```bash brew install carapace ``` **Ubuntu/Debian:** ```bash sudo apt install carapace-bin ``` **Windows:** ```powershell winget install carapace ``` For other platforms, see the [Carapace installation guide](https://carapace-sh.github.io/carapace-bin/install.html). ### Enable Carapace completion for Serverpod #### Bash Install the completion spec: ```bash serverpod completion install --tool carapace ``` Then add the following to your `~/.bashrc` to enable Carapace: ```bash source <(carapace _carapace) ``` Restart your shell or run `source ~/.bashrc` to apply. #### Zsh Install the completion spec: ```bash serverpod completion install --tool carapace ``` Then add the following to your `~/.zshrc` to enable Carapace: ```bash zstyle ':completion:*' format $'\e[2;37mCompleting %d\e[m' source <(carapace _carapace) ``` Restart your shell or run `source ~/.zshrc` to apply. #### Other shells Carapace supports fish, PowerShell, elvish, and more. After installing the completion spec with `serverpod completion install --tool carapace`, see the [Carapace setup documentation](https://carapace-sh.github.io/carapace-bin/setup.html) for instructions specific to your shell. ## Alternative: Completely The Serverpod CLI also supports [Completely](https://github.com/bashly-framework/completely), a lightweight alternative to Carapace that supports bash and zsh. Unlike Carapace, Completely does not require a separate tool running in your shell. It generates a standalone bash completion script. To generate a Completely spec and create a completion script: ```bash serverpod completion generate --tool completely -f serverpod.yaml completely generate serverpod.yaml serverpod.bash ``` Then source the generated script in your shell config (e.g., `source /path/to/serverpod.bash` in `~/.bashrc` or `~/.zshrc`). :::note Carapace is the recommended tool as it supports a wider range of shells and comes with a pre-built completion spec that can be installed directly with `serverpod completion install`. Completely requires generating and processing the script manually. ::: --- # Upgrade to 4.0 https://docs.serverpod.dev/upgrading/upgrade-to-four Serverpod 4.0 (Jetstream) adds `serverpod start`, one command that runs your server, database, and Flutter app together with hot reload. Upgrade your 3.4 project to 4.0, then run it with `serverpod start`. The upgrade itself takes about 15 minutes: update the CLI and your dependencies, regenerate your code, and create one migration. Compile errors after you regenerate are expected. Most match a section on [Breaking changes in 4.0](https://docs.serverpod.dev/upgrading/breaking-changes-in-four.md). Plan for more time if breaking changes affect your code, or if you have a Dockerfile or CI workflows to update. ## Before you start - You have Flutter 3.44.4 or later. It includes Dart 3.12.2, which Serverpod 4.0 requires. Check with `flutter --version`, and run `flutter upgrade` if your version is older. - Your Postgres is version 16 or later, which Serverpod 4.0 requires. Check with `SELECT version();`, and upgrade the database before you upgrade Serverpod. The `pgvector/pgvector:pg16` image from 3.4 projects, the `ghcr.io/serverpod/postgres:16` image, and the embedded Postgres all meet the requirement. On Postgres 14, the server can't read the database schema. Its startup integrity check fails, and `serverpod create-repair-migration` can't run. - Your project is on the latest Serverpod 3.4.x release. - Your project compiles and its tests pass. - You have committed your current state to Git, so you can roll back if needed. - You have no model changes waiting for a migration. If you do, create and apply that migration first. ## Update the Serverpod CLI The 3.4 CLI was installed with `dart pub global activate`. Deactivate it before you install the 4.0 CLI: ```bash $ dart pub global deactivate serverpod_cli ``` Then install the 4.0 CLI: ```bash $ dart install serverpod_cli ``` Check that it reports a 4.0 version: ```bash $ serverpod version ``` ## Update your dependencies Bump every Serverpod package your project uses to 4.0. Pin the exact version rather than a caret range, so the packages stay in sync with the CLI. The packages are in the `pubspec.yaml` files of `_server`, `_client`, and `_flutter`: ```yaml dependencies: serverpod: 4.0.0 # in the server package serverpod_client: 4.0.0 # in the client package serverpod_flutter: 4.0.0 # in the Flutter package # If you use the new auth module: serverpod_auth_idp_server: 4.0.0 # in the server package serverpod_auth_idp_client: 4.0.0 # in the client package serverpod_auth_idp_flutter: 4.0.0 # in the Flutter package # If you use the legacy serverpod_auth module: serverpod_auth_server: 4.0.0 # in the server package serverpod_auth_client: 4.0.0 # in the client package serverpod_auth_shared_flutter: 4.0.0 # in the Flutter package dev_dependencies: serverpod_test: 4.0.0 # in the server package ``` - **The block is not the full list.** Bump any other package whose name starts with `serverpod_` to the same version, such as `serverpod_cloud_storage_s3`. - **Don't skip `serverpod_test` in `dev_dependencies`.** It pins `serverpod` to an exact version. If it stays on 3.4, `dart pub upgrade` fails. - **If you use the legacy `serverpod_auth` module, expect code changes.** It keeps working on 4.0, but the client setup and Sign in with Apple need updating. See [the legacy auth changes](https://docs.serverpod.dev/upgrading/breaking-changes-in-four.md#if-you-use-the-legacy-auth-module). - **If you use the new auth module, check your Flutter app's `flutter_secure_storage` version.** The module needs 10.0.0 or newer, which most projects already have. If yours is still on 9.x, read [how to upgrade without signing out your Android users](https://docs.serverpod.dev/upgrading/breaking-changes-in-four.md#if-you-use-the-new-auth-module-on-android) first. Then bump the Dart SDK constraint in the root `pubspec.yaml` and `_server/pubspec.yaml` to the 4.0 minimum: ```yaml environment: sdk: '^3.12.2' ``` ## Regenerate your code **Check your model files first.** Rename each model file that ends in just `.yaml` or `.yml`, so `company.yaml` becomes `company.spy.yaml`. Otherwise, `serverpod generate` [stops with an error](https://docs.serverpod.dev/upgrading/breaking-changes-in-four.md#model-files-use-the-spyyaml-extension). Then refresh dependencies from the project's root folder: ```bash $ dart pub upgrade ``` Projects created with the 3.3 scaffold or later use a Dart workspace, so this one command covers every sub-package. If the root `pubspec.yaml` has no `workspace:` block, run `dart pub upgrade` in each sub-package instead. To adopt workspaces, see Dart's [pub workspaces documentation](https://dart.dev/tools/pub/workspaces). Now refresh the generated server and client code: ```bash $ serverpod generate ``` **Expect errors here.** If the command reports `analysis skipped due to invalid Dart syntax` for an endpoint or future call file, that file usually still uses an API that changed. Fix each file the same way: 1. Find the matching section on [Breaking changes in 4.0](https://docs.serverpod.dev/upgrading/breaking-changes-in-four.md). 2. Update the file. 3. Run `serverpod generate` again. Repeat until the command finishes without errors. Some changes don't cause an error, such as how future calls and messages behave. Scan the table on [Breaking changes in 4.0](https://docs.serverpod.dev/upgrading/breaking-changes-in-four.md) for anything else that affects your project. ## Create the 4.0 migration Version 4.0 adds a few internal Serverpod tables. It also updates some indexes to speed up logs in [Serverpod Insights](https://docs.serverpod.dev/tools/insights.md), the desktop log viewer. Finish the previous step first. Then create the migration: ```bash $ serverpod create-migration --tag "upgrade-4-0" --force ``` :::note Without `--force`, the command stops with a warning, because this migration changes the authentication module's rate-limit table. The change is safe: accounts, sessions, and other auth data are untouched. The only side effect is that the counters in that table reset to zero. If you don't use the authentication module, there is no warning, and the flag changes nothing. ::: The command writes a new migration to `_server/migrations/`. The `serverpod start` command applies it when you [start the server](#start-the-server) later in this guide. If you run the server yourself instead, start it once from the server package with `dart run bin/main.dart --apply-migrations`. See [Run the server directly](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md#run-the-server-directly). ## Update your deployment This step applies if you compile your server, or if your project has the GitHub Actions workflows that 3.4 created. Otherwise, skip to [Switch to `serverpod start`](#switch-to-serverpod-start). ### Update the server build If you compile your server with `dart compile exe`, for example in the `Dockerfile` in `_server`, switch to `dart build cli`. The 4.0 server has native build hooks that `dart compile exe` doesn't support. Instead of a single static binary, the new command produces a bundle: an executable plus its native libraries. If you deploy with the Dockerfile, copy the updated one from the [4.0 framework template](https://github.com/serverpod/serverpod/blob/main/templates/serverpod_templates/projectname_server/Dockerfile) or a fresh 4.0 project's `_server/Dockerfile`. Compared with the 3.4 Dockerfile, the new one: - Builds from the project root instead of the server directory. - Copies the bundle directory. - Points `ENTRYPOINT` at the bundled binary. - Uses the `dart:3.12.2` base image or newer. ### Update the GitHub Actions workflows If your project still has the GitHub Actions workflows that 3.4 created, replace them. The 3.4 workflows are `analyze.yml`, `format.yml`, and `tests.yml` in `.github/workflows/`. They set up an SDK older than the Dart 3.12.2 that 4.0 requires, so they fail after the upgrade. The `tests.yml` workflow also installs the 3.4 CLI. Copy the 4.0 versions from the [framework templates](https://github.com/serverpod/serverpod/tree/main/templates/serverpod_templates/github/workflows) and fill in their placeholders: - `projectname`: your project name. - `CLI_VERSION`: `4.0.0`. - `DB_TEST_PASSWORD` and `REDIS_TEST_PASSWORD`: the `test` passwords from `_server/config/passwords.yaml`. ## Switch to `serverpod start` Your project is now on 4.0, but you still run it the 3.4 way. The `serverpod start` command runs your server, your Flutter app, and your database in one terminal, and hot reloads your code when you save. It replaces running `docker compose up`, `dart bin/main.dart`, and `flutter run` separately. See [Running your server](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md) for the full workflow. Before you run `serverpod start`, choose how to handle the database. ### Choose your data store Both options work with `serverpod start`. For the fewest changes, keep your Docker Postgres. #### Keep your Docker Postgres (easiest upgrade) If you develop against a Docker Postgres on 3.4, you can keep it without changing your config. The `serverpod start` command runs `docker compose up -d` when all of these are true: - Your server package has a Docker Compose file. - Your config points at a Postgres on `localhost` with no `dataPath`. - The services aren't running. On exit, the command runs `docker compose stop` for the services it started. It leaves the other services running. New 4.0 projects use the `ghcr.io/serverpod/postgres:16` image, which bundles pgvector and PostGIS. Your existing `pgvector/pgvector:pg16` image keeps working. Switch to the new image when you need PostGIS. See [Upgrade to PostGIS](https://docs.serverpod.dev/upgrading/upgrade-to-postgis.md). #### Switch to the embedded Postgres (recommended for new development) The embedded Postgres runs as a child process of your server and uses the same Postgres dialect as production. It needs no Docker. It connects over a Unix domain socket, so it doesn't compete for TCP ports. You can reset it by deleting its data directory. See [Embedded PostgreSQL](https://docs.serverpod.dev/concepts/data-and-the-database/database/embedded-postgres.md). To switch, add `dataPath` to the database section of `_server/config/development.yaml` and `_server/config/test.yaml`: ```yaml database: host: localhost port: 8090 name: user: postgres dataPath: .serverpod/development/pgdata ``` For `test.yaml`, use a separate directory, as new projects do: `dataPath: .serverpod/test/pgdata`. The data directory holds a complete database, so keep it out of version control. Upgrading doesn't add the ignore rule that new projects get, so add this line to `_server/.gitignore`: ```text .serverpod/ ``` Once `dataPath` is set, `serverpod start` uses the embedded Postgres automatically. Don't add `dataPath` to `production.yaml` or `staging.yaml`. In production, use a managed Postgres, such as a [Serverpod Cloud](https://docs.serverpod.dev/cloud.md) database, Cloud SQL, or RDS. Pick an engine version of 16 or later, which Serverpod 4.0 requires. The server reaches the embedded Postgres over the socket, but database tools like `psql` connect over TCP. To connect a tool, stop the server and run `serverpod database start`. See [Connect a database tool](https://docs.serverpod.dev/concepts/data-and-the-database/database/embedded-postgres.md#connect-a-database-tool). :::note With `dataPath` set, the server connects to the embedded Postgres even if you pass `--docker`. The `--docker` flag only controls whether `serverpod start` brings up the Docker Compose stack. Your Docker volume stays intact, so remove `dataPath` to go back to the Docker Postgres. ::: ### Start the server From your project's root folder, run: ```bash $ serverpod start ``` :::note If you ran `serverpod start` before upgrading, delete the `_server/.dart_tool/serverpod` folder before you run this command. It holds the server that `serverpod start` compiled earlier, and an old copy can stop the upgraded server at startup. The next run rebuilds it. ::: On the first run, the command compiles the native build hooks, which can take about 30 seconds. It also applies the migration you created in [Create the 4.0 migration](#create-the-40-migration). Then the server starts and watches your project. When you save a file, the command hot reloads the code. The command also launches your `_flutter` app when that package exists. If the server's `pubspec.yaml` has a `serverpod: flutter_apps:` section, the command instead launches the apps in that section that set `auto_launch: true`. #### Debug in VS Code Your upgraded project keeps its 3.4 `launch.json`, which runs `bin/main.dart`. Projects created with 4.0 have a `launch.json` that attaches the debugger to the processes that `serverpod start` runs. Each configuration in that new file first runs the `serverpod_start` task from `tasks.json`. To copy both files into your project: 1. In an empty folder outside your project, create a throwaway 4.0 project with the same name as yours. Add `--template server` if your project has no Flutter app. ```bash $ serverpod create ``` 2. Copy `tasks.json` and `launch.json` from the throwaway project's `.vscode` folder into your project's `.vscode` folder, replacing the old files. 3. If `tasks.json` sets `SERVERPOD_PASSWORD_database`, replace its value with the `database` password under `development` in `_server/config/passwords.yaml`. The environment variable overrides `passwords.yaml`. If you leave the throwaway project's password in place, the server gets the wrong database password. ## Verify Your project runs on 4.0 when these three checks pass: - **The server starts.** With `serverpod start`, or with `dart run bin/main.dart --apply-migrations` if you run the server yourself. - **The migration is applied.** The log shows no `Database does not match target state` warning. If it does, see [Troubleshooting](#troubleshooting). - **Your tests pass.** Run them the way you did before the upgrade. ## Set up the agent workflow Version 4.0 ships AI agent skills and MCP servers for editors like Claude Code and Cursor. A new project gets them from `serverpod create`. An upgraded project does not, so this step installs the skills and registers the MCP servers by hand. Once set up, your agent can hot reload your server, create and apply migrations, launch your Flutter app, and read the logs. :::warning Don't use `serverpod create .` to add the agent setup to an existing project. The command writes its template files over yours without checking whether they exist. Your `server.dart` and config files lose their database, Redis, authentication, and web server setup. Your `passwords.yaml` gets new random passwords and secrets. Git can't restore the old file, because the default `.gitignore` excludes it. ::: Everything the command can overwrite In a folder that already has a Serverpod server, `serverpod create .` runs an upgrade. Depending on the options, it overwrites: - `_server/lib/server.dart`. - The `development`, `staging`, `production`, `test`, and `generator` YAML files in `_server/config/`. - `_server/config/passwords.yaml`, with new random passwords and secrets. - `_server/docker-compose.yaml` and the workflows in `.github/workflows/`. - The scaffolded authentication endpoints in `_server/lib/src/auth/`, and the scaffolded web server files in `_server/lib/src/web/` and `_server/web/`. - `lib/main.dart`, `lib/client.dart`, and `lib/screens/greetings_screen.dart` in your Flutter app. The command also adds dependencies to your `pubspec.yaml` files. In an interactive terminal on a project that already has migrations, it only asks which editors to set up. In that case, it turns every other option off, so the rewritten files lose your setup. ### Install the agent skills Install the skills tool: ```bash $ dart install skills ``` Then, from your project's root folder, pull the skills for your editor: ```bash $ skills get --ide ``` Replace `` with the editor you use: `antigravity`, `claude`, `cline`, `codex`, `copilot`, `cursor`, or `opencode`. For VS Code, or to install the skills in the `.agents` folder, use `generic`. ### Register the MCP servers Your agent uses two MCP servers: **Serverpod** and **Dart**. Register them in your editor's config file, at a path relative to your project's root folder. For Claude, create `.mcp.json`: ```json { "mcpServers": { "serverpod": { "command": "serverpod", "args": ["mcp-server", "--server-dir", "_server"] }, "dart": { "command": "dart", "args": ["mcp-server"] } } } ``` Replace `_server` with the path to your server package, relative to the project root. Each editor reads the servers from its own file: | Editor | Config file | Format | | ----------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Claude | `.mcp.json` | The sample above. | | Cursor | `.cursor/mcp.json` | The sample above. | | VS Code | `.vscode/mcp.json` | The sample above, with the entries under a `servers` key instead of `mcpServers`. | | Antigravity | `.agents/plugins/serverpod-local/mcp_config.json` | The sample above, with the Dart entry named `dart-mcp-server`. Antigravity also needs a `plugin.json` file in the same folder, with `"name"` set to `"serverpod-local"`. | | Codex | `.codex/config.toml` | Codex's TOML format. Pass `--force-roots-fallback` to `dart mcp-server`. | | OpenCode | `opencode.json` | OpenCode's `mcp` format. | In the Codex and OpenCode formats, register the same two commands as the sample: `serverpod mcp-server --server-dir _server` and `dart mcp-server`. If you are using Cursor, enable the **Serverpod** and **Dart** MCP servers: open **Customize** in the sidebar, click **MCPs**, and turn on both servers. ## What's new in 4.0 - **`serverpod start` terminal UI**: hot reload on save. Press **R** to hot restart, **M** to create and apply a migration, or **P** to create and apply a repair migration. - **Simplified server initialization**: the generated `Serverpod` class comes with `Protocol` and `Endpoints` already set up, so `server.dart` needs only `Serverpod(args)`. Projects that keep their existing imports can stay on `Serverpod(args, Protocol(), Endpoints())`. - **Flutter app launching** from `serverpod start`, so the Flutter app runs alongside the server in the same terminal UI. - **AI agent skills and MCP servers** set up during `serverpod create`. Existing projects can [add them manually](#set-up-the-agent-workflow). - **Embedded Postgres**: develop without Docker by setting `dataPath`. - **SQLite database support** as an alternative dialect to Postgres. - **Client-side database generation** for the Flutter app. - **`jsonb` column support** with GIN index operator classes, and **`dynamic` fields** on models and endpoints. - **`unique` keyword** for simpler unique indexes in model files. - **`upsert` and `upsertRow`** on the ORM, and **`asc()` / `desc()`** convenience methods on orderable columns. - **Recurring future calls** with `callRecurring`, on a fixed interval or a cron schedule. See [Recurring tasks](https://docs.serverpod.dev/concepts/scheduling/recurring-tasks.md). - **OAuth2 PKCE Flutter web redirect** for sign-in flows. - **Account merging** in the auth module, so a user can link a second sign-in method to an existing account. See [Merging accounts](https://docs.serverpod.dev/concepts/authentication/working-with-users.md#merging-accounts). - **httpOnly cookie authentication for the web**, which keeps browser sign-in tokens out of storage that JavaScript can read. See [web authentication](https://docs.serverpod.dev/concepts/authentication/web-authentication.md). - **Health endpoints** on the built-in web server. - **IDE and agent selection** in `serverpod create`. ## Troubleshooting ### Port conflicts on startup If you run more than one Serverpod server on the same machine, the servers can conflict on the default ports: 8080 for the main server and 8090 for the database. Stop the other server, or run on different ports. ### The server stops because the database doesn't match After the upgrade, `serverpod start` can report that the latest migration is applied and then stop the server with these warnings: ```text WARNING: The database does not match the target database: WARNING: Database does not match target state. Server stopped (exitCode: 1). ``` Stop `serverpod start`, delete the `_server/.dart_tool/serverpod` folder, and run `serverpod start` again. If the warnings remain, create and apply the migration from [Create the 4.0 migration](#create-the-40-migration). ### Agent skills or MCP servers aren't picked up after setup Run `skills get --ide ` again from the project's root folder. Then check that your editor's MCP config file is at the path listed in [Register the MCP servers](#register-the-mcp-servers). That section also covers the extra steps some editors need, such as enabling the servers in Cursor. ## Still stuck? If something here didn't go as expected, reach out on the [community page](https://docs.serverpod.dev/support.md). ## Related - [Breaking changes in 4.0](https://docs.serverpod.dev/upgrading/breaking-changes-in-four.md): every API and behavior change between 3.4 and 4.0, for looking up an error. - [Running your server](https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server.md): what the `serverpod start` command does, and the other ways to run your server. - [Embedded PostgreSQL](https://docs.serverpod.dev/concepts/data-and-the-database/database/embedded-postgres.md): how the embedded Postgres runs alongside your server, and how to reset it or connect a tool to it. - [Migrations](https://docs.serverpod.dev/concepts/data-and-the-database/database/migrations.md): how Serverpod's migration system works under the hood. - [Build your first app](https://docs.serverpod.dev/get-started/creating-endpoints.md): the hands-on tour of the 4.0 workflow, if you want to see `serverpod start` in a project built from scratch. --- # Breaking changes in 4.0 https://docs.serverpod.dev/upgrading/breaking-changes-in-four Serverpod 4.0 changes APIs and behavior that 3.4 code relies on. Find your error or symptom in the table, then read only that section. Other sign-in and token changes are under [Authentication changes](#authentication-changes). For the upgrade steps themselves, see [Upgrade to 4.0](https://docs.serverpod.dev/upgrading/upgrade-to-four.md). | Change | Affects you if | | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Model files use the `.spy.yaml` extension](#model-files-use-the-spyyaml-extension) | Your model files end in `.yaml` or `.yml` instead of `.spy.yaml`. | | [Client exceptions are a sealed hierarchy](#client-exceptions-are-a-sealed-hierarchy) | Your app catches `ServerpodClientException` or checks `statusCode == -1`. | | [Database exceptions are typed](#database-exceptions-are-typed) | Your server catches `DatabaseInsertRowException` or another row exception. | | [Future calls run at least once](#future-calls-run-at-least-once) | You use future calls. | | [Removed deprecated APIs](#removed-deprecated-apis) | You use the string-based future call methods, `orderDescending`, `ignoreEndpoint`, `SerializationManagerServer`, the old web widget classes, or `--mini`. | | [Message central delivers globally by default](#message-central-delivers-globally-by-default) | You call `session.messages.postMessage` with Redis enabled, or pass `global: true`. | | [File storage APIs are renamed and throw instead of returning `null`](#file-storage-apis-are-renamed-and-throw-instead-of-returning-null) | Your server uses `session.storage`, checks its results for `null`, subclasses `CloudStorage`, or serves public files from native Google Cloud Storage. | | [Google sign-in on the web uses the OAuth2 redirect flow](#google-sign-in-on-the-web-uses-the-oauth2-redirect-flow) | Your Flutter web app uses Google sign-in from `serverpod_auth_idp_flutter`. | | [Sign-in buttons share one set of style enums](#sign-in-buttons-share-one-set-of-style-enums) | You set style arguments on sign-in buttons from `serverpod_auth_idp_flutter`. | | [Legacy streaming endpoints are removed](#legacy-streaming-endpoints-are-removed) | Your endpoints use `StreamingSession`. | | [Insights database endpoints are disabled by default](#insights-database-endpoints-are-disabled-by-default) | Your own tooling calls the Insights database endpoints. | | [The `columnOverride` experimental feature is removed](#the-columnoverride-experimental-feature-is-removed) | You pass `--experimental-features columnOverride`. | | [`Session.close()` returns `Future`](#sessionclose-returns-futurevoid) | Your code uses the value that `session.close()` returns. | | [Unused email exceptions are removed](#unused-email-exceptions-are-removed) | Your app catches `EmailAccountRequestAlreadyExistsException` or `EmailPasswordResetAccountNotFoundException`. | | [If you use the legacy auth module](#if-you-use-the-legacy-auth-module) | Your project uses the `serverpod_auth` packages. | | [If you use the new auth module on Android](#if-you-use-the-new-auth-module-on-android) | Your Flutter app still has `flutter_secure_storage` 9.x. | ## Model files use the `.spy.yaml` extension In 4.0, model files must use the `.spy.yaml` extension. The `.spy.yml` and `.spy` extensions are also accepted. Serverpod ignores files in `lib/src/models` or `lib/src/protocol` that have a plain `.yaml` or `.yml` extension. The `serverpod generate` and `serverpod start` commands stop with an error that lists those files: ```text Model files must use the .spy.yaml extension. The following files are ignored: lib/src/models/company.yaml Rename the files to use the .spy.yaml extension and run the command again. ``` Rename the files and run `serverpod generate` again. The contents do not change. ## Client exceptions are a sealed hierarchy The `ServerpodClientException` class is sealed and carries only `message`. When the app can't reach the server, the call throws `ServerpodClientNetworkException`. When the server returns an error response, the call throws a subclass of the sealed `ServerpodClientHttpException`, which owns `statusCode`. Code that checks `statusCode == -1` for connection failures no longer compiles. See [Error handling and exceptions](https://docs.serverpod.dev/concepts/endpoints-and-apis/error-handling-and-exceptions.md#handle-errors-in-your-app). ## Database exceptions are typed The `DatabaseInsertRowException`, `DatabaseUpdateRowException`, `DatabaseDeleteRowException`, and `DatabaseUpsertRowException` classes are replaced by `DatabaseUnexpectedResultException`. Constraint failures throw `DatabaseUniqueViolationException` or `DatabaseForeignKeyViolationException`. SQLite lock failures throw `SqliteDatabaseLockedException`. All three are subclasses of `DatabaseQueryException`. See [Database exceptions](https://docs.serverpod.dev/concepts/data-and-the-database/database/exceptions.md). ## Future calls run at least once In 3.4, Serverpod removed a future call from the database before running it, so a crash mid-run lost the call. In 4.0, Serverpod removes the call only after it finishes. If a crash interrupts the call, the call runs again, possibly on another server instance. Make each future call safe to run more than once, for example by checking whether an email was already sent before sending it. See [Execution guarantees](https://docs.serverpod.dev/concepts/scheduling/overview.md#execution-guarantees). ## Removed deprecated APIs - The future call methods on `Serverpod` are removed: `registerFutureCall`, `futureCallWithDelay`, `futureCallAtTime`, and `cancelFutureCall`. The `invoke` method you overrode on `FutureCall` is removed too. Calls already scheduled under an old string name match no generated call, so Serverpod doesn't run them and reports them as [broken future calls](https://docs.serverpod.dev/concepts/scheduling/configuration.md#broken-future-calls). To port a string-registered call: - Replace the `invoke` override with a public `Future` method that takes a `Session` as its first parameter. See [Define a future call](https://docs.serverpod.dev/concepts/scheduling/future-calls.md#define-a-future-call). - Delete the `registerFutureCall` line from `server.dart`, and run `serverpod generate`. - Schedule with `pod.futureCalls.callWithDelay` or `pod.futureCalls.callAtTime`, and cancel with `pod.futureCalls.cancel`. See [Schedule a call](https://docs.serverpod.dev/concepts/scheduling/future-calls.md#schedule-a-call) and [Cancel scheduled calls](https://docs.serverpod.dev/concepts/scheduling/future-calls.md#cancel-scheduled-calls). - The `orderDescending` parameter on ORM methods is removed. You can no longer construct `Order` directly. Use `column.asc()` and `column.desc()`. See [Sorting](https://docs.serverpod.dev/concepts/data-and-the-database/database/sorting.md). - The `ignoreEndpoint` annotation is removed. Use `@doNotGenerate`. See [Exclude an endpoint from generation](https://docs.serverpod.dev/concepts/endpoints-and-apis.md#exclude-an-endpoint-from-generation). - The `SerializationManagerServer` class is removed. Use the generated `Protocol` class instead. It now extends `DatabaseSerializationManager` from `serverpod_database`. - The deprecated web-server widget aliases are removed. Rename them: - `AbstractWidget` to `WebWidget` - `Widget` to `TemplateWidget` - `WidgetList` to `ListWidget` - `WidgetJson` to `JsonWidget` - `WidgetRedirect` to `RedirectWidget` - If a widget extends `WebWidget` directly, implement the new abstract `String render({String? Function(String)? onMissingVariable})` method. The `WidgetRoute` class builds the response from `render` instead of `toString`. Widgets that extend `TemplateWidget`, `ListWidget`, `JsonWidget`, or `RedirectWidget` inherit `render`. The `WidgetRoute.build` method now returns `Future`. A `null` return yields a 404. See [Server-side HTML](https://docs.serverpod.dev/concepts/web-server/server-side-html.md#creating-a-widgetroute). - The `RouteStaticDirectory` and `PathCacheMaxAge` classes are removed. Serve directories with `StaticRoute.directory`, and set cache headers with its `cacheControlFactory` parameter. See [Static files](https://docs.serverpod.dev/concepts/web-server/static-files.md#cache-control). - The `--mini` flag on `serverpod create` is removed. To create a project without a database, deselect **Database (recommended)** on the setup screen, or pass `--no-interactive --no-database`. To create a project without a Flutter app, use `--template server`. ## Message central delivers globally by default Calls to `session.messages.postMessage` now default to `MessageScope.auto`. When Redis is enabled, the message goes through Redis to every server instance. Otherwise, the message stays local. If your code relied on local-only delivery, pass `scope: MessageScope.local`. The `global: true` argument is removed, so replace it with `scope: MessageScope.global`. See [Message scope](https://docs.serverpod.dev/concepts/endpoints-and-apis/server-events.md#message-scope). ## File storage APIs are renamed and throw instead of returning `null` Several `session.storage` methods are renamed in 4.0. The methods that return a single file or URL now throw instead of returning `null`. An existing `null` check still compiles. For example, `if (file == null)` after `retrieveFile` only triggers an analyzer warning that the check is unnecessary. The branch inside it never runs. Update your server code as follows: | 3.4 | 4.0 | | -------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `getPublicUrl` returns `Uri?` | `publicDownloadUrl` returns `Uri` | | `getPublicUrls` | `publicDownloadUrls` | | `retrieveFile` returns `ByteData?` | `retrieveFile` returns `ByteData` | | `storeFile` takes `expiration` and `preventOverwrite` | `storeFile` takes `options: StoreFileOptions(...)` | | `createDirectFileUploadDescription` returns `String?` and takes `expirationDuration`, `maxFileSize`, `contentLength`, and `preventOverwrite` | `createUploadDescription` returns `String` and takes `options: UploadOptions(...)` | | `verifyDirectFileUpload` | `verifyUpload` | Catch the new exceptions where your code checked for `null`: - **No file at the path:** the `retrieveFile` and `publicDownloadUrl` methods throw `CloudStorageFileNotFoundException`. The new `statFile` and `temporaryDownloadUrl` methods throw it too. - **No public URLs:** the `publicDownloadUrl` method throws `CloudStorageUnsupportedOperationException` when the storage doesn't serve public URLs, for example the default `private` storage. Both exceptions extend `CloudStorageException`. The `publicDownloadUrls` method still returns `null` for each path that fails. If you subclass `CloudStorage`, make these changes: - Override the renamed methods under their new names, and add `statFile` and `temporaryDownloadUrl`. - Throw `CloudStorageFileNotFoundException` from `retrieveFile`, `publicDownloadUrl`, and `temporaryDownloadUrl` when a file is missing. Throw it from `statFile` too, because the `CloudStorage` base class now implements `fileExists` by calling `statFile`. - Drop the `verified` argument from `storeFile`, and take a `StoreFileOptions` parameter in place of `expiration`. - Take an `UploadOptions` parameter in `createUploadDescription`, in place of `expirationDuration` and `maxFileSize`. Return a `BinaryUploadDescription` or a `MultipartUploadDescription` instead of a `String`. - Move the logic from `storeFileWithOptions` and `createDirectFileUploadDescriptionWithOptions` into `storeFile` and `createUploadDescription`, because the `CloudStorageWithOptions` mixin and the `CloudStorageOptions` class are removed. If the app uploads or downloads through the built-in `/serverpod_cloud_storage` URL, extend `DatabaseCloudStorage`. That URL rejects other storages. If the subclass keeps bytes outside the database, also override `storeUnverifiedFile`, `retrieveFileWithStat`, and `fileExists`. The URL calls the first two. The `DatabaseCloudStorage` version of `fileExists` checks the database instead of calling `statFile`. See [Store files on local disk](https://docs.serverpod.dev/concepts/endpoints-and-apis/custom-cloud-storage.md#store-files-on-local-disk). If you serve public files from `NativeGoogleCloudStorage`, make the bucket itself publicly readable, because Serverpod 4.0 no longer makes each uploaded file public. For example, turn on uniform bucket-level access and grant `allUsers` the Storage Object Viewer role. App code that uses `FileUploader` doesn't change. See [File uploads](https://docs.serverpod.dev/concepts/endpoints-and-apis/file-uploads.md) and [Custom cloud storage](https://docs.serverpod.dev/concepts/endpoints-and-apis/custom-cloud-storage.md#implement-the-cloudstorage-methods). ## Google sign-in on the web uses the OAuth2 redirect flow If your Flutter web app uses Google sign-in from `serverpod_auth_idp_flutter`, sign-in now redirects the browser to a callback page. The legacy `serverpod_auth` module is unaffected. When Serverpod serves your Flutter web app, update the setup: 1. Register a `FlutterWebAuth2CallbackRoute` on the web server. Its full URL is the callback URL, for example `http://localhost:8082/auth/callback`. 2. Pass the callback URL to `initializeGoogleSignIn` as `redirectUri`. Also pass the client ID as `clientId`, or set `GOOGLE_CLIENT_ID` with `--dart-define`. The web flow doesn't read the `google-signin-client_id` meta tag in `web/index.html`. 3. Add the full callback URL in two places: under **Authorized redirect URIs** on the server OAuth client, and in `redirect_uris` in the `googleClientSecret` entry of `passwords.yaml`. In 3.4, both places held the bare origin, for example `http://localhost:8082`. See [Google web setup](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#web). If the app runs on its own origin, for example with `flutter run -d chrome`, skip step 1, because the browser blocks the route's callback across origins. Place an `auth.html` file in the app's `web/` folder instead. In steps 2 and 3, use that file's URL, for example `http://localhost:49660/auth.html`. See [Separately-hosted Flutter web](https://docs.serverpod.dev/concepts/authentication/providers/google/customizations.md#separately-hosted-flutter-web). ## Sign-in buttons share one set of style enums If you set style arguments on a sign-in button from `serverpod_auth_idp_flutter` or `serverpod_auth_idp_flutter_facebook`, update them. The legacy `serverpod_auth` module is unaffected. - **All providers:** use the shared `SignInButtonSize`, `SignInButtonShape`, `SignInButtonLogoAlignment`, and `SignInButtonTextVariant` enums. The per-provider style types, such as `GSIButtonSize` and `AppleSignInStyle`, are removed. - **Apple and Facebook:** pass `text` instead of `type`. - **GitHub, Google, and Microsoft:** remove the `type` argument. - **Google:** pass a `GoogleButtonStyle` as `style` instead of `theme`, and remove `getButtonText`, `locale`, and `buttonWrapper`. To style every button inside `SignInWidget` at once, pass a `SignInButtonStyle` as `buttonStyle`. See [Styling the buttons](https://docs.serverpod.dev/concepts/authentication/ui-components.md#styling-the-buttons). Removed style types by provider - **Apple:** `AppleButtonSize`, `AppleButtonText`, `AppleButtonShape`, `AppleButtonLogoAlignment`, and `AppleSignInStyle`. - **Facebook:** `FacebookButtonSize`, `FacebookButtonText`, `FacebookButtonShape`, `FacebookButtonLogoAlignment`, and `FacebookSignInStyle`. - **GitHub:** `GitHubButtonType`, `GitHubButtonSize`, `GitHubButtonText`, `GitHubButtonShape`, `GitHubButtonLogoAlignment`, and `GitHubSignInStyle`. - **Google:** `GSIButtonType`, `GSIButtonTheme`, `GSIButtonSize`, `GSIButtonText`, `GSIButtonShape`, `GSIButtonLogoAlignment`, and `GoogleSignInStyle`. - **Microsoft:** `MicrosoftButtonType`, `MicrosoftButtonSize`, `MicrosoftButtonText`, `MicrosoftButtonShape`, `MicrosoftButtonLogoAlignment`, and `MicrosoftSignInStyle`. ## Legacy streaming endpoints are removed Serverpod's legacy streaming endpoints API was deprecated in 3.0 and is removed in 4.0. Endpoints that use the `StreamingSession` type no longer compile. The related server and client methods are gone too, for example `streamOpened`, `streamClosed`, `handleStreamMessage`, `sendStreamMessage`, `getUserObject`, `setUserObject`, and `openStreamingConnection`. Port code that uses the legacy API to [streaming methods](https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming.md). With streaming methods, the endpoint declares `Stream` parameters and return types, and Serverpod manages the connection. State that used to live in a user object becomes a local variable in the streaming method. The method stays alive as long as the stream is open. The old API stays documented in [Streaming endpoints](https://docs.serverpod.dev/upgrading/archive/streaming-endpoints.md) while you port. ## Insights database endpoints are disabled by default In 4.0, the Insights server endpoints that give direct database access are disabled by default: `fetchDatabaseBulkData`, `runQueries`, `getDatabaseRowCount`, and `executeSql`. They throw an `AccessDeniedException` until you enable them. The Insights app doesn't use these endpoints, so most projects need no change. If you have custom tooling that calls them through the `serverpod_service_client` package, opt in per environment. Set `enableDatabaseAccess` in the `insightsServer` block of the config file, or set the `SERVERPOD_INSIGHTS_SERVER_ENABLE_DATABASE_ACCESS` environment variable: ```yaml insightsServer: port: 8081 publicHost: localhost publicPort: 8081 publicScheme: http enableDatabaseAccess: true ``` The `hotReload`, `getOpenSessionLog`, and `shutdown` Insights methods are removed. See [Insights](https://docs.serverpod.dev/tools/insights.md#database-access) for details. ## The `columnOverride` experimental feature is removed The `column` keyword no longer needs an experimental feature, so `columnOverride` is removed. Remove `--experimental-features columnOverride` from your commands and scripts, because the flag stops the CLI with a usage error that reports `"columnOverride" is not in all|databaseSync`. Serverpod ignores a `columnOverride` entry under `experimental_features` in `_server/config/generator.yaml`, so you can delete the entry. See [Column name override](https://docs.serverpod.dev/concepts/data-and-the-database/database/tables.md#column-name-override). ## `Session.close()` returns `Future` The `Session.close()` method returns `Future` instead of `Future`. It no longer returns an ID when Serverpod logs the session to the database. Code that uses the returned value no longer compiles. A plain `await session.close();` still works. ## Unused email exceptions are removed The email identity provider's `EmailAccountRequestAlreadyExistsException` and `EmailPasswordResetAccountNotFoundException` classes are removed. Nothing threw them in 3.4, so the app sees the same responses as before. Remove any `catch` clause or `case` that names them. ## Authentication changes Version 4.0 changes a few authentication behaviors that can affect existing apps: - **The `?auth=` query parameter is no longer accepted.** HTTP calls authenticate through the `Authorization` header, or an auth cookie on the web. Streaming connections authenticate when the stream opens. Upgrade any client from before 4.0 that relied on the query parameter. - **Signing in on top of another account is rejected.** When a token is issued for a different user from an already-authenticated session, Serverpod throws a `SignInWhileAuthenticatedException` on every platform. Users must sign out before switching accounts. Server code that creates tokens on behalf of another user calls the token manager's `createToken` instead, for example in an admin flow. The `createToken` method skips the sign-in policy and returns the secrets in the response body. - **Method streams close when the signed-in user changes.** On sign-in and sign-out, open method streams close gracefully on all platforms. Their subscriptions receive `onDone` without an error. New streams connect with the current identity. A token refresh for the same identity keeps streams running. - **If you use Firebase sign-in, unverified emails are accepted by default.** The default `firebaseAccountDetailsValidation` no longer rejects accounts whose email is not verified. To keep the 3.4 behavior, pass `firebaseAccountDetailsValidation: FirebaseIdpConfig.requireVerifiedEmail`. The app then receives a `FirebaseEmailNotVerifiedException` for those accounts. In 3.4, the same case surfaced as a `FirebaseIdTokenVerificationException`, so update error handling that checks for the old exception. See [Custom account validation](https://docs.serverpod.dev/concepts/authentication/providers/firebase/configuration.md#custom-account-validation). - **If you enable cookie auth for the web, list every browser origin.** The new `authCookie` configuration requires you to list every browser origin in `allowedOrigins`. A browser on an origin that isn't listed loses cross-origin access, including to public endpoints. See [web authentication](https://docs.serverpod.dev/concepts/authentication/web-authentication.md). - **If you wrote a custom token manager, extend the base class.** The `TokenIssuer` and `TokenManager` classes are now base classes. Implement `createToken` to create the token itself, and leave `issueToken` alone. The `issueToken` method is non-virtual. It applies the sign-in policy and cookie delivery for every token type. - **If you wrote a custom identity provider, implement `IdentityProvider`.** The `IdentityProviderBuilder` class now requires a provider that implements `IdentityProvider`. A provider class written for 3.4 must add a `method` getter and a `mergeAuthUsers` method. Replace any `static const String method` with the getter, because a class can't declare a static and an instance member with the same name. Give each provider a unique, non-empty `method`, or Serverpod throws a `StateError` when you register the provider. See [the provider class](https://docs.serverpod.dev/concepts/authentication/providers/custom-providers/oauth2-utility/creating-an-oauth2-based-identity-provider.md#3-provider-class). Two more changes depend on which auth module your project uses. ## If you use the legacy auth module The legacy `serverpod_auth` packages ship 4.0 releases. Bump every `serverpod_auth` package your project uses to the same version as Serverpod itself, in the same `pubspec.yaml` files: ```yaml dependencies: serverpod_auth_server: 4.0.0 # in the server package serverpod_auth_client: 4.0.0 # in the client package serverpod_auth_shared_flutter: 4.0.0 # in the Flutter package ``` The `authenticationKeyManager` parameter on the generated `Client` was removed in 4.0. Assign the key manager to the `authKeyProvider` field instead: ```dart client = Client('http://$ipAddress:8080/') ..authKeyProvider = FlutterAuthenticationKeyManager() ..connectivityMonitor = FlutterConnectivityMonitor(); ``` If you use legacy Sign in with Apple, set `appleClientIds` on `AuthConfig`, because Sign in with Apple is disabled until it's set. See [Apple sign-in](https://docs.serverpod.dev/concepts/authentication/legacy/providers/apple.md#server-side-configuration). The legacy module keeps working on 4.0, so you can upgrade without moving to the new authentication framework. To make that move, finish this upgrade first, then see [Migrate from legacy auth](https://docs.serverpod.dev/upgrading/migrate-from-legacy-auth.md). ## If you use the new auth module on Android The `serverpod_auth_core_flutter` package now requires `flutter_secure_storage` 10.0.0 or newer and allows 11.x. Most projects already resolve 10.x and are not affected. If your Flutter app is still on 9.x, you have two options: - **Upgrade directly to 11.x.** Version 11 dropped the code that migrates data written by 9.x, so users on Android are signed out and have to sign in again. - **Upgrade to 10.x first.** Users stay signed in when you upgrade from 9.x to 10.x, and again from 10.x to 11.x, because each step keeps the session. Release a 10.x build, let it reach your users, then move to 11.x. Projects created with `serverpod create` pin 10.x with a dependency override. To do the same, add this to the Flutter app's `pubspec.yaml`: ```yaml dependency_overrides: flutter_secure_storage: ^10.0.0 ``` Version 11 also requires `compileSdk = 37` in `android/app/build.gradle.kts`. That value is higher than the current Flutter default. ## Related - [Upgrade to 4.0](https://docs.serverpod.dev/upgrading/upgrade-to-four.md): the step-by-step guide, from the CLI update to `serverpod start`. - [Migrate from legacy auth](https://docs.serverpod.dev/upgrading/migrate-from-legacy-auth.md): how to move off `serverpod_auth` to the new authentication framework, once the upgrade is done. --- # Migrate from legacy serverpod_auth https://docs.serverpod.dev/upgrading/migrate-from-legacy-auth This guide is for apps still running `serverpod_auth_server` on Serverpod 3.4 or later. At the end, your server runs the new modular auth stack. Email users sign in with their old passwords, and existing sessions keep working. Plan for about an hour, plus migration runtime. Two limits apply: - **Google, Apple, and Firebase accounts** aren't linked automatically. Read the warning in [Wire up sign-in for migrated users](#wire-up-sign-in-for-migrated-users) before you begin. - **Old client builds** can still sign in by email. The end of [Configure the server](#configure-the-server) lists the limits on their other auth calls. :::warning The `serverpod_auth_bridge` and `serverpod_auth_migration` packages are experimental. They may receive breaking changes and are not yet production-ready. ::: ## Before you start - A Serverpod 4.0.x project. If you are on an earlier version, follow [Upgrade to 4.0](https://docs.serverpod.dev/upgrading/upgrade-to-four.md) first. - Dart SDK 3.12.2 or later. - Flutter SDK 3.44.4 or later (only if you are migrating the Flutter app). - Postgres 16 or later, or SQLite3. - The new auth packages at `4.0.0`: the server, client, and Flutter variants of `serverpod_auth_core`, `serverpod_auth_idp`, and `serverpod_auth_bridge`, plus the server and client variants of `serverpod_auth_migration`. [Add the new auth packages](#add-the-new-auth-packages) shows which ones each `pubspec.yaml` needs. - Back up your production database. - Commit your current state on a clean branch. - Restore a copy of production data into a staging environment and rehearse this guide against it before running it for real. ## Add the new auth packages Add the new packages to each of your server, client, and Flutter `pubspec.yaml` files. Keep the legacy `serverpod_auth_*` packages installed; you will remove them after the migration completes. In `_server/pubspec.yaml`: ```yaml dependencies: serverpod: 4.0.0 serverpod_auth_server: 4.0.0 # legacy, keep during migration serverpod_auth_core_server: 4.0.0 serverpod_auth_idp_server: 4.0.0 serverpod_auth_bridge_server: 4.0.0 serverpod_auth_migration_server: 4.0.0 ``` In `_client/pubspec.yaml`: ```yaml dependencies: serverpod_client: 4.0.0 serverpod_auth_core_client: 4.0.0 serverpod_auth_idp_client: 4.0.0 serverpod_auth_bridge_client: 4.0.0 serverpod_auth_migration_client: 4.0.0 ``` In `_flutter/pubspec.yaml`: ```yaml dependencies: serverpod_flutter: 4.0.0 serverpod_auth_core_flutter: 4.0.0 serverpod_auth_idp_flutter: 4.0.0 serverpod_auth_bridge_flutter: 4.0.0 ``` `serverpod_auth_bridge_client` and `serverpod_auth_bridge_flutter` are required for the session import covered later under [Update the Flutter app](#update-the-flutter-app). The client also needs `serverpod_auth_migration_client`, because `serverpod generate` imports the client of every module the server depends on. From each package directory, run: ```bash dart pub upgrade serverpod generate ``` The project now builds with the new packages installed alongside the legacy ones. ## Configure the server In `_server/lib/server.dart`, call `pod.initializeAuthServices` before `pod.start` with the modular identity providers, register `LegacySessionTokenManager` so existing tokens keep validating, and enable legacy client forwarding so old client builds keep working. `serverpod_auth_bridge_server` exports its own `Endpoints` and `Protocol` classes from its generated code, which clash with your project's. Use a `show` clause to bring only the symbols you need into scope: ```dart import 'package:serverpod_auth_bridge_server/serverpod_auth_bridge_server.dart' show LegacySessionTokenManager, LegacyClientSupport; import 'package:serverpod_auth_idp_server/core.dart'; import 'package:serverpod_auth_idp_server/providers/email.dart'; import 'package:serverpod_auth_idp_server/providers/google.dart'; import 'src/generated/serverpod.dart'; void run(List args) async { final pod = Serverpod(args); pod.initializeAuthServices( tokenManagerBuilders: [ JwtConfigFromPasswords(), ServerSideSessionsConfigFromPasswords(), const LegacySessionTokenManager(), ], identityProviderBuilders: [ EmailIdpConfigFromPasswords(), GoogleIdpConfigFromPasswords(), ], ); pod.enableLegacyClientSupport(); await pod.start(); } ``` Put the password-importing email endpoint in its own file under `_server/lib/src/endpoints/`. Importing the full bridge package alongside the project's `Endpoints` and `Protocol` from the same file confuses the `serverpod generate` endpoint scanner, so keep it isolated: ```dart // lib/src/endpoints/password_importing_email_idp_endpoint.dart import 'package:serverpod/serverpod.dart'; import 'package:serverpod_auth_bridge_server/serverpod_auth_bridge_server.dart'; import 'package:serverpod_auth_idp_server/providers/email.dart'; class PasswordImportingEmailIdpEndpoint extends EmailIdpBaseEndpoint { @override Future login( Session session, { required String email, required String password, }) async { try { return await super.login(session, email: email, password: password); } on EmailAccountLoginException catch (e) { if (e.reason != EmailAccountLoginExceptionReason.invalidCredentials) { rethrow; } final account = await emailIdp.admin.findAccount(session, email: email); if (account == null || account.hasPassword) { rethrow; } } await AuthBackwardsCompatibility.importLegacyPasswordIfNeeded( session, email: email, password: password, ); return super.login(session, email: email, password: password); } } ``` If your project already has an endpoint that extends `EmailIdpBaseEndpoint`, such as `lib/src/auth/email_idp_endpoint.dart` from the project template, delete it. With two such endpoints, the Flutter email sign-in UI throws `ServerpodClientMultipleEndpointsFound`. Add the required entries to `_server/config/passwords.yaml` under the `development:` section (use distinct values per environment): ```yaml development: # ... existing keys (database, redis, serviceSecret) ... jwtRefreshTokenHashPepper: 'your-jwt-refresh-pepper' jwtHmacSha512PrivateKey: 'your-hmac-private-key-at-least-64-bytes' serverSideSessionKeyHashPepper: 'your-session-pepper' emailSecretHashPepper: 'your-email-pepper' googleClientSecret: | {"web": {"client_id": "your-client-id", "client_secret": "your-client-secret", "redirect_uris": []}} ``` If your legacy project set `email_password_salt` or `emailPasswordPepper`, keep those keys, because the password import reads them to check legacy passwords. See [Storing secrets](https://docs.serverpod.dev/concepts/authentication/setup.md#storing-secrets) for production handling and additional provider-specific secrets. For the `googleClientSecret` format, see [Store your credentials](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#store-your-credentials). The server now starts with both legacy and modular endpoints mounted. `enableLegacyClientSupport` forwards the legacy `email`, `status`, and `user` endpoints to the bridge. For old clients, this means: - **Email sign-in** works through the bridge. - **Account creation, password changes, and password resets** are stubs that return `false` or `null`. - **Apple, Firebase, and Google sign-in** still reach the legacy module, like every other legacy endpoint that isn't forwarded. ## Run the migration Create and apply the schema migrations for the new modular tables: ```bash serverpod create-migration --tag modular-auth dart run bin/main.dart --role maintenance --apply-migrations ``` The `--role maintenance` flag makes the server exit once the migrations are applied. Then run the user migration once. The example below migrates every legacy user, but you can pass `maxUsers` to process in batches if your dataset is large. The `userMigration` callback fires once per migrated user so you can remap your own foreign keys from the legacy `int` user ID to the new `UuidValue` auth user ID inside the same transaction. Put this helper in its own file so its imports do not collide with `server.dart`. `serverpod_auth_migration_server` exports its own `Endpoints` and `Protocol` classes, so use a `hide` clause when adding it elsewhere: ```dart // lib/src/auth_migration.dart import 'package:serverpod/serverpod.dart'; import 'package:serverpod_auth_idp_server/core.dart'; import 'package:serverpod_auth_migration_server/serverpod_auth_migration_server.dart' hide Endpoints, Protocol; Future runMigration(Serverpod pod) async { AuthMigrations.config = AuthMigrationConfig( emailIdp: AuthServices.instance.emailIdp, ); final session = await pod.createSession(); try { final migrated = await AuthMigrations.migrateUsers( session, userMigration: ( session, { required int oldUserId, required UuidValue newAuthUserId, Transaction? transaction, }) async { // Remap your own tables from oldUserId to newAuthUserId using transaction. }, ); session.log('Migrated $migrated users.', level: LogLevel.info); } finally { await session.close(); } } ``` Run this as a one-off from a dedicated entry point, not on a request path. The simplest pattern is a script under `_server/bin/migrate.dart` that imports the helper above and invokes it once on a started `Serverpod` instance. Don't start that instance with `--role maintenance`, because a maintenance process exits on its own and can cut the migration short. Do not call `runMigration` from inside an endpoint or future call. The migration is idempotent: re-running `migrateUsers` skips users that already have a row in `serverpod_auth_migration_migrated_user`. The returned count is "users selected this run," not "new users created." ## Wire up sign-in for migrated users For email accounts, the `PasswordImportingEmailIdpEndpoint` from the server step runs the base login first. If the login fails with `invalidCredentials` and the account has no password yet, the endpoint imports the legacy password and retries. The bridge upgrades the password hash and removes the `LegacyEmailPassword` row. A rate-limited login fails with `tooManyAttempts` and never reaches the import. Password import only works with the default legacy hashing. If your legacy `AuthConfig` set `extraSaltyHash: false`, a custom `passwordHashGenerator`, or a custom `passwordHashValidator`, the bridge can't import those passwords, and those users must reset their password. :::warning Google, Apple, and Firebase accounts aren't linked automatically: - **Google:** the legacy Google sign-in stored the user's email as the identifier, and `migrateUsers` copies it unchanged. `AuthBackwardsCompatibility.importGoogleAccount` matches only the Google user ID, so it never finds those rows. On first sign-in, a legacy Google user gets a new, empty account. To keep their data, link the account by email in a [Google endpoint](https://docs.serverpod.dev/concepts/authentication/providers/google/setup.md#create-the-endpoint) that extends `GoogleIdpBaseEndpoint`. - **Apple and Firebase:** the bridge has no import helper for these users, so they get no automatic linking. ::: A migrated email user can now sign in with their old password. ## Update the Flutter app Where your app creates the `Client` (`lib/client.dart` in projects created with 4.0, otherwise `lib/main.dart`), swap the auth setup to use `FlutterAuthSessionManager` and call `initAndImportLegacySessionIfNeeded` before any sign-in UI renders. This exchanges any old auth key stored on the device for a new modular session so existing installs do not have to sign in again. ```dart import 'package:serverpod_auth_bridge_flutter/serverpod_auth_bridge_flutter.dart'; import 'package:serverpod_auth_core_flutter/serverpod_auth_core_flutter.dart'; import 'package:your_client/your_client.dart'; late Client client; Future main() async { WidgetsFlutterBinding.ensureInitialized(); client = Client('https://api.example.com/') ..authSessionManager = FlutterAuthSessionManager(); await client.authSessionManager.initAndImportLegacySessionIfNeeded( client.modules.serverpod_auth_bridge, ); runApp(const MyApp()); } ``` If your app creates the client in `lib/client.dart`, add the `serverpod_auth_bridge_flutter` import there. Inside `initializeClient()`, replace `unawaited(client.auth.initialize());` with this call, which runs `initialize()` itself: ```dart await client.authSessionManager.initAndImportLegacySessionIfNeeded( client.modules.serverpod_auth_bridge, ); ``` This requires `serverpod_auth_bridge_client` and `serverpod_auth_bridge_flutter` in `_flutter/pubspec.yaml` from [Add the new auth packages](#add-the-new-auth-packages). Flutter clients that used the default legacy session storage carry the legacy session forward on first launch after the upgrade. If your project customized session storage, pass a `legacyStringGetter` to `initAndImportLegacySessionIfNeeded` that reads from your custom location. ## Verify and clean up ### Verify - Query `SELECT count(*) FROM serverpod_auth_migration_migrated_user` and confirm it matches your legacy user count. - Sign in with a known migrated account through the new IdP and confirm the `LegacyEmailPassword` row for that user is gone afterwards. - Over the following days, watch the `serverpod_auth_bridge_email_password` row count drop as email users sign in, and check whether requests still reach `serverpod_auth.*` endpoints. Don't track the other bridge tables' row counts, because they don't reliably shrink as users move. ### While clients catch up `LegacySessionTokenManager` validates legacy session tokens against the bridge's stored sessions. The legacy endpoints that `enableLegacyClientSupport` doesn't forward still answer only because the legacy module is still installed. Once no client signs in through the legacy Apple, Firebase, or Google endpoints, refuse the unforwarded endpoints, because those sign-ins issue legacy session keys the new stack doesn't know: ```dart pod.enableLegacyClientSupport(blockUnbridgedAuthEndpoints: true); ``` The legacy database tables stay untouched, so you can still roll back to the legacy stack. A rollback needs your own tables to resolve legacy `int` user IDs again. Either keep the legacy ID next to the new one in `userMigration`, or reverse the remap with the `serverpod_auth_migration_migrated_user` table. A rollback also loses everything created only in the new stack, such as new accounts, password changes, and linked Google accounts. ### When you are ready to remove legacy Remove the legacy packages once your clients have moved to the new stack. Work through these steps in order: 1. Remap your own fields and relations off the legacy integer user IDs, and drop the columns that still reference `userInfoId`. 2. Remove the code that uses bridge and migration symbols: `LegacySessionTokenManager`, `enableLegacyClientSupport`, the `runMigration` helper, `bin/migrate.dart`, and `initAndImportLegacySessionIfNeeded`. Replace the password-importing endpoint with a plain endpoint that extends `EmailIdpBaseEndpoint`. 3. Remove the legacy, bridge, and migration packages from the server `pubspec.yaml`, then from the client, then from the Flutter app. 4. Create a migration. It drops the legacy, bridge, and migration tables, so `serverpod create-migration` aborts with a warning until you pass `--force`. See [Force create migration](https://docs.serverpod.dev/concepts/data-and-the-database/database/migrations.md#force-create-migration). Once that migration is applied, users still on a legacy session must sign in again. Email users who never signed in after the migration have no password, so they must reset it. ## Troubleshooting | Symptom | Likely cause | What to do | | --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `migrateUsers` throws or rolls back | Migrations not applied, or the wrong `emailIdp` instance on `AuthMigrationConfig` | Apply all module migrations; set `AuthMigrations.config = AuthMigrationConfig(emailIdp: AuthServices.instance.emailIdp)` after `pod.initializeAuthServices`. | | Migrated email user cannot log in with their old password | `importLegacyPasswordIfNeeded` is not on the login path | Confirm your email endpoint subclasses `EmailIdpBaseEndpoint` and catches `EmailAccountLoginException` with reason `invalidCredentials`. For an account without a password, it must call `AuthBackwardsCompatibility.importLegacyPasswordIfNeeded` and then `super.login` again. | | Flutter app prompts the user to sign in again after upgrade | `initAndImportLegacySessionIfNeeded` didn't run before the sign-in UI, `migrateUsers` hasn't run, or the app stores its session in a custom location | Call `initAndImportLegacySessionIfNeeded` before any sign-in UI renders. Run `migrateUsers`, so the bridge holds the old sessions. For custom session storage, pass a `legacyStringGetter` that reads from it. | | Duplicate Google `AuthUser` created on first sign-in | Legacy Google rows store the email, and `importGoogleAccount` matches only the Google user ID | Link legacy Google accounts by email before the base login. Re-running `migrateUsers` does not help, because it skips users who are already migrated. | | Authentication handler rejects modular tokens | `pod.initializeAuthServices` was not called, or token managers list is missing | Call `pod.initializeAuthServices(...)` before `pod.start()`; include `JwtConfigFromPasswords` (or `ServerSideSessionsConfigFromPasswords`) plus `LegacySessionTokenManager` in `tokenManagerBuilders`. | | `serverpod generate` fails with "Endpoint analysis skipped due to invalid Dart syntax" or "The function 'Protocol' isn't defined" | The bridge or migration package exports its own `Endpoints` and `Protocol` classes that clash with your project's | Import the bridge with a `show` clause (`show LegacySessionTokenManager, LegacyClientSupport`) in `server.dart`, move the email endpoint subclass into its own file under `lib/src/endpoints/`, and use `hide Endpoints, Protocol` when importing the migration package in a helper file. | | `PasswordNotFoundException: jwtRefreshTokenHashPepper was not found` on startup | `JwtConfigFromPasswords` requires `jwtRefreshTokenHashPepper` and `jwtHmacSha512PrivateKey` in `passwords.yaml` | Add both keys to each environment section. The same error for another key means another builder is missing its key, for example `googleClientSecret` for `GoogleIdpConfigFromPasswords`. | ## Still stuck? Reach out on the [community page](https://docs.serverpod.dev/support.md). ## Related - [Upgrade to 4.0](https://docs.serverpod.dev/upgrading/upgrade-to-four.md): do this first. - [Authentication setup](https://docs.serverpod.dev/concepts/authentication/setup.md): modular configuration reference. - [Database migrations](https://docs.serverpod.dev/concepts/data-and-the-database/database/migrations.md): creating and applying schema changes safely. --- # Upgrading to PostGIS support https://docs.serverpod.dev/upgrading/upgrade-to-postgis New Serverpod projects already include PostGIS, both in the embedded PostgreSQL and in the Docker image. This guide is for older Docker setups and external PostgreSQL servers. To use geography fields in your models there, you need a PostgreSQL instance with the PostGIS extension installed. :::info This upgrade is only necessary if you want to use geography fields in your models. If you do not plan to use geography fields, you can skip this upgrade. ::: :::warning If trying to use geography fields without upgrading, you will encounter an error when applying migrations. ::: ## For Docker-based environments 1. Update your `docker-compose.yml` to use `ghcr.io/serverpod/postgres:16`, the Serverpod PostgreSQL image. It ships with [PostGIS and pgvector](https://docs.serverpod.dev/concepts/data-and-the-database/database/vector-and-geography-fields.md) already installed: ```yaml services: postgres: image: ghcr.io/serverpod/postgres:16 # <-- Change from postgres image here ports: - '8090:5432' environment: POSTGRES_USER: postgres POSTGRES_DB: POSTGRES_PASSWORD: volumes: - _data:/var/lib/postgresql/data # Other services... postgres_test: image: ghcr.io/serverpod/postgres:16 # <-- Change from postgres image here ports: - '9090:5432' environment: POSTGRES_USER: postgres POSTGRES_DB: _test POSTGRES_PASSWORD: volumes: - _test_data:/var/lib/postgresql/data ``` If your `docker-compose.yaml` came from Serverpod 4.0.0-beta.2 or later, it already references this image and needs no change. 2. Recreate your containers to use the new image: ```bash docker compose down docker compose up -d ``` 3. Create your first geography field in a model: ```yaml class: Store table: store fields: name: String location: GeographyPoint ``` 4. Generate and apply a migration: ```bash $ serverpod create-migration $ dart run bin/main.dart --apply-migrations ``` For more details on creating and applying migrations, see the [Migrations](https://docs.serverpod.dev/concepts/data-and-the-database/database/migrations.md) section. The PostGIS extension will be automatically enabled during the first migration that includes a geography column. ## For managed PostgreSQL services For cloud providers (AWS RDS, Google Cloud SQL, Azure Database, etc.), ensure that the PostGIS extension is available on your PostgreSQL instance. Most major managed services support PostGIS with no additional setup required. If available, the extension will be enabled automatically when applying the migration. If the cloud provider instructs you to run a `CREATE EXTENSION postgis;` command, you can skip that step. Serverpod handles it automatically during migration. ## Troubleshooting If you encounter issues with PostGIS: - Verify that your PostgreSQL version is 12 or later. - Check that the PostGIS extension is properly installed on the instance. - Ensure your database user has the necessary permissions to create extensions. --- # Upgrade to 1.2 https://docs.serverpod.dev/upgrading/archive/upgrade-to-one-point-two Serverpod 1.2. is backward compatible with Serverpod 1.0 and Serverpod 1.1. There are a few changes to the database layer, meaning you probably want to use the new methods. The old methods still works, but have been deprecated and will be permanently removed with the upcoming version 2. Database migrations are new in Serverpod 1.2. You can still opt to manage your database manually, but it is recommended that you move to the new migration system. Using the new migration will make keeping your database up-to-date much easier. ## Updating your CLI To update you Serverpod command line interface to the latest version, run: ```bash dart pub global activate serverpod_cli ``` You can verify that you have the latest version installed by running: ```bash serverpod version ``` ## Updating your pubspecs To move to Serverpod 1.2, you will need to update the `pubspec.yaml` files of your `server`, `client`, and `flutter` directories. Anywhere `serverpod` is mentioned, change the version to `1.2.0` (or any later version of Serverpod 1.2). It is recommended to use explicit versions of the Serverpod packages, to make sure that they are all compatible. ### Update to Dart 3 This update has bumped the minimum required dart version to `3.0.0`. You will have to change the Dart SDK version in all your `pubspec.yaml` files. Old pubspec.yaml configuration: ```yaml ... environment: sdk: '>=2.19.0 <4.0.0' ``` Updated pubspec.yaml configuration: ```yaml ... environment: sdk: '>=3.0.0 <4.0.0' ``` The `Dockerfile` in your project should be updated with the new Dart version: ```docker FROM dart:3.0 AS build ... ``` After updating your `pubspec.yaml` files, make sure to run `dart pub update` on all your packages. You must also run `serverpod generate` in your `server` directory. ## Deprecated methods In this version, we have completely reworked the database layer of Serverpod. The new methods have been placed under a static `db` field on the generated models. The old methods are still available, but the deprecation warnings will guide you toward moving to the updated API. :::important A few of the methods work slightly differently in their new versions. Most notably, the `insertRow` method will not modify the model you pass to it. Instead, it will return a modified copy with the inserted row `id`. ::: ```dart // The new find method is a drop-in replacement. Example.find(...); // old Example.db.find(...); // new // The old findSingleRow method has changed name to findFirstRow but is otherwise a drop-in replacement. Example.findSingleRow(...); Example.db.findFirstRow(...); // The new findById method is a drop-in replacement. Example.findById(...); // old Example.db.findById(...); // new // The old delete method has been renamed to deleteWhere and now returns a list of ids of rows that was deleted. Example.delete(...); Example.db.deleteWhere(...); // The new findById method is a drop-in replacement but returns the id of the row deleted. Example.deleteRow(...); // old Example.db.deleteRow(...); // new // The old update method has been renamed too updateRow and now returns the entire updated object as a new copy. Example.update(...); Example.db.updateRow(...); // The old insert method has been renamed too insertRow. The object you pass in is no longer modified, instead a new copy with the added row is returned which contains the inserted id. This means no mutations of the input object. Example.insert(...); Example.db.insertRow(...); // The new count method is a drop-in replacement. Example.count(...); Example.db.count(...); ``` ## Model changes We have made some improvements to the Serverpod model files (previously referred to as protocol files or serializable entities). By default, the model files are now located in the `lib/src/models/` directory, although using `lib/src/protocol` still works. When making the improvements to the model files, we made additions and changes to the syntax. All old keywords still work, but `serverpod generate` will give deprecation warnings, guiding you toward updating your models. The changes are listed below. The keyword `api` has been deprecated and replaced with the new keyword `!persist` as a drop-in replacement. Old syntax: ```yaml class: Example table: example fields: name: String apiField: String, api ``` New syntax: ```yaml class: Example table: example fields: name: String apiField: String, !persist ``` The keyword `database` has been deprecated and replaced with the new keyword `scope` with the value `serverOnly` as a drop-in replacement. Old syntax: ```yaml class: Example table: example fields: name: String serverField: String, database ``` New syntax: ```yaml class: Example table: example fields: name: String serverField: String, scope=serverOnly ``` The keyword `parent` has been moved and should be placed inside the new `relation` keyword, see the section on [relations](https://docs.serverpod.dev/concepts/data-and-the-database/database/relations.md) for the full new feature set. Old syntax: ```yaml class: Example table: example fields: name: String parentId: int, parent=example ``` New Syntax: ```yaml class: Example table: example fields: name: String parentId: int, relation(parent=example) ``` ## Moved and renamed SQL file Serverpod has moved and renamed the generated SQL file for the complete database schema. Instead of the file `generated/tables.pgsql`, Serverpod now includes it as a part of each migration located in the `migrations` directory, under the name `definition.sql`. ## Initialize the migration system If your project was created before migrations were introduced in Serverpod, you need to run a one-time setup to make it compatible with the new migration system. There are two guides to help you upgrade: one for projects that don't need to preserve any data and another for those that do. The guides assume that you have already installed Serverpod 1.2 and that you have a project created with an earlier version of Serverpod. ### No data to preserve If it is not important to preserve the data that is in your database, you can simply remove the database and let the migration system create a new one. 1. Generate the project. This ensures that the project is up to date with the latest version of Serverpod. Navigate to your project's `server` package directory and run the `generate` command. ```bash $ serverpod generate ``` 2. Create a migration for your project. The migration system will create a migration as if the database needs to be initialized from scratch. Navigate to your project's `server` package directory and run the `create-migration` command. ```bash $ serverpod create-migration ``` 3. Recreate database. In a Serverpod development project, the database is hosted in a docker container. To remove the existing database and start a new one run the following commands: ```bash $ docker compose down -v $ docker compose up --build --detach ``` The command first removes the running container along with its volume and the second command starts a new database from scratch. 4. Initialize database from migration. Initialize the database by applying the migration to it using the `--apply-migrations` flag when starting the server. ```bash $ dart run bin/main.dart --apply-migrations ``` ### Data to preserve If your project already has data in the database that should be preserved, we can use the repair migration system to bring the project up to date with the migration system. 1. Generate the project. This ensures that the project is up to date with the latest version of Serverpod. Navigate to your project's `server` package directory and run the `generate` command. ```bash $ serverpod generate ``` 2. Create a migration for your project. The migration system will create a migration as if the database needs to be initialized from scratch. Navigate to your project's `server` package directory and run the `create-migration` command. ```bash $ serverpod create-migration ``` 3. Create a repair migration. The repair migration system will create a repair migration that makes your live database schema match the newly created migration. To enable the command to fetch your live database schema it requires a running server. Navigate to your project's `server` package directory and start the server, then run the `create-repair-migration` command. ```bash $ dart run bin/main.dart $ serverpod create-repair-migration ``` :::info When starting the server, warnings will be displayed about the database schema not matching the target database schema. These warnings are expected and can safely be ignored when creating the repair migration. ::: Use the `--mode` option to specify the database source to use. By default, the repair migration system connects to your `development` database using the information specified in your Serverpod config. 4. Apply the repair migration to your database. To apply the repair migration to your database, restart the server using the `--apply-repair-migration` flag. ```bash $ dart run bin/main.dart --apply-repair-migration ``` ## Closing remarks Your project is now compatible with the database migration system in Serverpod 1.2. Happy coding! --- # Upgrade to 2.0 https://docs.serverpod.dev/upgrading/archive/upgrade-to-two ## Changes to authentication The base auth implementation has been removed from Serverpod core and moved into the `serverpod_auth` package. If you are not using authentication at all this change does not impact you. If you are using the auth module already the transition is simple. The default authentication handler will now throw an `UnimplementedError`. It is now required to supply the authentication handler to the Serverpod object, in your server.dart file make the following change: ```dart import 'package:serverpod_auth_server/serverpod_auth_server.dart' as auth; void run(List args) async { var pod = Serverpod( args, Protocol(), Endpoints(), authenticationHandler: auth.authenticationHandler, // Add this line ); ... } ``` ### Advanced integrations The methods `signInUser` and `signOutUser` now takes the session object as a param and is no longer available on the session object. Instead import the class `UserAuthentication` from the auth module to access these static methods. ```dart UserAuthentication.signInUser(session, userId, 'provider'); UserAuthentication.signOutUser(session); ``` The table `serverpod_auth_key` has been removed from Serverpod core but is available in the serverpod\_auth module instead. This means that if you wrote a custom integration before without using the serverpod\_auth module you have to take care of managing your token implementation. Adding the definition of the `serverpod_auth_key` table to your project is the simplest way to do a seamless migration. The table was defined in the following way: ```yaml ### Provides a method of access for a user to authenticate with the server. class: AuthKey table: serverpod_auth_key fields: ### The id of the user to provide access to. userId: int ### The hashed version of the key. hash: String ### The key sent to the server to authenticate. key: String?, !persist ### The scopes this key provides access to. scopeNames: List ### The method of signing in this key was generated through. This can be email ### or different social logins. method: String indexes: serverpod_auth_key_userId_idx: fields: userId ``` Your are then responsible for creating/removing entries in this table, the old `signInUser` and `signOutUser` that used to provide this functionality can be found [here](https://github.com/serverpod/serverpod/blob/13795a7bd4c0cc5a03101b6f378cb914673046dd/packages/serverpod/lib/src/server/session.dart#L359-L394). ## Changes to the Session Object ### Removed deprecated fields With Serverpod 2.0, we have removed the deprecated legacy database layer from the `Session` object. The `Session` object now incorporates the new database layer, accessed via the `dbNext` field in Serverpod 1.2, under the `db` field. ```dart session.dbNext.find(...); ``` becomes ```dart session.db.find(...); ``` ### Authenticated user information retrieval In Serverpod 2.0, we have removed the getters `scopes` and `authenticatedUser` from session. This information is now retrievable through the `authenticated` getter as fields of the returned object. Replace this: ```dart int? userId = await session.auth.authenticatedUser; Set? scopes = await session.scopes; ``` With this: ```dart final authenticated = await session.authenticated; //Read authenticated userId int? userId = authenticated?.userId; //Read scopes Set? scopes = authenticated?.scopes; ``` If the `authenticated` property is set on the session it effectively means there is an authenticated user making the request. ### Authentication helpers The field `auth` has been removed and the methods `signInUser` and `signOutUser` have been moved to the `serverpod_auth` module. ## Changes to database queries ### Removed unsafeQueryMappedResults(...) The `unsafeQueryMappedResults(...)` method has been removed. A similar result can now instead be formatted from the `unsafeQuery(...)` result by calling the `toColumnMap()` method for each row of the result. `toColumnMap` returns a map containing the query alias for the column as key and the row-column value as value. Given a query that performs a join like this: ```sql SELECT "company"."id" AS "company.id", "company"."name" AS "company.name", "company"."townId" AS "company.townId", "company_town_town"."id" AS "company_town_town.id", "company_town_town"."name" AS "company_town_town.name", "company_town_town"."mayorId" AS "company_town_town.mayorId" FROM "company" LEFT JOIN "town" AS "company_town_town" ON "company"."townId" = "company_town_town"."id" ORDER BY "company"."name" ``` The return type from `unsafeQueryMappedResults(...)` in 1.2 was: ```json [ { "company": { "company.id": 40, "company.name": "Apple", "company.townId": 64 }, "town": { "company_town_town.id": 64, "company_town_town.name": "San Francisco", "company_town_town.mayorId": null } }, { "company": { "company.id": 39, "company.name": "Serverpod", "company.townId": 63 }, "town": { "company_town_town.id": 63, "company_town_town.name": "Stockholm", "company_town_town.mayorId": null } } ] ``` And if `result.map((row) => row.toColumnMap())` is used to format the result from `unsafeQuery(...)` in 2.0, the following result is obtained: ```json [ { "company.id": 38, "company.name": "Apple", "company.townId": 62, "company_town_town.id": 62, "company_town_town.name": "San Francisco", "company_town_town.mayorId": null }, { "company.id": 37, "company.name": "Serverpod", "company.townId": 61, "company_town_town.id": 61, "company_town_town.name": "Stockholm", "company_town_town.mayorId": null } ] ``` or for a simple query without aliases: ```sql SELECT "id", "name", "townId" FROM "company" ORDER BY "name" ``` the return type from `unsafeQueryMappedResults(...)` in 1.2 was: ```json [ { "company": { "id": 54, "name": "Apple", "townId": 86 } }, { "company": { "id": 53, "name": "Serverpod", "townId": 85 } } ] ``` and if `result.map((row) => row.toColumnMap())` is used to format the result from `unsafeQuery(...)` in 2.0, the following result is obtained: ```json [ { "id": 54, "name": "Apple", "townId": 86 }, { "id": 53, "name": "Serverpod", "townId": 85 } ] ``` ### Update return type for delete operations The return type for all delete operations has been changed from the `id` of the deleted rows to the actual deleted rows. This makes the return type for the delete operations consistent with the return type of the other database operations. It also dramatically simplifies retrieving and removing rows in concurrent environments. Return type before the change: ```dart int companyId = await Company.db.deleteRow(session, company); List companyIds = await Company.db.delete(session, [company]); List companyIds = await Company.db.deleteWhere(session, where: (t) => t.name.like('%Ltd')); ``` Return types after the change: ```dart Company company = await Company.db.deleteRow(session, company); List companies = await Company.db.delete(session, [company]); List companies = await Company.db.deleteWhere(session, where: (t) => t.name.like('%Ltd')); ``` ## Changes to database tables ### Integer representation changed to bigint Integer representation in the database has changed from `int` to `bigint`. From now on, models with `int` fields will generate database migrations where that field is defined as a `bigint` type in the database. This change also applies to the `id` field of models where `bigserial` is now used to generate the id. The change is compatible with existing databases. Existing migrations therefore, won't be changed by the Serverpod migration system. No manual modification to the database is required if this data representation is not essential for the application. However, all new migrations will be created with the new representation. #### Why is this change made? The change was made to ensure that [Dart](https://dart.dev/guides/language/numbers) and the database representation of integers is consistent. Dart uses 64-bit integers, and the `int` type in Dart is a 64-bit integer. The `int` type in PostgreSQL is a 32-bit integer. This means that the `int` type in Dart can represent larger numbers than the `int` type in PostgreSQL. By using `bigint` in PostgreSQL, the integer representation is consistent between Dart and the database. In terms of performance, there are usually no significant drawbacks with using `bigint` instead of `int`. In most cases a good index strategy will be more important than the integer representation. Here is a guide that benchmarks the performance of `int` and `bigint` in PostgreSQL: [Use BIGINT in Postgres](https://blog.rustprooflabs.com/2021/06/postgres-bigint-by-default) #### Ensuring new databases are created with the new representation Since existing migrations won't be changed, databases that are created with these will still use `int` to represent integers. To ensure new databases are created with the new representation, the latest migration should be generated using Serverpod 2.0. It is enough to have an empty migration to ensure new databases use the new representation. A new empty migration can be created by running the following command in the terminal: ```bash $ serverpod create-migration --force ``` #### Migration of existing tables The migration of existing tables to use the new representation will vary depending on the database content. Utilizing the wrong migration strategy might cause downtime for your application. That is the reason Serverpod does not automatically migrate existing tables. ##### Small tables A simple way to migrate for small tables is to execute the following sql query to the database: ```sql ALTER SEQUENCE "my_table_id_seq" AS bigint; ALTER TABLE "my_table" ALTER "id" TYPE bigint; ALTER TABLE "my_table" ALTER "myNumber" TYPE bigint; ``` The first two lines modify the id sequence for a table named `"my_table"` to use `bigint` instead of `int`. The last line modifies a column of the same table to use `bigint`. The drawback of this approach is that it locks the table during the migration. Therefore, this strategy is not recommended for large tables. ##### Large tables Migrating large tables without application downtime is a more complex operation, and the approach will vary depending on the data structure. Below are some gathered resources on the subject. - [Zemata - Column migration from INT to BIGINT](http://zemanta.github.io/2021/08/25/column-migration-from-int-to-bigint-in-postgresql/) - [AM^2 - Changing a column from int to bigint, without downtime](https://am2.co/2019/12/changing-a-column-from-int-to-bigint-without-downtime/) - [Crunch data - The integer at the End of the Universe](https://www.crunchydata.com/blog/the-integer-at-the-end-of-the-universe-integer-overflow-in-postgres) ## Changes in the authentication module ### Unsecure random disabled by default The authentication module's default value for allowing unsecure random number generation is now `false`. An exception will be thrown when trying to hash a password if no secure random number generator is available. To preserve the old behavior and enable unsecure random number generation, set the `allowUnsecureRandom` property in the `AuthConfig` to `true`. ```dart auth.AuthConfig.set(auth.AuthConfig( allowUnsecureRandom: true, )); ``` ## Updates to Serialization in Serverpod 2.0 ### General Changes to Model Serialization Serverpod 2.0 significantly streamlines the model serialization process. In earlier versions, the `fromJson` factory constructors needed a `serializationManager` parameter to handle object deserialization. This parameter has now been removed, enhancing simplicity and usability. #### Before change ```dart final Map json = classInstance.toJson(); final SerializationManager serializationManager = Protocol(); final ClassName test = ClassName.fromJson(json, serializationManager); ``` #### After change ```dart final Map json = classInstance.toJson(); final ClassName test = ClassName.fromJson(json); ``` ### Enhancements for Custom Serialization The removal of the `serializationManager` parameter in Serverpod 2.0 simplifies the serialization process not only for general models but also significantly enhances custom serialization workflows. For custom classes that previously utilized unique serialization logic with the `serializationManager`, adjustments may be necessary. #### Previous Implementation In the previous versions, models required the `serializationManager` to be passed explicitly, as shown in the following code snippet: ```dart factory ClassName.fromJson( Map json, SerializationManager serializationManager, ) { return ClassName( json['name'], ); } ``` #### Updated Implementation With the release of Serverpod 2.0, the `fromJson` constructor has been simplified and the `serializationManager` has been removed: ```dart factory ClassName.fromJson( Map json, ) { return ClassName( json['name'], ); } ``` ## Deprecation Notice for `SerializableEntity` The `SerializableEntity` class is deprecated and will be removed in version 3. Please implement the `SerializableModel` interface instead for creating serializable models. ### Migration Guide To migrate your code from `SerializableEntity` to `SerializableModel`, replace `extends SerializableEntity` with `implements SerializableModel` in your model classes. #### Example **Before:** ```dart class CustomClass extends SerializableEntity { // Your code here } ``` **After:** ```dart class CustomClass implements SerializableModel { // Your code here } ``` --- # Upgrade from Mini to full https://docs.serverpod.dev/upgrading/archive/upgrade-from-mini If you have started with Serverpod Mini, you can upgrade to the full Serverpod version anytime. Before you upgrade, it's good practice to back up your project, as some configuration files can be overwritten if you have manually created them. To upgrade your project, change directory into your server, then run: ```bash serverpod create . ``` Serverpod will now add the configuration files required for the full version. --- # Upgrade to 2.2 https://docs.serverpod.dev/upgrading/archive/upgrade-to-two-point-two Serverpod 2.2 includes new test tools that make it easy to create tests for endpoint methods. For new projects they are configured by default, but existing projects need to go through some steps to enable it (see below). The full documentation of this feature can also be found [here](https://docs.serverpod.dev/concepts/testing/get-started.md). ## Add test tools to existing projects For existing non-Mini projects, a few extra things need to be done: 1. Add the `server_test_tools_path` key with the value `test/integration/test_tools` to `config/generator.yaml`: ```yaml server_test_tools_path: test/integration/test_tools ``` Without this key, the test tools file is not generated. With the above config the location of the test tools file is `test/integration/test_tools/serverpod_test_tools.dart`, but this can be set to any folder (though should be outside of `lib` as per Dart's test conventions). 2. New projects now come with a test profile in `docker-compose.yaml`. This is not strictly mandatory, but is recommended to ensure that the testing state is never polluted. Add the snippet below to the `docker-compose.yaml` file in the server directory: ```yaml # Add to the existing services postgres_test: image: postgres:16.3 ports: - '9090:5432' environment: POSTGRES_USER: postgres POSTGRES_DB: _test POSTGRES_PASSWORD: "" volumes: - _test_data:/var/lib/postgresql/data profiles: - '' # Default profile - test redis_test: image: redis:6.2.6 ports: - '9091:6379' command: redis-server --requirepass "" environment: - REDIS_REPLICATION_MODE=master profiles: - '' # Default profile - test volumes: # ... _test_data: ``` Or copy the complete file here. ```yaml services: # Development services postgres: image: postgres:16.3 ports: - '8090:5432' environment: POSTGRES_USER: postgres POSTGRES_DB: POSTGRES_PASSWORD: "" volumes: - _data:/var/lib/postgresql/data profiles: - '' # Default profile - dev redis: image: redis:6.2.6 ports: - '8091:6379' command: redis-server --requirepass "" environment: - REDIS_REPLICATION_MODE=master profiles: - '' # Default profile - dev # Test services postgres_test: image: postgres:16.3 ports: - '9090:5432' environment: POSTGRES_USER: postgres POSTGRES_DB: _test POSTGRES_PASSWORD: "" volumes: - _test_data:/var/lib/postgresql/data profiles: - '' # Default profile - test redis_test: image: redis:6.2.6 ports: - '9091:6379' command: redis-server --requirepass "" environment: - REDIS_REPLICATION_MODE=master profiles: - '' # Default profile - test volumes: _data: _test_data: ``` 3. Create a `test.yaml` file and add it to the `config` directory: ```yaml # This is the configuration file for your test environment. # All ports are set to zero in this file which makes the server find the next available port. # This is needed to enable running tests concurrently. To set up your server, you will # need to add the name of the database you are connecting to and the user name. # The password for the database is stored in the config/passwords.yaml. # Configuration for the main API test server. apiServer: port: 0 publicHost: localhost publicPort: 0 publicScheme: http # Configuration for the Insights test server. insightsServer: port: 0 publicHost: localhost publicPort: 0 publicScheme: http # Configuration for the web test server. webServer: port: 0 publicHost: localhost publicPort: 0 publicScheme: http # This is the database setup for your test server. database: host: localhost port: 9090 name: _test user: postgres # This is the setup for your Redis test instance. redis: enabled: false host: localhost port: 9091 ``` 4. Add this entry to `config/passwords.yaml` ```yaml test: database: '' redis: '' ``` 5. Add a `dart_test.yaml` file to the `server` directory (next to `pubspec.yaml`) with the following contents: ```yaml tags: integration: {} ``` 6. Finally, add the `test` and `serverpod_test` packages as dev dependencies in `pubspec.yaml`: ```yaml dev_dependencies: serverpod_test: # Should be same version as the `serverpod` package test: ^1.24.2 ``` That's it, the project setup should be ready to start using the test tools! --- # Upgrade to pgvector https://docs.serverpod.dev/upgrading/archive/upgrade-to-pgvector New Serverpod projects automatically included pgvector support through the `pgvector/pgvector` PostgreSQL Docker image. From 4.0.0-beta.2, the compose file uses `ghcr.io/serverpod/postgres:16` instead, which is that image plus PostGIS. Existing projects need to be upgraded to use vector functionality. :::info This upgrade is only necessary if you want to use vector fields in your models. If you do not plan to use vector fields, you can skip this upgrade. ::: :::warning If trying to use vector fields without upgrading, you will encounter an error when applying migrations. ::: ## For Docker-based environments 1. Update your `docker-compose.yaml` to use a PostgreSQL image with pgvector (e.g., `pgvector/pgvector:pg16`): ```yaml services: postgres: image: pgvector/pgvector:pg16 # <-- Change from postgres image here ports: - '8090:5432' environment: POSTGRES_USER: postgres POSTGRES_DB: POSTGRES_PASSWORD: volumes: - _data:/var/lib/postgresql/data # Other services... postgres_test: image: pgvector/pgvector:pg16 # <-- Change from postgres image here ports: - '9090:5432' environment: POSTGRES_USER: postgres POSTGRES_DB: _test POSTGRES_PASSWORD: volumes: - _test_data:/var/lib/postgresql/data ``` 2. Recreate your containers to use the new image: ```bash docker compose down docker compose up -d ``` 3. Create your first vector field in a model: ```yaml class: Document table: document fields: content: String embedding: Vector(1536) ``` 4. Generate and apply a migration: ```bash $ serverpod create-migration $ dart run bin/main.dart --apply-migrations ``` For more details on creating and applying migrations, see the [Migrations](https://docs.serverpod.dev/concepts/data-and-the-database/database/migrations.md) page. The pgvector extension will be automatically enabled during the first migration that includes a vector column. ## For managed PostgreSQL services For cloud providers (AWS RDS, Google Cloud SQL, Azure Database, etc.), ensure that **pgvector extension is available** on your PostgreSQL instance. Most managed services already support pgvector with no additional setup required. If available, the extension will be enabled automatically when applying the migration. :::tip If the cloud provider instructions is to run a `CREATE EXTENSION vector;` command, you can skip this step as Serverpod will handle it automatically during migration. ::: ## Troubleshooting If you encounter issues with pgvector: - Verify that your PostgreSQL version supports pgvector 0.7.0+ (PostgreSQL 12+). - Check that the pgvector extension is properly installed. - Ensure your database user has the necessary permissions to create extensions. --- # Upgrade to 3.0 https://docs.serverpod.dev/upgrading/archive/upgrade-to-three :::note The new authentication module has separate migration documentation. This guide covers only core framework changes. ::: ## Requirements - Dart SDK: 3.8.0+ - Flutter SDK: 3.32.0+ ## Breaking changes summary - [**Web server:**](#web-server-relic-framework) Relic framework integration (custom routes need updates) - [**Session.request:**](#sessionrequest-property) New property to access HTTP request from endpoints - [**Authentication:**](#authentication) `AuthenticationInfo` changes, `session.authenticated` now synchronous - [**Enum serialization:**](#enum-serialization) Default changed from `byIndex` to `byName` - [**Model changes:**](#model-changes) `SerializableEntity` removed, YAML keywords updated ## Updating your project ### Update the CLI Update the Serverpod command line interface to the latest version: ```bash dart pub global activate serverpod_cli ``` Verify the installed version: ```bash serverpod version ``` ### Update pubspec.yaml files Update the `pubspec.yaml` files in your `server`, `client`, and `flutter` directories. Change all Serverpod package versions to `3.0.0` (or later 3.x version). Update the Dart SDK constraint in all `pubspec.yaml` files: ```yaml environment: sdk: '>=3.8.0 <4.0.0' ``` ### Update the Dockerfile Update the Dart version in your project's `Dockerfile`: ```docker FROM dart:3.8 AS build ... ``` ### Run updates After updating your `pubspec.yaml` files, run these commands in each package directory: ```bash dart pub upgrade ``` Then regenerate your project in your `server` directory: ```bash serverpod generate ``` ### Create and apply migrations A database migration is required for Serverpod 3.0. The session log table now stores authenticated user IDs as `String` instead of `int` to support non-numeric identifiers (e.g., `UUID`s). Create and apply the migration: ```bash serverpod create-migration dart run bin/main.dart --apply-migrations ``` ## Migration checklist 1. Update CLI, pubspec files, and Dockerfile (see above) 2. Update custom Route classes (see Web Server section) 3. Update enum serialization strategy (see Enum section) 4. Replace `SerializableEntity` with `SerializableModel` in custom models 5. Update custom `AuthenticationInfo` usage (see Authentication section) 6. Test and deploy ## Web server (Relic framework) Serverpod 3.0 integrates the [Relic framework](https://pub.dev/packages/relic) for web server functionality, bringing it out of experimental status. This provides improved performance, better request/response handling, and built-in WebSocket support. ### Route.handleCall signature | Aspect | Serverpod 2.x | Serverpod 3.0 | | -------- | --------------------- | ---------------------- | | Request | `HttpRequest request` | `Request request` | | Return | `Future` | `FutureOr` | | Response | `return true` | `return Response.ok()` | **Before:** ```dart import 'dart:io'; class MyRoute extends Route { @override Future handleCall(Session session, HttpRequest request) async { request.response.write('Hello'); return true; } } ``` **After:** ```dart class MyRoute extends Route { @override FutureOr handleCall(Session session, Request request) async { return Response.ok( body: Body.fromString('Hello', mimeType: MimeType.html), ); } } ``` **Changes:** - `HttpRequest` → `Request` (from Relic) - `Future` → `FutureOr` - `request.remoteIpAddress` → `request.remoteInfo` (both return `String`) - `request.headers.value('name')` → `request.headers['name']` - Return `Response` instead of `bool` ### Widget class names The web server widget classes have been reorganized for better clarity. Legacy class names are deprecated but still available for backward compatibility. | Old (Deprecated) | New | | ---------------- | ---------------- | | `AbstractWidget` | `WebWidget` | | `Widget` | `TemplateWidget` | | `WidgetList` | `ListWidget` | | `WidgetJson` | `JsonWidget` | | `WidgetRedirect` | `RedirectWidget` | The `WidgetRoute` class remains unchanged and continues to be the base class for web routes. **Before:** ```dart Future build(...) => Widget(name: 'page')..values = {...}; ``` **After:** ```dart Future build(...) => TemplateWidget(name: 'page', values: {...}); ``` ### Static route updates The `RouteStaticDirectory` class has been deprecated in favor of `StaticRoute.directory()`: **Before:** ```dart pod.webServer.addRoute( RouteStaticDirectory( serverDirectory: 'web/static', basePath: '/', ), '/static/**', ); ``` **After:** ```dart pod.webServer.addRoute( StaticRoute.directory(Directory('web/static')), '/static/', ); ``` The new `StaticRoute` provides better cache control options. You can use the built-in static helper methods for common caching scenarios: ```dart pod.webServer.addRoute( StaticRoute.directory( Directory('web/static'), cacheControlFactory: StaticRoute.publicImmutable(maxAge: 3600), ), '/static/', ); ``` Available cache control factory methods: - `StaticRoute.public(maxAge: seconds)` - Public cache with optional max-age - `StaticRoute.publicImmutable(maxAge: seconds)` - Public immutable cache with optional max-age - `StaticRoute.privateNoCache()` - Private cache with no-cache directive - `StaticRoute.noStore()` - No storage allowed You can also provide a custom factory function: ```dart pod.webServer.addRoute( StaticRoute.directory( Directory('web/static'), cacheControlFactory: (ctx, fileInfo) => CacheControlHeader( publicCache: true, maxAge: 3600, immutable: true, ), ), '/static/', ); ``` ## Session.request property With the Relic framework integration, the `Session` object now provides access to the underlying HTTP request through the `request` property. This allows endpoint methods to access request metadata such as client IP address and headers directly from the session. Use optional chaining as it's `null` on some session types (e.g., sessions created for background tasks). ```dart class MyEndpoint extends Endpoint { Future getClientInfo(Session session) async { final ip = session.request?.remoteInfo ?? 'unknown'; final userAgent = session.request?.headers.userAgent ?? 'unknown'; return 'IP: $ip, UA: $userAgent'; } } ``` ## Authentication Serverpod 3.0 includes several changes to the authentication system that improve type safety and performance. :::note Besides minor interface changes described in this section, the legacy authentication system is still supported and can be used alongside the new version. This allows to safely upgrade your project to Serverpod 3.0 while using the legacy authentication and gradually migrate to the new one. ::: ### Custom authentication handlers In the new authentication system, the default authentication header has changed from `Basic` to `Bearer` - which is now officially supported. This introduced a change for custom `AuthenticationHandler` implementations: the `token` parameter will now receive the token unwrapped from the `Bearer` prefix - just as it does for `Basic` tokens. If your application relies on the default `authenticationHandler` function, no change is required. Only custom implementations that previously implemented `Bearer` authentication will need to be updated to expect the unwrapped token. Another change when using custom authentication handlers is that tokens without scheme prefix are no longer supported by default to enforce HTTP header standards. The preferred fix is to wrap the token with the `Bearer` prefix, but this will invalidate existing tokens. If you are you are using a custom authentication handler in production and need to keep the old behavior, you can set the `validateHeaders` parameter to `false` in your `production.yaml` config file. ```yaml apiServer: ... insightsServer: ... webServer: ... validateHeaders: false # <-- Add this line sessionLogs: consoleEnabled: false ``` ### AuthenticationInfo changes The `AuthenticationInfo` class has been updated: - `authId` is now non-nullable (previously optional) - `userIdentifier` parameter type changed from `Object` to `String` ### session.authenticated is now synchronous Authentication is now resolved when the session is created, making `session.authenticated` synchronous. This improves performance by eliminating repeated async lookups. **Before (async):** ```dart final auth = await session.authenticated; ``` **After (sync):** ```dart final auth = session.authenticated; ``` ### Client auth key provider The `authKeyProvider` interface replaces the previous `authenticationKeyManager`. This interface has been simplified to make it more explicit what the client needs. It now only requires something that can provide an auth key wrapped as a header. **Before:** ```dart Client(host)..authenticationKeyManager = myManager; ``` **After:** ```dart Client(host)..authKeyProvider = myProvider; ``` :::note To keep backwards compatibility with the old authentication system, the `FlutterAuthenticationKeyManager` from the legacy package now implements the `AuthKeyProvider` interface, so you can safely pass it to the client as the `authKeyProvider`. ::: ## Enum serialization The default enum serialization strategy has changed from `byIndex` to `byName`. This change improves robustness when reordering or adding enum values, as serialized data remains valid even if the enum definition changes. With `byName`, the string representation is stored instead of the numeric index, making your data more resilient and easier to debug. **Keep old behavior:** ```yaml enum: UserRole serialized: byIndex # Add this values: - admin - user ``` **Use new default:** ```yaml enum: UserRole # serialized: byName - is now default values: - admin - user ``` ## Model changes ### SerializableEntity removed The `SerializableEntity` class, deprecated since Serverpod 2.0, has been removed. Replace `extends SerializableEntity` with `implements SerializableModel` in your custom model classes. **Before:** ```dart class CustomClass extends SerializableEntity { Map toJson() => {'name': name}; } ``` **After:** ```dart class CustomClass implements SerializableModel { Map toJson() => {'name': name}; factory CustomClass.fromJson(Map json) => CustomClass(json['name']); } ``` ### Removed YAML keywords The following deprecated YAML keywords have been removed. Run `serverpod generate` to see errors with migration guidance. | Old keyword | Replacement | | -------------- | ------------------------ | | `parent=table` | `relation(parent=table)` | | `database` | `scope=serverOnly` | | `api` | `!persist` | **Before:** ```yaml class: Company table: company fields: name: String ceoId: int, parent=employee internalNotes: String, database tempData: String, api ``` **After:** ```yaml class: Company table: company fields: name: String ceoId: int, relation(parent=employee) internalNotes: String, scope=serverOnly tempData: String, !persist ``` ## Deprecated APIs The following APIs have been deprecated but will continue to work for the foreseeable future to maintain compatibility with older clients. They will be removed in a future major version: - Legacy streaming endpoints → Use [streaming methods](https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming.md) for new code ## Other changes Additional improvements in Serverpod 3.0: - **Session log user ID as String:** The session log table now stores authenticated user IDs as `String` instead of `int`. This enables support for non-numeric identifiers such as UUIDs. A database migration is required (see [Create and apply migrations](#create-and-apply-migrations)). - **Graceful SIGTERM shutdown:** The server now handles `SIGTERM` signals gracefully, allowing in-flight requests to complete before shutting down. This improves behavior in containerized deployments (Docker, Kubernetes) where orchestrators send `SIGTERM` before terminating processes. - **Auto-stop on integrity check failure:** In development mode, the server now automatically stops if the database integrity check fails (e.g., schema mismatch). This prevents running with an inconsistent database state during development. ## Resources - [Relic Framework](https://pub.dev/packages/relic) - [GitHub Issues](https://github.com/serverpod/serverpod/issues) - [Discord Community](https://serverpod.dev/discord) --- # Streaming endpoints (removed in 4.0) https://docs.serverpod.dev/upgrading/archive/streaming-endpoints :::warning Streaming endpoints were deprecated in Serverpod 3.0 and removed in 4.0. Use [streaming methods](https://docs.serverpod.dev/concepts/endpoints-and-apis/streaming.md#streaming-methods) instead. This page documents the old API for projects that have not ported off it yet. ::: Streaming endpoints were Serverpod's first attempt at streaming data. This approach is more manual, requiring you to manage the WebSocket connection to the server. ## Handling streams server-side The Endpoint class has three methods you override to work with streams. - `streamOpened` is called when a user connects to a stream on the Endpoint. - `streamClosed` is called when a user disconnects from a stream on the Endpoint. - `handleStreamMessage` is called when a serialized message is received from a client. To send a message to a client, call the `sendStreamMessage` method. You will need to include the session associated with the user. ### The user object Associate state with a streaming session by setting a user object when the stream is opened. ```dart Future streamOpened(StreamingSession session) async { setUserObject(session, MyUserObject()); } ``` You can access the user object at any time by calling the `getUserObject` method. The user object is automatically discarded when a session ends. ## Handling streams in your app Before you can access streams in your client, you need to connect to the server's web socket by calling `openStreamingConnection` on your client. ```dart await client.openStreamingConnection(); ``` You can monitor the state of the connection by adding a listener to the client. Once connected to your server's web socket, you can pass and receive serialized objects. Listen to its web socket stream to receive updates from an endpoint on the server. ```dart await for (var message in client.myEndpoint.stream) { _handleMessage(message); } ``` You send messages to the server's endpoint by calling `sendStreamMessage`. ```dart client.myEndpoint.sendStreamMessage(MyMessage(text: 'Hello')); ``` :::info Authentication is handled automatically. If you have signed in, your web socket connection will be authenticated. ::: --- # Roadmap & contributions https://docs.serverpod.dev/contribute Serverpod is built by the community, for the community. We believe the best software comes from collaboration, where many people contribute their ideas and skills. That's why we strive to make contributing as easy as possible and share our plans for future development in our public roadmap. [Watch the video](https://www.youtube-nocookie.com/embed/V3CqPx4jykE) ## Roadmap Serverpod's [roadmap](https://github.com/orgs/serverpod/projects/4) outlines the features and improvements that are planned for the project. The roadmap is subject to change, but provides a good overview over work in progress and what is planned for the future. If you are considering contributing code to Serverpod, please check the roadmap to see if your contribution aligns with the project's goals. ## Contributions There are many ways to contribute to Serverpod’s development. If you're interested, check out our [contribution guidelines](https://github.com/serverpod/serverpod/blob/main/CONTRIBUTING.md) to learn how you can contribute code, report bugs, and suggest improvements.