Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/content/docs/api/component-client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,33 @@ export const createAuth = (ctx: GenericCtx<DataModel>) => {
});
```

## `typedAdapter`

Typed wrappers around `components.betterAuth.adapter` for use in your Convex
functions. Prefer this over calling
`ctx.runQuery(components.betterAuth.adapter.findOne, …)` directly — `model`
narrows return types, and `where`, `select`, and `sortBy` are checked against
that table's fields.

With [local install](/features/local-install), pass your schema in `createClient`
config. See [Type Utilities](/api/type-utilities#typedadapter-on-the-component-client).

```ts
import { query } from "./_generated/server";
import { v } from "convex/values";
import { authComponent } from "./auth";

export const getUserByEmail = query({
args: { email: v.string() },
handler: async (ctx, args) => {
return await authComponent.typedAdapter.findOne(ctx, {
model: "user",
where: [{ field: "email", value: args.email }],
});
},
});
```

## `getAuth()`

Better Auth [API endpoints](https://www.better-auth.com/docs/concepts/api) can
Expand Down
69 changes: 69 additions & 0 deletions docs/content/docs/api/type-utilities.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,75 @@ title: Type Utilities
description: Type utilities for Better Auth
---

## `typedAdapter` on the component client

The recommended wrapper for typed adapter calls. `createClient` exposes
`typedAdapter` so you don't call
`ctx.runQuery(components.betterAuth.adapter.findOne, …)` directly — use
`authComponent.typedAdapter.findOne(ctx, …)` instead. The `model` argument
narrows return types, and `where`, `select`, and `sortBy` (on `findMany`) are
checked against that table's fields.

With [local install](/features/local-install), pass your schema in
`createClient` config so types match your tables (including plugin tables):

```ts title="convex/auth.ts"
import { createClient } from "@convex-dev/better-auth";
import authSchema from "./betterAuth/schema";
import { components } from "./_generated/api";
import { type DataModel } from "./_generated/dataModel";

export const authComponent = createClient<DataModel, typeof authSchema>(
components.betterAuth,
{ local: { schema: authSchema } }
);
```

```ts title="convex/users.ts"
import { query } from "./_generated/server";
import { v } from "convex/values";
import { authComponent } from "./auth";

export const getUserByEmail = query({
args: { email: v.string() },
handler: async (ctx, args) => {
return await authComponent.typedAdapter.findOne(ctx, {
model: "user",
where: [{ field: "email", value: args.email }],
select: ["email", "name"],
});
// Pick<Doc<"user">, "email" | "name"> | null
},
});
```

## `createTypedAdapter()`

Lower-level helper when you need to build a typed wrapper yourself (same
methods as `typedAdapter`). `createClient` uses this internally.

```ts
import { createTypedAdapter } from "@convex-dev/better-auth";
import schema from "./betterAuth/schema";
import { components } from "./_generated/api";

export const adapter = createTypedAdapter(schema, components.betterAuth.adapter);
```

The adapter exposes the same operations as
[`createApi`](/features/local-install#export-adapter-functions): `findOne`,
`findMany`, `create`, `updateOne`, `updateMany`, `deleteOne`, and `deleteMany`.

### Exported types

| Type | Description |
| --- | --- |
| `TypedAdapter<S>` | Return type of `createTypedAdapter` for schema `S` |
| `AdapterFunctions` | Function references passed as the second argument |
| `AdapterWhere<S, T>` | Where-clause for table `T` in schema `S`; `field` and `value` narrow with the model |
| `SortBy<S, T>` | `{ direction: "asc" \| "desc"; field: … }` for `findMany`; `field` narrows with the model |
| `BatchResult` | Paginated batch result from `updateMany` and `deleteMany` |

## `requireRunMutationCtx()`

A type guard to ensure a given context object has a `runMutation` property.
Expand Down
54 changes: 54 additions & 0 deletions docs/content/docs/features/local-install.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,58 @@ custom indexes aren't overwritten when the schema is regenerated.
</div>
</div>

### Querying auth tables from app code

When you read or write Better Auth tables from your app (outside the component),
use `authComponent.typedAdapter`. It wraps the adapter the same way as
`ctx.runQuery(components.betterAuth.adapter.findOne, …)` but infers document
types from `model`, and type-checks `where`, `select`, and `sortBy` against that
table.

**1. Pass your local schema to `createClient`** (from the install step above):

```ts title="convex/auth.ts"
import { createClient } from "@convex-dev/better-auth";
import authSchema from "./betterAuth/schema";
import { components } from "./_generated/api";
import { type DataModel } from "./_generated/dataModel";

export const authComponent = createClient<DataModel, typeof authSchema>(
components.betterAuth,
{ local: { schema: authSchema } }
);

export const createAuth = (ctx: GenericCtx<DataModel>) => {
return betterAuth({
database: authComponent.adapter(ctx),
// ... rest of auth config
});
};
```

**2. Call `typedAdapter` from your queries and mutations:**

```ts title="convex/users.ts"
import { query } from "./_generated/server";
import { v } from "convex/values";
import { authComponent } from "./auth";

export const getUserByEmail = query({
args: { email: v.string() },
handler: async (ctx, args) => {
return await authComponent.typedAdapter.findOne(ctx, {
model: "user",
where: [{ field: "email", value: args.email }],
});
// Doc<"user"> | null — inferred from model
},
});
```

`typedAdapter` exposes the same methods as the adapter API: `findOne`, `findMany`,
`create`, `updateOne`, `updateMany`, `deleteOne`, and `deleteMany`. See
[Type Utilities](/api/type-utilities#typedadapter-on-the-component-client).

### Accessing component data

Convex functions within your Better Auth component directory can access the
Expand All @@ -266,6 +318,8 @@ internet, even if they are public**.
type won't be inferred unless a return validator is provided.
</Callout>

#### Custom component functions

```ts title="convex/betterAuth/someFile.ts"
import { query, mutation } from "./_generated/server";
import { doc } from "convex-helpers/validators";
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@
"./utils": {
"types": "./dist/utils/index.d.ts",
"default": "./dist/utils/index.js"
},
"./typed-adapter": {
"types": "./dist/client/create-typed-adapter.d.ts",
"default": "./dist/client/create-typed-adapter.js"
}
},
"typesVersions": {
Expand Down
33 changes: 23 additions & 10 deletions src/client/create-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
queryGeneric,
} from "convex/server";
import type {
DataModelFromSchemaDefinition,
FunctionReference,
GenericDataModel,
GenericMutationCtx,
Expand All @@ -15,8 +14,10 @@ import type {
import { ConvexError, v } from "convex/values";
import type { Infer } from "convex/values";
import { convexAdapter } from "./adapter.js";
import { createTypedAdapter } from "./create-typed-adapter.js";
import type { AdapterFunctions, TypedAdapter } from "./create-typed-adapter.js";
import { corsRouter } from "convex-helpers/server/cors";
import type defaultSchema from "../component/schema.js";
import defaultSchema from "../component/schema.js";
import type { CreateAuth, GenericCtx } from "./index.js";
import type { TrustedOriginsOption } from "../utils/index.js";

Expand Down Expand Up @@ -139,14 +140,18 @@ export const createClient = <
| { triggers?: undefined }
)
) => {
type BetterAuthDataModel = DataModelFromSchemaDefinition<Schema>;
const adapterSchema = config?.local?.schema ?? defaultSchema;
const typedAdapter = createTypedAdapter(
adapterSchema,
component.adapter as AdapterFunctions
) as TypedAdapter<Schema>;

const safeGetAuthUser = async (ctx: GenericCtx<DataModel>) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
return;
}
const session = (await ctx.runQuery(component.adapter.findOne, {
const session = await typedAdapter.findOne(ctx, {
model: "session",
where: [
{
Expand All @@ -159,21 +164,21 @@ export const createClient = <
value: new Date().getTime(),
},
],
})) as BetterAuthDataModel["session"]["document"] | null;
});

if (!session) {
return;
}

const doc = (await ctx.runQuery(component.adapter.findOne, {
const doc = await typedAdapter.findOne(ctx, {
model: "user",
where: [
{
field: "_id",
value: identity.subject,
},
],
})) as BetterAuthDataModel["user"]["document"] | null;
});
if (!doc) {
return;
}
Expand All @@ -194,7 +199,7 @@ export const createClient = <
return new Headers();
}
// Don't validate the session here, let Better Auth handle that
const session = await ctx.runQuery(component.adapter.findOne, {
const session = await typedAdapter.findOne(ctx, {
model: "session",
where: [
{
Expand Down Expand Up @@ -223,6 +228,14 @@ export const createClient = <
debugLogs: config?.verbose,
}),

/**
* Typed wrappers around `components.betterAuth.adapter` queries and
* mutations. Use this instead of calling
* `ctx.runQuery(components.betterAuth.adapter.findOne, …)` directly when
* you need document types inferred from the `model` argument.
*/
typedAdapter,

/**
* Returns the Better Auth auth object and headers for using Better Auth API
* methods directly in a Convex mutation or query. Convex functions don't
Expand Down Expand Up @@ -275,10 +288,10 @@ export const createClient = <
* @returns The user or null if the user is not found
*/
getAnyUserById: async (ctx: GenericCtx<DataModel>, id: string) => {
return (await ctx.runQuery(component.adapter.findOne, {
return await typedAdapter.findOne(ctx, {
model: "user",
where: [{ field: "_id", value: id }],
})) as BetterAuthDataModel["user"]["document"] | null;
});
},

/**
Expand Down
Loading