Skip to content

Instantly share code, notes, and snippets.

@nogipx
Created April 25, 2025 21:18
Show Gist options
  • Save nogipx/aa5fa13554d84b81b09c9d87e16d51ec to your computer and use it in GitHub Desktop.
Save nogipx/aa5fa13554d84b81b09c9d87e16d51ec to your computer and use it in GitHub Desktop.
└── versioned_docs
└── version-2.6.0
└── 06-concepts
├── 01-working-with-endpoints.md
├── 02-models.md
├── 03-serialization.md
├── 04-exceptions.md
├── 05-sessions.md
├── 06-database
├── 01-connection.md
├── 02-models.md
├── 03-relations
│ ├── 01-one-to-one.md
│ ├── 02-one-to-many.md
│ ├── 03-many-to-many.md
│ ├── 04-self-relations.md
│ ├── 05-referential-actions.md
│ └── 06-modules.md
├── 04-indexing.md
├── 05-crud.md
├── 06-filter.md
├── 07-relation-queries.md
├── 08-sort.md
├── 08-transactions.md
├── 09-pagination.md
├── 10-raw-access.md
└── 11-migrations.md
├── 07-configuration.md
├── 08-caching.md
├── 09-logging.md
├── 10-modules.md
├── 11-authentication
├── 01-setup.md
├── 02-basics.md
├── 03-working-with-users.md
├── 04-providers
│ ├── 01-email.md
│ ├── 02-google.md
│ ├── 03-apple.md
│ ├── 05-firebase.md
│ └── 06-custom-providers.md
└── 05-custom-overrides.md
├── 12-file-uploads.md
├── 13-health-checks.md
├── 14-scheduling.md
├── 15-streams.md
├── 16-server-events.md
├── 17-backward-compatibility.md
├── 18-webserver.md
├── 19-testing
├── 01-get-started.md
├── 02-the-basics.md
├── 03-advanced-examples.md
└── 04-best-practises.md
├── 20-security-configuration.md
└── 21-experimental.md
/versioned_docs/version-2.6.0/06-concepts/01-working-with-endpoints.md:
--------------------------------------------------------------------------------
1 | # Working with endpoints
2 |
3 | Endpoints are the connection points to the server from the client. With Serverpod, you add methods to your endpoint, and your client code will be generated to make the method call. For the code to be generated, you need to place the endpoint file anywhere under the `lib` directory of your server. Your endpoint should extend the `Endpoint` class. For methods to be generated, they need to return a typed `Future`, and its first argument should be a `Session` object. The `Session` object holds information about the call being made and provides access to the database.
4 |
5 | ```dart
6 | import 'package:serverpod/serverpod.dart';
7 |
8 | class ExampleEndpoint extends Endpoint {
9 | Future<String> hello(Session session, String name) async {
10 | return 'Hello $name';
11 | }
12 | }
13 | ```
14 |
15 | The above code will create an endpoint called `example` (the Endpoint suffix will be removed) with the single `hello` method. To generate the client-side code run `serverpod generate` in the home directory of the server.
16 |
17 | On the client side, you can now call the method by calling:
18 |
19 | ```dart
20 | var result = await client.example.hello('World');
21 | ```
22 |
23 | The client is initialized like this:
24 |
25 | ```dart
26 | // Sets up a singleton client object that can be used to talk to the server from
27 | // anywhere in our app. The client is generated from your server code.
28 | // The client is set up to connect to a Serverpod running on a local server on
29 | // the default port. You will need to modify this to connect to staging or
30 | // production servers.
31 | var client = Client('http://$localhost:8080/')
32 | ..connectivityMonitor = FlutterConnectivityMonitor();
33 | ```
34 |
35 | If you run the app in an Android emulator, the `localhost` parameter points to `10.0.2.2`, rather than `127.0.0.1` as this is the IP address of the host machine. To access the server from a different device on the same network (such as a physical phone) replace `localhost` with the local ip address. You can find the local ip by running `ifconfig` (Linux/MacOS) or `ipconfig` (Windows).
36 |
37 | Make sure to also update the `publicHost` in the development config to make sure the server always serves the client with the correct path to assets etc.
38 |
39 | ```yaml
40 | # your_project_server/config/development.yaml
41 |
42 | apiServer:
43 | port: 8080
44 | publicHost: localhost # Change this line
45 | publicPort: 8080
46 | publicScheme: http
47 | ...
48 | ```
49 |
50 | :::info
51 |
52 | You can pass the `--watch` flag to `serverpod generate` to watch for changed files and generate code whenever your source files are updated. This is useful during the development of your server.
53 |
54 | :::
55 |
56 | ## Passing parameters
57 |
58 | There are some limitations to how endpoint methods can be implemented. Parameters and return types can be of type `bool`, `int`, `double`, `String`, `UuidValue`, `Duration`, `DateTime`, `ByteData`, `Uri`, `BigInt`, or generated serializable objects (see next section). A typed `Future` should always be returned. Null safety is supported. When passing a `DateTime` it is always converted to UTC.
59 |
60 | You can also pass `List`, `Map`, `Record` and `Set` as parameters, but they need to be strictly typed with one of the types mentioned above.
61 |
62 | :::warning
63 |
64 | While it's possible to pass binary data through a method call and `ByteData`, it is not the most efficient way to transfer large files. See our [file upload](file-uploads) interface. The size of a call is by default limited to 512 kB. It's possible to change by adding the `maxRequestSize` to your config files. E.g., this will double the request size to 1 MB:
65 |
66 | ```yaml
67 | maxRequestSize: 1048576
68 | ```
69 |
70 | :::
71 |
72 | ## Return types
73 |
74 | The return type must be a typed Future. Supported return types are the same as for parameters.
75 |
76 | ## Ignore endpoint definition
77 |
78 | ### Ignore an entire `Endpoint` class
79 |
80 | If you want the code generator to ignore an endpoint definition, you can annotate either the entire class or individual methods with `@ignoreEndpoint`. This can be useful if you want to keep the definition in your codebase without generating server or client bindings for it.
81 |
82 | ```dart
83 | import 'package:serverpod/serverpod.dart';
84 |
85 | @ignoreEndpoint
86 | class ExampleEndpoint extends Endpoint {
87 | Future<String> hello(Session session, String name) async {
88 | return 'Hello $name';
89 | }
90 | }
91 | ```
92 |
93 | The above code will not generate any server or client bindings for the example endpoint.
94 |
95 | ### Ignore individual `Endpoint` methods
96 |
97 | Alternatively, you can disable single methods by annotation them with `@ignoreEndpoint`.
98 |
99 | ```dart
100 | import 'package:serverpod/serverpod.dart';
101 |
102 | class ExampleEndpoint extends Endpoint {
103 | Future<String> hello(Session session, String name) async {
104 | return 'Hello $name';
105 | }
106 |
107 | @ignoreEndpoint
108 | Future<String> goodbye(Session session, String name) async {
109 | return 'Bye $name';
110 | }
111 | }
112 | ```
113 |
114 | In this case the `ExampleEndpoint` will only expose the `hello` method, whereas the `goodbye` method will not be accessible externally.
115 |
116 | ## Endpoint method inheritance
117 |
118 | Endpoints can be based on other endpoints using inheritance, like `class ChildEndpoint extends ParentEndpoint`. If the parent endpoint was marked as `abstract` or `@ignoreEndpoint`, no client code is generated for it, but a client will be generated for your subclass – as long as it does not opt out again.
119 | Inheritance gives you the possibility to modify the behavior of `Endpoint` classes defined in other Serverpod modules.
120 |
121 | Currently, there are the following possibilities to extend another `Endpoint` class:
122 |
123 | ### Inheriting from an `Endpoint` class
124 |
125 | Given an existing `Endpoint` class, it is possible to extend or modify its behavior while retaining the already exposed methods.
126 |
127 | ```dart
128 | import 'package:serverpod/serverpod.dart';
129 |
130 | class CalculatorEndpoint extends Endpoint {
131 | Future<int> add(Session session, int a, int b) async {
132 | return a + b;
133 | }
134 | }
135 |
136 | class MyCalculatorEndpoint extends CalculatorEndpoint {
137 | Future<int> subtract(Session session, int a, int b) async {
138 | return a - b;
139 | }
140 | }
141 | ```
142 |
143 | The generated client code will now be able to access both `CalculatorEndpoint` and `MyCalculatorEndpoint`.
144 | Whereas the `CalculatorEndpoint` only exposes the original `add` method, `MyCalculatorEndpoint` now exposes both the inherited `add` and its own `subtract` methods.
145 |
146 | ### Inheriting from an `Endpoint` class marked `abstract`
147 |
148 | Endpoints marked as `abstract` are not added to the server. But if they are subclassed, their methods will be exposed through the subclass.
149 |
150 | ```dart
151 | import 'package:serverpod/serverpod.dart';
152 |
153 | abstract class CalculatorEndpoint extends Endpoint {
154 | Future<int> add(Session session, int a, int b) async {
155 | return a + b;
156 | }
157 | }
158 |
159 | class MyCalculatorEndpoint extends CalculatorEndpoint {}
160 | ```
161 |
162 | The generated client code will only be able to access `MyCalculatorEndpoint`, as the abstract `CalculatorEndpoint` is not exposed on the server.
163 | `MyCalculatorEndpoint` exposes the `add` method it inherited from `CalculatorEndpoint`.
164 |
165 | #### Extending an `abstract` `Endpoint` class
166 |
167 | In the above example, the `MyCalculatorEndpoint` only exposed the inherited `add` method. It can be further extended with custom methods like this:
168 |
169 | ```dart
170 | import 'package:serverpod/serverpod.dart';
171 |
172 | class MyCalculatorEndpoint extends CalculatorEndpoint {
173 | Future<int> subtract(Session session, int a, int b) async {
174 | return a - b;
175 | }
176 | }
177 | ```
178 |
179 | In this case, it will expose both an `add` and a `subtract` method.
180 |
181 | ### Inheriting from an `Endpoint` class annotated with `@ignoreEndpoint`
182 |
183 | Suppose you had an `Endpoint` class marked with `@ignoreEndpoint` and a subclass that extends it:
184 |
185 | ```dart
186 | import 'package:serverpod/serverpod.dart';
187 |
188 | @ignoreEndpoint
189 | class CalculatorEndpoint extends Endpoint {
190 | Future<int> add(Session session, int a, int b) async {
191 | return a + b;
192 | }
193 | }
194 |
195 | class MyCalculatorEndpoint extends CalculatorEndpoint {}
196 | ```
197 |
198 | Since `CalculatorEndpoint` is marked as `@ignoreEndpoint` it will not be exposed on the server. Only `MyCalculatorEndpoint` will be accessible from the client, which provides the inherited `add` methods from its parent class.
199 |
200 | ### Overriding endpoint methods
201 |
202 | 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.
203 |
204 | ```dart
205 | import 'package:serverpod/serverpod.dart';
206 |
207 | abstract class GreeterBaseEndpoint extends Endpoint {
208 | Future<String> greet(Session session, String name) async {
209 | return 'Hello $name';
210 | }
211 | }
212 |
213 | class ExcitedGreeterEndpoint extends GreeterBaseEndpoint {
214 | @override
215 | Future<String> greet(Session session, String name) async {
216 | return '${super.hello(session, name)}!!!';
217 | }
218 | }
219 | ```
220 |
221 | 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.
222 |
223 | 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.
224 |
225 | ### Hiding endpoint methods with `@ignoreEndpoint`
226 |
227 | In case you want to hide methods from an endpoint use `@ignoreEndpoint` in the child class like so:
228 |
229 | ```dart
230 | import 'package:serverpod/serverpod.dart';
231 |
232 | abstract class CalculatorEndpoint extends Endpoint {
233 | Future<int> add(Session session, int a, int b) async {
234 | return a + b;
235 | }
236 |
237 | Future<int> subtract(Session session, int a, int b) async {
238 | return a - b;
239 | }
240 | }
241 |
242 | class AdderEndpoint extends CalculatorEndpoint {
243 | @ignoreEndpoint
244 | Future<int> subtract(Session session, int a, int b) async {
245 | throw UnimplementedError();
246 | }
247 | }
248 | ```
249 |
250 | Since `CalculatorEndpoint` is `abstract`, it will not be exposed on the server. `AdderEndpoint` inherits all methods from its parent class, but since it opts to hide `subtract` by annotating it with `@ignoreEndpoint` only the `add` method will be exposed.
251 | 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`.
252 |
253 | Hiding endpoints from a super class is only appropriate in case the parent `class` is `abstract` or annotated with `@ignoreEndpoint`. Otherwise, the method that should be hidden on the child would still be accessible via the parent class.
254 |
255 | ### Unhiding endpoint methods annotated with `@ignoreEndpoint` in the super class
256 |
257 | The reverse of the previous example would be a base endpoint that has a method marked with `@ignoreEndpoint`, which you now want to expose on the subclass.
258 |
259 | ```dart
260 | import 'package:serverpod/serverpod.dart';
261 |
262 | abstract class CalculatorEndpoint extends Endpoint {
263 | Future<int> add(Session session, int a, int b) async {
264 | return a + b;
265 | }
266 |
267 | // Ignored, as this expensive computation should not be exposed by default
268 | @ignoreEndpoint
269 | Future<BigInt> addBig(Session session, BigInt a, BigInt b) async {
270 | return a + b;
271 | }
272 | }
273 |
274 | class MyCalculatorEndpoint extends CalculatorEndpoint {
275 | @override
276 | Future<BigInt> addBig(Session session, BigInt a, BigInt b) async {
277 | return super.addBig(session, a, b);
278 | }
279 | }
280 | ```
281 |
282 | Since `CalculatorEndpoint` is `abstract`, it will not be exposed on the server. `MyCalculatorEndpoint` will expose both the `add` and `addBig` methods, since `addBig` was overridden and thus lost the `@ignoreEndpoint` annotation.
283 |
284 | ### Building base endpoints for behavior
285 |
286 | Endpoint subclassing is not just useful to inherit (or hide) methods, it can also be used to pre-configure any other property of the `Endpoint` class.
287 |
288 | For example, you could define a base class that requires callers to be logged in:
289 |
290 | ```dart
291 | abstract class LoggedInEndpoint extends Endpoint {
292 | @override
293 | bool get requireLogin => true;
294 | }
295 | ```
296 |
297 | And now every endpoint that extends `LoggedInEndpoint` will check that the user is logged in.
298 |
299 | Similarly, you could wrap up a specific set of required scopes in a base endpoint, which you can then easily use for the app's endpoints instead of repeating the scopes in each:
300 |
301 | ```dart
302 | abstract class AdminEndpoint extends Endpoint {
303 | @override
304 | Set<Scope> get requiredScopes => {Scope.admin};
305 | }
306 | ```
307 |
308 | Again, just have your custom endpoint extend `AdminEndpoint` and you can be sure that the user has the appropriate permissions.
309 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/03-serialization.md:
--------------------------------------------------------------------------------
1 | # Custom serialization
2 |
3 | For most purposes, you will want to use Serverpod's native serialization. However, there may be cases where you want to serialize more advanced objects. With Serverpod, you can pass any serializable objects as long as they conform to three simple rules:
4 |
5 | 1. Your objects must have a method called `toJson()` which returns a JSON serialization of the object.
6 |
7 | ```dart
8 | Map<String, dynamic> toJson() {
9 | return {
10 | name: 'John Doe',
11 | };
12 | }
13 | ```
14 |
15 | 2. There must be a constructor or factory called `fromJson()`, which takes a JSON serialization as parameters.
16 |
17 | ```dart
18 | factory ClassName.fromJson(
19 | Map<String, dynamic> json,
20 | ) {
21 | return ClassName(
22 | name: json['name'] as String,
23 | );
24 | }
25 | ```
26 |
27 | 3. There must be a method called `copyWith()`, which returns a new instance of the object with the specified fields replaced.
28 | :::tip
29 | 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.
30 | :::
31 |
32 | ```dart
33 | ClassName copyWith({
34 | String? name,
35 | }) {
36 | return ClassName(
37 | name: name ?? this.name,
38 | );
39 | }
40 | ```
41 |
42 | 4. You must declare your custom serializable objects 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.
43 |
44 | ```yaml
45 | ...
46 | extraClasses:
47 | - package:my_project_shared/my_project_shared.dart:ClassName
48 | ```
49 |
50 | ## Setup example
51 |
52 | We recommend creating a new dart package specifically for sharing these types of classes and importing it into the server and client `pubspec.yaml`. This can easily be done by running `$ dart create -t package <my_project>_shared` in the root folder of your project.
53 |
54 | Your folder structure should then look like this:
55 |
56 | ```text
57 | ├── my_project_client
58 | ├── my_project_flutter
59 | ├── my_project_server
60 | ├── my_project_shared
61 | ```
62 |
63 | Then you need to update both your `my_project_server/pubspec.yaml` and `my_project_client/pubspec.yaml` and add the new package as a dependency.
64 |
65 | ```yaml
66 | dependencies:
67 | ...
68 | my_project_shared:
69 | path: ../my_project_shared
70 | ...
71 | ```
72 |
73 | Now you can create your custom class in your new shared package:
74 |
75 | ```dart
76 | class ClassName {
77 | String name;
78 | ClassName(this.name);
79 |
80 | toJson() {
81 | return {
82 | 'name': name,
83 | };
84 | }
85 |
86 | factory ClassName.fromJson(
87 | Map<String, dynamic> jsonSerialization,
88 | ) {
89 | return ClassName(
90 | jsonSerialization['name'],
91 | );
92 | }
93 | }
94 | ```
95 |
96 | After adding a new serializable class, you must run `serverpod generate`. You are now able to use this class in your endpoints and leverage the full serialization/deserialization management that comes with Serverpod.
97 |
98 | In your server project, you can create an endpoint returning your custom object.
99 |
100 | ```dart
101 | import 'package:relation_test_shared/relation_test_shared.dart';
102 | import 'package:serverpod/serverpod.dart';
103 |
104 | class ExampleEndpoint extends Endpoint {
105 | Future<ClassName> getMyCustomClass(Session session) async {
106 | return ClassName(
107 | 'John Doe',
108 | );
109 | }
110 | }
111 | ```
112 |
113 | ## Custom class with Freezed
114 |
115 | Serverpod also has support for using custom classes created with the [Freezed](https://pub.dev/packages/freezed) package.
116 |
117 | ```dart
118 | import 'package:freezed_annotation/freezed_annotation.dart';
119 |
120 | part 'freezed_custom_class.freezed.dart';
121 | part 'freezed_custom_class.g.dart';
122 |
123 | @freezed
124 | class FreezedCustomClass with _$FreezedCustomClass {
125 | const factory FreezedCustomClass({
126 | required String firstName,
127 | required String lastName,
128 | required int age,
129 | }) = _FreezedCustomClass;
130 |
131 | factory FreezedCustomClass.fromJson(
132 | Map<String, Object?> json,
133 | ) =>
134 | _$FreezedCustomClassFromJson(json);
135 | }
136 | ```
137 |
138 | In the config/generator.yaml, you declare the package and the class:
139 |
140 | ```yaml
141 | extraClasses:
142 | - package:my_shared_package/my_shared_package.dart:FreezedCustomClass
143 | ```
144 |
145 | ## Custom class with ProtocolSerialization
146 |
147 | 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.
148 |
149 | ### Implementation Example
150 |
151 | Here’s how you can implement it:
152 |
153 | ```dart
154 | class CustomClass implements ProtocolSerialization {
155 | final String? value;
156 | final String? serverSideValue;
157 |
158 | .......
159 |
160 | // Serializes fields specifically for protocol communication
161 | Map<String, dynamic> toJsonForProtocol() {
162 | return {
163 | "value":value,
164 | };
165 | }
166 |
167 | // Serializes all fields, including those intended only for server-side use
168 | Map<String, dynamic> toJson() {
169 | return {
170 | "value": value,
171 | "serverSideValue": serverSideValue,
172 | };
173 | }
174 | }
175 | ```
176 |
177 | This structure ensures that sensitive or server-only data is not exposed to the client, enhancing security and data integrity.
178 |
179 | Importantly, this implementation is not required for client-side custom models.
180 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/04-exceptions.md:
--------------------------------------------------------------------------------
1 | # Error handling and exceptions
2 |
3 | Handling errors well is essential when you are building your server. To simplify things, Serverpod allows you to throw an exception on the server, serialize it, and catch it in your client app.
4 |
5 | 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 will throw a non-specific ServerpodException, which provides no more data than a session id number which can help identifiy the call in your logs.
6 |
7 | :::tip
8 |
9 | Use the Serverpod Insights app to view your logs. It will show any failed or slow calls and will make it easy to pinpoint any errors in your server.
10 |
11 | :::
12 |
13 | ## Serializable exceptions
14 |
15 | Serverpod allows adding data to an exception you throw on the server and extracting that data in the client. This is useful for passing error messages back to the client when a call fails. You use the same YAML-files to define the serializable exceptions as you would with any serializable model (see [serialization](serialization) for details). The only difference is that you use the keyword `exception` instead of `class`.
16 |
17 | ```yaml
18 | exception: MyException
19 | fields:
20 | message: String
21 | errorType: MyEnum
22 | ```
23 |
24 | After you run `serverpod generate`, you can throw that exception when processing a call to the server.
25 |
26 | ```dart
27 | class ExampleEndpoint extends Endpoint {
28 | Future<void> doThingy(Session session) {
29 | // ... do stuff ...
30 | if (failure) {
31 | throw MyException(
32 | message: 'Failed to do thingy',
33 | errorType: MyEnum.thingyError,
34 | );
35 | }
36 | }
37 | }
38 | ```
39 |
40 | In your app, catch the exception as you would catch any exception.
41 |
42 | ```dart
43 | try {
44 | client.example.doThingy();
45 | }
46 | on MyException catch(e) {
47 | print(e.message);
48 | }
49 | catch(e) {
50 | print('Something else went wrong.');
51 | }
52 | ```
53 |
54 | ### Default values in exceptions
55 |
56 | Serverpod allows you to specify default values for fields in exceptions, similar to how it's done in models using the `default` and `defaultModel` keywords. If you're unfamiliar with how these keywords work, you can refer to the [Default Values](models#default-values) section in the [Working with Models](models) documentation.
57 |
58 | :::info
59 | Since exceptions are not persisted in the database, the `defaultPersist` keyword is not supported. If both `default` and `defaultModel` are specified, `defaultModel` will always take precedence, making it unnecessary to use both.
60 | :::
61 |
62 | **Example:**
63 |
64 | ```yaml
65 | exception: MyException
66 | fields:
67 | message: String, default="An error occurred"
68 | errorCode: int, default=1001
69 | ```
70 |
71 | In this example:
72 |
73 | - The `message` field will default to `"An error occurred"` if not provided.
74 | - The `errorCode` field will default to `1001`.
75 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/05-sessions.md:
--------------------------------------------------------------------------------
1 | # Sessions
2 |
3 | The `Session` object provides information about the current context in a method call in Serverpod. It provides access to the database, caching, authentication, data storage, and messaging within the server. It will also contain information about the HTTP request object.
4 |
5 | If you need additional information about a call, you may need to cast the Session to one of its subclasses, e.g., `MethodCallSession` or `StreamingSession`. The `MethodCallSession` object provides additional properties, such as the name of the endpoint and method and the underlying `HttpRequest` object.
6 |
7 | :::tip
8 |
9 | You can use the Session object to access the IP address of the client calling a method. Serverpod includes an extension on `HttpRequest` that allows you to access the IP address even if your server is running behind a load balancer.
10 |
11 | ```dart
12 | session as MethodCallSession;
13 | var ipAddress = session.httpRequest.remoteIpAddress;
14 | ```
15 |
16 | :::
17 |
18 | ## Creating sessions
19 |
20 | In most cases, Serverpod manages the life cycle of the Session objects for you. A session is created for a call or a streaming connection and is disposed of when the call has been completed. In rare cases, you may want to create a session manually. For instance, if you are making a database call outside the scope of a method or a future call. In these cases, you can create a new session with the `createSession` method of the `Serverpod` singleton. You can access the singleton by the static `Serverpod.instance` field. If you create a new session, you are also responsible for closing it using the `session.close()` method.
21 |
22 | :::note
23 |
24 | It's not recommended to keep a session open indefinitely as it can lead to memory leaks, as the session stores logs until it is closed. It's inexpensive to create a new session, so keep them short.
25 |
26 | :::
27 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/06-database/01-connection.md:
--------------------------------------------------------------------------------
1 | # Connection
2 |
3 | In Serverpod the connection details and password for the database are stored inside the `config` directory in your server package. Serverpod automatically establishes a connection to the Postgres instance by using these configuration details when you start the server.
4 |
5 | The easiest way to get started is to use a Docker container to run your local Postgres server, and this is how Serverpod is set up out of the box. This page contains more detailed information if you want to connect to another database instance or run Postgres locally yourself.
6 |
7 |
8 | ### Connection details
9 |
10 | Each environment configuration contains a `database` keyword that specifies the connection details.
11 | For your development build you can find the connection details in the `config/development.yaml` file.
12 |
13 | This is an example:
14 |
15 | ```yaml
16 | ...
17 | database:
18 | host: localhost
19 | port: 8090
20 | name: <YOUR_PROJECT_NAME>
21 | user: postgres
22 | ...
23 | ```
24 |
25 | 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.
26 |
27 | :::caution
28 |
29 | By default, Postgres is listening for connections on port 5432. However, the Docker container shipped with Serverpod uses port 8090 to avoid conflicts. If you host your own instance, double-check that the correct port is specified in your configuration files.
30 |
31 | :::
32 |
33 | #### Configure search paths
34 |
35 | You can customize the search paths for your database connection—helpful if you're working with multiple schemas. By default, Postgres uses the `public` schema unless otherwise specified.
36 |
37 | To override this, use the optional `searchPaths` setting in your configuration:
38 |
39 | ```yaml
40 | ...
41 | database:
42 | host: localhost
43 | port: 8090
44 | name: <YOUR_PROJECT_NAME>
45 | user: postgres
46 | searchPaths: custom, public
47 | ...
48 | ```
49 |
50 | In this example, Postgres will look for tables in the `custom` schema first, and then fall back to `public` if needed. This gives you more control over where your data lives and how it’s accessed
51 |
52 |
53 | ### Database password
54 |
55 | 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.
56 |
57 | An example of this could look like this:
58 |
59 | ```yaml
60 | ...
61 | development:
62 | database: '<MY DATABASE PASSWORD>'
63 | ...
64 | ```
65 |
66 | ## Development database
67 |
68 | A newly created Serverpod project has a preconfigured Docker instance with a Postgres database set up. Run the following command from the root of the `server` package to start the database:
69 |
70 | ```bash
71 | $ docker compose up --build --detach
72 | ```
73 |
74 | To stop the database run:
75 |
76 | ```bash
77 | $ docker compose stop
78 | ```
79 |
80 | To remove the database and __delete__ all associated data, run:
81 |
82 | ```bash
83 | $ docker compose down -v
84 | ```
85 |
86 | ## Connecting to a custom Postgres instance
87 |
88 | Just like you can connect to the Postgres database inside the Docker container, you can connect to any other Postgres instance. There are a few things you need to take into consideration:
89 |
90 | - Make sure that your Postgres instance is up and running and is reachable from your Serverpod server.
91 | - You will need to create a user with a password, and a database.
92 |
93 | ### Connecting to a local Postgres server
94 |
95 | If you want to connect to a local Postgres Server (with the default setup) then the `development.yaml` will work fine if you set the correct port, user, database, and update the password in the `passwords.yaml` file.
96 |
97 | ### Connecting to a remote Postgres server
98 |
99 | To connect to a remote Postgres server (that you have installed on a VPS or VDS), you need to follow a couple of steps:
100 |
101 | - Make sure that the Postgres server has a reachable network address and that it accepts incoming traffic.
102 | - You may need to open the database port on the machine. This may include configuring its firewall.
103 | - Update your Serverpod `database` config to use the public network address, database name, port, user, and password.
104 |
105 |
106 | ### Connecting to Google Cloud SQL
107 |
108 | You can connect to a Google Cloud SQL Postgres instance in two ways:
109 |
110 | 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_.
111 | 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`.
112 |
113 | The next step is to update the database password in `passwords.yaml` and the connection details for the desired environment in the `config` folder.
114 |
115 | :::info
116 |
117 | 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.
118 |
119 | :::
120 |
121 | ### Connecting to AWS RDS
122 |
123 | You can connect to an AWS RDS Instance in two ways:
124 | 1. Enable public access to the database and configure VPC/Subnets to accept your Serverpod's IP address.
125 | 2. Use the Endpoint `database-name.some-unique-id.server-location.rds.amazonaws.com` to connect to it from AWS ECS.
126 |
127 | The next step is to update the database password in `passwords.yaml` and the connection details for the desired environment in the `config` folder.
128 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/06-database/02-models.md:
--------------------------------------------------------------------------------
1 | # Models
2 |
3 | It's possible to map serializable models to tables in your database. To do this, add the `table` key to your yaml file:
4 |
5 | ```yaml
6 | class: Company
7 | table: company
8 | fields:
9 | name: String
10 | ```
11 |
12 | When the `table` keyword is added to the model, the `serverpod generate` command will generate new methods for [interacting](crud) with the database. The addition of the keyword will also be detected by the `serverpod create-migration` command that will generate the necessary [migrations](migrations) needed to update the database.
13 |
14 | :::info
15 |
16 | 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.
17 |
18 | :::
19 |
20 | ### Non persistent fields
21 |
22 | You can opt out of creating a column in the database for a specific field by using the `!persist` keyword.
23 |
24 | ```yaml
25 | class: Company
26 | table: company
27 | fields:
28 | name: String, !persist
29 | ```
30 |
31 | All fields are persisted by default and have an implicit `persist` set on each field.
32 |
33 | ### Data representation
34 |
35 | Storing a field with a primitive / core dart type will be handled as its respective type. However, if you use a complex type, such as another model, a `List`, or a `Map`, these will be stored as a `json` object in the database.
36 |
37 | ```yaml
38 | class: Company
39 | table: company
40 | fields:
41 | address: Address # Stored as a json column
42 | ```
43 |
44 | 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.
45 |
46 | This creates a database relation between two tables and always keeps the data in sync.
47 |
48 | ```yaml
49 | class: Company
50 | table: company
51 | fields:
52 | address: Address?, relation
53 | ```
54 |
55 | For a complete guide on how to work with relations see the [relation section](relations/one-to-one).
56 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/06-database/03-relations/01-one-to-one.md:
--------------------------------------------------------------------------------
1 | # One-to-one
2 |
3 | One-to-one (1:1) relationships represent a unique association between two entities, there is at most one model that 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.
4 |
5 | ## Defining the Relationship
6 |
7 | In the following examples we show how to configure a 1:1 relationship between `User` and `Address`.
8 |
9 | ### With an id field
10 |
11 | In the most simple case, all we have to do is add an `id` field on one of the models.
12 |
13 | ```yaml
14 | # address.yaml
15 | class: Address
16 | table: address
17 | fields:
18 | street: String
19 |
20 | # user.yaml
21 | class: User
22 | table: user
23 | fields:
24 | addressId: int, relation(parent=address) // Foreign key field
25 | indexes:
26 | user_address_unique_idx:
27 | fields: addressId
28 | unique: true
29 | ```
30 |
31 | In the example, the `relation` keyword annotates the `addressId` field to hold the foreign key. The field needs to be of type `int` and 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.
32 |
33 | 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, change the datatype from `int` to `int?`.
34 |
35 | When fetching a `User` from the database the `addressId` field will automatically be populated with the related `Address` object `id`.
36 |
37 | ### With an object
38 |
39 | 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.
40 |
41 | ```yaml
42 | # address.yaml
43 | class: Address
44 | table: address
45 | fields:
46 | street: String
47 |
48 | # user.yaml
49 | class: User
50 | table: user
51 | fields:
52 | address: Address?, relation // Object relation field
53 | indexes:
54 | user_address_unique_idx:
55 | fields: addressId
56 | unique: true
57 | ```
58 |
59 | 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?`.
60 |
61 | 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`.
62 |
63 | The object field, in this case `address`, must always be nullable (as indicated by `Address?`).
64 |
65 | An object relation field gives a big advantage when fetching data. Utilizing [relational queries](../relation-queries) enables filtering based on relation attributes or optionally including the related data in the result.
66 |
67 | No `parent` keyword is needed here because the relational table is inferred from the type on the field.
68 |
69 | ### Optional relation
70 |
71 | ```yaml
72 | # user.yaml
73 | class: User
74 | table: user
75 | fields:
76 | address: Address?, relation(optional)
77 | indexes:
78 | user_address_unique_idx:
79 | fields: addressId
80 | unique: true
81 | ```
82 |
83 | 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.
84 |
85 | ### Custom foreign key field
86 |
87 | Serverpod also provides a way to customize the name of the foreign key field used in an object relation.
88 |
89 | ```yaml
90 | # user.yaml
91 | class: User
92 | table: user
93 | fields:
94 | customIdField: int
95 | address: Address?, relation(field=customIdField)
96 | indexes:
97 | user_address_unique_idx:
98 | fields: customIdField
99 | unique: true
100 | ```
101 |
102 | In this example, we define a custom foreign key field with the `field` parameter. The argument defines what field that is used as the foreign key field. In this case, `customIdField` is used instead of the default auto-generated name.
103 |
104 | If you want the custom foreign key to be nullable, simply define its type as `int?`. Note that the `field` keyword cannot be used in conjunction with the `optional` keyword. Instead, directly mark the field as nullable.
105 |
106 | ### Generated SQL
107 |
108 | The following code block shows how to set up the same relation with raw SQL. Serverpod will generate this code behind the scenes.
109 |
110 | ```sql
111 | CREATE TABLE "address" (
112 | "id" serial PRIMARY KEY,
113 | "street" text NOT NULL
114 | );
115 |
116 | CREATE TABLE "user" (
117 | "id" serial PRIMARY KEY,
118 | "addressId" integer NOT NULL
119 | );
120 |
121 |
122 | CREATE UNIQUE INDEX "user_address_unique_idx" ON "user" USING btree ("addressId");
123 |
124 | ALTER TABLE ONLY "user"
125 | ADD CONSTRAINT "user_fk_0"
126 | FOREIGN KEY("addressId")
127 | REFERENCES "address"("id")
128 | ON DELETE CASCADE
129 | ON UPDATE NO ACTION;
130 | ```
131 |
132 | ## Independent relations defined on both sides
133 |
134 | 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.
135 |
136 | ```yaml
137 | # user.yaml
138 | class: User
139 | table: user
140 | fields:
141 | friendsAddress: Address?, relation
142 | indexes:
143 | user_address_unique_idx:
144 | fields: friendsAddressId
145 | unique: true
146 |
147 | # address.yaml
148 | class: Address
149 | table: address
150 | fields:
151 | street: String
152 | resident: User?, relation
153 | indexes:
154 | address_user_unique_idx:
155 | fields: residentId
156 | unique: true
157 | ```
158 |
159 | Both relations operate independently of each other, resulting in two distinct relationships with their respective unique indexes.
160 |
161 | ## Bidirectional relations
162 |
163 | If access to the same relation is desired from both sided, a bidirectional relation can be defined.
164 |
165 | ```yaml
166 | # user.yaml
167 | class: User
168 | table: user
169 | fields:
170 | addressId: int
171 | address: Address?, relation(name=user_address, field=addressId)
172 | indexes:
173 | user_address_unique_idx:
174 | fields: addressId
175 | unique: true
176 |
177 | # address.yaml
178 | class: Address
179 | table: address
180 | fields:
181 | street: String
182 | user: User?, relation(name=user_address)
183 | ```
184 |
185 | The example illustrates a 1:1 relationship between User and Address where both sides of the relationship are explicitly specified.
186 |
187 | 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.
188 |
189 | Without specifying the `name` parameter, you'd end up with two unrelated relationships.
190 |
191 | When the relationship is defined on both sides, it's **required** to specify the `field` keyword. This is because Serverpod cannot automatically determine which side should hold the foreign key field. You decide which side is most logical for your data.
192 |
193 | In a relationship where there is an object on both sides a unique index is always **required** on the foreign key field.
194 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/06-database/03-relations/02-one-to-many.md:
--------------------------------------------------------------------------------
1 | # One-to-many
2 |
3 | One-to-many (1:n) relationships describes a scenario where multiple records from one table can relate to a single record in another table. An example of this would the relationship between a company and its employees, where multiple employees can be employed at a single company.
4 |
5 | The Serverpod framework provides versatility in establishing these relations. Depending on the specific use case and clarity desired, you can define the model relationship either from the 'many' side (like `Employee`) or the 'one' side (like `Company`).
6 |
7 | ## Defining the relationship
8 |
9 | In the following examples we show how to configure a 1:n relationship between `Company` and `Employee`.
10 |
11 | ### Implicit definition
12 |
13 | With an implicit setup, Serverpod determines and establishes the relationship based on the table and class structures.
14 |
15 | ```yaml
16 | # company.yaml
17 | class: Company
18 | table: company
19 | fields:
20 | name: String
21 | employees: List<Employee>?, relation
22 |
23 | # employee.yaml
24 | class: Employee
25 | table: employee
26 | fields:
27 | name: String
28 | ```
29 |
30 | In the example, we define a 1:n relation between `Company` and `Employee` by using the `List<Employee>` type on the `employees` field together with the `relation` keyword.
31 |
32 | The corresponding foreign key field is automatically integrated into the 'many' side (e.g., `Employee`) as a concealed column.
33 |
34 | When fetching companies it now becomes possible to include any or all employees in the query. 1:n relations also enables additional [filtering](../filter#one-to-many) and [sorting](../sort#sort-on-relations) operations for [relational queries](../relation-queries).
35 |
36 | ### Explicit definition
37 |
38 | In an explicit definition, you directly specify the relationship in a one-to-many relation.
39 |
40 | This can be done by through an [object relation](one-to-one#with-an-object):
41 |
42 | ```yaml
43 | # company.yaml
44 | class: Company
45 | table: company
46 | fields:
47 | name: String
48 |
49 | # employee.yaml
50 | class: Employee
51 | table: employee
52 | fields:
53 | name: String
54 | company: Company?, relation
55 | ```
56 |
57 | Or through a [foreign key field](one-to-one#with-an-id-field):
58 |
59 | ```yaml
60 | # company.yaml
61 | class: Company
62 | table: company
63 | fields:
64 | name: String
65 |
66 | # employee.yaml
67 | class: Employee
68 | table: employee
69 | fields:
70 | name: String
71 | companyId: int, relation
72 | ```
73 |
74 | 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.
75 |
76 | ## Bidirectional relations
77 |
78 | For a more comprehensive representation, you can define the relationship from both sides.
79 |
80 | Either through an [object relation](one-to-one#with-an-object) on the many side:
81 |
82 | ```yaml
83 | # company.yaml
84 | class: Company
85 | table: company
86 | fields:
87 | name: String
88 | employees: List<Employee>?, relation(name=company_employees)
89 |
90 | # employee.yaml
91 | class: Employee
92 | table: employee
93 | fields:
94 | name: String
95 | company: Company?, relation(name=company_employees)
96 | ```
97 |
98 | Or through a [foreign key field](one-to-one#with-an-id-field) on the many side:
99 |
100 | ```yaml
101 | # company.yaml
102 | class: Company
103 | table: company
104 | fields:
105 | name: String
106 | employees: List<Employee>?, relation(name=company_employees)
107 |
108 | # employee.yaml
109 | class: Employee
110 | table: employee
111 | fields:
112 | name: String
113 | companyId: int, relation(name=company_employees, parent=company)
114 | ```
115 |
116 | Just as in the 1:1 examples, the `name` parameter with a unique string that links both sides together.
117 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/06-database/03-relations/03-many-to-many.md:
--------------------------------------------------------------------------------
1 | # Many-to-many
2 |
3 | Many-to-many (n:m) relationships describes 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.
4 |
5 | The Serverpod framework supports these complex relationships by explicitly creating a separate model, often called a junction or bridge table, that records the relation.
6 |
7 | ## Overview
8 |
9 | In the context of many-to-many relationships, neither table contains a direct reference to the other. Instead, a separate table holds the foreign keys of both tables. This setup allows for a flexible and normalized approach to represent n:m relationships.
10 |
11 | Modeling the relationship between `Student` and `Course`, we would create an `Enrollment` model as a junction table to store the relationship explicitly.
12 |
13 | ## Defining the relationship
14 |
15 | In the following examples we show how to configure a n:m relationship between `Student` and `Course`.
16 |
17 | ### Many tables
18 |
19 | Both the `Course` and `Student` tables have a direct relationship with the `Enrollment` table but no direct relationship with each other.
20 |
21 | ```yaml
22 | # course.yaml
23 | class: Course
24 | table: course
25 | fields:
26 | name: String
27 | enrollments: List<Enrollment>?, relation(name=course_enrollments)
28 | ```
29 |
30 | ```yaml
31 | # student.yaml
32 | class: Student
33 | table: student
34 | fields:
35 | name: String
36 | enrollments: List<Enrollment>?, relation(name=student_enrollments)
37 | ```
38 |
39 | 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 both many tables, `Course` and `Student`.
40 |
41 | ### Junction table
42 |
43 | The `Enrollment` table acts as the bridge between `Course` and `Student`. It contains foreign keys from both tables, representing the many-to-many relationship.
44 |
45 | ```yaml
46 | # enrollment.yaml
47 | class: Enrollment
48 | table: enrollment
49 | fields:
50 | student: Student?, relation(name=student_enrollments)
51 | course: Course?, relation(name=course_enrollments)
52 | indexes:
53 | enrollment_index_idx:
54 | fields: studentId, courseId
55 | unique: true
56 | ```
57 |
58 | 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.
59 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/06-database/03-relations/04-self-relations.md:
--------------------------------------------------------------------------------
1 | # Self-relations
2 |
3 | A self-referential or self-relation occurs when a table has a foreign key that references its own primary key within the same table. This creates a relationship between different rows within the same table.
4 |
5 | ## One-to-one
6 |
7 | 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.
8 |
9 | ```yaml
10 | class: Post
11 | table: post
12 | fields:
13 | content: String
14 | previous: Post?, relation(name=next_previous_post)
15 | nextId: int?
16 | next: Post?, relation(name=next_previous_post, field=nextId, onDelete=SetNull)
17 | indexes:
18 | next_unique_idx:
19 | fields: nextId
20 | unique: true
21 | ```
22 |
23 | In this example, there is a named relation holding the data on both sides of the relation. The field `nextId` is a nullable field that stores the id of the next post. It is nullable as it would be impossible to create the first entry if we already needed to have a post created. The next post represents the object on "this" side while the previous post is the corresponding object on the "other" side. Meaning that the previous post is connected to the `nextId` of the post that came before it.
24 |
25 | ## One-to-many
26 |
27 | 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 `n` kittens, for brevity, we have only modeled the mother.
28 |
29 | ```yaml
30 | class: Cat
31 | table: cat
32 | fields:
33 | name: String
34 | mother: Cat?, relation(name=cat_kittens, optional, onDelete=SetNull)
35 | kittens: List<Cat>?, relation(name=cat_kittens)
36 | ```
37 |
38 | The field `motherId: int?` is injected into the dart class, the field is nullable since we marked the field `mother` as an `optional` relation. We can now find all the kittens by looking at the `motherId` of other cats which should match the `id` field of the current cat. The other cat can instead be found by looking at the `motherId` of the current cat and matching it against one other cat `id` field.
39 |
40 | ## Many-to-many
41 |
42 | Let's imagine we have a system where we have members that can block other members. We would like to be able to query who I'm blocking and who is blocking me. This can be achieved by modeling the data as a many-to-many relation ship.
43 |
44 | Each member has a list of all other members they are blocking and another list of all members that are blocking them. But since the list side needs to point to a foreign key and cannot point to another list directly, we have to define a junction table that holds the connection between the rows.
45 |
46 | ```yaml
47 | class: Member
48 | table: member
49 | fields:
50 | name: String
51 | blocking: List<Blocking>?, relation(name=member_blocked_by_me)
52 | blockedBy: List<Blocking>?, relation(name=member_blocking_me)
53 | ```
54 |
55 | ```yaml
56 | class: Blocking
57 | table: blocking
58 | fields:
59 | blocked: Member?, relation(name=member_blocking_me, onDelete=Cascade)
60 | blockedBy: Member?, relation(name=member_blocked_by_me, onDelete=Cascade)
61 | indexes:
62 | blocking_blocked_unique_idx:
63 | fields: blockedId, blockedById
64 | unique: true
65 | ```
66 |
67 | 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 both the `blockedId` and `blockedById`, this makes sure we only ever have one entry per relation, meaning I can only block one other member one time.
68 |
69 | The cascade delete means that if a member is deleted all the blocking entries are also removed for that member.
70 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/06-database/03-relations/05-referential-actions.md:
--------------------------------------------------------------------------------
1 | # Referential actions
2 |
3 | In Serverpod, the behavior of update and delete for relations can be precisely defined using the onUpdate and onDelete properties. These properties map directly to the corresponding referential actions in PostgreSQL.
4 |
5 | ## Available referential actions
6 |
7 | | Action | Description |
8 | | --- | --- |
9 | | **NoAction** | If any constraint violation occurs, no action will be taken, and an error will be raised. |
10 | | **Restrict** | If any referencing rows still exist when the constraint is checked, an error is raised. |
11 | | **SetDefault** | The field will revert to its default value. Note: This action necessitates that a default value is configured for the field. |
12 | | **Cascade** | Any action taken on the parent (update/delete) will be mirrored in the child. |
13 | | **SetNull** | The field value is set to null. This action is permissible only if the field has been marked as optional. |
14 |
15 | ## Syntax
16 |
17 | Use the following syntax to apply referential actions
18 |
19 | ```yaml
20 | relation(onUpdate=<ACTION>, onDelete=<ACTION>)
21 | ```
22 |
23 | ## Default values
24 | If no referential actions are specified, the default behavior will be applied.
25 |
26 | If the relation is defined as an [object relation](one-to-one#with-an-object), the default behavior is `NoAction` for both onUpdate and onDelete.
27 |
28 | ```yaml
29 | parent: Model?, relation(onUpdate=NoAction, onDelete=NoAction)
30 | ```
31 |
32 |
33 | If the relation is defined as an [id relation](one-to-one#with-an-id-field), the default behavior is `NoAction` for onUpdate and `Cascade` for onDelete.
34 |
35 |
36 | ```yaml
37 | parentId: int?, relation(parent=model_table, onUpdate=NoAction, onDelete=Cascade)
38 | ```
39 |
40 | :::info
41 |
42 | The sequence of onUpdate and onDelete is interchangeable.
43 |
44 | :::
45 |
46 | ### Full example
47 |
48 | ```yaml
49 | class: Example
50 | table: example
51 | fields:
52 | parentId: int?, relation(parent=example, onUpdate=SetNull, onDelete=NoAction)
53 | ```
54 |
55 | 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.
56 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/06-database/03-relations/06-modules.md:
--------------------------------------------------------------------------------
1 | # Relations with modules
2 |
3 | Serverpod [modules](../../modules) 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 some restrictions since you do not own the definition of the table, you cannot change the table structure of a module table.
4 |
5 | Since you do not directly control the models inside the modules it is recommended to create a so-called "bridge" table/model linking the module's model to your own. This can be done in the same way we normally would setup a one-to-one relation.
6 |
7 | ```yaml
8 | class: User
9 | table: user
10 | fields:
11 | userInfo: module:auth:UserInfo?, relation
12 | age: int
13 | indexes:
14 | user_info_id_unique_idx:
15 | fields: userInfoId
16 | unique: true
17 | ```
18 |
19 | Or by referencing the table name if you only want to access the id.
20 |
21 | ```yaml
22 | class: User
23 | table: user
24 | fields:
25 | userInfoId: int, relation(parent=serverpod_user_info)
26 | age: int
27 | indexes:
28 | user_info_id_unique_idx:
29 | fields: userInfoId
30 | unique: true
31 | ```
32 |
33 | It is now possible to make any other relation to our model as described in [one-to-one](./one-to-one), [one-to-many](./one-to-many), [many-to-many](./many-to-many) and [self-relations](./self-relations).
34 |
35 | ## Advanced example
36 |
37 | A one-to-many relation with the "bridge" table could look like this.
38 |
39 | ```yaml
40 | class: User
41 | table: user
42 | fields:
43 | userInfo: module:auth:UserInfo?, relation
44 | age: int
45 | company: Company?, relation(name=company_employee)
46 | indexes:
47 | user_info_id_unique_idx:
48 | fields: userInfoId
49 | unique: true
50 | company_unique_idx:
51 | fields: companyId
52 | unique: true
53 | ```
54 |
55 | ```yaml
56 | class: Company
57 | table: company
58 | fields:
59 | name: String
60 | employees: List<User>?, relation(name=company_employee)
61 | ```
62 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/06-database/04-indexing.md:
--------------------------------------------------------------------------------
1 | # Indexing
2 |
3 | For performance reasons, you may want to add indexes to your database tables. These are added in the YAML-files defining the serializable objects.
4 |
5 | ### Add an index
6 |
7 | To add an index, add an `indexes` section to the YAML-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.
8 |
9 | ```yaml
10 | class: Company
11 | table: company
12 | fields:
13 | name: String
14 | indexes:
15 | company_name_idx:
16 | fields: name
17 | ```
18 |
19 | 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.
20 |
21 | ```yaml
22 | class: Company
23 | table: company
24 | fields:
25 | name: String
26 | foundedAt: DateTime
27 | indexes:
28 | company_idx:
29 | fields: name, foundedAt
30 | ```
31 |
32 | ### Making fields unique
33 |
34 | Adding a unique index ensures that the value or combination of values stored in the fields are unique for the table. This can be useful for example if you want to make sure that no two companies have the same name.
35 |
36 | ```yaml
37 | class: Company
38 | table: company
39 | fields:
40 | name: String
41 | indexes:
42 | company_name_idx:
43 | fields: name
44 | unique: true
45 | ```
46 |
47 | The `unique` keyword is a bool that can toggle the index to be unique, the default is set to false. If the `unique` keyword is applied to a multi-column index, the index will be unique for the combination of the fields.
48 |
49 | ### Specifying index type
50 |
51 | It is possible to add a type key to specify the index type.
52 |
53 | ```yaml
54 | class: Company
55 | table: company
56 | fields:
57 | name: String
58 | indexes:
59 | company_name_idx:
60 | fields: name
61 | type: brin
62 | ```
63 |
64 | If no type is specified the default is `btree`. All [PostgreSQL index types](https://www.postgresql.org/docs/current/indexes-types.html) are supported, `btree`, `hash`, `gist`, `spgist`, `gin`, `brin`.
65 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/06-database/05-crud.md:
--------------------------------------------------------------------------------
1 | # CRUD
2 |
3 | To interact with the database you need a [`Session`](../sessions) 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.
4 |
5 | For the following examples we will use this model:
6 |
7 | ```yaml
8 | class: Company
9 | table: company
10 | fields:
11 | name: String
12 | ```
13 |
14 | :::note
15 |
16 | 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.
17 |
18 | :::
19 |
20 | ## Create
21 |
22 | There are two ways to create a new row in the database.
23 |
24 | ### Inserting a single row
25 |
26 | 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.
27 |
28 | ```dart
29 | var row = Company(name: 'Serverpod');
30 | var company = await Company.db.insertRow(session, row);
31 | ```
32 |
33 | ### Inserting several rows
34 |
35 | 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.
36 |
37 | ```dart
38 | var rows = [Company(name: 'Serverpod'), Company(name: 'Google')];
39 | var companies = await Company.db.insert(session, rows);
40 | ```
41 |
42 | :::info
43 | In previous versions of Serverpod the `insert` method mutated the input object by setting the `id` field. In the example above the input variable remains unmodified after the `insert`/`insertRow` call.
44 | :::
45 |
46 | ## Read
47 |
48 | There are three different read operations available.
49 |
50 | ### Finding by id
51 |
52 | You can retrieve a single row by its `id`.
53 |
54 | ```dart
55 | var company = await Company.db.findById(session, companyId);
56 | ```
57 |
58 | This operation either returns the model or `null`.
59 |
60 | ### Finding a single row
61 |
62 | You can find a single row using an expression.
63 |
64 | ```dart
65 | var company = await Company.db.findFirstRow(
66 | session,
67 | where: (t) => t.name.equals('Serverpod'),
68 | );
69 | ```
70 |
71 | This operation returns the first model matching the filtering criteria or `null`. See [filter](filter) and [sort](sort) for all filter operations.
72 |
73 | :::info
74 | 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.
75 | :::
76 |
77 | ### Finding multiple rows
78 |
79 | To find multiple rows, use the same principle as for finding a single row.
80 |
81 | ```dart
82 | var companies = await Company.db.find(
83 | session,
84 | where: (t) => t.id < 100,
85 | limit: 50,
86 | );
87 | ```
88 |
89 | This operation returns a `List` of your models matching the filtering criteria.
90 |
91 | See [filter](filter) and [sort](sort) for all filter and sorting operations and [pagination](pagination) for how to paginate the result.
92 |
93 | ## Update
94 |
95 | There are two update operations available.
96 |
97 | ### Update a single row
98 |
99 | To update a single row, use the `updateRow` method.
100 |
101 | ```dart
102 | var company = await Company.db.findById(session, companyId); // Fetched company has its id set
103 | company.name = 'New name';
104 | var updatedCompany = await Company.db.updateRow(session, company);
105 | ```
106 |
107 | 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.
108 |
109 | ### Update several rows
110 |
111 | To batch update several rows use the `update` method.
112 |
113 | ```dart
114 | var companies = await Company.db.find(session);
115 | companies = companies.map((c) => c.copyWith(name: 'New name')).toList();
116 | var updatedCompanies = await Company.db.update(session, companies);
117 | ```
118 |
119 | 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.
120 |
121 | ### Update a specific column
122 |
123 | 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.
124 |
125 | Update a single row, the following code will update the company name, but will not change the address column.
126 |
127 | ```dart
128 | var company = await Company.db.findById(session, companyId);
129 | company.name = 'New name';
130 | company.address = 'Baker street';
131 | var updatedCompany = await Company.db.updateRow(session, company, columns: (t) => [t.name]);
132 | ```
133 |
134 | The same syntax is available for multiple rows.
135 |
136 | ```dart
137 | var companies = await Company.db.find(session);
138 | companies = companies.map((c) => c.copyWith(name: 'New name', address: 'Baker Street')).toList();
139 | var updatedCompanies = await Company.db.update(session, companies, columns: (t) => [t.name]);
140 | ```
141 |
142 | ## Delete
143 |
144 | Deleting rows from the database is done in a similar way to updating rows. However, there are three delete operations available.
145 |
146 | ### Delete a single row
147 |
148 | To delete a single row, use the `deleteRow` method.
149 |
150 | ```dart
151 | var company = await Company.db.findById(session, companyId); // Fetched company has its id set
152 | var companyDeleted = await Company.db.deleteRow(session, company);
153 | ```
154 |
155 | The input object needs to have the `id` field set. The `deleteRow` method returns the deleted model.
156 |
157 | ### Delete several rows
158 |
159 | To batch delete several rows, use the `delete` method.
160 |
161 | ```dart
162 | var companiesDeleted = await Company.db.delete(session, companies);
163 | ```
164 |
165 | 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.
166 |
167 | ### Delete by filter
168 |
169 | You can also do a [filtered](filter) delete and delete all entries matching a `where` query, by using the `deleteWhere` method.
170 |
171 | ```dart
172 | var companiesDeleted = await Company.db.deleteWhere(
173 | session,
174 | where: (t) => t.name.like('%Ltd'),
175 | );
176 | ```
177 |
178 | The above example will delete any row that ends in *Ltd*. The `deleteWhere` method returns a `List` of the models deleted.
179 |
180 | ## Count
181 |
182 | Count is a special type of query that helps counting the number of rows in the database that matches a specific [filter](filter).
183 |
184 | ```dart
185 | var count = await Company.db.count(
186 | session,
187 | where: (t) => t.name.like('s%'),
188 | );
189 | ```
190 |
191 | The return value is an `int` for the number of rows matching the filter.
192 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/06-database/06-filter.md:
--------------------------------------------------------------------------------
1 | # Filter
2 |
3 | Serverpod makes it easy to build expressions that are statically type-checked. Columns and relational fields are referenced using table descriptor objects. The table descriptors, `t`, are accessible from each model and are passed as an argument to a model specific expression builder function. A callback is then used as argument to the `where` parameter when fetching data from the database.
4 |
5 | ## Column operations
6 |
7 | The following column operations are supported in Serverpod, each column datatype supports a different set of operations that make sense for that type.
8 |
9 | :::info
10 | When using the operators, it's a good practice to place them within a set of parentheses as the precedence rules are not always what would be expected.
11 | :::
12 |
13 | ### Equals
14 |
15 | Compare a column to an exact value, meaning only rows that match exactly will remain in the result.
16 |
17 | ```dart
18 | await User.db.find(
19 | where: (t) => t.name.equals('Alice')
20 | );
21 | ```
22 |
23 | In the example we fetch all users with the name Alice.
24 |
25 | Not equals is the negated version of equals.
26 |
27 | ```dart
28 | await User.db.find(
29 | where: (t) => t.name.notEquals('Bob')
30 | );
31 | ```
32 |
33 | 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.
34 |
35 | ### Comparison operators
36 |
37 | Compare a column to a value, these operators are support for `int`, `double`, `Duration`, and `DateTime`.
38 |
39 | ```dart
40 | await User.db.find(
41 | where: (t) => t.age > 25
42 | );
43 | ```
44 |
45 | In the example we fetch all users that are older than 25 years old.
46 |
47 | ```dart
48 | await User.db.find(
49 | where: (t) => t.age >= 25
50 | );
51 | ```
52 |
53 | In the example we fetch users that are 25 years old or older.
54 |
55 | ```dart
56 | await User.db.find(
57 | where: (t) => t.age < 25
58 | );
59 | ```
60 |
61 | In the example we fetch all users that are younger than 25 years old.
62 |
63 | ```dart
64 | await User.db.find(
65 | where: (t) => t.age <= 25
66 | );
67 | ```
68 |
69 | In the example we fetch all users that are 25 years old or younger.
70 |
71 | ### Between
72 |
73 | The between method takes two values and checks if the columns value is between the two input variables *inclusively*.
74 |
75 | ```dart
76 | await User.db.find(
77 | where: (t) => t.age.between(18, 65)
78 | );
79 | ```
80 |
81 | 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)`.
82 |
83 | The 'not between' operation functions similarly to 'between' but it negates the condition. It also works inclusively with the boundaries.
84 |
85 | ```dart
86 | await User.db.find(
87 | where: (t) => t.age.notBetween(18, 65)
88 | );
89 | ```
90 |
91 | 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)`.
92 |
93 | ### In set
94 |
95 | In set can be used to match with several values at once. This method functions the same as equals but for multiple values, `inSet` will make an exact comparison.
96 |
97 | ```dart
98 | await User.db.find(
99 | where: (t) => t.name.inSet({'Alice', 'Bob'})
100 | );
101 | ```
102 |
103 | 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.
104 |
105 | The 'not in set' operation functions similarly to `inSet`, but it negates the condition.
106 |
107 | ```dart
108 | await User.db.find(
109 | where: (t) => t.name.notInSet({'Alice', 'Bob'})
110 | );
111 | ```
112 |
113 | 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.
114 |
115 | ### Like
116 |
117 | Like can be used to perform match searches against `String` entries in the database, this matcher is case-sensitive. This is useful when matching against partial entries.
118 |
119 | Two special characters enables matching against partial entries.
120 |
121 | - **`%`** Matching any sequence of character.
122 | - **`_`** Matching any single character.
123 |
124 | | String | Matcher | Is matching |
125 | |--|--|--|
126 | | abc | a% | true |
127 | | abc | _b% | true |
128 | | abc | a_c | true |
129 | | abc | b_ | false |
130 |
131 | We use like to match against a partial string.
132 |
133 | ```dart
134 | await User.db.find(
135 | where: (t) => t.name.like('A%')
136 | );
137 | ```
138 |
139 | In the example we fetch all users with a name that starts with A.
140 |
141 | There is a negated version of like that can be used to exclude rows from the result.
142 |
143 | ```dart
144 | await User.db.find(
145 | where: (t) => t.name.notLike('B%')
146 | );
147 | ```
148 |
149 | In the example we fetch all users with a name that does not start with B.
150 |
151 | ### ilike
152 |
153 | `ilike` works the same as `like` but is case-insensitive.
154 |
155 | ```dart
156 | await User.db.find(
157 | where: (t) => t.name.ilike('a%')
158 | );
159 | ```
160 |
161 | In the example we fetch all users with a name that starts with a or A.
162 |
163 | There is a negated version of `ilike` that can be used to exclude rows from the result.
164 |
165 | ```dart
166 | await User.db.find(
167 | where: (t) => t.name.notIlike('b%')
168 | );
169 | ```
170 |
171 | In the example we fetch all users with a name that does not start with b or B.
172 |
173 | ### Logical operators
174 |
175 | Logical operators are also supported when filtering, allowing you to chain multiple statements together to create more complex queries.
176 |
177 | The `&` operator is used to chain two statements together with an `and` operation.
178 |
179 | ```dart
180 | await User.db.find(
181 | where: (t) => (t.name.equals('Alice') & (t.age > 25))
182 | );
183 | ```
184 |
185 | In the example we fetch all users with the name "Alice" *and* are older than 25.
186 |
187 | The `|` operator is used to chain two statements together with an `or` operation.
188 |
189 | ```dart
190 | await User.db.find(
191 | where: (t) => (t.name.like('A%') | t.name.like('B%'))
192 | );
193 | ```
194 |
195 | In the example we fetch all users that has a name that starts with A *or* B.
196 |
197 | ## Relation operations
198 |
199 | If a relation between two models is defined a [one-to-one](relations/one-to-one) or [one-to-many](relations/one-to-many) object relation, then relation operations are supported in Serverpod.
200 |
201 | ### One-to-one
202 |
203 | For 1:1 relations the columns of the relation can be accessed directly on the relation field. This enables filtering on related objects properties.
204 |
205 | ```dart
206 | await User.db.find(
207 | where: (t) => t.address.street.like('%road%')
208 | );
209 | ```
210 |
211 | In the example each user has a relation to an address that has a street field. Using relation operations we then fetch all users where the related address has a street that contains the word "road".
212 |
213 | ### One-to-many
214 |
215 | For 1:n relations, there are special filter methods where you can create sub-filters on all the related data. With them, you can answer questions on the aggregated result on many relations.
216 |
217 | #### Count
218 |
219 | 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 static value.
220 |
221 | ```dart
222 | await User.db.find(
223 | where: (t) => t.orders.count() > 3
224 | );
225 | ```
226 |
227 | In the example we fetch all users with more than three orders.
228 |
229 | We can apply a sub-filter to the `count` operator filter the related entries before they are counted.
230 |
231 | ```dart
232 | await User.db.find(
233 | where: (t) => t.orders.count((o) => o.itemType.equals('book')) > 3
234 | );
235 | ```
236 |
237 | In the example we fetch all users with more than three "book" orders.
238 |
239 | #### None
240 |
241 | None can be used to retrieve rows that have no related entries in a 1:n relation. Meaning if there exists a related entry then the row is omitted from the result. The operation is useful if you want to ensure that a many relation does not contain any related rows.
242 |
243 | ```dart
244 | await User.db.find(
245 | where: (t) => t.orders.none()
246 | );
247 | ```
248 |
249 | In the example we fetch all users that have no orders.
250 |
251 | We can apply a sub-filter to the `none` operator to filter the related entries. Meaning if there is a match in the sub-filter the row will be omitted from the result.
252 |
253 | ```dart
254 | await User.db.find(
255 | where:((t) => t.orders.none((o) => o.itemType.equals('book')))
256 | );
257 | ```
258 |
259 | In the example we fetch all users that have no "book" orders.
260 |
261 | #### Any
262 |
263 | Any works similarly to the `any` method on lists in Dart. If there exists any related entry then include the row in the result.
264 |
265 | ```dart
266 | await User.db.find(
267 | where: (t) => t.orders.any()
268 | );
269 | ```
270 |
271 | In the example we fetch all users that have any order.
272 |
273 | We can apply a sub-filter to the `any` operator to filter the related entries. Meaning if there is a match in the sub-filter the row will be included in the result.
274 |
275 | ```dart
276 | await User.db.find(
277 | where:((t) => t.orders.any((o) => o.itemType.equals('book')))
278 | );
279 | ```
280 |
281 | In the example we fetch all users that have any "book" order.
282 |
283 | #### Every
284 |
285 | 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.
286 |
287 | ```dart
288 | await User.db.find(
289 | where: (t) => t.orders.every((o) => o.itemType.equals('book'))
290 | );
291 | ```
292 |
293 | In the example we fetch all users that have only "book" orders.
294 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/06-database/07-relation-queries.md:
--------------------------------------------------------------------------------
1 | # Relation queries
2 |
3 | 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](relations/one-to-one) or [one-to-many](relations/one-to-many) object relations between two models.
4 |
5 | ## Include relational data
6 |
7 | 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.
8 |
9 | ```dart
10 | var employee = await Employee.db.findById(
11 | session,
12 | employeeId,
13 | include: Employee.include(
14 | address: Address.include(),
15 | ),
16 | );
17 | ```
18 |
19 | The example above return a employee including the related address object.
20 |
21 | ### Nested includes
22 |
23 | It is also possible to include deeply nested objects.
24 |
25 | ```dart
26 | var employee = await Employee.db.findById(
27 | session,
28 | employeeId,
29 | include: Employee.include(
30 | company: Company.include(
31 | address: Address.include(),
32 | ),
33 | ),
34 | );
35 | ```
36 |
37 | The example above returns an employee including the related company object that has the related address object included.
38 |
39 | 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.
40 |
41 | ```dart
42 | var user = await Employee.db.findById(
43 | session,
44 | employeeId,
45 | include: Employee.include(
46 | address: Address.include(),
47 | company: Company.include(
48 | address: Address.include(),
49 | ),
50 | ),
51 | );
52 | ```
53 |
54 | The example above includes several different objects configured by specifying the named parameters.
55 |
56 | ## Include relational lists
57 |
58 | 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.
59 |
60 | ```dart
61 | var user = await Company.db.findById(
62 | session,
63 | employeeId,
64 | include: Company.include(
65 | employees: Employee.includeList(),
66 | ),
67 | );
68 | ```
69 |
70 | The example above returns a company with all related employees included.
71 |
72 | ### Nested includes
73 |
74 | The `includeList` method works slightly differently from a normal `include` and to include nested objects the `includes` field must be used. When including something on a list it means that every entry in the list will each have access to the nested object.
75 |
76 | ```dart
77 | var user = await Company.db.findById(
78 | session,
79 | employeeId,
80 | include: Company.include(
81 | employees: Employee.includeList(
82 | includes: Employee.include(
83 | address: Address.include(),
84 | ),
85 | ),
86 | ),
87 | );
88 | ```
89 |
90 | The example above returns a company with all related employees included. Each employee will have the related address object included.
91 |
92 | It is even possible to include lists within lists.
93 |
94 | ```dart
95 | var user = await Company.db.findById(
96 | session,
97 | employeeId,
98 | include: Company.include(
99 | employees: Employee.includeList(
100 | includes: Employee.include(
101 | tools: Tool.includeList(),
102 | ),
103 | ),
104 | ),
105 | );
106 | ```
107 |
108 | The example above returns a company with all related employees included. Each employee will have the related tools list included.
109 |
110 | :::note
111 | For each call to includeList (nested or not) the Serverpod Framework will perform one additional query to the database.
112 | :::
113 |
114 | ### Filter and sort
115 |
116 | When working with large datasets, it's often necessary to [filter](filter) and [sort](sort) the records to retrieve the most relevant data. Serverpod offers methods to refine the included list of related objects:
117 |
118 | #### Filter
119 |
120 | Use the `where` clause to filter the results based on certain conditions.
121 |
122 | ```dart
123 | var user = await Company.db.findById(
124 | session,
125 | employeeId,
126 | include: Company.include(
127 | employees: Employee.includeList(
128 | where: (t) => t.name.ilike('a%')
129 | ),
130 | ),
131 | );
132 | ```
133 |
134 | The example above retrieves only employees whose names start with the letter 'a'.
135 |
136 | #### Sort
137 |
138 | The orderBy clause lets you sort the results based on a specific field.
139 |
140 | ```dart
141 | var user = await Company.db.findById(
142 | session,
143 | employeeId,
144 | include: Company.include(
145 | employees: Employee.includeList(
146 | orderBy: (t) => t.name,
147 | ),
148 | ),
149 | );
150 | ```
151 |
152 | The example above sorts the employees by their names in ascending order.
153 |
154 | ### Pagination
155 |
156 | [Paginate](pagination) results by specifying a limit on the number of records and an offset.
157 |
158 | ```dart
159 | var user = await Company.db.findById(
160 | session,
161 | employeeId,
162 | include: Company.include(
163 | employees: Employee.includeList(
164 | limit: 10,
165 | offset: 10,
166 | ),
167 | ),
168 | );
169 | ```
170 |
171 | The example above retrieves the next 10 employees starting from the 11th record:
172 |
173 | Using these methods in conjunction provides a powerful way to query, filter, and sort relational data efficiently.
174 |
175 | ## Update
176 |
177 | Managing relationships between tables is a common task. Serverpod provides methods to link (attach) and unlink (detach) related records:
178 |
179 | ### Attach single row
180 |
181 | Link an individual employee to a company. This operation associates an employee with a specific company:
182 |
183 | ```dart
184 | var company = await Company.db.findById(session, companyId);
185 | var employee = await Employee.db.findById(session, employeeId);
186 |
187 | await Company.db.attachRow.employees(session, company!, employee!);
188 | ```
189 |
190 | ### Bulk attach rows
191 |
192 | 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:
193 |
194 | ```dart
195 | var company = await Company.db.findById(session, companyId);
196 | var employee = await Employee.db.findById(session, employeeId);
197 |
198 | await Company.db.attach.employees(session, company!, [employee!]);
199 | ```
200 |
201 | ### Detach single row
202 |
203 | To remove the association between an employee and a company, use the detach row method:
204 |
205 | ```dart
206 | var employee = await Employee.db.findById(session, employeeId);
207 |
208 | await Company.db.detachRow.employees(session, employee!);
209 | ```
210 |
211 | ### Bulk detach rows
212 |
213 | In cases where you need to remove associations for multiple employees simultaneously, use the bulk detach method. This operation is atomic:
214 |
215 | ```dart
216 | var employee = await Employee.db.findById(session, employeeId);
217 |
218 | await Company.db.detach.employees(session, [employee!]);
219 | ```
220 |
221 | :::note
222 | When using the attach and detach methods the objects passed to them have to have the `id` field set.
223 |
224 | 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.
225 | :::
226 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/06-database/08-sort.md:
--------------------------------------------------------------------------------
1 | # Sort
2 |
3 | It is often desirable to order the results of a database query. The 'find' method has an `orderBy` parameter where you can specify a column for sorting. The parameter takes a callback as an argument that passes a model-specific table descriptor, also accessible through the `t` field on the model. The table descriptor represents the database table associated with the model and includes fields for each corresponding column. The callback is then used to specify the column to sort by.
4 |
5 | ```dart
6 | var companies = await Company.db.find(
7 | session,
8 | orderBy: (t) => t.name,
9 | );
10 | ```
11 |
12 | In the example we fetch all companies and sort them by their name.
13 |
14 | By default the order is set to ascending, this can be changed to descending by setting the param `orderDecending: true`.
15 |
16 | ```dart
17 | var companies = await Company.db.find(
18 | session,
19 | orderBy: (t) => t.name,
20 | orderDescending: true,
21 | );
22 | ```
23 |
24 | In the example we fetch all companies and sort them by their name in descending order.
25 |
26 | To order by several different columns use `orderByList`, note that this cannot be used in conjunction with `orderBy` and `orderDescending`.
27 |
28 | ```dart
29 | var companies = await Company.db.find(
30 | session,
31 | orderByList: (t) => [
32 | Order(column: t.name, orderDescending: true),
33 | Order(column: t.id),
34 | ],
35 | );
36 | ```
37 |
38 | In the example we fetch all companies and sort them by their name in descending order, and then by their id in ascending order.
39 |
40 | ## Sort on relations
41 |
42 | To sort based on a field from a related model, use the chained field reference.
43 |
44 | ```dart
45 | var companies = await Company.db.find(
46 | session,
47 | orderBy: (t) => t.ceo.name,
48 | );
49 | ```
50 |
51 | In the example we fetch all companies and sort them by their CEO's name.
52 |
53 | You can order results based on the count of a list relation (1:n).
54 |
55 | ```dart
56 | var companies = await Company.db.find(
57 | session,
58 | orderBy: (t) => t.employees.count(),
59 | );
60 | ```
61 |
62 | In the example we fetch all companies and sort them by the number of employees.
63 |
64 | The count used for sorting can also be filtered using a sub-filter.
65 |
66 | ```dart
67 | var companies = await Company.db.find(
68 | session,
69 | orderBy: (t) => t.employees.count(
70 | (employee) => employee.role.equals('developer'),
71 | ),
72 | );
73 | ```
74 |
75 | In the example we fetch all companies and sort them by the number of employees with the role of "developer".
76 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/06-database/08-transactions.md:
--------------------------------------------------------------------------------
1 | # Transactions
2 |
3 | The essential point of a database transaction is that it bundles multiple steps into a single, all-or-nothing operation. The intermediate states between the steps are not visible to other concurrent transactions, and if some failure occurs that prevents the transaction from completing, then none of the steps affect the database at all.
4 |
5 | Serverpod handles database transactions through the `session.db.transaction` method. The method takes a callback function that receives a transaction object.
6 |
7 | 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.
8 |
9 | Simply pass the transaction object to each database operation method to include them in the same atomic operation:
10 |
11 | ```dart
12 | var result = await session.db.transaction((transaction) async {
13 | // Do some database queries here.
14 | await Company.db.insertRow(session, company, transaction: transaction);
15 | await Employee.db.insertRow(session, employee, transaction: transaction);
16 |
17 | // Optionally return a value.
18 | return true;
19 | });
20 | ```
21 |
22 | 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`.
23 |
24 | ## Transaction isolation
25 |
26 | 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.
27 |
28 | :::info
29 |
30 | At the time of writing, the default isolation level for the PostgreSQL database engine is `IsolationLevel.readCommitted`.
31 |
32 | :::
33 |
34 | To set the isolation level, configure the `isolationLevel` property of the `TransactionSettings` object:
35 |
36 | ```dart
37 | await session.db.transaction(
38 | (transaction) async {
39 | await Company.db.insertRow(session, company, transaction: transaction);
40 | await Employee.db.insertRow(session, employee, transaction: transaction);
41 | },
42 | settings: TransactionSettings(isolationLevel: IsolationLevel.serializable),
43 | );
44 | ```
45 |
46 | In the example the isolation level is set to `IsolationLevel.serializable`.
47 |
48 | The available isolation levels are:
49 |
50 | | Isolation Level | Constant | Description |
51 | |-----------------|-----------------------|-------------|
52 | | Read uncommitted | `IsolationLevel.readUncommitted` | Exhibits the same behavior as `IsolationLevel.readCommitted` in PostgresSQL |
53 | | Read committed | `IsolationLevel.readCommitted` | Each statement in the transaction sees a snapshot of the database as of the beginning of that statement. |
54 | | 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. |
55 | | Serializable | `IsolationLevel.serializable` | Gives the same guarantees as `IsolationLevel.repeatableRead` but also throws if read rows are updated by other transactions. |
56 |
57 | For a detailed explanation of the different isolation levels, see the [PostgreSQL documentation](https://www.postgresql.org/docs/current/transaction-iso.html).
58 |
59 | ## Savepoints
60 |
61 | 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.
62 |
63 | Read more about savepoints in the [PostgreSQL documentation](https://www.postgresql.org/docs/current/sql-savepoint.html).
64 |
65 | ### Creating savepoints
66 | To create a savepoint, call the `createSavepoint` method on the transaction object:
67 |
68 | ```dart
69 | await session.db.transaction((transaction) async {
70 | await Company.db.insertRow(session, company, transaction: transaction);
71 | // Create savepoint
72 | var savepoint = await transaction.createSavepoint();
73 | await Employee.db.insertRow(session, employee, transaction: transaction);
74 | });
75 | ```
76 |
77 | 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.
78 |
79 | #### Rolling back to savepoints
80 |
81 | Once a savepoint is created, you can roll back to it by calling the `rollback` method on the savepoint object:
82 |
83 | ```dart
84 | await session.db.transaction((transaction) async {
85 | // Changes preserved in the database
86 | await Company.db.insertRow(session, company, transaction: transaction);
87 |
88 | // Create savepoint
89 | var savepoint = await transaction.createSavepoint();
90 |
91 | await Employee.db.insertRow(session, employee, transaction: transaction);
92 | // Changes rolled back
93 | await savepoint.rollback();
94 | });
95 | ```
96 |
97 | 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.
98 |
99 | #### Releasing savepoints
100 |
101 | 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.
102 |
103 | To release a savepoint, call the `release` method on the savepoint object:
104 |
105 | ```dart
106 | await session.db.transaction((transaction) async {
107 | // Create two savepoints
108 | var savepoint = await transaction.createSavepoint();
109 | var secondSavepoint = await transaction.createSavepoint();
110 |
111 | await Company.db.insertRow(session, company, transaction: transaction);
112 | await savepoint.release();
113 | });
114 | ```
115 |
116 | 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.
117 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/06-database/09-pagination.md:
--------------------------------------------------------------------------------
1 | # Pagination
2 |
3 | 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.
4 |
5 | ## Limit
6 |
7 | 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.
8 |
9 | ```dart
10 | var companies = await Company.db.find(
11 | session,
12 | limit: 10,
13 | );
14 | ```
15 |
16 | In the example we fetch the first 10 companies.
17 |
18 | ## Offset
19 |
20 | The `offset` parameter determines the starting point from which to retrieve records. It essentially skips the first `n` records.
21 |
22 | ```dart
23 | var companies = await Company.db.find(
24 | session,
25 | limit: 10,
26 | offset: 30,
27 | );
28 | ```
29 |
30 | In the example we skip the first 30 rows and fetch the 31st to 40th company.
31 |
32 | ## Using limit and offset for pagination
33 |
34 | Together, `limit` and `offset` can be used to implement pagination.
35 |
36 | ```dart
37 | int page = 3;
38 | int companiesPerPage = 10;
39 |
40 | var companies = await Company.db.find(
41 | session,
42 | orderBy: (t) => t.id,
43 | limit: companiesPerPage,
44 | offset: (page - 1) * companiesPerPage,
45 | );
46 | ```
47 |
48 | In the example we fetch the third page of companies, with 10 companies per page.
49 |
50 | ### Tips
51 |
52 | 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.
53 | 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.
54 | 3. **Page numbering**: Page numbers usually start from 1. Adjust the offset calculation accordingly.
55 |
56 | ## Cursor-based pagination
57 |
58 | A limit-offset pagination may not be the best solution if the table is changed frequently and rows are added or removed between requests.
59 |
60 | 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.
61 |
62 | ### How it works
63 |
64 | 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`.
65 |
66 | ### Implementing cursor-based pagination
67 |
68 | 1. **Initial request**:
69 | For the initial request, where no cursor is provided, retrieve the first `n` records:
70 |
71 | ```dart
72 | int recordsPerPage = 10;
73 |
74 | var companies = await Company.db.find(
75 | session,
76 | orderBy: (t) => t.id,
77 | limit: recordsPerPage,
78 | );
79 | ```
80 |
81 | 2. **Subsequent requests**:
82 | For the subsequent requests, use the cursor (for example, the last `id` from the previous result) to fetch the next set of records:
83 |
84 | ```dart
85 | int cursor = lastCompanyIdFromPreviousPage; // This is typically sent by the client
86 |
87 | var companies = await Company.db.find(
88 | session,
89 | where: Company.t.id > cursor,
90 | orderBy: (t) => t.id,
91 | limit: recordsPerPage,
92 | );
93 | ```
94 |
95 | 3. **Returning the cursor**:
96 | When returning data to the client, also return the cursor, so it can be used to compute the starting point for the next page.
97 |
98 | ```dart
99 | return {
100 | 'data': companies,
101 | 'lastCursor': companies.last.id,
102 | };
103 | ```
104 |
105 | ### Tips
106 |
107 | 1. **Choosing a cursor**: While IDs are commonly used as cursors, timestamps or other unique, sequentially ordered fields can also serve as effective cursors.
108 | 2. **Backward pagination**: To implement backward pagination, use the first item from the current page as the cursor and adjust the query accordingly.
109 | 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.
110 | 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.
111 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/06-database/10-raw-access.md:
--------------------------------------------------------------------------------
1 | # Raw access
2 |
3 | The library provides methods to execute raw SQL queries directly on the database for advanced scenarios.
4 |
5 | ## `unsafeQuery`
6 |
7 | 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.
8 |
9 | ```dart
10 | DatabaseResult result = await session.db.unsafeQuery(
11 | r'SELECT * FROM mytable WHERE id = @id',
12 | parameters: QueryParameters.named({'id': 1}),
13 | );
14 | ```
15 |
16 | ## `unsafeExecute`
17 |
18 | 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.
19 |
20 | ```dart
21 | int result = await session.db.unsafeExecute(
22 | r'DELETE FROM mytable WHERE id = @id',
23 | parameters: QueryParameters.named({'id': 1}),
24 | );
25 | ```
26 |
27 | ## `unsafeSimpleQuery`
28 |
29 | 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.**
30 |
31 | Simple query mode is suitable for:
32 |
33 | * Queries containing multiple statements.
34 | * Situations where the extended query protocol is not available (e.g., replication mode or with proxies like PGBouncer).
35 |
36 |
37 | ```dart
38 | DatabaseResult result = await session.db.unsafeSimpleQuery(
39 | r'SELECT * FROM mytable WHERE id = 1; SELECT * FROM othertable;'
40 | );
41 | ```
42 |
43 | ## `unsafeSimpleExecute`
44 |
45 | Similar to `unsafeExecute`, but uses the simple query protocol. It does not return any results. **Use with extreme caution and only when absolutely necessary.**
46 |
47 | Simple query mode is suitable for the same scenarios as `unsafeSimpleQuery`.
48 |
49 | ```dart
50 | int result = await session.db.unsafeSimpleExecute(
51 | r'DELETE FROM mytable WHERE id = 1; DELETE FROM othertable;'
52 | );
53 | ```
54 |
55 | ## Query parameters
56 |
57 | 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:
58 |
59 | * **Named parameters:** Use `@` to denote named parameters in your query and pass a `Map` of parameter names and values.
60 | * **Positional parameters:** Use `$1`, `$2`, etc., to denote positional parameters and pass a `List` of parameter values in the correct order.
61 |
62 | ```dart
63 | // Named parameters
64 | var result = await db.unsafeQuery(
65 | r'SELECT id FROM apparel WHERE color = @color AND size = @size',
66 | QueryParameters.named({
67 | 'color': 'green',
68 | 'size': 'XL',
69 | }));
70 |
71 | // Positional parameters
72 | var result = await db.unsafeQuery(
73 | r'SELECT id FROM apparel WHERE color = $1 AND size = $2',
74 | QueryParameters.positional(['green', 'XL']),
75 | );
76 | ```
77 |
78 | :::danger
79 | 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.
80 | :::
81 |
82 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/06-database/11-migrations.md:
--------------------------------------------------------------------------------
1 | # Migrations
2 |
3 | Serverpod comes bundled with a simple-to-use but powerful 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.
4 |
5 | A migration is a set of database operations (e.g. creating a table, adding a column, etc.) 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.
6 |
7 | 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.
8 |
9 | ## Opt out of migrations
10 |
11 | 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.
12 |
13 | ```yaml
14 | class: Example
15 | table: example
16 | managedMigration: false
17 | fields:
18 | name: String
19 | ```
20 |
21 | 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:
22 |
23 | - 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.
24 | - 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.
25 |
26 | ## Creating a migration
27 |
28 | To create a migration navigate to your project's `server` package directory and run the `create-migration` command.
29 |
30 | ```bash
31 | $ serverpod create-migration
32 | ```
33 |
34 | 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.
35 |
36 | If no previous migration exists it will create a migration assuming there is no initial state.
37 |
38 | See the [Pre-migration project upgrade path](../../upgrading/upgrade-to-one-point-two) section for more information on how to get started with migrations for any project created before migrations were introduced in Serverpod.
39 |
40 | ### Force create migration
41 |
42 | The migration command aborts and displays an error under two conditions:
43 |
44 | 1. When no changes are identified between the database schema in the latest migration and the schema required by the project.
45 | 2. When there is a risk of data loss.
46 |
47 | To override these safeguards and force the creation of a migration, use the `--force` flag.
48 |
49 | ```bash
50 | $ serverpod create-migration --force
51 | ```
52 |
53 | ### Tag migration
54 |
55 | 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.
56 |
57 | ```bash
58 | $ serverpod create-migration --tag "v1-0-0"
59 | ```
60 |
61 | This would create a migration named `<timestamp>-v1-0-0`:
62 |
63 | ```text
64 | ├── migrations
65 | │ └── 20231205080937028-v1-0-0
66 | ```
67 |
68 | ### Add data in a migration
69 |
70 | Since the migrations are written in SQL, it is possible to add data to the database in a migration. This can be useful if you want to add initial data to the database.
71 |
72 | The developer is responsible for ensuring that any added SQL statements are compatible with the database schema and rolling forward from the previous migrations.
73 |
74 | ### Migrations directory structure
75 |
76 | The `migrations` directory contains a folder for each migration that is created, looking like this for a project with two migrations:
77 |
78 | ```text
79 | ├── migrations
80 | │ ├── 20231205080937028
81 | │ ├── 20231205081959122
82 | │ └── migration_registry.txt
83 | ```
84 |
85 | 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.
86 |
87 | For each migration, five files are created:
88 |
89 | - **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.
90 | - **definition.sql** - Contains SQL statements to create the complete database schema. This file is applied when initializing a new database.
91 | - **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.
92 | - **migration.json** - Contains the actions that are part of the migration. This file is parsed by the Serverpod CLI.
93 | - **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.
94 |
95 | ## Apply migrations
96 |
97 | Migrations are applied using the server runtime. To apply migrations, navigate to your project's `server` package directory, then start the server with the `--apply-migrations` flag. Migrations are applied as part of the startup sequence and the framework asserts that each migration is only applied once to the database.
98 |
99 | ```bash
100 | $ dart run bin/main.dart --apply-migrations
101 | ```
102 |
103 | Migrations can also be applied using the maintenance role. In maintenance, after migrations are applied, the server exits with an exit code indicating if migrations were successfully applied, zero for success or non-zero for failure.
104 |
105 | ```bash
106 | $ dart run bin/main.dart --role maintenance --apply-migrations
107 | ```
108 |
109 | This is useful if migrations are applied as part of an automated process.
110 |
111 | If migrations are applied at the same time as repair migration, the repair migration is applied first.
112 |
113 | ## Creating a repair migration
114 |
115 | 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.
116 |
117 | By default, the command connects to and pulls a live database schema from a running development server.
118 |
119 | To create a repair migration, navigate to your project's `server` package directory and run the `create-repair-migration` command.
120 |
121 | ```bash
122 | $ serverpod create-repair-migration
123 | ```
124 |
125 | This creates a repair migration in the `repair-migration` directory targeting the project's latest migration.
126 |
127 | 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.
128 |
129 | :::warning
130 | 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.
131 | :::
132 |
133 | 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.
134 |
135 | ### Migration database source
136 |
137 | 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.
138 |
139 | ```bash
140 | $ serverpod create-repair-migration --mode production
141 | ```
142 |
143 | The command connects and pulls the live database schema from a running server.
144 |
145 | ### Targeting a specific migration
146 |
147 | Repair migrations can also target a specific migration version by specifying the migration name with the `--version` option.
148 |
149 | ```bash
150 | $ serverpod create-repair-migration --version 20230821135718-v1-0-0
151 | ```
152 |
153 | This makes it possible to revert your database schema back to any older migration version.
154 |
155 | ### Force create repair migration
156 |
157 | The repair migration command aborts and displays an error under two conditions:
158 |
159 | 1. When no changes are identified between the database schema in the latest migration and the schema required by the project.
160 | 2. When there is a risk of data loss.
161 |
162 | To override these safeguards and force the creation of a repair migration, use the `--force` flag.
163 |
164 | ```bash
165 | $ serverpod create-repair-migration --force
166 | ```
167 |
168 | ### Tag repair migration
169 |
170 | Repair migrations can be tagged just like regular migrations. Tags are appended to the migration name and can be added with the `--tag` option.
171 |
172 | ```bash
173 | $ serverpod create-repair-migration --tag "reset-migrations"
174 | ```
175 |
176 | This would create a repair migration named `<timestamp>-reset-migrations` in the `repair` directory:
177 |
178 | ```text
179 | ├── repair
180 | │ └── 20230821135718-v1-0-0.sql
181 | ```
182 |
183 | ### Repair migrations directory structure
184 |
185 | The `repair` directory only exists if a repair migration has been created and contains a single SQL file containing statements to repair the database schema.
186 |
187 | ```text
188 | ├── repair
189 | │ └── 20230821135718-v1-0-0.sql
190 | ```
191 |
192 | ## Applying a repair migration
193 |
194 | 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.
195 |
196 | ```bash
197 | $ dart run bin/main.dart --apply-repair-migration
198 | ```
199 |
200 | The repair migration can also be applied using the maintenance role. In maintenance, after migrations are applied, the server exits with an exit code indicating if migrations were successfully applied, zero for success or non-zero for failure.
201 |
202 | ```bash
203 | $ dart run bin/main.dart --role maintenance --apply-repair-migration
204 | ```
205 |
206 | If a repair migration is applied at the same time as migrations, the repair migration is applied first.
207 |
208 | ## Rolling back migrations
209 |
210 | 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.
211 |
212 | Note that data is not rolled back, only the database schema.
213 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/07-configuration.md:
--------------------------------------------------------------------------------
1 | # Configurations
2 |
3 | Serverpod can be configured in a few different ways. The minimum required settings to provide is the configuration for the API server. If no settings are provided at all, the default settings for the API server are used.
4 |
5 | ## Configuration options
6 |
7 | There are three different ways to configure Serverpod: with environment variables, via yaml config files, or by supplying the dart configuration object to the Serverpod constructor. The environment variables take precedence over the yaml configurations but both can be used simultaneously. The dart configuration object will override any environment variable or config file. The tables show all available configuration options provided in the Serverpod core library.
8 |
9 | ### Configuration options for the server
10 |
11 | These can be separately declared for each run mode in the corresponding yaml file (`development.yaml`,`staging.yaml`, `production.yaml` and `testing.yaml`) or as environment variables.
12 |
13 | | Environment variable | Config file | Default | Description |
14 | | ---------------------------------------- | ----------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------- |
15 | | SERVERPOD_API_SERVER_PORT | apiServer.port | 8080 | The port number for the API server |
16 | | SERVERPOD_API_SERVER_PUBLIC_HOST | apiServer.publicHost | localhost | The public host address of the API server |
17 | | SERVERPOD_API_SERVER_PUBLIC_PORT | apiServer.publicPort | 8080 | The public port number for the API server |
18 | | SERVERPOD_API_SERVER_PUBLIC_SCHEME | apiServer.publicScheme | http | The public scheme (http/https) for the API server |
19 | | SERVERPOD_INSIGHTS_SERVER_PORT | insightsServer.port | - | The port number for the Insights server |
20 | | SERVERPOD_INSIGHTS_SERVER_PUBLIC_HOST | insightsServer.publicHost | - | The public host address of the Insights server |
21 | | SERVERPOD_INSIGHTS_SERVER_PUBLIC_PORT | insightsServer.publicPort | - | The public port number for the Insights server |
22 | | SERVERPOD_INSIGHTS_SERVER_PUBLIC_SCHEME | insightsServer.publicScheme | - | The public scheme (http/https) for the Insights server |
23 | | SERVERPOD_WEB_SERVER_PORT | webServer.port | - | The port number for the Web server |
24 | | SERVERPOD_WEB_SERVER_PUBLIC_HOST | webServer.publicHost | - | The public host address of the Web server |
25 | | SERVERPOD_WEB_SERVER_PUBLIC_PORT | webServer.publicPort | - | The public port number for the Web server |
26 | | SERVERPOD_WEB_SERVER_PUBLIC_SCHEME | webServer.publicScheme | - | The public scheme (http/https) for the Web server |
27 | | SERVERPOD_DATABASE_HOST | database.host | - | The host address of the database |
28 | | SERVERPOD_DATABASE_PORT | database.port | - | The port number for the database connection |
29 | | SERVERPOD_DATABASE_NAME | database.name | - | The name of the database |
30 | | SERVERPOD_DATABASE_USER | database.user | - | The user name for database authentication |
31 | | SERVERPOD_DATABASE_SEARCH_PATHS | database.searchPaths | - | The search paths used for all database connections |
32 | | SERVERPOD_DATABASE_REQUIRE_SSL | database.requireSsl | false | Indicates if SSL is required for the database |
33 | | SERVERPOD_DATABASE_IS_UNIX_SOCKET | database.isUnixSocket | false | Specifies if the database connection is a Unix socket |
34 | | SERVERPOD_REDIS_HOST | redis.host | - | The host address of the Redis server |
35 | | SERVERPOD_REDIS_PORT | redis.port | - | The port number for the Redis server |
36 | | SERVERPOD_REDIS_USER | redis.user | - | The user name for Redis authentication |
37 | | SERVERPOD_REDIS_ENABLED | redis.enabled | false | Indicates if Redis is enabled |
38 | | SERVERPOD_MAX_REQUEST_SIZE | maxRequestSize | 524288 | The maximum size of requests allowed in bytes |
39 | | 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`. |
40 | | SERVERPOD_SESSION_CONSOLE_LOG_ENABLED | sessionLogs.consoleEnabled | - | Enables or disables logging session data to the console. Defaults to `true` if no database is configured, otherwise `false`. |
41 |
42 | | Environment variable | Command line option | Default | Description |
43 | | --------------------- | ------------------- | ------- | ----------------------------------------- |
44 | | SERVERPOD_SERVER_ID | `--server-id` | default | Configures the id of the server instance. |
45 |
46 | ### Secrets
47 |
48 | Secrets are declared in the `passwords.yaml` file. The password file is structured with a common `shared` section, any secret put here will be used in all run modes. The other sections are the names of the run modes followed by respective key/value pairs.
49 |
50 | | Environment variable | Passwords file | Default | Description |
51 | | --------------------------- | -------------- | ------- | ----------------------------------------------------------------- |
52 | | SERVERPOD_DATABASE_PASSWORD | database | - | The password for the database |
53 | | SERVERPOD_SERVICE_SECRET | serviceSecret | - | The token used to connect with insights must be at least 20 chars |
54 | | SERVERPOD_REDIS_PASSWORD | redis | - | The password for the Redis server |
55 |
56 | #### Secrets for first party packages
57 |
58 | - [serverpod_cloud_storage_gcp](https://pub.dev/packages/serverpod_cloud_storage_gcp): Google Cloud Storage
59 | - [serverpod_cloud_storage_s3](https://pub.dev/packages/serverpod_cloud_storage_s3): Amazon S3
60 |
61 | | Environment variable | Passwords file | Default | Description |
62 | | ---------------------------- | --------------- | ------- | ------------------------------------------------------------------------- |
63 | | SERVERPOD_HMAC_ACCESS_KEY_ID | HMACAccessKeyId | - | The access key ID for HMAC authentication for serverpod_cloud_storage_gcp |
64 | | SERVERPOD_HMAC_SECRET_KEY | HMACSecretKey | - | The secret key for HMAC authentication for serverpod_cloud_storage_gcp |
65 | | SERVERPOD_AWS_ACCESS_KEY_ID | AWSAccessKeyId | - | The access key ID for AWS authentication for serverpod_cloud_storage_s3 |
66 | | SERVERPOD_AWS_SECRET_KEY | AWSSecretKey | - | The secret key for AWS authentication for serverpod_cloud_storage_s3 |
67 |
68 | ### Config file example
69 |
70 | The config file should be named after the run mode you start the server in and it needs to be placed inside the `config` directory in the root of the server project. As an example, you have the `config/development.yaml` that will be used when running in the `development` run mode.
71 |
72 | ```yaml
73 | apiServer:
74 | port: 8080
75 | publicHost: localhost
76 | publicPort: 8080
77 | publicScheme: http
78 |
79 | insightsServer:
80 | port: 8081
81 | publicHost: localhost
82 | publicPort: 8081
83 | publicScheme: http
84 |
85 | webServer:
86 | port: 8082
87 | publicHost: localhost
88 | publicPort: 8082
89 | publicScheme: http
90 |
91 | database:
92 | host: localhost
93 | port: 8090
94 | name: database_name
95 | user: postgres
96 |
97 | redis:
98 | enabled: false
99 | host: localhost
100 | port: 8091
101 |
102 | maxRequestSize: 524288
103 |
104 | sessionLogs:
105 | persistentEnabled: true
106 | consoleEnabled: true
107 | ```
108 |
109 | ### Passwords file example
110 |
111 | The password file contains the secrets used by the server to connect to different services but you can also supply your secrets if you want. This file is structured with a common `shared` section, any secret put here will be used in all run modes. The other sections are the names of the run modes followed by respective key/value pairs.
112 |
113 | ```yaml
114 | shared:
115 | myCustomSharedSecret: 'secret_key'
116 |
117 | development:
118 | database: 'development_password'
119 | redis: 'development_password'
120 | serviceSecret: 'development_service_secret'
121 |
122 | production:
123 | database: 'production_password'
124 | redis: 'production_password'
125 | serviceSecret: 'production_service_secret'
126 | ```
127 |
128 | ### Dart config object example
129 |
130 | To configure Serverpod in Dart you simply pass an instance of the `ServerpodConfig` class to the `Serverpod` constructor. This config will override any environment variables or config files present. The `Serverpod` constructor is normally used inside the `run` function in your `server.dart` file. At a minimum, the `apiServer` has to be provided.
131 |
132 | ```dart
133 | Serverpod(
134 | args,
135 | Protocol(),
136 | Endpoints(),
137 | config: ServerpodConfig(
138 | apiServer: ServerConfig(
139 | port: 8080,
140 | publicHost: 'localhost',
141 | publicPort: 8080,
142 | publicScheme: 'http',
143 | ),
144 | insightsServer: ServerConfig(
145 | port: 8081,
146 | publicHost: 'localhost',
147 | publicPort: 8081,
148 | publicScheme: 'http',
149 | ),
150 | webServer: ServerConfig(
151 | port: 8082,
152 | publicHost: 'localhost',
153 | publicPort: 8082,
154 | publicScheme: 'http',
155 | ),
156 | ),
157 | );
158 | ```
159 |
160 | ### Default
161 |
162 | If no yaml config files exist, no environment variables are configured and no dart config file is supplied this default configuration will be used.
163 |
164 | ```dart
165 | ServerpodConfig(
166 | apiServer: ServerConfig(
167 | port: 8080,
168 | publicHost: 'localhost',
169 | publicPort: 8080,
170 | publicScheme: 'http',
171 | ),
172 | );
173 | ```
174 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/08-caching.md:
--------------------------------------------------------------------------------
1 | # Caching
2 |
3 | Accessing the database can be expensive for complex queries or if you need to run many different queries for a specific task. Serverpod makes it easy to cache frequently requested objects in the memory of your server. Any serializable object can be cached. Objects can be stored in the Redis cache if your Serverpod is hosted across multiple servers in a cluster.
4 |
5 | Caches can be accessed through the `Session` object. This is an example of an endpoint method for requesting data about a user:
6 |
7 | ```dart
8 | Future<UserData> getUserData(Session session, int userId) async {
9 | // Define a unique key for the UserData object
10 | var cacheKey = 'UserData-$userId';
11 |
12 | // Try to retrieve the object from the cache
13 | var userData = await session.caches.local.get<UserData>(cacheKey);
14 |
15 | // If the object wasn't found in the cache, load it from the database and
16 | // save it in the cache. Make it valid for 5 minutes.
17 | if (userData == null) {
18 | userData = UserData.db.findById(session, userId);
19 | await session.caches.local.put(cacheKey, userData!, lifetime: Duration(minutes: 5));
20 | }
21 |
22 | // Return the user data to the client
23 | return userData;
24 | }
25 | ```
26 |
27 | In total, there are three caches where you can store your objects. Two caches are local to the server handling the current session, and one is distributed across the server cluster through Redis. There are two variants for the local cache, one regular cache, and a priority cache. Place objects that are frequently accessed in the priority cache.
28 |
29 | Depending on the type and number of objects that are cached in the global cache, you may want to specify custom caching rules in Redis. This is currently not handled automatically by Serverpod.
30 |
31 | ### Cache miss handler
32 |
33 | If you want to handle cache misses in a specific way, you can pass in a `CacheMissHandler` to the `get` method. The `CacheMissHandler` makes it possible to store an object in the cache when a cache miss occurs.
34 |
35 | The above example rewritten using the `CacheMissHandler`:
36 |
37 | ```dart
38 | Future<UserData> getUserData(Session session, int userId) async {
39 | // Define a unique key for the UserData object
40 | var cacheKey = 'UserData-$userId';
41 |
42 | // Try to retrieve the object from the cache
43 | var userData = await session.caches.local.get(
44 | cacheKey,
45 | // If the object wasn't found in the cache, load it from the database and
46 | // save it in the cache. Make it valid for 5 minutes.
47 | CacheMissHandler(
48 | () async => UserData.db.findById(session, userId),
49 | lifetime: Duration(minutes: 5),
50 | ),
51 | );
52 |
53 | // Return the user data to the client
54 | return userData;
55 | }
56 | ```
57 |
58 | If the `CacheMissHandler` returns `null`, no object will be stored in the cache.
59 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/09-logging.md:
--------------------------------------------------------------------------------
1 | # Logging
2 |
3 | Serverpod uses the database for storing logs; this makes it easy to search for errors, slow queries, or debug messages. To log custom messages during the execution of a session, use the `log` method of the `session` object. When the session is closed, either from successful execution or by failing from throwing an exception, the messages are written to the log. By default, session log entries are written for every completed session.
4 |
5 | ```dart
6 | session.log('This is working well');
7 | ```
8 |
9 | You can also pass exceptions and stack traces to the `log` method or set the logging level.
10 |
11 | ```dart
12 | session.log(
13 | 'Oops, something went wrong',
14 | level: LogLevel.warning,
15 | exception: e,
16 | stackTrace: stackTrace,
17 | );
18 | ```
19 |
20 | Log entries are stored in the following tables of the database: `serverpod_log` for text messages, `serverpod_query_log` for queries, and `serverpod_session_log` for completed sessions. Optionally, it's possible to pass a log level with the message to filter out messages depending on the server's runtime settings.
21 |
22 | ### Controlling Session Logs with Environment Variables or Configuration Files
23 |
24 | You can control whether session logs are written to the database, the console, both, or neither, using environment variables or configuration files. **Environment variables take priority** over configuration file settings if both are provided.
25 |
26 | #### Environment Variables
27 |
28 | - `SERVERPOD_SESSION_PERSISTENT_LOG_ENABLED`: Controls whether session logs are written to the database.
29 | - `SERVERPOD_SESSION_CONSOLE_LOG_ENABLED`: Controls whether session logs are output to the console.
30 |
31 | #### Configuration File Example
32 |
33 | You can also configure logging behavior directly in the configuration file:
34 |
35 | ```yaml
36 | sessionLogs:
37 | persistentEnabled: true # Logs are stored in the database
38 | consoleEnabled: true # Logs are output to the console
39 | ```
40 |
41 | ### Default Behavior for Session Logs
42 |
43 | By default, session logging behavior depends on whether the project has database support:
44 |
45 | - **When a database is present**
46 |
47 | - `persistentEnabled` is set to `true`, meaning logs are stored in the database.
48 | - `consoleEnabled` is set to `false` by default, meaning logs are not printed to the console unless explicitly enabled.
49 |
50 | - **When no database is present**
51 |
52 | - `persistentEnabled` is set to `false` since persistent logging requires a database.
53 | - `consoleEnabled` is set to `true`, meaning logs are printed to the console by default.
54 |
55 | ### Important: Persistent Logging Requires a Database
56 |
57 | If `persistentEnabled` is set to `true` but **no database is configured**, a `StateError` will be thrown. Persistent logging requires database support, and Serverpod ensures that misconfigurations are caught early by raising this error.
58 |
59 | :::info
60 |
61 | You can use the companion app **[Serverpod Insights](../tools/insights)** to read, search, and configure the logs.
62 |
63 | :::
64 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/10-modules.md:
--------------------------------------------------------------------------------
1 | # Modules
2 |
3 | Serverpod is built around the concept of modules. A Serverpod module is similar to a Dart package but contains both server, client, and Flutter code. A module contains its own namespace for endpoints and methods to minimize the risk of conflicts.
4 |
5 | Examples of modules are the `serverpod_auth` module and the `serverpod_chat` module, which both are maintained by the Serverpod team.
6 |
7 | ## Adding a module to your project
8 |
9 | ### Server setup
10 |
11 | To add a module to your project, you must include the server and client/Flutter packages in your project's `pubspec.yaml` files.
12 |
13 | For example, to add the `serverpod_auth` module to your project, you need to add `serverpod_auth_server` to your server's `pubspec.yaml`:
14 |
15 | ```yaml
16 | dependencies:
17 | serverpod_auth_server: ^1.x.x
18 | ```
19 |
20 | :::info
21 |
22 | Make sure to replace `1.x.x` with the Serverpod version you are using. Serverpod uses the same version number for all official packages. If you use the same version, you will be sure that everything works together.
23 |
24 | :::
25 |
26 | In your `config/generator.yaml` you can optionally add the `serverpod_auth` module and give it a `nickname`. The nickname will determine how you reference the module from the client. If the module isn't added in the `generator.yaml`, the default nickname for the module will be used.
27 |
28 | ```yaml
29 | modules:
30 | serverpod_auth:
31 | nickname: auth
32 | ```
33 |
34 | Then run `pub get` and `serverpod generate` from your server's directory (e.g., `mypod_server`) to add the module to your project's deserializer.
35 |
36 | ```bash
37 | $ dart pub get
38 | $ serverpod generate
39 | ```
40 |
41 | Finally, since modules might include modifications to the database schema, you should create a new database migration and apply it by running `serverpod create-migration` then `dart bin/main.dart --apply-migrations` from your server's directory.
42 |
43 | ```bash
44 | $ serverpod create-migration
45 | $ dart bin/main.dart --apply-migrations
46 | ```
47 |
48 | ### Client setup
49 |
50 | In your client's `pubspec.yaml`, you will need to add the generated client code from the module.
51 |
52 | ```yaml
53 | dependencies:
54 | serverpod_auth_client: ^1.x.x
55 | ```
56 |
57 | ### Flutter app setup
58 |
59 | In your Flutter app, add the corresponding dart or Flutter package(s) to your `pubspec.yaml`.
60 |
61 | ```yaml
62 | dependencies:
63 | serverpod_auth_shared_flutter: ^1.x.x
64 | serverpod_auth_google_flutter: ^1.x.x
65 | serverpod_auth_apple_flutter: ^1.x.x
66 | ```
67 |
68 | ## Referencing a module
69 |
70 | It can be useful to reference serializable objects in other modules from the YAML-files in your models. You do this by adding the module prefix, followed by the nickname of the package. For instance, this is how you reference a serializable class in the auth package.
71 |
72 | ```yaml
73 | class: MyClass
74 | fields:
75 | userInfo: module:auth:UserInfo
76 | ```
77 |
78 | ## Creating custom modules
79 |
80 | With the `serverpod create` command, it is possible to create new modules 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.
81 |
82 | ```bash
83 | $ serverpod create --template module my_module
84 | ```
85 |
86 | The create command will create a server and a client Dart package. If you also want to add custom Flutter code, use `flutter create` to create a package.
87 |
88 | ```bash
89 | $ flutter create --template package my_module_flutter
90 | ```
91 |
92 | In your Flutter package, you most likely want to import the client libraries created by `serverpod create`.
93 |
94 | :::info
95 |
96 | Most modules will need a set of database tables to function. When naming the tables, you should use the module name as a prefix to the table name to avoid any conflicts. For instance, the Serverpod tables are prefixed with `serverpod_`.
97 |
98 | :::
99 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/11-authentication/01-setup.md:
--------------------------------------------------------------------------------
1 | # Setup
2 |
3 | Serverpod comes with built-in user management and authentication. It is possible to build a [custom authentication implementation](custom-overrides), but the recommended way to authenticate users is to use the `serverpod_auth` module. The module makes it easy to authenticate with email or social sign-ins and currently supports signing in with email, Google, Apple, and Firebase.
4 |
5 | Future versions of the authentication module will include more options. If you write another authentication module, please consider [contributing](/contribute) your code.
6 |
7 | ![Sign-in with Serverpod](https://github.com/serverpod/serverpod/raw/main/misc/images/sign-in.png)
8 |
9 | ## Installing the auth module
10 |
11 | Serverpod's auth module makes it easy to authenticate users through email or 3rd parties. The authentication module also 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.
12 |
13 | ## Server setup
14 |
15 | Add the module as a dependency to the server project's `pubspec.yaml`.
16 |
17 | ```sh
18 | $ dart pub add serverpod_auth_server
19 | ```
20 |
21 | Add the authentication handler to the Serverpod instance.
22 |
23 | ```dart
24 | import 'package:serverpod_auth_server/serverpod_auth_server.dart' as auth;
25 |
26 | void run(List<String> args) async {
27 | var pod = Serverpod(
28 | args,
29 | Protocol(),
30 | Endpoints(),
31 | authenticationHandler: auth.authenticationHandler, // Add this line
32 | );
33 |
34 | ...
35 | }
36 | ```
37 | 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.
38 |
39 | ```yaml
40 | modules:
41 | serverpod_auth:
42 | nickname: auth
43 | ```
44 |
45 | 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.
46 |
47 | ```bash
48 | $ serverpod generate
49 | ```
50 |
51 | ### Initialize the auth database
52 |
53 | 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.
54 |
55 | ```bash
56 | $ serverpod create-migration
57 | ```
58 |
59 | Start your database container from the server project.
60 |
61 | ```bash
62 | $ docker-compose up --build --detach
63 | ```
64 |
65 | Then apply the migration by starting the server with the `apply-migrations` flag.
66 |
67 | ```bash
68 | $ dart run bin/main.dart --role maintenance --apply-migrations
69 | ```
70 |
71 | The full migration instructions can be found in the [migration guide](../database/migrations).
72 |
73 | ### Configure Authentication
74 | Serverpod's auth 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:
75 |
76 |
77 | ```dart
78 | import 'package:serverpod_auth_server/module.dart' as auth;
79 |
80 | void run(List<String> args) async {
81 |
82 | auth.AuthConfig.set(auth.AuthConfig(
83 | minPasswordLength: 12,
84 | ));
85 |
86 | // Start the Serverpod server.
87 | await pod.start();
88 | }
89 |
90 | ```
91 |
92 | | **Property** | **Description** | **Default** |
93 | |:-------------|:----------------|:-----------:|
94 | | **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 |
95 | | **emailSignInFailureResetTime** | The reset period for email sign in attempts. Defaults to 5 minutes. | 5min |
96 | | **enableUserImages** | True if user images are enabled. | true |
97 | | **extraSaltyHash** | True if the server should use the accounts email address as part of the salt when storing password hashes (strongly recommended). | true |
98 | | **firebaseServiceAccountKeyJson** | Firebase service account key JSON file. Generate and download from the Firebase console. | - |
99 | | **importUserImagesFromGoogleSignIn** | True if user images should be imported when signing in with Google. | true |
100 | | **legacyUserSignOutBehavior** | Defines the default behavior for the deprecated `signOut` method used in the status endpoint. This setting controls whether users are signed out from all active devices (`SignOutOption.allDevices`) or just the current device (`SignOutOption.currentDevice`). | `SignOutOption.allDevices` |
101 | | **maxAllowedEmailSignInAttempts** | Max allowed failed email sign in attempts within the reset period. | 5 |
102 | | **maxPasswordLength** | The maximum length of passwords when signing up with email. | 128 |
103 | | **minPasswordLength** | The minimum length of passwords when signing up with email. | 8 |
104 | | **onUserCreated** | Called after a user has been created. Listen to this callback if you need to do additional setup. | - |
105 | | **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. | - |
106 | | **onUserWillBeCreated** | Called when a user is about to be created, gives a chance to abort the creation by returning false. | - |
107 | | **passwordResetExpirationTime** | The time for password resets to be valid. | 24h |
108 | | **sendPasswordResetEmail** | Called when a user should be sent a reset code by email. | - |
109 | | **sendValidationEmail** | Called when a user should be sent a validation code on account setup. | - |
110 | | **userCanEditFullName** | True if users can edit their full name. | false |
111 | | **userCanEditUserImage** | True if users can update their profile images. | true |
112 | | **userCanEditUserName** | True if users can edit their user names. | true |
113 | | **userCanSeeFullName** | True if users can view their full name. | true |
114 | | **userCanSeeUserName** | True if users can view their user name. | true |
115 | | **userImageFormat** | The format used to store user images. | jpg |
116 | | **userImageGenerator** | Generator used to produce default user images. | - |
117 | | **userImageQuality** | The quality setting for images if JPG format is used. | 70 |
118 | | **userImageSize** | The size of user images. | 256 |
119 | | **userInfoCacheLifetime** | The duration which user infos are cached locally in the server. | 1min |
120 | | **validationCodeLength** | The length of the validation code used in the authentication process. This value determines the number of digits in the validation code. Setting this value to less than 3 reduces security. | 8 |
121 |
122 | ## Client setup
123 |
124 | Add the auth client in your client project's `pubspec.yaml`.
125 |
126 | ```yaml
127 | dependencies:
128 | ...
129 | serverpod_auth_client: ^1.x.x
130 | ```
131 |
132 | ## App setup
133 |
134 | First, add dependencies to your app's `pubspec.yaml` file for the methods of signing in that you want to support.
135 |
136 | ```yaml
137 | dependencies:
138 | flutter:
139 | sdk: flutter
140 | serverpod_flutter: ^1.x.x
141 | auth_example_client:
142 | path: ../auth_example_client
143 |
144 | serverpod_auth_shared_flutter: ^1.x.x
145 | ```
146 |
147 | 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.
148 |
149 | ```dart
150 | late SessionManager sessionManager;
151 | late Client client;
152 |
153 | void main() async {
154 | // Need to call this as we are using Flutter bindings before runApp is called.
155 | WidgetsFlutterBinding.ensureInitialized();
156 |
157 | // The android emulator does not have access to the localhost of the machine.
158 | // const ipAddress = '10.0.2.2'; // Android emulator ip for the host
159 |
160 | // On a real device replace the ipAddress with the IP address of your computer.
161 | const ipAddress = 'localhost';
162 |
163 | // Sets up a singleton client object that can be used to talk to the server from
164 | // anywhere in our app. The client is generated from your server code.
165 | // The client is set up to connect to a Serverpod running on a local server on
166 | // the default port. You will need to modify this to connect to staging or
167 | // production servers.
168 | client = Client(
169 | 'http://$ipAddress:8080/',
170 | authenticationKeyManager: FlutterAuthenticationKeyManager(),
171 | )..connectivityMonitor = FlutterConnectivityMonitor();
172 |
173 | // The session manager keeps track of the signed-in state of the user. You
174 | // can query it to see if the user is currently signed in and get information
175 | // about the user.
176 | sessionManager = SessionManager(
177 | caller: client.modules.auth,
178 | );
179 | await sessionManager.initialize();
180 |
181 | runApp(MyApp());
182 | }
183 | ```
184 |
185 | The `SessionManager` has useful methods for viewing and monitoring the user's current state.
186 |
187 | #### Check authentication state
188 | To check if the user is signed in:
189 |
190 | ```dart
191 | sessionManager.isSignedIn;
192 | ```
193 | Returns `true` if the user is signed in, or `false` otherwise.
194 |
195 | #### Access current user
196 | To retrieve information about the current user:
197 |
198 | ```dart
199 | sessionManager.signedInUser;
200 | ```
201 | Returns a `UserInfo` object if the user is currently signed in, or `null` if the user is not.
202 |
203 | #### Register authentication
204 | To register a signed in user in the session manager:
205 |
206 | ```dart
207 | await sessionManager.registerSignedInUser(
208 | userInfo,
209 | keyId,
210 | authKey,
211 | );
212 | ```
213 | This will persist the user information and refresh any open streaming connection, see [Custom Providers - Client Setup](providers/custom-providers#client-setup) for more details.
214 |
215 | #### Monitor authentication changes
216 | To add a listener that tracks changes in the user's authentication state, useful for updating the UI:
217 |
218 | ```dart
219 | @override
220 | void initState() {
221 | super.initState();
222 |
223 | // Rebuild the page if authentication state changes.
224 | sessionManager.addListener(() {
225 | setState(() {});
226 | });
227 | }
228 | ```
229 | The listener is triggered whenever the user's sign-in state changes.
230 |
231 | #### Sign out current device
232 | To sign the user out on from the current device:
233 |
234 | ```dart
235 | await sessionManager.signOutDevice();
236 | ```
237 | Returns `true` if the sign-out is successful, or `false` if it fails.
238 |
239 | #### Sign out all devices
240 | To sign the user out across all devices:
241 |
242 | ```dart
243 | await sessionManager.signOutAllDevices();
244 | ```
245 | Returns `true` if the user is successfully signed out from all devices, or `false` if it fails.
246 |
247 |
248 | :::info
249 |
250 | The `signOut` method is deprecated. This method calls the deprecated `signOut` status endpoint. For additional details, see the [deprecated signout endpoint](basics#deprecated-signout-endpoint) section. Use `signOutDevice` or `signOutAllDevices` instead.
251 |
252 | ```dart
253 | await sessionManager.signOut(); // Deprecated
254 | ```
255 | :::
256 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/11-authentication/02-basics.md:
--------------------------------------------------------------------------------
1 | # The basics
2 |
3 | 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.
4 |
5 | The `Session` object provides information about the current user. A unique `userId` identifies a user. You should use this id whenever you a referring to a user. Access the id of a signed-in user through the `authenticated` asynchronous getter of the `Session` object.
6 |
7 | ```dart
8 | Future<void> myMethod(Session session) async {
9 | final authenticationInfo = await session.authenticated;
10 | final userId = authenticationInfo?.userId;
11 | ...
12 | }
13 | ```
14 |
15 | You can also use the Session object to check if a user is authenticated:
16 |
17 | ```dart
18 | Future<void> myMethod(Session session) async {
19 | var isSignedIn = await session.isUserSignedIn;
20 | ...
21 | }
22 | ```
23 |
24 | ## Requiring authentication on endpoints
25 |
26 | 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.
27 |
28 | ```dart
29 | class MyEndpoint extends Endpoint {
30 | @override
31 | bool get requireLogin => true;
32 |
33 | Future<void> myMethod(Session session) async {
34 | ...
35 | }
36 | ...
37 | }
38 | ```
39 |
40 | ## Authorization on endpoints
41 |
42 | 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.
43 |
44 | ```dart
45 | class MyEndpoint extends Endpoint {
46 | @override
47 | bool get requireLogin => true;
48 |
49 | @override
50 | Set<Scope> get requiredScopes => {Scope.admin};
51 |
52 | Future<void> myMethod(Session session) async {
53 | ...
54 | }
55 | ...
56 | }
57 | ```
58 |
59 | ### Managing scopes
60 |
61 | 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.
62 |
63 | ```dart
64 | await Users.updateUserScopes(session, userId, {Scope.admin});
65 | ```
66 |
67 | ### Custom scopes
68 |
69 | You may need more granular access control for specific endpoints. To create custom scopes, extend the Scope class, as shown below:
70 |
71 | ```dart
72 | class CustomScope extends Scope {
73 | const CustomScope(String name) : super(name);
74 |
75 | static const userRead = CustomScope('userRead');
76 | static const userWrite = CustomScope('userWrite');
77 | }
78 | ```
79 |
80 | Then use the custom scopes like this:
81 |
82 | ```dart
83 | class MyEndpoint extends Endpoint {
84 | @override
85 | bool get requireLogin => true;
86 |
87 | @override
88 | Set<Scope> get requiredScopes => {CustomScope.userRead, CustomScope.userWrite};
89 |
90 | Future<void> myMethod(Session session) async {
91 | ...
92 | }
93 | ...
94 | }
95 | ```
96 |
97 | :::caution
98 | 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.
99 | :::
100 |
101 | ## User authentication
102 |
103 | The `StatusEndpoint` class includes methods for handling user sign-outs, whether from a single device or all devices.
104 |
105 | :::info
106 |
107 | In addition to the `StatusEndpoint` methods, Serverpod provides more comprehensive tools for managing user authentication and sign-out processes across multiple devices.
108 |
109 | For more detailed information on managing and revoking authentication keys, please refer to the [Revoking authentication keys](providers/custom-providers#revoking-authentication-keys) section.
110 |
111 | :::
112 |
113 | #### Sign out device
114 |
115 | To sign out a single device:
116 |
117 | ```dart
118 | await client.modules.auth.status.signOutDevice();
119 | ```
120 |
121 | This status endpoint method obtains the authentication key from session's authentication information, then revokes that key.
122 |
123 | #### Sign out all devices
124 |
125 | To sign the user out across all devices:
126 |
127 | ```dart
128 | await client.modules.auth.status.signOutAllDevices();
129 | ```
130 |
131 | This status endpoint retrieves the user ID from session's authentication information, then revokes all authentication keys related to that user.
132 |
133 | :::info
134 | <span id="deprecated-signout-endpoint"></span>
135 | The `signOut` status endpoint is deprecated. Use `signOutDevice` or `signOutAllDevices` instead.
136 |
137 | ```dart
138 | await client.modules.auth.status.signOut(); // Deprecated
139 | ```
140 |
141 | The behavior of `signOut` is controlled by `legacyUserSignOutBehavior`, which you can adjust in the [configure authentication](setup#configure-authentication) section. This allows you to control the signout behaviour of already shipped clients.
142 | :::
143 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/11-authentication/03-working-with-users.md:
--------------------------------------------------------------------------------
1 | # Working with users
2 |
3 | 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.
4 |
5 | ```dart
6 | var userId = (await session.authenticated)?.userId;
7 | ```
8 |
9 | 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.
10 |
11 | ```dart
12 | var userInfo = await Users.findUserByUserId(session, userId!);
13 | ```
14 |
15 | The `UserInfo` is automatically populated when the user signs in. Different data may be available depending on which method was used for authentication.
16 |
17 | :::tip
18 |
19 | 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).
20 |
21 | :::
22 |
23 | ## Displaying or editing user images
24 |
25 | The module has built-in methods for handling a user's basic settings, including uploading new profile pictures.
26 |
27 | ![UserImageButton](https://github.com/serverpod/serverpod/raw/main/misc/images/user-image-button.png)
28 |
29 | To display a user's profile picture, use the `CircularUserImage` widget and pass a `UserInfo` retrieved from the `SessionManager`.
30 |
31 | 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.
32 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/11-authentication/04-providers/01-email.md:
--------------------------------------------------------------------------------
1 | # Email
2 |
3 | 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.
4 |
5 | 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).
6 |
7 | :::caution
8 | You need to install the auth module before you continue, see [Setup](../setup).
9 | :::
10 |
11 | ## Server-side configuration
12 |
13 | In your main `server.dart` file, import the `serverpod_auth_server` module, and set up the authentication configuration:
14 |
15 | ```dart
16 | import 'package:serverpod_auth_server/module.dart' as auth;
17 |
18 | auth.AuthConfig.set(auth.AuthConfig(
19 | sendValidationEmail: (session, email, validationCode) async {
20 | // Send the validation email to the user.
21 | // Return `true` if the email was successfully sent, otherwise `false`.
22 | return true;
23 | },
24 | sendPasswordResetEmail: (session, userInfo, validationCode) async {
25 | // Send the password reset email to the user.
26 | // Return `true` if the email was successfully sent, otherwise `false`.
27 | return true;
28 | },
29 | ));
30 |
31 | // Start the Serverpod server.
32 | await pod.start();
33 | ```
34 |
35 | :::info
36 |
37 | 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/chat/chat_server/lib/server.dart).
38 |
39 | :::
40 |
41 | ## Client-side configuration
42 |
43 | Add the dependencies to your `pubspec.yaml` in your **client** project.
44 |
45 | ```yaml
46 | dependencies:
47 | ...
48 | serverpod_auth_client: ^1.x.x
49 | ```
50 |
51 | Add the dependencies to your `pubspec.yaml` in your **Flutter** project.
52 |
53 | ```yaml
54 | dependencies:
55 | ...
56 | serverpod_auth_email_flutter: ^1.x.x
57 | serverpod_auth_shared_flutter: ^1.x.x
58 | ```
59 |
60 | ### Prebuilt sign in button
61 |
62 | 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.
63 |
64 | ```dart
65 | SignInWithEmailButton(
66 | caller: client.modules.auth,
67 | onSignedIn: () {
68 | // Optional callback when user successfully signs in
69 | },
70 | ),
71 | ```
72 |
73 | ![SignInWithEmailButton](/img/authentication/providers/email/1-sign-in-with-email-button.png)
74 |
75 | ### Modal example
76 |
77 | The triggered modal will look like this:
78 |
79 | ![SignInWithEmailDialog](/img/authentication/providers/email/2-auth-email-dialog.png)
80 |
81 | ## Custom UI with EmailAuthController
82 |
83 | 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.
84 |
85 | ```dart
86 | import 'package:serverpod_auth_email_flutter/serverpod_auth_email_flutter.dart';
87 |
88 | final authController = EmailAuthController(client.modules.auth);
89 | ```
90 |
91 | 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.
92 |
93 | ```dart
94 | await authController.createAccountRequest(userName, email, password);
95 | ```
96 |
97 | Then let the user type in the code and send it to the backend with the `validateAccount` method. This method will create the user and sign them in if the code is valid.
98 |
99 | ```dart
100 | await authController.validateAccount(email, verificationCode);
101 | ```
102 |
103 | To let users log in if they already have an account you can use the `signIn` method.
104 |
105 | ```dart
106 | await authController.signIn(email, password);
107 | ```
108 |
109 | 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.
110 |
111 | ```dart
112 | await authController.initiatePasswordReset(email);
113 | ```
114 |
115 | Let the user type in the verification code along with the new password and send it to the backend with the `resetPassword` method.
116 |
117 | ```dart
118 | await authController.resetPassword(email, verificationCode, password);
119 | ```
120 |
121 | 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 simply chaining the `resetPassword` method and the `singIn` method for a seamless UX.
122 |
123 | ## Password storage security
124 |
125 | Serverpod provides some additional configurable options to provide extra layers of security for stored password hashes.
126 |
127 | :::info
128 | 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.
129 | :::
130 |
131 | ### Peppering
132 |
133 | 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.
134 |
135 | The [recommended pepper length](https://www.ietf.org/archive/id/draft-ietf-kitten-password-storage-04.html#name-storage-2) is 32 bytes.
136 |
137 | To configure a pepper, set the `emailPasswordPepper` property in the `config/passwords.yaml` file.
138 |
139 | ```yaml
140 | development:
141 | emailPasswordPepper: 'your-pepper'
142 | ```
143 |
144 | It is essential to keep the pepper secret and never expose it to the client.
145 |
146 | :::warning
147 |
148 | If the pepper is changed, all passwords in the database will need to be re-hashed with the new pepper.
149 |
150 | :::
151 |
152 | ### Secure random
153 |
154 | Serverpod uses the `dart:math` library to generate random salts for password hashing. By default, if no secure random number generator is available, a cryptographically unsecure random number is used.
155 |
156 | It is possible to prevent this fallback by setting the `allowUnsecureRandom` property in the `AuthConfig` to `false`. If the `allowUnsecureRandom` property is false, the server will throw an exception if a secure random number generator is unavailable.
157 |
158 | ```dart
159 | auth.AuthConfig.set(auth.AuthConfig(
160 | allowUnsecureRandom: false,
161 | ));
162 | ```
163 |
164 | ## Custom password hash generator
165 |
166 | 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`.
167 |
168 | ```dart
169 | AuthConfig(
170 | passwordHashValidator: (
171 | password,
172 | email,
173 | hash, {
174 | onError,
175 | onValidationFailure,
176 | },
177 | ) {
178 | // Custom hash validator.
179 | },
180 | passwordHashGenerator: (password) {
181 | // Custom hash generator.
182 | },
183 | )
184 |
185 | ```
186 |
187 | It could be useful if you already have stored passwords that should be preserved or migrated.
188 |
189 | :::warning
190 |
191 | Using a custom hash generator will permanently disrupt compatibility with the default hash generator.
192 |
193 | :::
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/11-authentication/04-providers/02-google.md:
--------------------------------------------------------------------------------
1 | # Google
2 |
3 | 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.
4 |
5 | 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).
6 |
7 | :::note
8 | Right now, we have official support for iOS, Android, and Web for Google Sign In.
9 | :::
10 |
11 | :::caution
12 | You need to install the auth module before you continue, see [Setup](../setup).
13 | :::
14 |
15 | ## Create your credentials
16 |
17 | 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/).
18 |
19 | ### Enable Peoples API
20 |
21 | To be allowed to access user data and use the authentication method in Serverpod we have to enable the Peoples API in our project.
22 |
23 | [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_.
24 |
25 | ### Setup OAuth consent screen
26 |
27 | 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_.
28 |
29 | 1. Fill in all the required information, for production use you need a domain that adds under `Authorized` domains.
30 |
31 | 2. Add the scopes `.../auth/userinfo.email` and `.../auth/userinfo.profile`.
32 |
33 | 3. Add your email to the test users so that you can test your integration in development mode.
34 |
35 | ![Scopes](/img/authentication/providers/google/1-scopes.png)
36 |
37 | ## Server-side configuration
38 |
39 | 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.
40 |
41 | 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.
42 |
43 | :::warning
44 |
45 | The `google_client_secret.json` contains a private key and should not be version controlled.
46 |
47 | :::
48 |
49 | ![Google credentials](/img/6-google-credentials.jpg)
50 |
51 | ## Client-side configuration
52 |
53 | 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.
54 |
55 | :::info
56 | 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.
57 | :::
58 |
59 | ### iOS
60 |
61 | 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**_.
62 |
63 | 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.
64 |
65 | Open the `GoogleService-Info.plist` in your editor and add the SERVER_CLIENT_ID if it does not exist:
66 |
67 | ```xml
68 | <dict>
69 | ...
70 | <key>SERVER_CLIENT_ID</key>
71 | <string>your_server_client_id</string>
72 | </dict>
73 | ```
74 |
75 | Replace `your_server_client_id` with the client id from the JSON file you put inside the config folder in the server.
76 |
77 | #### Add the URL scheme
78 |
79 | 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.
80 |
81 | Open the `info.plist` file in your editor and add the following to register the URL Scheme.
82 |
83 | ```xml
84 | <dict>
85 | ...
86 | <key>CFBundleURLTypes</key>
87 | <array>
88 | <dict>
89 | <key>CFBundleTypeRole</key>
90 | <string>Editor</string>
91 | <key>CFBundleURLSchemes</key>
92 | <array>
93 | <string>your_reversed_client_id</string>
94 | </array>
95 | </dict>
96 | </array>
97 | </dict>
98 | ```
99 |
100 | Replace `your_reversed_client_id` with your reversed client ID.
101 |
102 | :::info
103 |
104 | 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/)).
105 |
106 | :::
107 |
108 | ### Android
109 |
110 | 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**_.
111 |
112 | 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.
113 |
114 | Put the file inside the `android/app/` directory and rename it to `google-services.json`.
115 |
116 | :::info
117 | 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)).
118 |
119 | ```bash
120 | $ keytool -list -v -keystore /path/to/keystore
121 | ```
122 |
123 | :::
124 |
125 | ### Web
126 |
127 | There is no need to create any client credentials for the web we will simply pass the `serverClientId` to the sign-in button.
128 | However, we have to modify the server credentials inside the google cloud console.
129 |
130 | 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.
131 |
132 | :::info
133 |
134 | Force flutter to run on a specific port by running.
135 |
136 | ```bash
137 | $ flutter run -d chrome --web-port=49660
138 | ```
139 |
140 | :::
141 |
142 | 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.
143 |
144 | For example, using the path `/googlesignin`.
145 |
146 | For development inside `Authorized redirect URIs` add `http://localhost:8082/googlesignin`, in production use `https://example.com/googlesignin`.
147 |
148 | ![Google credentials](/img/authentication/providers/google/2-credentials.png)
149 |
150 | #### Serve the redirect page
151 |
152 | Register the Google Sign In route inside `server.dart`.
153 |
154 | ```dart
155 | import 'package:serverpod_auth_server/module.dart' as auth
156 |
157 |
158 | void run(List<String> args) async {
159 | ...
160 | pod.webServer.addRoute(auth.RouteGoogleSignIn(), '/googlesignin');
161 | ...
162 | }
163 | ```
164 |
165 | This page is needed for the web app to receive the authentication code given by Google.
166 |
167 | ### Flutter implementation
168 |
169 | ![Scopes](/img/authentication/providers/google/3-button.png)
170 |
171 | Add the `SignInWithGoogleButton` to your widget.
172 |
173 | ```dart
174 | import 'package:serverpod_auth_google_flutter/serverpod_auth_google_flutter.dart';
175 |
176 |
177 | SignInWithGoogleButton(
178 | caller: client.modules.auth,
179 | serverClientId: _googleServerClientId, // needs to be supplied for the web integration
180 | redirectUri: Uri.parse('http://localhost:8082/googlesignin'),
181 | )
182 | ```
183 |
184 | As an alternative to adding the JSON files in your client projects, you can supply the client and server ID on iOS and Android.
185 |
186 | ```dart
187 | import 'package:serverpod_auth_google_flutter/serverpod_auth_google_flutter.dart';
188 |
189 |
190 | SignInWithGoogleButton(
191 | caller: client.modules.auth,
192 | clientId: _googleClientId, // Client ID of the client (null on web)
193 | serverClientId: _googleServerClientId, // Client ID from the server (required on web)
194 | redirectUri: Uri.parse('http://localhost:8082/googlesignin'),
195 | )
196 | ```
197 |
198 | ## Calling Google APIs
199 |
200 | 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:
201 |
202 | - Add the required scopes to the OAuth consent screen.
203 | - Request access to the scopes when signing in. Do this by setting the `additionalScopes` parameter of the `signInWithGoogle` method or the `SignInWithGoogleButton` widget.
204 |
205 | A full list of available scopes can be found [here](https://developers.google.com/identity/protocols/oauth2/scopes).
206 |
207 | :::info
208 |
209 | Adding additional scopes may require approval by Google. On the OAuth consent screen, you can see which of your scopes are considered sensitive.
210 |
211 | :::
212 |
213 | 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.
214 |
215 | For instance, to access the Youtube APIs, add the scope to your `SignInWithGoogleButton` in your app:
216 |
217 | ```dart
218 | SignInWithGoogleButton(
219 | ...
220 | additionalScopes: const ['https://www.googleapis.com/auth/youtube'],
221 | )
222 | ```
223 |
224 | 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.
225 |
226 | ```dart
227 | import 'package:serverpod_auth_server/module.dart';
228 | import 'package:googleapis/youtube/v3.dart';
229 |
230 |
231 | final googleClient = await GoogleAuth.authClientForUser(session, userId);
232 |
233 | if (googleClient != null) {
234 | var youTubeApi = YouTubeApi(googleClient);
235 |
236 | var favorites = await youTubeApi.playlistItems.list(
237 | ['snippet'],
238 | playlistId: 'LL', // Liked List
239 | );
240 |
241 | } else {
242 | // The user hasn't signed in with Google.
243 | }
244 | ```
245 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/11-authentication/04-providers/03-apple.md:
--------------------------------------------------------------------------------
1 | # Apple
2 |
3 | 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.
4 |
5 | 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).
6 |
7 | :::note
8 | Right now, we have official support for iOS and MacOS for Sign in with Apple.
9 | :::
10 |
11 | :::caution
12 | You need to install the auth module before you continue, see [Setup](../setup).
13 | :::
14 |
15 | ## Server-side configuration
16 |
17 | No extra steps outside installing the auth module are required.
18 |
19 | ## Client-side configuration
20 |
21 | Add the dependency to your `pubspec.yaml` in your flutter project.
22 |
23 | ```yaml
24 | dependencies:
25 | ...
26 | serverpod_auth_apple_flutter: ^1.x.x
27 | ```
28 |
29 | ### Config
30 |
31 | 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.
32 |
33 | ![Add capabilities](/img/authentication/providers/apple/1-xcode-add.png)
34 |
35 | ![Sign in with Apple](/img/authentication/providers/apple/2-xcode-sign-in-with-apple.png)
36 |
37 | ### Sign in button
38 |
39 | `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.
40 |
41 | ```dart
42 | import 'package:serverpod_auth_email_flutter/serverpod_auth_email_flutter.dart';
43 |
44 | SignInWithAppleButton(
45 | caller: client.modules.auth,
46 | );
47 | ```
48 |
49 | The SignInWithAppleButton widget takes a caller parameter that you pass in the authentication module from your Serverpod client, in this case, client.modules.auth.
50 |
51 | ![Sign-in button](/img/authentication/providers/apple/3-button.png)
52 |
53 | ## Extra
54 |
55 | 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.
56 |
57 | _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._
58 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/11-authentication/04-providers/05-firebase.md:
--------------------------------------------------------------------------------
1 | # Firebase
2 |
3 | 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.
4 |
5 | :::warning
6 |
7 | 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.
8 |
9 | :::
10 |
11 | ## Server-side configuration
12 |
13 | 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`.
14 |
15 | ![Service account](/img/authentication/providers/firebase/1-server-key.png)
16 |
17 | 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.
18 |
19 | :::danger
20 | The firebase_service_account_key.json file gives admin access to your Firebase project, never store it in version control.
21 | :::
22 |
23 | ## Client-side configuration
24 |
25 | 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.
26 |
27 | ## Firebase config
28 |
29 | The short version:
30 |
31 | ```bash
32 | $ flutter pub add firebase_core firebase_auth firebase_ui_auth
33 | $ flutterfire configure
34 | ```
35 |
36 | In the Firebase console, configure the different social sign-ins you plan to use, under `Authentication > Sign-in method`.
37 |
38 | ![Auth provider](/img/authentication/providers/firebase/2-auth-provider.png)
39 |
40 | In your `main.dart` in your flutter project add:
41 |
42 | ```dart
43 | import 'package:firebase_ui_auth/firebase_ui_auth.dart' as firebase;
44 | import 'package:firebase_core/firebase_core.dart';
45 | import 'firebase_options.dart';
46 |
47 | ...
48 | void main() async {
49 | ...
50 | await Firebase.initializeApp(
51 | options: DefaultFirebaseOptions.currentPlatform,
52 | );
53 |
54 | firebase.FirebaseUIAuth.configureProviders([
55 | firebase.PhoneAuthProvider(),
56 | ]);
57 |
58 | ...
59 | runApp(const MyApp());
60 | }
61 | ```
62 |
63 | ## Trigger the auth UI with Serverpod
64 |
65 | Add the [serverpod_auth_firebase_flutter](https://pub.dev/packages/serverpod_auth_firebase_flutter) package.
66 |
67 | ```bash
68 | $ flutter pub add serverpod_auth_firebase_flutter
69 | ```
70 |
71 | The `SignInWithFirebaseButton` is a convenient button that triggers the sign-in flow and can be used like this:
72 |
73 | ```dart
74 | SignInWithFirebaseButton(
75 | caller: client.modules.auth,
76 | authProviders: [
77 | firebase.PhoneAuthProvider(),
78 | ],
79 | onFailure: () => print('Failed to sign in with Firebase.'),
80 | onSignedIn: () => print('Signed in with Firebase.'),
81 | )
82 | ```
83 |
84 | 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.
85 |
86 | You can also trigger the Firebase auth UI by calling the method `signInWithFirebase` like so:
87 |
88 | ```dart
89 | await signInWithFirebase(
90 | context: context,
91 | caller: client.modules.auth,
92 | authProviders: [
93 | firebase.PhoneAuthProvider(),
94 | ],
95 | );
96 | ```
97 |
98 | Where `context` is your `BuildContext`, `caller` and `authProviders` are the same as for the button. The method returns a nullable [UserInfo](../working-with-users) object, if the object is null the Sign-in failed, if not the Sign-in was successful.
99 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/11-authentication/04-providers/06-custom-providers.md:
--------------------------------------------------------------------------------
1 | # Custom providers
2 |
3 | Serverpod's authentication 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.
4 |
5 | ## Server setup
6 |
7 | After successfully authenticating a user through a customer 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.
8 |
9 | ### Connect user
10 |
11 | The authentication module provides methods to find or create users. This ensures that all authentication tokens from the same user are connected.
12 |
13 | Users can be identified either by their email through the `Users.findUserByEmail(...)` method or by a unique identifier through the `Users.findUserByIdentifier(...)` method.
14 |
15 | If no user is found, a new user can be created through the `Users.createUser(...)` method.
16 |
17 | ```dart
18 | UserInfo? userInfo;
19 | userInfo = await Users.findUserByEmail(session, email);
20 | userInfo ??= await Users.findUserByIdentifier(session, userIdentifier);
21 | if (userInfo == null) {
22 | userInfo = UserInfo(
23 | userIdentifier: userIdentifier,
24 | userName: name,
25 | email: email,
26 | blocked: false,
27 | created: DateTime.now().toUtc(),
28 | scopeNames: [],
29 | );
30 | userInfo = await Users.createUser(session, userInfo, _authMethod);
31 | }
32 | ```
33 |
34 | 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.
35 |
36 | :::note
37 | 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.
38 | :::
39 |
40 | ### Custom identification methods
41 |
42 | 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.
43 |
44 | ```dart
45 | var userInfo = await UserInfo.db.findFirstRow(
46 | session,
47 | where: (t) => t.fullName.equals(name),
48 | );
49 | ```
50 |
51 | The example above shows how to find a user by name using the `UserInfo` model.
52 |
53 | ### Create auth token
54 |
55 | When a user has been found or created, an auth token that is connected to the user should be created.
56 |
57 | To create an auth token, call the `signInUser` method in the `UserAuthentication` class, accessible as a static method, e.g. `UserAuthentication.signInUser`.
58 |
59 | 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.
60 |
61 | ```dart
62 | var authToken = await UserAuthentication.signInUser(userInfo.id, 'myAuthMethod', scopes: {
63 | Scope('delete'),
64 | Scope('create'),
65 | });
66 | ```
67 |
68 | 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`.
69 |
70 | :::info
71 | 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.
72 | :::
73 |
74 | ### Send auth token to client
75 |
76 | 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.
77 |
78 | ```dart
79 | class MyAuthenticationEndpoint extends Endpoint {
80 | Future<AuthenticationResponse> login(
81 | Session session,
82 | String username,
83 | String password,
84 | ) async {
85 | // Authenticates a user with email and password.
86 | if (!authenticateUser(session, username, password)) {
87 | return AuthenticationResponse(success: false);
88 | }
89 |
90 | // Finds or creates a user in the database using the User methods.
91 | var userInfo = findOrCreateUser(session, username);
92 |
93 | // Creates an authentication key for the user.
94 | var authToken = await UserAuthentication.signInUser(
95 | session,
96 | userInfo.id!,
97 | 'myAuth',
98 | scopes: {},
99 | );
100 |
101 | // Returns the authentication response.
102 | return AuthenticationResponse(
103 | success: true,
104 | keyId: authToken.id,
105 | key: authToken.key,
106 | userInfo: userInfo,
107 | );
108 | }
109 | }
110 | ```
111 |
112 | The example above shows how to create an `AuthenticationResponse` with the auth token and user information.
113 |
114 | ### Revoking authentication keys
115 |
116 | 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:
117 |
118 | - Closing all affected method streaming connections to maintain connection integrity.
119 | - Synchronizing authentication state across all connected servers.
120 | - Updating the session's authentication state with `session.updateAuthenticated(null)` if the affected user is currently authenticated.
121 |
122 | #### Revoking specific keys
123 |
124 | To revoke specific authentication keys, use the `revokeAuthKey` method:
125 |
126 | ```dart
127 | await UserAuthentication.revokeAuthKey(
128 | session,
129 | authKeyId: 'auth-key-id-here',
130 | );
131 | ```
132 |
133 | ##### Fetching and revoking an authentication key using AuthenticationInfo
134 |
135 | To revoke a specific authentication key for the current session, you can directly access the session's authentication information and call the `revokeAuthKey` method:
136 |
137 | ```dart
138 | // Fetch the authentication information for the current session
139 | var authId = (await session.authenticated)?.authId;
140 |
141 | // Revoke the authentication key if the session is authenticated and has an authId
142 | if (authId != null) {
143 | await UserAuthentication.revokeAuthKey(
144 | session,
145 | authKeyId: authId,
146 | );
147 | }
148 | ```
149 |
150 | ##### Fetching and revoking a specific authentication key for a user
151 |
152 | 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:
153 |
154 | ```dart
155 | // Fetch all authentication keys for the user
156 | var authKeys = await AuthKey.db.find(
157 | session,
158 | where: (t) => t.userId.equals(userId),
159 | );
160 |
161 | // Revoke a specific key (for example, the last one)
162 | if (authKeys.isNotEmpty) {
163 | var authKeyId = authKeys.last.id.toString(); // Convert the ID to string
164 | await UserAuthentication.revokeAuthKey(
165 | session,
166 | authKeyId: authKeyId,
167 | );
168 | }
169 | ```
170 |
171 | ##### Removing specific tokens (direct deletion)
172 |
173 | ```dart
174 | await AuthKey.db.deleteWhere(
175 | session,
176 | where: (t) => t.userId.equals(userId) & t.method.equals('username'),
177 | );
178 | ```
179 |
180 | :::warning
181 |
182 | 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.
183 |
184 | :::
185 |
186 | #### Signing out all devices
187 |
188 | The `signOutUser` method signs a user out from all devices:
189 |
190 | ```dart
191 | await UserAuthentication.signOutUser(
192 | session,
193 | userId: 123, // Optional: If omitted, the currently authenticated user will be signed out
194 | );
195 | ```
196 | This method deletes all authentication keys associated with the user.
197 |
198 | ##### Signing out a specific user
199 |
200 | In this example, a specific `userId` is provided to sign out that user from all their devices:
201 |
202 | ```dart
203 | // Sign out the user with ID 123 from all devices
204 | await UserAuthentication.signOutUser(
205 | session,
206 | userId: 123,
207 | );
208 | ```
209 |
210 | ##### Signing out the currently authenticated user
211 |
212 | If no `userId` is provided, `signOutUser` will automatically sign out the user who is currently authenticated in the session:
213 |
214 | ```dart
215 | // Sign out the currently authenticated user
216 | await UserAuthentication.signOutUser(
217 | session, // No userId provided, signs out the current user
218 | );
219 | ```
220 |
221 | #### Creating a logout endpoint
222 |
223 | To sign out a user on all devices using an endpoint, the `signOutUser` method in the `UserAuthentication` class can be used:
224 |
225 | ```dart
226 | class AuthenticatedEndpoint extends Endpoint {
227 | @override
228 | bool get requireLogin => true;
229 |
230 | Future<void> logout(Session session) async {
231 | await UserAuthentication.signOutUser(session);
232 | }
233 | }
234 | ```
235 |
236 | ## Client setup
237 |
238 | 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`.
239 |
240 | 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.
241 |
242 | 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`.
243 |
244 | ```dart
245 | var serverResponse = await caller.myAuthentication.login(username, password);
246 |
247 | if (serverResponse.success) {
248 | // Store the user info in the session manager.
249 | SessionManager sessionManager = await SessionManager.instance;
250 | await sessionManager.registerSignedInUser(
251 | serverResponse.userInfo!,
252 | serverResponse.keyId!,
253 | serverResponse.key!,
254 | );
255 | }
256 | ```
257 |
258 | The example above shows how to register a signed-in user in the session manager.
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/11-authentication/05-custom-overrides.md:
--------------------------------------------------------------------------------
1 | # Custom overrides
2 |
3 | It is recommended to use the `serverpod_auth` package but if you have special requirements not fulfilled by it, you can implement your authentication module. Serverpod is designed to make it easy to add custom authentication overrides.
4 |
5 | ## Server setup
6 |
7 | When running a custom auth integration it is up to you to build the authentication model and issuing auth tokens.
8 |
9 | ### Token validation
10 |
11 | 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`.
12 |
13 | ```dart
14 | // Initialize Serverpod and connect it with your generated code.
15 | final pod = Serverpod(
16 | args,
17 | Protocol(),
18 | Endpoints(),
19 | authenticationHandler: (Session session, String token) async {
20 | /// Custom validation handler
21 | if (token != 'valid') return null;
22 |
23 | return AuthenticationInfo(1, <Scope>{});
24 | },
25 | );
26 | ```
27 |
28 | In the above example, the `authenticationHandler` callback is overridden with a custom validation method. The method returns an `AuthenticationInfo` object with user id `1` and no scopes if the token is valid, otherwise `null`.
29 |
30 | :::note
31 | 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.
32 | :::
33 |
34 | :::info
35 | By specifying the optional `authId` field in the `AuthenticationInfo` object you can link the user to a specific authentication id. This is useful when revoking authentication for a specific device.
36 | :::
37 |
38 | #### Scopes
39 |
40 | 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.
41 |
42 | 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.
43 |
44 | ```dart
45 | List<String> scopes = extractScopes(token);
46 | Set<Scope> userScopes = scopes.map((scope) => Scope(scope)).toSet();
47 | ```
48 |
49 | ### Handling revoked authentication
50 |
51 | 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.
52 |
53 | ```dart
54 | var userId = 1;
55 | var revokedScopes = ['write'];
56 | var message = RevokedAuthenticationScope(
57 | scopes: revokedScopes,
58 | );
59 |
60 | await session.messages.authenticationRevoked(
61 | userId,
62 | message,
63 | );
64 | ```
65 |
66 | ##### Parameters
67 |
68 | - `userId` - The user id belonging to the `AuthenticationInfo` object to be revoked.
69 | - `message` - The revoked authentication event message. See below for the different type of messages.
70 |
71 | #### Revoked authentication messages
72 | There are three types of `RevokedAuthentication` messages that are used to specify the extent of the authentication revocation:
73 |
74 | | Message type | Description |
75 | |-----------|-------------|
76 | | `RevokedAuthenticationUser` | All authentication is revoked for a user. |
77 | | `RevokedAuthenticationAuthId` | A single authentication id is revoked for the user. This should match the `authId` field in the `AuthenticationInfo` object. |
78 | | `RevokedAuthenticationScope` | List of scopes that have been revoked for a user. |
79 |
80 | Each message type provides a tailored approach to revoke authentication based on different needs.
81 |
82 | ### Send token to client
83 |
84 | You are responsible for implementing the endpoints to authenticate/authorize the user. But as an example such an endpoint could look like the following.
85 |
86 | ```dart
87 | class UserEndpoint extends Endpoint {
88 | Future<LoginResponse> login(
89 | Session session,
90 | String username,
91 | String password,
92 | ) async {
93 | var identifier = authenticateUser(session, username, password);
94 | if (identifier == null) return null;
95 |
96 | return issueMyToken(identifier, scopes: {});
97 | }
98 | }
99 | ```
100 |
101 | In the above example, the `login` method authenticates the user and creates an auth token. The token is then returned to the client.
102 |
103 | ## Client setup
104 |
105 | 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.
106 |
107 | ### Configure key manager
108 |
109 | Key managers need to implement the `AuthenticationKeyManager` interface. The key manager is configured when creating the client by passing it as the named parameter `authenticationKeyManager`. If no key manager is configured, the client will not include tokens in requests to the server.
110 |
111 | ```dart
112 | class SimpleAuthKeyManager extends AuthenticationKeyManager {
113 | String? _key;
114 |
115 | @override
116 | Future<String?> get() async {
117 | return _key;
118 | }
119 |
120 | @override
121 | Future<void> put(String key) async {
122 | _key = key;
123 | }
124 |
125 | @override
126 | Future<void> remove() async {
127 | _key = null;
128 | }
129 | }
130 |
131 |
132 | var client = Client('http://$localhost:8080/',
133 | authenticationKeyManager: SimpleAuthKeyManager())
134 | ..connectivityMonitor = FlutterConnectivityMonitor();
135 | ```
136 |
137 | In the above example, the `SimpleAuthKeyManager` is configured as the client's authentication key manager. The `SimpleAuthKeyManager` stores the token in memory.
138 |
139 | :::info
140 |
141 | 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.
142 |
143 | :::
144 |
145 | The key manager is then available through the client's `authenticationKeyManager` field.
146 |
147 | ```dart
148 | var keyManager = client.authenticationKeyManager;
149 | ```
150 |
151 | ### Store token
152 |
153 | 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.
154 |
155 | ```dart
156 | await client.authenticationKeyManager?.put(token);
157 | ```
158 |
159 | In the above example, the `token` is placed in the key manager. It will now be included in communication with the server.
160 |
161 | ### Remove token
162 |
163 | To remove the token from the key manager, call the `remove` method.
164 |
165 | ```dart
166 | await client.authenticationKeyManager?.remove();
167 | ```
168 |
169 | The above example removes any token from the key manager.
170 |
171 | ### Retrieve token
172 |
173 | To retrieve the token from the key manager, call the `get` method.
174 |
175 | ```dart
176 | var token = await client.authenticationKeyManager?.get();
177 | ```
178 |
179 | The above example retrieves the token from the key manager and stores it in the `token` variable.
180 |
181 | ## Authentication schemes
182 |
183 | By default Serverpod will pass 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.
184 |
185 | The default implementation encodes and wraps the user-provided token in a `basic` scheme which is automatically unwrapped on the server side before being handed to the user-provided authentication handler described above.
186 |
187 | In other words the default transport implementation is "invisible" to user code.
188 |
189 | ### Implementing your own authentication scheme
190 |
191 | 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.
192 |
193 | If you are implementing a different authentication scheme, for example OAuth 2 using bearer tokens, you should override the default method `toHeaderValue` of `AuthenticationKeyManager`. This client-side method converts the authentication key to the format that shall be sent as a transport header to the server.
194 |
195 | You will also need to implement the `AuthenticationHandler` accordingly, in order to process that header value server-side.
196 |
197 | The header value must be compliant with the HTTP header format defined in RFC 9110 HTTP Semantics, 11.6.2. Authorization.
198 | See:
199 | - [HTTP Authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization)
200 | - [RFC 9110, 11.6.2. Authorization](https://httpwg.org/specs/rfc9110.html#field.authorization)
201 |
202 | An approach to adding OAuth handling might make changes to the above code akin to the following.
203 |
204 | Client side:
205 |
206 | ```dart
207 | class MyOAuthKeyManager extends AuthenticationKeyManager {
208 | String? _key;
209 |
210 | @override
211 | Future<String?> get() async {
212 | return _key;
213 | }
214 |
215 | @override
216 | Future<void> put(String key) async {
217 | _key = key;
218 | }
219 |
220 | @override
221 | Future<void> remove() async {
222 | _key = null;
223 | }
224 |
225 | @override
226 | Future<String?> toHeaderValue(String? key) async {
227 | if (key == null) return null;
228 | return 'Bearer ${myBearerTokenObtainer(key)}';
229 | }
230 | }
231 |
232 |
233 | var client = Client('http://$localhost:8080/',
234 | authenticationKeyManager: SimpleAuthKeyManager())
235 | ..connectivityMonitor = FlutterConnectivityMonitor();
236 | ```
237 |
238 | Server side:
239 |
240 | ```dart
241 | // Initialize Serverpod and connect it with your generated code.
242 | final pod = Serverpod(
243 | args,
244 | Protocol(),
245 | Endpoints(),
246 | authenticationHandler: (Session session, String token) async {
247 | /// Bearer token validation handler
248 | var (uid, scopes) = myBearerTokenValidator(token)
249 | if (uid == null) return null;
250 |
251 | return AuthenticationInfo(uid, scopes);
252 | },
253 | );
254 | ```
255 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/12-file-uploads.md:
--------------------------------------------------------------------------------
1 | # Uploading files
2 |
3 | Serverpod has built-in support for handling file uploads. Out of the box, your server is configured to use the database for storing files. This works well for testing but may not be performant in larger-scale applications. You should set up your server to use Google Cloud Storage or S3 in production scenarios.
4 |
5 | ## How to upload a file
6 |
7 | A `public` and `private` file storage are set up by default to use the database. You can replace these or add more configurations for other file storages.
8 |
9 | ### Server-side code
10 |
11 | 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.
12 |
13 | ```dart
14 | Future<String?> getUploadDescription(Session session, String path) async {
15 | return await session.storage.createDirectFileUploadDescription(
16 | storageId: 'public',
17 | path: path,
18 | );
19 | }
20 | ```
21 |
22 | After the file is uploaded, you should verify that the upload has been completed. If you are uploading a file to a third-party service, such as S3 or Google Cloud Storage, there is no other way of knowing if the file was uploaded or if the upload was canceled.
23 |
24 | ```dart
25 | Future<bool> verifyUpload(Session session, String path) async {
26 | return await session.storage.verifyDirectFileUpload(
27 | storageId: 'public',
28 | path: path,
29 | );
30 | }
31 | ```
32 |
33 | ### Client-side code
34 |
35 | To upload a file from the app side, first request the upload description. Next, upload the file. You can upload from either a `Stream` or a `ByteData` object. If you are uploading a larger file, using a `Stream` is better because not all of the data must be held in RAM memory. Finally, you should verify the upload with the server.
36 |
37 | ```dart
38 | var uploadDescription = await client.myEndpoint.getUploadDescription('myfile');
39 | if (uploadDescription != null) {
40 | var uploader = FileUploader(uploadDescription);
41 | await uploader.upload(myStream);
42 | var success = await client.myEndpoint.verifyUpload('myfile');
43 | }
44 | ```
45 |
46 | :::info
47 |
48 | 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. E.g.:
49 |
50 | ```dart
51 | 'profile/$userId/images/avatar.png'
52 | ```
53 |
54 | :::
55 |
56 | ## Accessing stored files
57 |
58 | It's possible to quickly check if an uploaded file exists or access the file itself. If a file is in a public storage, it is also accessible to the world through an URL. If it is private, it can only be accessed from the server.
59 |
60 | To check if a file exists, use the `fileExists` method.
61 |
62 | ```dart
63 | var exists = await session.storage.fileExists(
64 | storageId: 'public',
65 | path: 'my/file/path',
66 | );
67 | ```
68 |
69 | If the file is in a public storage, you can access it through its URL.
70 |
71 | ```dart
72 | var url = await session.storage.getPublicUrl(
73 | storageId: 'public',
74 | path: 'my/file/path',
75 | );
76 | ```
77 |
78 | You can also directly retrieve or store a file from your server.
79 |
80 | ```dart
81 | var myByteData = await session.storage.retrieveFile(
82 | storageId: 'public',
83 | path: 'my/file/path',
84 | );
85 | ```
86 |
87 | ## Add a configuration for GCP
88 |
89 | Serverpod uses Google Cloud Storage's HMAC interoperability to handle file uploads to Google Cloud. To make file uploads work, you must make a few custom configurations in your Google Cloud console:
90 |
91 | 1. Create a service account with the _Storage Admin_ role.
92 | 2. Under _Cloud Storage_ > _Settings_ > _Interoperability_, create a new HMAC key for your newly created service account.
93 | 3. Add the two keys you received in the previous step to your `config/password.yaml` file. The keys should be named `HMACAccessKeyId` and `HMACSecretKey`, respectively. You can also pass them in as environment variables. The environment variable names are `SERVERPOD_HMAC_ACCESS_KEY_ID` and `SERVERPOD_HMAC_SECRET_KEY`.
94 | 4. When creating a new bucket, set the _Access control_ to _Fine-grained_ and disable the _Prevent public access_ option.
95 |
96 | You may also want to add the bucket as a backend for your load balancer to give it a custom domain name.
97 |
98 | When you have set up your GCP bucket, you need to configure it in Serverpod. Add the GCP package to your `pubspec.yaml` file and import it in your `server.dart` file.
99 |
100 | ```bash
101 | $ dart pub add serverpod_cloud_storage_gcp
102 | ```
103 |
104 | ```dart
105 | import 'package:serverpod_cloud_storage_gcp/serverpod_cloud_storage_gcp.dart'
106 | as gcp;
107 | ```
108 |
109 | After creating your Serverpod, you add a storage configuration. If you want to replace the default `public` or `private` storages, set the `storageId` to `public` or `private`. Set the public host if you have configured your GCP bucket to be accessible on a custom domain through a load balancer. You should add the cloud storage before starting your pod. The `bucket` parameter refers to the GCP bucket name (you can find it in the console) and the `publicHost` is the domain name used to access the bucket via https.
110 |
111 | ```dart
112 | pod.addCloudStorage(gcp.GoogleCloudStorage(
113 | serverpod: pod,
114 | storageId: 'public',
115 | public: true,
116 | region: 'auto',
117 | bucket: 'my-bucket-name',
118 | publicHost: 'storage.myapp.com',
119 | ));
120 | ```
121 |
122 | ## Add a configuration for AWS S3
123 |
124 | This section shows how to set up a storage using S3. Before you write your Dart code, you need to set up an S3 bucket. Most likely, you will also want to set up a CloudFront for the bucket, where you can use a custom domain and your own SSL certificate. Finally, you will need to get a set of AWS access keys and add them to your Serverpod password file (`AWSAccessKeyId` and `AWSAccessKey`) or pass them in as environment variables (`SERVERPOD_AWS_ACCESS_KEY_ID` and `SERVERPOD_AWS_ACCESS_KEY`).
125 |
126 | When you are all set with the AWS setup, include the S3 package in your `pubspec.yaml` file and import it in your `server.dart` file.
127 |
128 | ```bash
129 | $ dart pub add serverpod_cloud_storage_s3
130 | ```
131 |
132 | ```dart
133 | import 'package:serverpod_cloud_storage_s3/serverpod_cloud_storage_s3.dart'
134 | as s3;
135 | ```
136 |
137 | After creating your Serverpod, you add a storage configuration. If you want to replace the default `public` or `private` storages, set the `storageId` to `public` or `private`. Set the public host if you have configured your S3 bucket to be accessible on a custom domain through CloudFront. You should add the cloud storage before starting your pod.
138 |
139 | ```dart
140 | pod.addCloudStorage(s3.S3CloudStorage(
141 | serverpod: pod,
142 | storageId: 'public',
143 | public: true,
144 | region: 'us-west-2',
145 | bucket: 'my-bucket-name',
146 | publicHost: 'storage.myapp.com',
147 | ));
148 | ```
149 |
150 | For your S3 configuration to work, you will also need to add your AWS credentials to the `passwords.yaml` file. You create the access keys from your AWS console when signed in as the root user.
151 |
152 | ```yaml
153 | shared:
154 | AWSAccessKeyId: 'XXXXXXXXXXXXXX'
155 | AWSSecretKey: 'XXXXXXXXXXXXXXXXXXXXXXXXXXX'
156 | ```
157 |
158 | :::info
159 |
160 | If you are using the GCP or AWS Terraform scripts that are created with your Serverpod project, the required GCP or S3 buckets will be created automatically. The scripts will also configure your load balancer or Cloudfront and the certificates needed to access the buckets securely.
161 |
162 | :::
163 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/13-health-checks.md:
--------------------------------------------------------------------------------
1 | # Health checks
2 |
3 | Serverpod automatically performs health checks while running. It measures CPU and memory usage and the response time to the database. The metrics are stored in the database every minute in the serverpod_health_metric and serverpod_health_connection_info tables. However, the best way to visualize the data is through Serverpod Insights, which gives you a graphical view.
4 |
5 | ## Adding custom metrics
6 |
7 | Sometimes it is helpful to add custom health metrics. This can be for monitoring external services or internal processes within your Serverpod. To set up your custom metrics, you must create a `HealthCheckHandler` and register it with your Serverpod.
8 |
9 | ```dart
10 | // Create your custom health metric handler.
11 | Future<List<ServerHealthMetric>> myHealthCheckHandler(
12 | Serverpod pod, DateTime timestamp) async {
13 | // Actually perform some checks.
14 |
15 | // Return a list of health metrics for the given timestamp.
16 | return [
17 | ServerHealthMetric(
18 | name: 'MyMetric',
19 | serverId: pod.serverId,
20 | timestamp: timestamp,
21 | isHealthy: true,
22 | value: 1.0,
23 | ),
24 | ];
25 | }
26 | ```
27 |
28 | Register your handler when you create your Serverpod object.
29 |
30 | ```dart
31 | final pod = Serverpod(
32 | args,
33 | Protocol(),
34 | Endpoints(),
35 | healthCheckHandler: myHealthCheckHandler,
36 | );
37 | ```
38 |
39 | Once registered, your health check handler will be called once a minute to perform any health checks that you have configured. You can view the status of your checks in Serverpod Insights or in the database.
40 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/14-scheduling.md:
--------------------------------------------------------------------------------
1 | # Scheduling
2 |
3 | With Serverpod you can schedule future work with the `future call` feature. Future calls are calls that will be invoked at a later time. An example is if you want to send a drip-email campaign after a user signs up. You can schedule a future call for a day, a week, or a month. The calls are stored in the database, so they will persist even if the server is restarted.
4 |
5 | A future call is guaranteed to only execute once across all your instances that are running, but execution failures are not handled automatically. It is your responsibility to schedule a new future call if the work was not able to complete.
6 |
7 | Creating a future call is simple, extend the `FutureCall` class and override the `invoke` method. The method takes two params the first being the [`Session`](sessions) object and the second being an optional SerializableModel ([See models](models)).
8 |
9 | :::info
10 | The future call feature is not enabled when running Serverpod in serverless mode.
11 | :::
12 |
13 | ```dart
14 | import 'package:serverpod/serverpod.dart';
15 |
16 | class ExampleFutureCall extends FutureCall<MyModelEntity> {
17 | @override
18 | Future<void> invoke(Session session, MyModelEntity? object) async {
19 | // Do something interesting in the future here.
20 | }
21 | }
22 | ```
23 |
24 | To let your Server get access to the future call you have to register it in the main run method in your `server.dart` file. You register the future call by calling `registerFutureCall` on the Serverpod object and giving it an instance of the future call together with a string that gives the future call a name. The name has to be globally unique and is used to later invoke the future call.
25 |
26 | ```dart
27 | void run(List<String> args) async {
28 | final pod = Serverpod(
29 | args,
30 | Protocol(),
31 | Endpoints(),
32 | );
33 |
34 | ...
35 |
36 | pod.registerFutureCall(ExampleFutureCall(), 'exampleFutureCall');
37 |
38 | ...
39 | }
40 | ```
41 |
42 | You are now able to register a future call to be invoked in the future by calling either `futureCallWithDelay` or `futureCallAtTime` depending on your needs.
43 |
44 | Invoke the future call 1 hour from now by calling `futureCallWithDelay`.
45 |
46 | ```dart
47 | await session.serverpod.futureCallWithDelay(
48 | 'exampleFutureCall',
49 | data,
50 | const Duration(hours: 1),
51 | );
52 | ```
53 |
54 | Invoke the future call at a specific time and/or date in the future by calling `futureCallAtTime`.
55 |
56 | ```dart
57 | await session.serverpod.futureCallAtTime(
58 | 'exampleFutureCall',
59 | data,
60 | DateTime(2025, 1, 1),
61 | );
62 | ```
63 |
64 | :::note
65 | `data` is an object created from a class defined in one of your yaml files and has to be the same as the one you expect to receive in the future call. in the `model` folder, `data` may also be null if you don't need it.
66 | :::
67 |
68 | When registering a future call it is also possible to give it an `identifier` so that it can be referenced later. The same identifier can be applied to multiple future calls.
69 |
70 | ```dart
71 | await session.serverpod.futureCallWithDelay(
72 | 'exampleFutureCall',
73 | data,
74 | const Duration(hours: 1),
75 | identifier: 'an-identifying-string',
76 | );
77 | ```
78 |
79 | This identifier can then be used to cancel all future calls registered with said identifier.
80 |
81 | ```dart
82 | await session.serverpod.cancelFutureCall('an-identifying-string');
83 | ```
84 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/15-streams.md:
--------------------------------------------------------------------------------
1 | # Streams
2 |
3 | For some applications, it's not enough to be able to call server-side methods. You may also want to push data from the server to the client or send data two-way. Examples include real-time games or chat applications. Luckily, Serverpod supports a framework for streaming data. It's possible to stream any serialized objects to or from any endpoint.
4 |
5 | Serverpod supports two ways to stream data. The first approach, [streaming methods](#streaming-methods), imitates how `Streams` work in Dart and offers a simple interface that automatically handles the connection with the server. In contrast, the second approach, [streaming endpoint](#streaming-endpoints), requires developers to manage the web socket connection. The second approach was Serverpod's initial solution for streaming data but will be removed in future updates.
6 |
7 | :::tip
8 |
9 | For a real-world example, check out [Pixorama](https://pixorama.live). It's a multi-user drawing experience showcasing Serverpod's real-time capabilities and comes with complete source code.
10 |
11 | :::
12 |
13 | ## Streaming Methods
14 |
15 | 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. Streaming methods transmit data over a shared, self-managed web socket connection that automatically connects and disconnects from the server.
16 |
17 | ### Defining a streaming method
18 |
19 | Streaming methods are defined by using the `Stream` type as either the return value or a parameter.
20 |
21 | Following is an example of a streaming method that echoes back any message:
22 |
23 | ```dart
24 | class ExampleEndpoint extends Endpoint {
25 | Stream echoStream(Session session, Stream stream) async* {
26 | await for (var message in stream) {
27 | yield message;
28 | }
29 | }
30 | }
31 | ```
32 |
33 | The generic for the `Stream` can also be defined, e.g., `Stream<String>`. This definition is then included in the client, enabling static type validation.
34 |
35 | The streaming method above can then be called from the client like this:
36 |
37 | ```dart
38 | var inStream = StreamController();
39 | var outStream = client.example.echoStream(inStream.stream);
40 | outStream.listen((message) {
41 | print('Received message: $message');
42 | });
43 |
44 | inStream.add('Hello');
45 | inStream.add(42);
46 |
47 | // This will print
48 | // Received message: Hello
49 | // Received message: 42
50 | ```
51 |
52 | In the example above, the `echoStream` method passes back any message sent through the `outStream`.
53 |
54 | :::tip
55 |
56 | Note that we can mix different types in the stream. This stream is defined as dynamic and can contain any type that can be serialized by Serverpod.
57 |
58 | :::
59 |
60 | ### Lifecycle of a streaming method
61 |
62 | Each time the client calls a streaming method, a new `Session` is created, and a call with that `Session` is made to the method endpoint on the server. The `Session` is automatically closed when the streaming method call is over.
63 |
64 | If the web socket connection is lost, all streaming methods are closed on the server and the client.
65 |
66 | When the streaming method is defined with a returning `Stream`, the method is kept alive until the stream subscription is canceled on the client or the method returns.
67 |
68 | When the streaming method returns a `Future`, the method is kept alive until the method returns.
69 |
70 | Streams in parameters are closed when the stream is closed. This can be done by either closing the stream on the client or canceling the subscription on the server.
71 |
72 | All streams in parameters are closed when the method call is over.
73 |
74 | ### Authentication
75 |
76 | Authentication is seamlessly integrated into streaming method calls. When a client initiates a streaming method, the server automatically authenticates the session.
77 |
78 | Authentication is validated when the stream is first established, utilizing the authentication data stored in the `Session` object. If a user's authentication is subsequently revoked—requiring denial of access to the stream—the stream will be promptly closed, and an exception will be thrown.
79 |
80 | For more details on handling revoked authentication, refer to the section on [handling revoked authentication](authentication/custom-overrides#Handling-revoked-authentication).
81 |
82 | ### Error handling
83 |
84 | Error handling works just like in regular endpoint methods in Serverpod. If an exception is thrown on a stream, the stream is closed with an exception. If the exception thrown is a serializable exception, the exception is first serialized and passed over the stream before it is closed.
85 |
86 | This is supported in both directions; stream parameters can pass exceptions to the server, and return streams can pass exceptions to the client.
87 |
88 | ```dart
89 | class ExampleEndpoint extends Endpoint {
90 | Stream echoStream(Session session, Stream stream) async* {
91 | stream.listen((message) {
92 | // Do nothing
93 | }, onError: (error) {
94 | print('Server received error: $error');
95 | throw SerializableException('Error from server');
96 | });
97 | }
98 | }
99 | ```
100 |
101 | ```dart
102 | var inStream = StreamController();
103 | var outStream = client.example.echoStream(inStream.stream);
104 | outStream.listen((message) {
105 | // Do nothing
106 | }, onError: (error) {
107 | print('Client received error: $error');
108 | });
109 |
110 | inStream.addError(SerializableException('Error from client'));
111 |
112 | // This will print
113 | // Server received error: Error from client
114 | // Client received error: Error from server
115 | ```
116 |
117 | In the example above, the client sends an error to the server, which then throws an exception back to the client. And since the exception is serializable, it is passed over the stream before the stream is closed.
118 |
119 | Read more about serializable exceptions here: [Serializable exceptions](exceptions).
120 |
121 | ## Streaming Endpoints
122 |
123 | Streaming endpoints were Serverpod's first attempt at streaming data. This approach is more manual, requiring developers to manage the WebSocket connection to the server.
124 |
125 | ### Handling streams server-side
126 |
127 | The Endpoint class has three methods you override to work with streams.
128 |
129 | - `streamOpened` is called when a user connects to a stream on the Endpoint.
130 | - `streamClosed` is called when a user disconnects from a stream on the Endpoint.
131 | - `handleStreamMessage` is called when a serialized message is received from a client.
132 |
133 | To send a message to a client, call the `sendStreamMessage` method. You will need to include the session associated with the user.
134 |
135 | #### The user object
136 |
137 | It's often handy to associate a state together with a streaming session. Typically, you do this when a stream is opened.
138 |
139 | ```dart
140 | Future<void> streamOpened(StreamingSession session) async {
141 | setUserObject(session, MyUserObject());
142 | }
143 | ```
144 |
145 | You can access the user object at any time by calling the `getUserObject` method. The user object is automatically discarded when a session ends.
146 |
147 | ### Handling streams in your app
148 |
149 | Before you can access streams in your client, you need to connect to the server's web socket. You do this by calling connectWebSocket on your client.
150 |
151 | ```dart
152 | await client.openStreamingConnection();
153 |
154 | ```
155 |
156 | You can monitor the state of the connection by adding a listener to the client.
157 | Once connected to your server's web socket, you can pass and receive serialized objects.
158 |
159 | Listen to its web socket stream to receive updates from an endpoint on the server.
160 |
161 | ```dart
162 | await for (var message in client.myEndpoint.stream) {
163 | _handleMessage(message);
164 | }
165 | ```
166 |
167 | You send messages to the server's endpoint by calling `sendStreamMessage`.
168 |
169 | ```dart
170 | client.myEndpoint.sendStreamMessage(MyMessage(text: 'Hello'));
171 | ```
172 |
173 | :::info
174 |
175 | Authentication is handled automatically. If you have signed in, your web socket connection will be authenticated.
176 |
177 | :::
178 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/16-server-events.md:
--------------------------------------------------------------------------------
1 | # Server events
2 |
3 | Serverpod framework comes with a built-in event messaging system. This enables efficient message exchange within and across servers, making it ideal for scenarios where shared state is needed, such as coordinating streams or managing data across a server cluster.
4 |
5 | The event message system is accessed on the `Session` object through the field `messages`.
6 |
7 | ## Quick Reference
8 |
9 | Here is a quick reference to the key messaging methods:
10 |
11 | | Method | Description |
12 | |--------|---------|
13 | | `postMessage` | Send a message to a channel. |
14 | | `addListener` | Add a listener to a channel. |
15 | | `removeListener` | Remove a listener from a channel. |
16 | | `createStream` | Create a stream that listens to a channel. |
17 | | `revokeAuthentication` | Revoke authentication tokens. |
18 |
19 | ## Sending messages
20 |
21 | To send a message, you can use the `postMessage` method. The message is published to the specified channel and needs to be a Serverpod model.
22 |
23 | ```dart
24 | var message = UserUpdate(); // Model that represents changes to user data.
25 | session.messages.postMessage('user_updates', message);
26 | ```
27 |
28 | In the example above, the message published on the `user_updates` channel. Any subscriber to this channel in the server will receive the message.
29 |
30 | ### Global messages
31 |
32 | Serverpod uses Redis to pass messages between servers. To send a message to another server, enable Redis and then set the `global` parameter to `true` when posting a message.
33 |
34 | ```dart
35 | var message = UserUpdate(); // Model that represents changes to user data.
36 | session.messages.postMessage('user_updates', message, global: true);
37 | ```
38 |
39 | In the example above, the message is published to the `user_updates` channel and will be received by all servers connected to the same Redis instance.
40 |
41 | :::warning
42 |
43 | If Redis is not enabled, sending global messages will throw an exception.
44 |
45 | :::
46 |
47 | ## Receiving messages
48 |
49 | Receiving messages is just as simple as sending them! 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.
50 |
51 | ### Creating a stream
52 |
53 | To create a stream that subscribes to a channel, use the `createStream` method. The stream will emit a value whenever a message is posted to the channel.
54 |
55 | ```dart
56 | var stream = session.messages.createStream('user_updates');
57 | stream.listen((message) {
58 | print('Received message: $message');
59 | })
60 | ```
61 |
62 | In the above example, a stream is created that listens to the `user_updates` channel and processes incoming requests.
63 |
64 | #### Stream lifecycle
65 |
66 | The stream is automatically closed when the session is closed. If you want to close the stream manually, you simply cancel the stream subscription.
67 |
68 | ```dart
69 | var stream = session.messages.createStream('user_updates');
70 | var subscription = stream.listen((message) {
71 | print('Received message: $message');
72 | });
73 |
74 | subscription.cancel();
75 | ```
76 |
77 | In the above example, the stream is first created and then canceled.
78 |
79 | ### Adding a listener
80 |
81 | To add a listener to a channel, use the `addListener` method. The listener will be called whenever a message is posted to the channel.
82 |
83 | ```dart
84 | session.messages.addListener('user_updates', (message) {
85 | print('Received message: $message');
86 | });
87 | ```
88 |
89 | In the above example, the listener will be called whenever a message is posted to the `user_updates` channel. The listener will be called regardless if a message is published globally by another server or internally by the same server.
90 |
91 | #### Listener lifecycle
92 |
93 | The listener is automatically removed when the session is closed. To manually remove a listener, use the `removeListener` method.
94 |
95 | ```dart
96 | var myListenerCallback = (message) {
97 | print('Received message: $message');
98 | };
99 | // Register the listener
100 | session.messages.addListener('user_updates', myListenerCallback);
101 |
102 | // Remove the listener
103 | session.messages.removeListener('user_updates', myListenerCallback);
104 | ```
105 |
106 | In the above example, the listener is first added and then removed from the `user_updates` channel.
107 |
108 | ## Revoke authentication
109 |
110 | The messaging interface also exposes a method for revoking authentication. For more details on handling revoked authentication, refer to the section on [handling revoked authentication](authentication/custom-overrides#Handling-revoked-authentication).
111 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/17-backward-compatibility.md:
--------------------------------------------------------------------------------
1 | # Backward compatibility
2 |
3 | As your app evolves, features will be added or changed. However, your users may still use older versions of the app as not everyone will update to the latest version and automatic updates through the app stores take time. Therefore it may be essential to make updates to your server compatible with older app versions.
4 |
5 | Following a simple set of rules, your server will stay compatible with older app versions:
6 |
7 | 1. __Avoid changing parameter names in endpoint methods.__ In the REST API Serverpod generates, the parameters are passed by name. This means that changing the parameter names of the endpoint methods will break backward compatibility.
8 | 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.
9 | 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.
10 |
11 | ## Managing breaking changes with endpoint inheritance
12 |
13 | An [endpoint sub-class](/concepts/working-with-endpoints) 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.
14 |
15 | 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.
16 |
17 | Transitioning from the current to the new endpoint structure might look like this:
18 |
19 | ```dart
20 | @Deprecated('Use TeamV2Endpoint instead')
21 | class TeamEndpoint extends Endpoint {
22 | Future<TeamInfo> join(Session session) async {
23 | // …
24 | }
25 |
26 | // many more methods, like `leave`, etc.
27 | }
28 |
29 | class TeamV2Endpoint extends TeamEndpoint {
30 | @override
31 | @ignoreEndpoint
32 | Future<TeamInfo> join(Session session) async {
33 | throw UnimplementedError();
34 | }
35 |
36 | Future<NewTeamInfo> joinWithCode(Session session, String invitationCode) async {
37 | // …
38 | }
39 | }
40 | ```
41 |
42 | 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.
43 |
44 | 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.
45 |
46 | 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 `@ignoreEndpoint` on the class or deleting it and moving the re-used method implementations you want to keep to the new V2 endpoint class.
47 |
48 | 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. In case you want to create a V2 version later, you might be able to reuse most of the underlying business logic through that helper class, and don't have to subclass 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.
49 |
50 | Either approach has pros and cons, and it depends on the concrete circumstances to pick the most useful one. Both give you all the tools you need to extend and update your API while gracefully moving clients along and giving them time to update.
51 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/18-webserver.md:
--------------------------------------------------------------------------------
1 | # Web server
2 |
3 | In addition to the application server, Serverpod comes with a built-in web server. The web server allows you to access your database and business layer the same way you would from a method call from an app. This makes it very easy to share data for applications that need both an app and traditional web pages. You can also use the web server to create webhooks or generate custom REST APIs to communicate with 3rd party services.
4 |
5 | :::caution
6 |
7 | Serverpod's web server is still experimental, and the APIs may change in the future. This documentation should give you some hints on getting started, but we plan to add more extensive documentation as the web server matures.
8 |
9 | :::
10 |
11 | When you create a new Serverpod project, it sets up a web server by default. When working with the web server, there are two main classes to understand; `WidgetRoute` and `Widget`. The `WidgetRoute` provides an entry point for a call to the server and returns a `Widget`. The `Widget` renders a web page or response using templates, JSON, or other custom means.
12 |
13 | ## Creating new routes and widgets
14 |
15 | To add new pages to your web server, you add new routes. Typically, you do this in your server.dart file before you start the Serverpod. By default, Serverpod comes with a `RootRoute` and a static directory.
16 |
17 | When receiving a web request, Serverpod will search and match the routes in the order they were added. You can end a route's path with an asterisk (`*`) to match all paths with the same beginning.
18 |
19 | ```dart
20 | // Add a single page.
21 | pod.webServer.addRoute(MyRoute(), '/my/page/address');
22 |
23 | // Match all paths that start with /item/
24 | pod.webServer.addRoute(AnotherRoute(), '/item/*');
25 | ```
26 |
27 | Typically, you want to create custom routes for your pages. Do this by overriding the WidgetRoute class and implementing the build method.
28 |
29 | ```dart
30 | class MyRoute extends WidgetRoute {
31 | @override
32 | Future<Widget> build(Session session, HttpRequest request) async {
33 | return MyPageWidget(title: 'Home page');
34 | }
35 | }
36 | ```
37 |
38 | Your route's build method returns a Widget. The Widget consists of an HTML template file and a corresponding Dart class. Create a new custom Widget by overriding the Widget class. Then add a corresponding HTML template and place it in the `web/templates` directory. The HTML file uses the [Mustache](https://mustache.github.io/) template language. You set your template parameters by updating the `values` field of your `Widget` class. The values are converted to `String` objects before being passed to the template. This makes it possible to nest widgets, similarly to how widgets work in Flutter.
39 |
40 | ```dart
41 | class MyPageWidget extends Widget {
42 | MyPageWidget({String title}) : super(name: 'my_page') {
43 | values = {
44 | 'title': title,
45 | };
46 | }
47 | }
48 | ```
49 |
50 | :::info
51 |
52 | In the future, we plan to add a widget library to Serverpod with widgets corresponding to the standard widgets used by Flutter, such as Column, Row, Padding, Container, etc. This would make it possible to render server-side widgets with the same code used within Flutter.
53 |
54 | :::
55 |
56 | ## Special widgets and routes
57 |
58 | Serverpod comes with a few useful special widgets and routes you can use out of the box. When returning these special widget types, Serverpod's web server will automatically set the correct HTTP status codes and content types.
59 |
60 | - `WidgetList` concatenates a list of other widgets into a single widget.
61 | - `WidgetJson` renders a JSON document from a serializable structure of maps, lists, and basic values.
62 | - `WidgetRedirect` creates a redirect to another URL.
63 |
64 | To serve a static directory, use the `RouteStaticDirectory` class. Serverpod will set the correct content types for most file types automatically.
65 |
66 | :::caution
67 |
68 | Static files are configured to be cached hard by the web browser and through Cloudfront's content delivery network (if you use the AWS deployment). If you change static files, they will need to be renamed, or users will most likely access old files. To make this easier, you can add a version number when referencing the static files. The version number will be ignored when looking up the actual file. E.g., `/static/my_image@v42.png` will serve to the `/static/my_image.png` file. More advanced cache management will be coming to a future version of Serverpod.
69 |
70 | :::
71 |
72 | ## Database access and logging
73 |
74 | 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.
75 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/19-testing/01-get-started.md:
--------------------------------------------------------------------------------
1 | # Get started
2 |
3 | Serverpod provides simple but feature rich test tools to make testing your backend a breeze.
4 |
5 | :::info
6 |
7 | For Serverpod Mini projects, everything related to the database in this guide can be ignored.
8 |
9 | :::
10 |
11 | <details>
12 | <summary> Have an existing project? Follow these steps first!</summary>
13 | <p>
14 | For existing non-Mini projects, a few extra things need to be done:
15 | 1. Add the `server_test_tools_path` key with the value `test/integration/test_tools` to `config/generator.yaml`:
16 |
17 | ```yaml
18 | server_test_tools_path: test/integration/test_tools
19 | ```
20 |
21 | 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).
22 |
23 | 2. New projects now come with a test postgres and redis instance 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:
24 |
25 | ```yaml
26 | # Add to the existing services
27 | postgres_test:
28 | image: postgres:16.3
29 | ports:
30 | - '9090:5432'
31 | environment:
32 | POSTGRES_USER: postgres
33 | POSTGRES_DB: <projectname>_test
34 | POSTGRES_PASSWORD: "<insert database test password>"
35 | volumes:
36 | - <projectname>_test_data:/var/lib/postgresql/data
37 | redis_test:
38 | image: redis:6.2.6
39 | ports:
40 | - '9091:6379'
41 | command: redis-server --requirepass 'REDIS_TEST_PASSWORD'
42 | environment:
43 | - REDIS_REPLICATION_MODE=master
44 | volumes:
45 | # ...
46 | <projectname>_test_data:
47 | ```
48 |
49 | <details>
50 | <summary>Or copy the complete file here.</summary>
51 | <p>
52 |
53 | ```yaml
54 | services:
55 | # Development services
56 | postgres:
57 | image: postgres:16.3
58 | ports:
59 | - '8090:5432'
60 | environment:
61 | POSTGRES_USER: postgres
62 | POSTGRES_DB: <projectname>
63 | POSTGRES_PASSWORD: "<insert database development password>"
64 | volumes:
65 | - <projectname>_data:/var/lib/postgresql/data
66 | redis:
67 | image: redis:6.2.6
68 | ports:
69 | - '8091:6379'
70 | command: redis-server --requirepass "<insert redis development password>"
71 | environment:
72 | - REDIS_REPLICATION_MODE=master
73 |
74 | # Test services
75 | postgres_test:
76 | image: postgres:16.3
77 | ports:
78 | - '9090:5432'
79 | environment:
80 | POSTGRES_USER: postgres
81 | POSTGRES_DB: <projectname>_test
82 | POSTGRES_PASSWORD: "<insert database test password>"
83 | volumes:
84 | - <projectname>_test_data:/var/lib/postgresql/data
85 | redis_test:
86 | image: redis:6.2.6
87 | ports:
88 | - '9091:6379'
89 | command: redis-server --requirepass "<insert redis test password>"
90 | environment:
91 | - REDIS_REPLICATION_MODE=master
92 |
93 | volumes:
94 | <projectname>_data:
95 | <projectname>_test_data:
96 | ```
97 |
98 | </p>
99 | </details>
100 | 3. Create a `test.yaml` file and add it to the `config` directory:
101 |
102 | ```yaml
103 | # This is the configuration file for your test environment.
104 | # All ports are set to zero in this file which makes the server find the next available port.
105 | # This is needed to enable running tests concurrently. To set up your server, you will
106 | # need to add the name of the database you are connecting to and the user name.
107 | # The password for the database is stored in the config/passwords.yaml.
108 |
109 | # Configuration for the main API test server.
110 | apiServer:
111 | port: 0
112 | publicHost: localhost
113 | publicPort: 0
114 | publicScheme: http
115 |
116 | # Configuration for the Insights test server.
117 | insightsServer:
118 | port: 0
119 | publicHost: localhost
120 | publicPort: 0
121 | publicScheme: http
122 |
123 | # Configuration for the web test server.
124 | webServer:
125 | port: 0
126 | publicHost: localhost
127 | publicPort: 0
128 | publicScheme: http
129 |
130 | # This is the database setup for your test server.
131 | database:
132 | host: localhost
133 | port: 9090
134 | name: <projectname>_test
135 | user: postgres
136 |
137 | # This is the setup for your Redis test instance.
138 | redis:
139 | enabled: false
140 | host: localhost
141 | port: 9091
142 | ```
143 |
144 | 4. Add this entry to `config/passwords.yaml`
145 |
146 | ```yaml
147 | test:
148 | database: '<insert database test password>'
149 | redis: '<insert redis test password>'
150 | ```
151 |
152 | 5. Add a `dart_test.yaml` file to the `server` directory (next to `pubspec.yaml`) with the following contents:
153 |
154 | ```yaml
155 | tags:
156 | integration: {}
157 |
158 | ```
159 |
160 | 6. Finally, add the `test` and `serverpod_test` packages as dev dependencies in `pubspec.yaml`:
161 |
162 | ```yaml
163 | dev_dependencies:
164 | serverpod_test: <serverpod version> # Should be same version as the `serverpod` package
165 | test: ^1.24.2
166 | ```
167 |
168 | That's it, the project setup should be ready to start using the test tools!
169 | </p>
170 | </details>
171 |
172 | Go to the server directory and generate the test tools:
173 |
174 | ```bash
175 | serverpod generate
176 | ```
177 |
178 | The default location for the generated file is `test/integration/test_tools/serverpod_test_tools.dart`. The folder name `test/integration` is chosen to differentiate from unit tests (see the [best practises section](best-practises#unit-and-integration-tests) for more information on this).
179 |
180 | The generated file exports a `withServerpod` helper that enables you to call your endpoints directly like regular functions:
181 |
182 | ```dart
183 | import 'package:test/test.dart';
184 |
185 | // Import the generated file, it contains everything you need.
186 | import 'test_tools/serverpod_test_tools.dart';
187 |
188 | void main() {
189 | withServerpod('Given Example endpoint', (sessionBuilder, endpoints) {
190 | test('when calling `hello` then should return greeting', () async {
191 | final greeting = await endpoints.example.hello(sessionBuilder, 'Michael');
192 | expect(greeting, 'Hello Michael');
193 | });
194 | });
195 | }
196 | ```
197 |
198 | A few things to note from the above example:
199 |
200 | - The test tools should be imported from the generated test tools file and not the `serverpod_test` package.
201 | - The `withServerpod` callback takes two parameters: `sessionBuilder` and `endpoints`.
202 | - `sessionBuilder` is used to build a `session` object that represents the server state during an endpoint call and is used to set up scenarios.
203 | - `endpoints` contains all your Serverpod endpoints and lets you call them.
204 |
205 | :::tip
206 |
207 | The location of the test tools can be changed by changing the `server_test_tools_path` key in `config/generator.yaml`. If you remove the `server_test_tools_path` key, the test tools will stop being generated.
208 |
209 | :::
210 |
211 | Before the test can be run the Postgres and Redis also have to be started:
212 |
213 | ```bash
214 | docker-compose up --build --detach
215 | ```
216 | Now the test is ready to be run:
217 |
218 | ```bash
219 | dart test
220 | ```
221 |
222 | Happy testing!
223 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/19-testing/02-the-basics.md:
--------------------------------------------------------------------------------
1 | # The basics
2 |
3 | ## Set up a test scenario
4 |
5 | The `withServerpod` helper provides a `sessionBuilder` that helps with setting up different scenarios for tests. To modify the session builder's properties, call its `copyWith` method. It takes the following named parameters:
6 |
7 | |Property|Description|Default|
8 | |:---|:---|:---:|
9 | |`authentication`|See section [Setting authenticated state](#setting-authenticated-state).|`AuthenticationOverride.unauthenticated()`|
10 | |`enableLogging`|Whether logging is turned on for the session.|`false`|
11 |
12 | The `copyWith` method creates a new unique session builder with the provided properties. This can then be used in endpoint calls (see section [Setting authenticated state](#setting-authenticated-state) for an example).
13 |
14 | To build out a `Session` (to use for [database calls](#seeding-the-database) or [pass on to functions](advanced-examples#test-business-logic-that-depends-on-session)), simply call the `build` method:
15 |
16 | ```dart
17 | Session session = sessionBuilder.build();
18 | ```
19 |
20 | Given the properties set on the session builder through the `copyWith` method, this returns a Serverpod `Session` that has the corresponding state.
21 |
22 | ### Setting authenticated state
23 |
24 | To control the authenticated state of the session, the `AuthenticationOverride` class can be used.
25 |
26 | To create an unauthenticated override (this is the default value for new sessions), call `AuthenticationOverride unauthenticated()`:
27 |
28 | ```dart
29 | static AuthenticationOverride unauthenticated();
30 | ```
31 |
32 | To create an authenticated override, call `AuthenticationOverride.authenticationInfo(...)`:
33 |
34 | ```dart
35 | static AuthenticationOverride authenticationInfo(
36 | int userId,
37 | Set<Scope> scopes, {
38 | String? authId,
39 | })
40 | ```
41 |
42 | Pass these to `sessionBuilder.copyWith` to simulate different scenarios. Below follows an example for each case:
43 |
44 | ```dart
45 | withServerpod('Given AuthenticatedExample endpoint', (sessionBuilder, endpoints) {
46 | // Corresponds to an actual user id
47 | const int userId = 1234;
48 |
49 | group('when authenticated', () {
50 | var authenticatedSessionBuilder = sessionBuilder.copyWith(
51 | authentication:
52 | AuthenticationOverride.authenticationInfo(userId, {Scope('user')}),
53 | );
54 |
55 | test('then calling `hello` should return greeting', () async {
56 | final greeting = await endpoints.authenticatedExample
57 | .hello(authenticatedSessionBuilder, 'Michael');
58 | expect(greeting, 'Hello, Michael!');
59 | });
60 | });
61 |
62 | group('when unauthenticated', () {
63 | var unauthenticatedSessionBuilder = sessionBuilder.copyWith(
64 | authentication: AuthenticationOverride.unauthenticated(),
65 | );
66 |
67 | test(
68 | 'then calling `hello` should throw `ServerpodUnauthenticatedException`',
69 | () async {
70 | final future = endpoints.authenticatedExample
71 | .hello(unauthenticatedSessionBuilder, 'Michael');
72 | await expectLater(
73 | future, throwsA(isA<ServerpodUnauthenticatedException>()));
74 | });
75 | });
76 | });
77 | ```
78 |
79 | ### Seeding the database
80 |
81 | To seed the database before tests, `build` a `session` and pass it to the database call just as in production code.
82 |
83 | :::info
84 |
85 | By default `withServerpod` does all database operations inside a transaction that is rolled back after each `test` case. See the [rollback database configuration](#rollback-database-configuration) for how to configure this behavior.
86 |
87 | :::
88 |
89 | ```dart
90 | withServerpod('Given Products endpoint', (sessionBuilder, endpoints) {
91 | var session = sessionBuilder.build();
92 |
93 | setUp(() async {
94 | await Product.db.insert(session, [
95 | Product(name: 'Apple', price: 10),
96 | Product(name: 'Banana', price: 10)
97 | ]);
98 | });
99 |
100 | test('then calling `all` should return all products', () async {
101 | final products = await endpoints.products.all(sessionBuilder);
102 | expect(products, hasLength(2));
103 | expect(products.map((p) => p.name), contains(['Apple', 'Banana']));
104 | });
105 | });
106 | ```
107 |
108 | ## Environment
109 |
110 | By default `withServerpod` uses the `test` run mode and the database settings will be read from `config/test.yaml`.
111 |
112 | It is possible to override the default run mode by setting the `runMode` setting:
113 |
114 | ```dart
115 | withServerpod(
116 | 'Given Products endpoint',
117 | (sessionBuilder, endpoints) {
118 | /* test code */
119 | },
120 | runMode: ServerpodRunMode.development,
121 | );
122 | ```
123 |
124 | ## Configuration
125 |
126 | The following optional configuration options are available to pass as a second argument to `withServerpod`:
127 |
128 | |Property|Description|Default|
129 | |:-----|:-----|:---:|
130 | |`applyMigrations`|Whether pending migrations should be applied when starting Serverpod.|`true`|
131 | |`enableSessionLogging`|Whether session logging should be enabled.|`false`|
132 | |`rollbackDatabase`|Options for when to rollback the database during the test lifecycle (or disable it). See detailed description [here](#rollback-database-configuration).|`RollbackDatabase.afterEach`|
133 | |`runMode`|The run mode that Serverpod should be running in.|`ServerpodRunmode.test`|
134 | |`serverpodLoggingMode`|The logging mode used when creating Serverpod.|`ServerpodLoggingMode.normal`|
135 | |`serverpodStartTimeout`|The timeout to use when starting Serverpod, which connects to the database among other things. Defaults to `Duration(seconds: 30)`.|`Duration(seconds: 30)`|
136 | |`testGroupTagsOverride`|By default Serverpod test tools tags the `withServerpod` test group with `"integration"`. This is to provide a simple way to only run unit or integration tests. This property allows this tag to be overridden to something else. Defaults to `['integration']`.|`['integration']`|
137 |
138 | ### `rollbackDatabase` {#rollback-database-configuration}
139 |
140 | By default `withServerpod` does all database operations inside a transaction that is rolled back after each `test` case. Just like the following enum describes, the behavior of the automatic rollbacks can be configured:
141 |
142 | ```dart
143 | /// Options for when to rollback the database during the test lifecycle.
144 | enum RollbackDatabase {
145 | /// After each test. This is the default.
146 | afterEach,
147 |
148 | /// After all tests.
149 | afterAll,
150 |
151 | /// Disable rolling back the database.
152 | disabled,
153 | }
154 | ```
155 |
156 | There are a few reasons to change the default setting:
157 |
158 | 1. **Scenario tests**: when consecutive `test` cases depend on each other. While generally considered an anti-pattern, it can be useful when the set up for the test group is very expensive. In this case `rollbackDatabase` can be set to `RollbackDatabase.afterAll` to ensure that the database state persists between `test` cases. At the end of the `withServerpod` scope, all database changes will be rolled back.
159 |
160 | 2. **Concurrent transactions in endpoints**: when concurrent calls are made to `session.db.transaction` inside an endpoint, it is no longer possible for the Serverpod test tools to do these operations as part of a top level transaction. In this case this feature should be disabled by passing `RollbackDatabase.disabled`.
161 |
162 | ```dart
163 | Future<void> concurrentTransactionCalls(
164 | Session session,
165 | ) async {
166 | await Future.wait([
167 | session.db.transaction((tx) => /*...*/),
168 | // Will throw `InvalidConfigurationException` if `rollbackDatabase`
169 | // is not set to `RollbackDatabase.disabled` in `withServerpod`
170 | session.db.transaction((tx) => /*...*/),
171 | ]);
172 | }
173 | ```
174 |
175 | When setting `rollbackDatabase.disabled` to be able to test `concurrentTransactionCalls`, remember that the database has to be manually cleaned up to not leak data:
176 |
177 | ```dart
178 | withServerpod(
179 | 'Given ProductsEndpoint when calling concurrentTransactionCalls',
180 | (sessionBuilder, endpoints) {
181 | tearDownAll(() async {
182 | var session = sessionBuilder.build();
183 | // If something was saved to the database in the endpoint,
184 | // for example a `Product`, then it has to be cleaned up!
185 | await Product.db.deleteWhere(
186 | session,
187 | where: (_) => Constant.bool(true),
188 | );
189 | });
190 |
191 | test('then should execute and commit all transactions', () async {
192 | var result =
193 | await endpoints.products.concurrentTransactionCalls(sessionBuilder);
194 | // ...
195 | });
196 | },
197 | rollbackDatabase: RollbackDatabase.disabled,
198 | );
199 | ```
200 |
201 | Additionally, when setting `rollbackDatabase.disabled`, it may also be needed to pass the `--concurrency=1` flag to the dart test runner. Otherwise multiple tests might pollute each others database state:
202 |
203 | ```bash
204 | dart test -t integration --concurrency=1
205 | ```
206 |
207 | For the other cases this is not an issue, as each `withServerpod` has its own transaction and will therefore be isolated.
208 |
209 | 3. **Database exceptions that are quelled**: There is a specific edge case where the test tools behavior deviates from production behavior. See example below:
210 |
211 | ```dart
212 | var transactionFuture = session.db.transaction((tx) async {
213 | var data = UniqueData(number: 1, email: 'test@test.com');
214 | try {
215 | await UniqueData.db.insertRow(session, data, transaction: tx);
216 | await UniqueData.db.insertRow(session, data, transaction: tx);
217 | } on DatabaseException catch (_) {
218 | // Ignore the database exception
219 | }
220 | });
221 |
222 | // ATTENTION: This will throw an exception in production
223 | // but not in the test tools.
224 | await transactionFuture;
225 | ```
226 |
227 | In production, the transaction call will throw if any database exception happened during its execution, _even_ if the exception was first caught inside the transaction. However, in the test tools this will not throw an exception due to how the nested transactions are emulated. Quelling exceptions like this is not best practise, but if the code under test does this setting `rollbackDatabase` to `RollbackDatabse.disabled` will ensure the code behaves like in production.
228 |
229 | ## Test exceptions
230 |
231 | The following exceptions are exported from the generated test tools file and can be thrown by the test tools in various scenarios, see below.
232 |
233 | |Exception|Description|
234 | |:-----|:-----|
235 | |`ServerpodUnauthenticatedException`|Thrown during an endpoint method call when the user was not authenticated.|
236 | |`ServerpodInsufficientAccessException`|Thrown during an endpoint method call when the authentication key provided did not have sufficient access.|
237 | |`ConnectionClosedException`|Thrown during an endpoint method call if a stream connection was closed with an error. For example, if the user authentication was revoked.|
238 | |`InvalidConfigurationException`|Thrown when an invalid configuration state is found.|
239 |
240 | ## Test helpers
241 |
242 | ### `flushEventQueue`
243 |
244 | Test helper to flush the event queue.
245 | Useful for waiting for async events to complete before continuing the test.
246 |
247 | ```dart
248 | Future<void> flushEventQueue();
249 | ```
250 |
251 | For example, if depending on a generator function to execute up to its `yield`, then the
252 | event queue can be flushed to ensure the generator has executed up to that point:
253 |
254 | ```dart
255 | var stream = endpoints.someEndoint.generatorFunction(session);
256 | await flushEventQueue();
257 | ```
258 |
259 | See also [this complete example](advanced-examples#multiple-users-interacting-with-a-shared-stream).
260 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/19-testing/03-advanced-examples.md:
--------------------------------------------------------------------------------
1 | # Advanced examples
2 |
3 | ## Run unit and integration tests separately
4 |
5 | To run unit and integration tests separately, the `"integration"` tag can be used as a filter. See the following examples:
6 |
7 | ```bash
8 | # All tests (unit and integration)
9 | dart test
10 |
11 | # Only integration tests: add --tags (-t) flag
12 | dart test -t integration
13 |
14 | # Only unit tests: add --exclude-tags (-x) flag
15 | dart test -x integration
16 | ```
17 |
18 | To change the name of this tag, see the [`testGroupTagsOverride`](the-basics#configuration) configuration option.
19 |
20 | ## Test business logic that depends on `Session`
21 |
22 | 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 simply be ignored:
23 |
24 | ```dart
25 | withServerpod('Given decreasing product quantity when quantity is zero', (
26 | sessionBuilder,
27 | _,
28 | ) {
29 | var session = sessionBuilder.build();
30 |
31 | setUp(() async {
32 | await Product.db.insertRow(session, [
33 | Product(
34 | id: 123,
35 | name: 'Apple',
36 | quantity: 0,
37 | ),
38 | ]);
39 | });
40 |
41 | test('then should throw `InvalidOperationException`',
42 | () async {
43 | var future = ProductsBusinessLogic.updateQuantity(
44 | session,
45 | id: 123,
46 | decrease: 1,
47 | );
48 |
49 | await expectLater(future, throwsA(isA<InvalidOperationException>()));
50 | });
51 | });
52 | ```
53 |
54 | ## Multiple users interacting with a shared stream
55 |
56 | 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.
57 |
58 | Given the following simplified endpoint:
59 |
60 | ```dart
61 | class CommunicationExampleEndpoint {
62 | static const sharedStreamName = 'shared-stream';
63 | Future<void> postNumberToSharedStream(Session session, int number) async {
64 | await session.messages
65 | .postMessage(sharedStreamName, SimpleData(num: number));
66 | }
67 |
68 | Stream<int> listenForNumbersOnSharedStream(Session session) async* {
69 | var sharedStream =
70 | session.messages.createStream<SimpleData>(sharedStreamName);
71 |
72 | await for (var message in sharedStream) {
73 | yield message.num;
74 | }
75 | }
76 | }
77 | ```
78 |
79 | 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.
80 |
81 | ```dart
82 | withServerpod('Given CommunicationExampleEndpoint', (sessionBuilder, endpoints) {
83 | const int userId1 = 1;
84 | const int userId2 = 2;
85 |
86 | test(
87 | 'when calling postNumberToSharedStream and listenForNumbersOnSharedStream '
88 | 'with different sessions then number should be echoed',
89 | () async {
90 | var userSession1 = sessionBuilder.copyWith(
91 | authentication: AuthenticationOverride.authenticationInfo(
92 | userId1,
93 | {},
94 | ),
95 | );
96 | var userSession2 = sessionBuilder.copyWith(
97 | authentication: AuthenticationOverride.authenticationInfo(
98 | userId2,
99 | {},
100 | ),
101 | );
102 |
103 | var stream =
104 | endpoints.testTools.listenForNumbersOnSharedStream(userSession1);
105 | // Wait for `listenForNumbersOnSharedStream` to execute up to its
106 | // `yield` statement before continuing
107 | await flushEventQueue();
108 |
109 | await endpoints.testTools.postNumberToSharedStream(userSession2, 111);
110 | await endpoints.testTools.postNumberToSharedStream(userSession2, 222);
111 |
112 | await expectLater(stream.take(2), emitsInOrder([111, 222]));
113 | });
114 | });
115 | ```
116 |
117 | ## Optimising number of database connections
118 |
119 | By default, Dart's test runner runs tests concurrently. The number of concurrent tests depends on the running hosts' available CPU cores. If the host has a lot of cores it could trigger a case where the number of connections to the database exceeeds the maximum connections limit set for the database, which will cause tests to fail.
120 |
121 | Each `withServerpod` call will lazily create its own Serverpod instance which will connect to the database. Specifically, the code that causes the Serverpod instance to be created is `sessionBuilder.build()`, which happens at the latest in an endpoint call if not called by the test before.
122 |
123 | If a test needs a session before the endpoint call (e.g. to seed the database), `sessionBuilder.build()` has to be called which then triggers a database connection attempt.
124 |
125 | If the max connection limit is hit, there are two options:
126 |
127 | - Raise the max connections limit on the database.
128 | - Build out the session in `setUp`/`setUpAll` instead of the top level scope:
129 |
130 | ```dart
131 | withServerpod('Given example test', (sessionBuilder, endpoints) {
132 | // Instead of this
133 | var session = sessionBuilder.build();
134 |
135 |
136 | // Do this to postpone connecting to the database until the test group is running
137 | late Session session;
138 | setUpAll(() {
139 | session = sessionBuilder.build();
140 | });
141 | // ...
142 | });
143 | ```
144 |
145 | :::info
146 |
147 | This case should be rare and the above example is not a recommended best practice unless this problem is anticipated, or it has started happening.
148 |
149 | :::
150 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/19-testing/04-best-practises.md:
--------------------------------------------------------------------------------
1 | ---
2 | # Don't display do's and don'ts in the table of contents
3 | toc_max_heading_level: 2
4 | ---
5 |
6 | # Best practises
7 |
8 | ## Imports
9 |
10 | While it's possible to import types and test helpers from the `serverpod_test`, it's completely redundant. The generated file exports everything that is needed. Adding an additional import is just unnecessary noise and will likely also be flagged as duplicated imports by the Dart linter.
11 |
12 | ### Don't
13 |
14 | ```dart
15 | import 'serverpod_test_tools.dart';
16 | // Don't import `serverpod_test` directly.
17 | import 'package:serverpod_test/serverpod_test.dart'; ❌
18 | ```
19 |
20 | ### Do
21 |
22 | ```dart
23 | // Only import the generated test tools file.
24 | // It re-exports all helpers and types that are needed.
25 | import 'serverpod_test_tools.dart'; ✅
26 | ```
27 |
28 | ### Database clean up
29 |
30 | Unless configured otherwise, by default `withServerpod` does all database operations inside a transaction that is rolled back after each `test` (see [the configuration options](the-basics#rollback-database-configuration) for more info on this behavior).
31 |
32 | ### Don't
33 |
34 | ```dart
35 | withServerpod('Given ProductsEndpoint', (sessionBuilder, endpoints) {
36 | var session = sessionBuilder.build();
37 |
38 | setUp(() async {
39 | await Product.db.insertRow(session, Product(name: 'Apple', price: 10));
40 | });
41 |
42 | tearDown(() async {
43 | await Product.db.deleteWhere( ❌ // Unnecessary clean up
44 | session,
45 | where: (_) => Constant.bool(true),
46 | );
47 | });
48 |
49 | // ...
50 | });
51 | ```
52 |
53 | ### Do
54 |
55 | ```dart
56 | withServerpod('Given ProductsEndpoint', (sessionBuilder, endpoints) {
57 | var session = sessionBuilder.build();
58 |
59 | setUp(() async {
60 | await Product.db.insertRow(session, Product(name: 'Apple', price: 10));
61 | });
62 |
63 | ✅ // Clean up can be omitted since the transaction is rolled back after each by default
64 |
65 | // ...
66 | });
67 | ```
68 |
69 | ## Calling endpoints
70 |
71 | 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.
72 |
73 | ### Don't
74 |
75 | ```dart
76 | void main() {
77 | // ❌ Don't instantiate endpoints directly
78 | var exampleEndpoint = ExampleEndpoint();
79 |
80 | withServerpod('Given Example endpoint', (
81 | sessionBuilder,
82 | _ /* not using the provided endpoints */,
83 | ) {
84 | var session = sessionBuilder.build();
85 |
86 | test('when calling `hello` then should return greeting', () async {
87 | // ❌ Don't call and endpoint method directly on the endpoint class.
88 | final greeting = await exampleEndpoint.hello(session, 'Michael');
89 | expect(greeting, 'Hello, Michael!');
90 | });
91 | });
92 | }
93 | ```
94 |
95 | ### Do
96 |
97 | ```dart
98 | void main() {
99 | withServerpod('Given Example endpoint', (sessionBuilder, endpoints) {
100 | var session = sessionBuilder.build();
101 |
102 | test('when calling `hello` then should return greeting', () async {
103 | // ✅ Use the provided `endpoints` to call the endpoint that should be tested.
104 | final greeting =
105 | await endpoints.example.hello(session, 'Michael');
106 | expect(greeting, 'Hello, Michael!');
107 | });
108 | });
109 | }
110 | ```
111 |
112 | ## Unit and integration tests
113 |
114 | It is significantly easier to navigate a project if the different types of tests are clearly separated.
115 |
116 | ### Don't
117 |
118 | ❌ Mix different types of tests together.
119 |
120 | ### Do
121 |
122 | ✅ Have a clear structure for the different types of test. Serverpod recommends the following two folders in the `server`:
123 |
124 | - `test/unit`: Unit tests.
125 | - `test/integration`: Tests for endpoints or business logic modules using the `withServerpod` helper.
126 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/20-security-configuration.md:
--------------------------------------------------------------------------------
1 | # Security Configuration
2 |
3 | :::info
4 |
5 | In a **production environment**, TLS termination is **normally handled by a load balancer** or **reverse proxy** (e.g., Nginx, AWS ALB, or Cloudflare).
6 | However, Serverpod also supports setting up **TLS/SSL directly on the server**, allowing you to provide your own certificates if needed.
7 |
8 | :::
9 |
10 | Serverpod supports **TLS/SSL security configurations** through the **Dart configuration object**.
11 | To enable SSL/TLS, you must pass a **`SecurityContextConfig`** to the `Serverpod` constructor.
12 |
13 | ## Server Security Configuration
14 |
15 | To enable SSL/TLS in Serverpod, configure the `SecurityContextConfig` and pass it to the `Serverpod` instance.
16 |
17 | ### Dart Configuration Example
18 |
19 | ```dart
20 | final securityContext = SecurityContext()
21 | ..useCertificateChain('path/to/server_cert.pem')
22 | ..usePrivateKey('path/to/server_key.pem', password: 'password');
23 |
24 | Serverpod(
25 | args,
26 | Protocol(),
27 | Endpoints(),
28 | securityContextConfig: SecurityContextConfig(
29 | apiServer: securityContext,
30 | webServer: securityContext,
31 | insightsServer: securityContext,
32 | ),
33 | );
34 | ```
35 |
36 | ## Client Security Configuration
37 |
38 | When connecting to a **Serverpod server over HTTPS**, the client must be configured to trust the server's certificate.
39 |
40 | ### Dart Configuration Example
41 |
42 | To enable SSL/TLS when using the Serverpod client, pass a **`SecurityContext`** to the `Client` constructor.
43 |
44 | ```dart
45 | final securityContext = SecurityContext()
46 | ..setTrustedCertificates('path/to/server_cert.pem');
47 |
48 |
49 | final client = Client(
50 | 'https://yourserver.com',
51 | securityContext: securityContext,
52 | ...
53 | );
54 | ```
55 |
--------------------------------------------------------------------------------
/versioned_docs/version-2.6.0/06-concepts/21-experimental.md:
--------------------------------------------------------------------------------
1 | # Experimental features
2 |
3 | :::warning
4 | Be cautious when using experimental features in production environments, as their stability is uncertain and they may receive breaking changes in upcoming releases.
5 | :::
6 |
7 | "Experimental Features" are cutting-edge additions to Serverpod that are currently under development or testing or whose API is not yet stable.
8 | These features allow developers to explore new functionalities and provide feedback, helping shape the future of Serverpod.
9 | However, they may not be fully stable or complete and are subject to change.
10 |
11 | Experimental features are disabled by default, i.e. they are not active unless the developer opts-in.
12 |
13 | ## Experimental internal APIs
14 |
15 | Experimental internal APIs are placed under the `experimental` sub-API of the `Serverpod` class.
16 | When an experimental feature matures it is moved from `experimental` to `Serverpod` proper.
17 | If possible, the experimental API will remain for some time as `@deprecated`, and then removed.
18 |
19 | ## Command-line enabled features
20 |
21 | Some of the experimental features are enabled by including the `--experimental-features` flag when running the serverpod command:
22 |
23 | ```bash
24 | $ serverpod generate --experimental-features=all
25 | ```
26 |
27 | The current options you can pass are:
28 |
29 | | **Feature** | Description |
30 | | :--------------- | :----------------------------------------------------------------------------------------- |
31 | | **all** | Enables all available experimental features. |
32 | | **inheritance** | Allows using the `extends` keyword in your model files to create class hierarchies. |
33 | | **changeIdType** | Allows declaring the `id` field in table model files to change the type of the `id` field. |
34 |
35 | ## Inheritance
36 |
37 | :::warning
38 | Adding a new subtype to a class hierarchy may introduce breaking changes for older clients. Ensure client compatibility when expanding class hierarchies to avoid deserialization issues.
39 | :::
40 |
41 | Inheritance allows you to define class hierarchies in your model files by sharing fields between parent and child classes, simplifying class structures and promoting consistency by avoiding duplicate field definitions.
42 |
43 | ### Extending a Class
44 |
45 | To inherit from a class, use the `extends` keyword in your model files, as shown below:
46 |
47 | ```yaml
48 | class: ParentClass
49 | fields:
50 | name: String
51 | ```
52 |
53 | ```yaml
54 | class: ChildClass
55 | extends: ParentClass
56 | fields:
57 | int: age
58 | ```
59 |
60 | This will generate a class with both `name` and `age` field.
61 |
62 | ```dart
63 | class ChildClass extends ParentClass {
64 | String name
65 | int age
66 | }
67 | ```
68 |
69 | ### Sealed Classes
70 |
71 | In addition to the `extends` keyword, you can also use the `sealed` keyword to create sealed class hierarchies, enabling exhaustive type checking. With sealed classes, the compiler knows all subclasses, ensuring that every possible case is handled when working with the model.
72 |
73 | ```yaml
74 | class: ParentClass
75 | sealed: true
76 | fields:
77 | name: String
78 | ```
79 |
80 | ```yaml
81 | class: ChildClass
82 | extends: ParentClass
83 | fields:
84 | age: int
85 | ```
86 |
87 | This will generate the following classes:
88 |
89 | ```dart
90 | sealed class ParentClass {
91 | String name;
92 | }
93 |
94 | class ChildClass extends ParentClass {
95 | String name;
96 | int age;
97 | }
98 | ```
99 |
100 | :::info
101 | All files in a sealed hierarchy need to be located in the same directory.
102 | :::
103 |
104 | ## Change ID type
105 |
106 | 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`, as have always been.
107 |
108 | The following types are supported for the `id` field:
109 |
110 | | **Type** | Default | Default Persist options | Default Model options | Description |
111 | | :------------ | :------ | :---------------------- | :-------------------- | :--------------------- |
112 | | **int** | serial | serial (optional) | - | 64-bit serial integer. |
113 | | **UuidValue** | random | random | random | UUID v4 value. |
114 |
115 | ### Declaring a Custom ID Type
116 |
117 | To declare a custom type for the `id` field in a table model file, use the following syntax:
118 |
119 | ```yaml
120 | class: UuidIdTable
121 | table: uuid_id_table
122 | fields:
123 | id: UuidValue?, defaultPersist=random
124 | ```
125 |
126 | ```yaml
127 | class: IntIdTable
128 | table: int_id_table
129 | fields:
130 | id: int?, defaultPersist=serial // The default keyword for 'int' is optional.
131 | ```
132 |
133 | #### Default Uuid model value
134 |
135 | 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.
136 |
137 | ```yaml
138 | class: UuidIdTable
139 | table: uuid_id_table
140 | fields:
141 | id: UuidValue, defaultModel=random
142 | ```
143 |
144 | When using `defaultModel=random`, the UUID will be generated when the object is created. Since an id is always assigned the `id` field can be non-nullable.
145 |
146 | ## Exception monitoring
147 |
148 | Serverpod allows you to monitor exceptions in a central and flexible way by using the new diagnostic event handlers.
149 | These work both for exceptions thrown in application code and from the framework (e.g. server startup or shutdown errors).
150 |
151 | This can be used to get all exceptions reported in realtime to services for monitoring and diagnostics,
152 | such as [Sentry](https://sentry.io/), [Highlight](https://www.highlight.io/), and [Datadog](https://www.datadoghq.com/).
153 |
154 | It is easy to implement handlers and define custom filters within them.
155 | Any number of handlers can be added.
156 | They are run asynchronously and should not affect the behavior or response times of the server.
157 |
158 | These event handlers are for diagnostics only,
159 | they do not allow any behavior-changing action such as suppressing exceptions or converting them to another exception type.
160 |
161 | ### Setup
162 |
163 | This feature is enabled by providing one ore more `DiagnosticEventHandler` implementations
164 | to the Serverpod constructor's `experimentalFeatures` specification.
165 |
166 | Example:
167 |
168 | ```dart
169 | var serverpod = Serverpod(
170 | ...
171 | experimentalFeatures: ExperimentalFeatures(
172 | diagnosticEventHandlers: [
173 | AsEventHandler((event, {required space, required context}) {
174 | print('$event Origin is $space\n Context is ${context.toJson()}');
175 | }),
176 | ],
177 | ),
178 | );
179 | ```
180 |
181 | ### Submitting diagnostic events
182 |
183 | The API for submitting diagnostic events from user code, e.g. from endpoint methods, web calls, and future calls,
184 | is the new method `submitDiagnosticEvent` under the `experimental` member of the Serverpod class.
185 |
186 | ```dart
187 | void submitDiagnosticEvent(
188 | DiagnosticEvent event, {
189 | required Session session,
190 | })
191 | ```
192 |
193 | Usage example:
194 |
195 | ```dart
196 | class DiagnosticEventTestEndpoint extends Endpoint {
197 | Future<String> submitExceptionEvent(Session session) async {
198 | try {
199 | throw Exception('An exception is thrown');
200 | } catch (e, stackTrace) {
201 | session.serverpod.experimental.submitDiagnosticEvent(
202 | ExceptionEvent(e, stackTrace),
203 | session: session,
204 | );
205 | }
206 | return 'success';
207 | }
208 | }
209 | ```
210 |
211 | ### Guidelines for handlers
212 |
213 | A `DiagnosticEvent` represents an event that occurs in the server.
214 | `DiagnosticEventHandler` implementations can react to these events
215 | in order to gain insights into the behavior of the server.
216 |
217 | As the name suggests the handlers should perform diagnostics only,
218 | and not have any responsibilities that the regular functioning
219 | of the server depends on.
220 |
221 | The registered handlers are typically run concurrently,
222 | can not depend on each other, and asynchronously -
223 | they are not awaited by the operation they are triggered from.
224 |
225 | If a handler throws an exception it will be logged to stderr
226 | and otherwise ignored.
227 |
228 | ### Test support
229 |
230 | This feature also includes support via the Serverpod test framework.
231 | This means that the `withServerpod` construct can be used together with diagnostic event handlers to test that the events are submitted and propagated as intended.
232 |
233 | Example:
234 |
235 | ```dart
236 | void main() {
237 | var exceptionHandler = TestExceptionHandler();
238 |
239 | withServerpod('Given withServerpod with a diagnostic event handler',
240 | experimentalFeatures: ExperimentalFeatures(
241 | diagnosticEventHandlers: [exceptionHandler],
242 | ), (sessionBuilder, endpoints) {
243 | test(
244 | 'when calling an endpoint method that submits an exception event '
245 | 'then the diagnostic event handler gets called', () async {
246 | final result = await endpoints.diagnosticEventTest
247 | .submitExceptionEvent(sessionBuilder);
248 | expect(result, 'success');
249 |
250 | final record = await exceptionHandler.events.first.timeout(Duration(seconds: 1));
251 | expect(record.event.exception, isA<Exception>());
252 | expect(record.space, equals(OriginSpace.application));
253 | expect(record.context, isA<DiagnosticEventContext>());
254 | expect(
255 | record.context.toJson(),
256 | allOf([
257 | containsPair('serverId', 'default'),
258 | containsPair('serverRunMode', 'test'),
259 | containsPair('serverName', 'Server default'),
260 | ]));
261 | });
262 | });
263 | }
264 | ```
265 |
--------------------------------------------------------------------------------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment