Security is evaluated at the database execution boundary, not only in a web route or client.
database-kit defines the client-visible security contracts. A model can conform to SecurityPolicy and decide whether a resource may be read, listed, created, updated, or deleted.
extension Post: SecurityPolicy {
static func permitsRead(
of resource: borrowing Post,
in context: borrowing AuthorizationContext
) -> Bool {
resource.isPublic
|| resource.authorID == context.principal?.identifier
}
static func permitsQuery(
_ query: borrowing SecurityQuery,
in context: borrowing AuthorizationContext
) -> Bool {
context.isAuthenticated && (query.limit ?? 0) <= 100
}
static func permitsDelete(
_ resource: borrowing Post,
in context: borrowing AuthorizationContext
) -> Bool {
resource.authorID == context.principal?.identifier
}
}A list policy authorizes the query as a whole. It is not a post-query filter. This avoids returning unauthorized rows and makes authorization behavior independent of index selection.
Register every policy explicitly in the container-scoped runtime. Conformance alone does not mutate a global registry and is not discovered through reflection.
let runtime = try DatabaseFrameworkRuntime.configuration(
entityRuntimes: [try DatabaseFrameworkRuntime.entity(Post.self)],
authorizationPolicies: [AuthorizationPolicyHandler(Post.self)]
)Set authorization information around each request:
let authorization = AuthorizationContext.authenticated(
Principal(identifier: authenticatedUserID, roles: authenticatedRoles)
)
let context = container.newContext(authorization: authorization)
let posts = try await context.fetch(Post.self).execute()
_ = postsDBContainer installs the security delegate when security is enabled:
let container = try await DBContainer.open(
for: schema,
configuration: configuration,
monotonicClock: applicationMonotonicClock,
wallClock: applicationWallClock,
runtimeConfiguration: runtime,
security: .enabled()
)The delegate evaluates reads and writes immediately before the operation is accepted. Failed checks throw a typed security error.
Tenant isolation has two independent parts:
| Concern | Mechanism |
|---|---|
| Physical/logical boundary | database root, plus explicit Base roots with MultipleBases |
| Resource authorization | application boundary by default; persisted direct and role Grants with MultipleBases |
| Entity and field authorization | SecurityPolicy and @Restricted |
The standard runtime binds data operations to its single data root and applies
registered entity and field policy. It has no persisted Grant store. The
MultipleBases trait additionally accepts one exact Base or a read-only
Composition and enables persisted Grants. #Directory and dynamic partitions
are relative paths inside the selected root and are not credentials. The
trait-specific runtime opens the selected transaction, unions matching direct
and role Grants, requires the exact access bits, and only then executes entity
and field policy. A Composition read requires .read on every retained member
and fails as a whole when any member is unavailable or unauthorized.
- Establish
AuthorizationContextat the request boundary and bind it to the database root or aDatabaseSessionselector. - Use
.databaseby default; whenMultipleBasesis enabled, select a Base or Composition explicitly when crossing that boundary. - With
MultipleBases, manage Base access through persisted Grants; role names are claims, not bypasses. - Register each AuthorizationPolicyHandler in DatabaseRuntimeConfiguration.
- Keep the security configuration enabled in production.
- Never use client-side filtering as the authorization mechanism.
- Validate tenant and workspace identifiers before binding them to a directory.
- Test create, read, update, delete, list, and cross-tenant move behavior.
- Keep credentials and encryption keys outside source control.
Security policy definitions live with model code; execution and delegate implementation lives in DatabaseEngine.