---
title: Custom models
description: Add typed, named queries with defineModel, use the injected Drizzle query builder, and preserve transaction and authorization boundaries.
pagination: false
sidebar:
  icon: box
---

Add a model when a table needs a named operation beyond the built-in CRUD methods. A model extends the table client with
named functions; it does not create a new table, require a class, or replace the owning business service.
If the six base methods are sufficient, do not create an empty model file.

> **Before you begin**
>
> Complete [database installation](/docs/modules/database/define) before importing the model API.
> Select the native model entrypoint for your dialect: `@relkit/drizzle/sqlite`,
> `@relkit/drizzle/pg`, or `@relkit/drizzle/mysql`. Each keeps the real native query-builder types
> in that selected declaration graph. The general root supports portable service operations and
> table-only model extensions; it does not implicitly select every dialect's native database type.
> Install your chosen Drizzle driver explicitly. In particular, the pinned MySQL declaration graph
> requires `mysql2`; a SQLite application does not need that driver.

Only when selecting the MySQL model entrypoint, add its declaration requirement:

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

From this guide's application/package root, install only its selected requirements:

```sh
bun add --dev 'mysql2@>=2.0.0'
```

Keep these owner-specific versions and the lockfile together. Runtime SDKs belong in `dependencies`; build/type tooling belongs in `devDependencies`. Installing an adapter does not supply its optional SDK peers or enable the feature's configuration.

1. ## Define an extension beside the schema

    ```ts title="src/services/database/models/users.model.ts"
    /** Extends the native users model with email-domain selection and an update that conceals missing or inactive rows. */
    import { defineModel, type UpdateArgs } from "@relkit/drizzle/sqlite";
    import { eq, like } from "drizzle-orm";
    import { users } from "../schema/index.js";

    export default defineModel({
      table: users,
      extend: {
        byEmailDomain: ({ table, database }, domain: string) =>
          database
            .select()
            .from(table)
            .where(like(table.email, `%@${domain}`)),
        findOneAndUpdate: async ({ table, database }, args: UpdateArgs<typeof users>) => {
          const id = args.where.id;
          if (id === undefined) return null;
          const [existing] = await database.select().from(table).where(eq(table.id, id));
          if (existing === undefined || existing.active === false) return null;
          const [updated] = await database
            .update(table)
            .set(args.data)
            .where(eq(table.id, id))
            .returning();
          return updated ?? null;
        },
      },
    });
    ```

    `table` must be the exact table object exported by the service's schema. `extend` must contain at least
    one function. Names such as `byEmailDomain` and `findOneAndUpdate` become methods on the Users table client.

    The first extension argument is injected: `{ table, database }`. Additional arguments form the consumer
    API, and return types are preserved. Use this parameter instead of class state such as `this.drizzle`
    or `this.table`.

2. ## Register and call the model

    Import the model into `src/services/database/service.ts` and add `models: { users: usersModel }` to
    `defineDrizzleService`. The registration key must match the schema export key, and the model must use
    that same table object. A file under `models/` is not automatically registered just because it exists.

    After `bun run check` refreshes context types, functions call the method without supplying the injected
    first argument:

    ```ts title="src/services/users/functions/list-users.function.ts"
    /** Lists validated user fields through the database model's email-domain extension. */
    import { defineFunction } from "@relkit/app/functions";
    import { z } from "@relkit/app/schema";

    export default defineFunction({
      input: z.object({ domain: z.string() }),
      output: z.array(z.object({ id: z.number(), email: z.string(), active: z.boolean() })),
      handler: ({ domain }, context) => context.database.users.byEmailDomain(domain),
    });
    ```

    Keep business entry points in their owning domains and expose them through [Service](/docs/modules/service).
    Do not make routes or other domains import private table files just to construct a query.

## Use native Drizzle where it helps

The injected `database` provides the dialect's query builder for joins, aggregations, projections, and
native mutations. It is not the portable `context.database`, so do not expect `database.users.findOne`
inside the extension. The public extension type intentionally omits Drizzle's relational `query` API;
use the supported query-builder surface rather than assuming relation-loading types are present.

An extension called through the context supplied by `transaction` receives the transaction-bound client.
Use that injected client, not a separately imported connection. See [Transactions](/docs/modules/database/transactions).

## Make custom behavior explicit

Custom methods bypass the portable CRUD implementation and its operation overrides. Add the validation,
tenant restrictions, limits, and result checks the query needs. You cannot use an extension named
`findOne`, `findMany`, `insert`, `update`, `upsert`, or `delete`; use
[operation overrides](/docs/modules/database/overrides) to customize those names.

The sample `byEmailDomain` uses Drizzle's `like()` expression: values are parameterized, but `%` and `_`
in the supplied domain still act as pattern wildcards. It also has no pagination. Validate or escape
patterns and add an explicit limit before using it as a public search feature. For native statements
beyond the query builder, refer to [Drizzle's documentation](https://orm.drizzle.team/docs/sql).

Likewise, `findOneAndUpdate` performs a read followed by an update; its name does not make the sequence
atomic. Protect concurrent state changes with an appropriate database operation and test it. The
[model test](/docs/modules/database/testing) verifies the demonstration behavior without implying stronger guarantees.

{/* 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/models/users.model.ts`](https://github.com/rel-kit/relkit/blob/main/examples/data-model/src/services/database/models/users.model.ts)
