# Client extensions

> Attach app-specific helpers and shared values to a Better Drizzle client with extends().

URL: https://better-drizzle.com/docs/guides/client-extensions

`client.extends(...)` adds application-specific helpers or shared values to a Better Drizzle client. It takes a plain object or a callback, merges the result onto the client, and reapplies it to every `$withContext()` clone and transaction client derived later.

## Object form and callback form

The object form is for static values:

```ts
const client = better(db).extends({
	limits: { maxPageSize: 100 },
});

client.limits.maxPageSize; // 100
```

The callback form receives the client it is being applied to and returns the extension:

```ts
const client = better(db).extends((client) => ({
	findUserByEmail(email: string) {
		return client.users.findUnique({ where: { email } });
	},
}));

const user = await client.findUserByEmail('alice@example.com');
```

The callback runs immediately for the current client, then **again for each derived client**: every `$withContext()` clone, every transaction client, and every nested savepoint client. Inside it, `client` is that derived instance, so a helper called as `tx.findUserByEmail(...)` queries through the transaction.

The object form does not get this. The same object is copied onto each derived client, so a method that closes over an outer variable keeps using that variable:

```ts
const base = better(db);

const client = base.extends({
	countUsers: () => base.users.count(), // always the root client
});

await client.transaction(async (tx) => {
	await tx.countUsers(); // runs on the root client, not on tx
});
```

Use the object form for values, and the callback form for anything that runs queries.

Returning `undefined` from the callback adds nothing. `extends()` modifies the client it is called on and returns that same instance.

## Typing

`extends()` returns the client type intersected with the extension type:

```ts title="lib/db.ts"
const base = better(db);

const helpers = (client: typeof base) => ({
	findUserByEmail(email: string) {
		return client.users.findUnique({ where: { email } });
	},
	publishPost(id: number) {
		return client.posts.update({ where: { id }, data: { published: true } });
	},
});

export const client = base.extends(helpers);
export type AppHelpers = ReturnType<typeof helpers>;
```

The extension keys stay typed on every client derived from the extended one:

* **Chained calls keep every extension.** `client.extends(a).extends(b)` has the keys of both `a` and `b`.
* **`$withContext()` clones** keep the extension keys.
* **The `tx` parameter of `transaction()`** keeps the extension keys.

```ts
import { client } from './lib/db';

const scoped = client.$withContext({ requestId });
await scoped.findUserByEmail('alice@example.com');

await client.transaction(async (tx) => {
	await tx.publishPost(1);
});
```

Keys added by a plugin's `extendClient()` are typed the same way. See [extends vs plugins](#extends-vs-plugins).

## Conflicts fail fast

An extension cannot replace an existing key. The call throws `BetterDrizzleError` with code `PLUGIN_EXTENSION_CONFLICT` and the message `Client extension cannot override "<key>".` This covers:

* built-in client keys such as `repository`, `transaction`, `$raw`, and `$withContext`
* table delegates such as `users`
* keys added by plugins through `extendClient()`
* keys added by an earlier `extends()` call

Plugin extensions are applied first, so a plugin key always wins the conflict and the `extends()` call is the one that fails. A key that conflicts with the client type is also a compile-time error.

> **Call extends() once, at startup:** Extensions are registered on state shared by the root client and everything
> derived from it. Calling `extends()` on a per-request `$withContext()` clone
> registers the extension for all future clones, and the next request that
> adds the same key fails with a conflict. Register extensions where you build
> the client, not in request handlers.

## Example: domain helpers that respect transactions

Helpers that compose several operations can call `client.transaction(...)`. Because the callback form rebinds `client`, the same helper opens a transaction when called on the root client and a nested savepoint when called on a transaction client:

```ts
const base = better(drizzle({ client: pool, relations }));

const helpers = (client: typeof base) => ({
	async transferPost(postId: number, toUserId: number) {
		return client.transaction(async (tx) => {
			await tx.users.findUnique({ where: { id: toUserId } }).throw();

			return tx.posts.update({
				where: { id: postId },
				data: { authorId: toUserId },
			});
		});
	},
});

const client = base.extends(helpers);

// Own transaction
await client.transferPost(10, 2);

// Savepoint inside a larger transaction, rolled back with it
await client.transaction(async (tx) => {
	await tx.transferPost(10, 2);
	await tx.users.update({ where: { id: 1 }, data: { active: false } });
});
```

## Example: shared services next to the data

An extension can carry values that helpers and callers both need, such as configuration, next to the helpers that use them:

```ts
const base = better(drizzle({ client: pool, relations }));
const config = { maxPageSize: 100 };

const helpers = (client: typeof base) => ({
	config,
	listPublished(page: number, perPage = 20) {
		const limit = Math.min(perPage, config.maxPageSize);

		return client.posts.paginate({
			where: { published: true },
			orderBy: [{ id: 'desc' }],
			limit,
			skip: (page - 1) * limit,
		});
	},
});

const client = base.extends(helpers);

const { data, pagination } = await client.listPublished(2);
const { maxPageSize } = client.config;
```

Plain values are safe in either form, because they do not depend on which client they are attached to.

## extends vs plugins

Use `extends()` when:

* the helper is specific to this application and its tables
* you only need client-level helpers or shared values
* you do not need transforms, lifecycle hooks, or typed operation args

Use a [plugin](/docs/plugins/writing-plugins) with `extendClient()` when:

* the behavior is reusable across schemas, since plugin code sees the schema generically rather than as your tables
* the extension must be typed on scoped and transaction clients without assertions
* it has to change repository operations, add model-level helpers, or observe hooks

A plugin's `extendClient(ctx)` also runs for every bound client, and `ctx.client` is that client, so the same rebinding rules apply.
