---
title: Override operations
description: Wrap or replace portable CRUD methods with service-level overrides, while keeping security policy and custom queries explicit.
pagination: false
sidebar:
  icon: replace
---

Use an override when an existing CRUD operation needs different behavior. Use [models](/docs/modules/database/models) when adding a new named operation instead. Overrides are registered
by schema table key, then by one of the six reserved CRUD names.

> **Before you begin**
>
> Before using these examples, complete [database installation and
> configuration](/docs/modules/database/define), including the selected adapter, native ORM, tooling
> and application-owned patch.

## Wrap the base behavior

The data-model example hides inactive users from `findOne`:

```ts title="src/services/database/service.ts — overrides"
overrides: {
  users: {
    findOne: async ({ args, base }) => {
      const user = await base(args);
      return user?.active === false ? null : user;
    },
  },
},
```

The callback receives `{ args, base }`. `args` has that operation's argument shape. `base(args)` runs
the original implementation directly, without calling the same override again. You may transform the
arguments, inspect the result, or return a replacement result that respects the operation's contract.

For example, `findOne` returns a row or `null`, while `findMany` returns an array. Do not change the
return shape to an HTTP response or an unrelated DTO. Put that mapping in a public business function.

## Know the scope of an override

This override affects calls to `users.findOne`. It does not automatically filter `findMany`, mutations,
custom model methods, or the internal helper reads used by other base operations such as `upsert`.
An inactive user is hidden by `findOne` but is still returned by an explicit `findMany` query. This is a
read-convenience example, not a complete soft-delete or access-control policy.

If you need soft deletion, decide how every read, write, custom query, unique constraint, and restoration
path behaves. If you need tenant isolation, enforce it at the business boundary and/or database layer;
do not assume one filter override secures the whole table client.

## Work within the callback contract

Overrides receive neither request context nor the injected native client that model extensions receive.
Do not store a current user or request-specific state in a module-level variable used by a shared service.
Pass validated business facts through a suitable function or named operation instead.

For advanced Drizzle queries, prefer a custom method using its injected client. Capturing an unrelated global
client inside an override can escape the active transaction. Calling `base` uses the current binding.
Keyless tables require replacements for `findOne`, `update`, `upsert`, and `delete`; delegating unchanged
arguments to `base` does not remove its unique-selector requirement.

## Verify the changed contract

Test both the overridden operation and the paths it does not cover. Include missing rows, invalid input,
and transaction behavior. Overrides bypass base checks when they do not call `base`, so replacement
implementations own the equivalent validation and safety checks.

From the data-model example root, run:

```sh
bun test tests/data-model.test.ts
bun run typecheck
```

Use [Testing](/docs/modules/database/testing) for safe database lifetimes and
[Transactions](/docs/modules/database/transactions) before composing multiple writes.

{/* Generated by apps/docs/scripts/generate.ts. Do not edit. */}

**Reference and source**

- [`@relkit/drizzle` API](/docs/api/drizzle)

- [`examples/data-model/src/services/database/service.ts`](https://github.com/rel-kit/relkit/blob/main/examples/data-model/src/services/database/service.ts)
