diff --git a/docs/06-concepts/03-data-and-the-database/02-database/12-raw-access.md b/docs/06-concepts/03-data-and-the-database/02-database/12-raw-access.md index c3e74bf9..e794cfd8 100644 --- a/docs/06-concepts/03-data-and-the-database/02-database/12-raw-access.md +++ b/docs/06-concepts/03-data-and-the-database/02-database/12-raw-access.md @@ -112,19 +112,4 @@ When a query selects multiple columns with the same name, `toColumnMap()` keeps ## Intercept database access -To add cross-cutting behavior around every database call, such as logging, tracing, metrics, tenant scoping, or safety guards, you can replace the `Database` object that sessions use. Pass a `databaseInterceptor` callback to the `Serverpod` constructor in your server's `lib/server.dart`. It is called once per session with the framework-provided `Database`, and the `Database` you return becomes `session.db` for that session. - -```dart -var pod = Serverpod( - args, - Protocol(), - Endpoints(), - databaseInterceptor: (session, inner) => AuditingDatabase(inner), -); -``` - -In this example, `AuditingDatabase` is your own `Database` implementation that wraps `inner`, adds behavior around the calls you care about, and forwards the rest. - -:::warning -The `Database` class is part of the internal `serverpod_database` package, and its API may change in minor version bumps. -::: +To apply tracing, metrics, tenant scoping, or policy enforcement across every database operation of a session, including the raw methods above, see [Database interceptors](./database-interceptors). diff --git a/docs/06-concepts/03-data-and-the-database/02-database/18-database-interceptors.md b/docs/06-concepts/03-data-and-the-database/02-database/18-database-interceptors.md new file mode 100644 index 00000000..a88e2389 --- /dev/null +++ b/docs/06-concepts/03-data-and-the-database/02-database/18-database-interceptors.md @@ -0,0 +1,56 @@ +--- +description: A database interceptor replaces session.db once per session with a custom database layer for tracing, policy enforcement, and tenant scoping. +--- + +# Database interceptors + +A database interceptor lets you wrap every database operation of a session in the same behavior, such as query tracing, tenant scoping, or a guard that rejects writes in a protected environment. Serverpod calls the interceptor once when it creates a session, passing the session and the framework database, and whatever the interceptor returns becomes `session.db` for the rest of that session. + +Register the interceptor when you construct `Serverpod`: + +```dart +var pod = Serverpod( + args, + Protocol(), + Endpoints(), + databaseInterceptor: (session, inner) { + session.log('Created a database layer for this session.'); + return inner; + }, +); +``` + +Returning `inner` leaves the framework database in place, so this example only observes session creation. Repeated reads of `session.db` return the same instance for the lifetime of the session. + +## Return a custom database layer + +To intercept the operations themselves, return your own implementation of the `Database` interface exported by `package:serverpod/serverpod.dart`: + +```dart +var pod = Serverpod( + args, + Protocol(), + Endpoints(), + databaseInterceptor: (session, inner) { + return PolicyDatabase( + session: session, + inner: inner, + ); + }, +); +``` + +The `Database` class has no public constructor to subclass, so implement the interface and let your IDE generate the overrides. Hold on to the supplied `inner` database and delegate every operation the layer does not change. The object you return becomes `session.db`, so generated calls such as `User.db.find(session)` pass through it as well. + +Typical uses are: + +- Recording query traces, timings, and metrics. +- Applying a tenant scope to every supported query. +- Enforcing read and write policies. +- Blocking unsafe operations or writes in protected environments. + +Pass transaction objects, [runtime parameters](./runtime-parameters), ordering, pagination, [row locks](./row-locking), and return options through without changing their meaning. A layer that enforces a policy also needs to cover [raw access](./raw-access) and transactions, otherwise a caller can reach the database around it. Do not hold on to the session or its inner database after the session closes. + +:::warning +Custom database layers build on the `Database` API from `serverpod_database`, which can change in a breaking way in a minor Serverpod release. Review and test your implementation whenever you upgrade Serverpod. +:::