Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 0.0.91

- Add triggers support to `convexAuth` config for auth table modifications. You
can now configure `onCreate`, `onUpdate`, and `onDelete` handlers for any
table defined in `authTables`.

## 0.0.90

- fix negative `shouldHandleCode` logic for client
Expand Down
90 changes: 90 additions & 0 deletions docs/pages/advanced.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,96 @@ export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
This is helpful when the default user creation implementation in the library
satisfies your app's needs.

## Triggers

Triggers allow you to run custom code when auth-related tables are modified.
This is useful for audit logging, history tracking, or other side effects that
need to run atomically with auth operations.

### Supported Tables

Triggers can be configured for any table defined in
[`authTables`](/api_reference/server#authtables).

### Trigger Types

Each table supports three trigger types:

- `onCreate` - Called after a new record is inserted
- `onUpdate` - Called after a record is updated (receives both old and new doc)
- `onDelete` - Called after a record is deleted
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

### Usage

Configure triggers in your `convexAuth` setup:

```ts filename="convex/auth.ts"
import { convexAuth } from "@convex-dev/auth/server";
import type { AuthTriggers } from "@convex-dev/auth/server";
import Password from "@convex-dev/auth/providers/Password";

const triggers: AuthTriggers = {
users: {
onCreate: async (ctx, doc) => {
// Log new user creation
await ctx.db.insert("auditLog", {
action: "user_created",
userId: doc._id,
timestamp: Date.now(),
});
},
onUpdate: async (ctx, newDoc, oldDoc) => {
// Track user profile changes
await ctx.db.insert("userHistory", {
userId: newDoc._id,
before: oldDoc,
after: newDoc,
timestamp: Date.now(),
});
},
},
authAccounts: {
onCreate: async (ctx, doc) => {
console.log(`New auth account created: ${doc.provider}`);
},
onUpdate: async (ctx, newDoc, oldDoc) => {
// Audit password changes, etc.
console.log(`Auth account updated: ${newDoc._id}`);
},
},
};

export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [Password],
triggers,
});
```

### Type Safety

The trigger configuration is fully type-safe. The `AuthTriggers` type is derived
from `authTables`, so TypeScript will enforce correct table names and document
types:

```ts
import type {
AuthTableName,
AuthTriggers,
TableTriggers,
OnCreateTrigger, // (ctx, doc?) => Promise<void>
OnUpdateTrigger, // (ctx, newDoc?, oldDoc?) => Promise<void>
OnDeleteTrigger, // (ctx, id, doc?) => Promise<void>
} from "@convex-dev/auth/server";
```

### Important Notes

- Triggers run within the same transaction as the auth operation
- If a trigger throws an error, the entire operation will be rolled back
- Triggers receive a full `MutationCtx` so you can perform any database operations
- Not all operations fire all triggers - only the triggers for operations that
actually occur will be called

Comment thread
coderabbitai[bot] marked this conversation as resolved.
## Session validity

Convex Auth issues JWTs which allow your client to authenticate.
Expand Down
195 changes: 195 additions & 0 deletions docs/pages/api_reference/server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1576,6 +1576,16 @@ The `shouldLink` argument passed to `createAccount`.

`Promise`\<`void`\>

#### triggers?

> `optional` **triggers**: [`AuthTriggers`](server.mdx#authtriggers)

Database triggers for auth tables.
Triggers run in the same transaction as the auth operation,
allowing for atomic audit logging, history tracking, etc.

See [Triggers](/advanced#triggers) for usage examples.

<h3 className="nx-font-semibold nx-tracking-tight nx-text-slate-900 dark:nx-text-slate-100 nx-mt-8 nx-text-2xl">Defined in</h3>

[src/server/types.ts:22](https://github.com/get-convex/convex-auth/blob/main/src/server/types.ts#L22)
Expand Down Expand Up @@ -2324,3 +2334,188 @@ Materialized Auth.js provider config.
<h3 className="nx-font-semibold nx-tracking-tight nx-text-slate-900 dark:nx-text-slate-100 nx-mt-8 nx-text-2xl">Defined in</h3>

[src/server/types.ts:372](https://github.com/get-convex/convex-auth/blob/main/src/server/types.ts#L372)

***

## AuthTableName


Table names managed by the auth library, derived from [authTables](server.mdx#authtables).
Use these for type-safe trigger configurations.

```ts
type AuthTableName = "users" | "authAccounts" | "authSessions" |
"authRefreshTokens" | "authVerificationCodes" | "authVerifiers" | "authRateLimits"
```

<h3 className="nx-font-semibold nx-tracking-tight nx-text-slate-900 dark:nx-text-slate-100 nx-mt-8 nx-text-2xl">Defined in</h3>

[src/server/implementation/types.ts:165](https://github.com/get-convex/convex-auth/blob/main/src/server/implementation/types.ts#L165)

***

## AuthTriggers


Configuration for auth table triggers.
Triggers run in the same transaction as the auth operation,
allowing for atomic audit logging, history tracking, etc.

```ts
type AuthTriggers = {
[K in AuthTableName]?: TableTriggers<K>;
}
```

See [Triggers](/advanced#triggers) for usage examples.

<h3 className="nx-font-semibold nx-tracking-tight nx-text-slate-900 dark:nx-text-slate-100 nx-mt-8 nx-text-2xl">Defined in</h3>

[src/server/implementation/types.ts:231](https://github.com/get-convex/convex-auth/blob/main/src/server/implementation/types.ts#L231)

***

## TableTriggers\<TableName\>


Trigger configuration for a single table.

<h3 className="nx-font-semibold nx-tracking-tight nx-text-slate-900 dark:nx-text-slate-100 nx-mt-8 nx-text-2xl">Type Parameters</h3>

<table className="api_reference_table"><tbody>
<tr>
<th>Type Parameter</th>
</tr>
<tr>
<td>

`TableName` *extends* [`AuthTableName`](server.mdx#authtablename)

</td>
</tr>
</tbody></table>

<h3 className="nx-font-semibold nx-tracking-tight nx-text-slate-900 dark:nx-text-slate-100 nx-mt-8 nx-text-2xl">Type declaration</h3>

#### onCreate?

> `optional` **onCreate**: [`OnCreateTrigger`](server.mdx#oncreatetriggertablename)\<`TableName`\>

Called after a new record is inserted.

#### onUpdate?

> `optional` **onUpdate**: [`OnUpdateTrigger`](server.mdx#onupdatetriggertablename)\<`TableName`\>

Called after a record is updated. Receives both the new and old document.

#### onDelete?

> `optional` **onDelete**: [`OnDeleteTrigger`](server.mdx#ondeletetriggertablename)\<`TableName`\>

Called after a record is deleted.

<h3 className="nx-font-semibold nx-tracking-tight nx-text-slate-900 dark:nx-text-slate-100 nx-mt-8 nx-text-2xl">Defined in</h3>

[src/server/implementation/types.ts:196](https://github.com/get-convex/convex-auth/blob/main/src/server/implementation/types.ts#L196)

***

## OnCreateTrigger\<TableName\>


Trigger handler called when a document is created.
Omit the doc parameter to skip the read.

```ts
type OnCreateTrigger<TableName> =
| ((ctx: MutationCtx) => Promise<void>)
| ((ctx: MutationCtx, doc: Doc<TableName>) => Promise<void>)
```

<h3 className="nx-font-semibold nx-tracking-tight nx-text-slate-900 dark:nx-text-slate-100 nx-mt-8 nx-text-2xl">Type Parameters</h3>

<table className="api_reference_table"><tbody>
<tr>
<th>Type Parameter</th>
</tr>
<tr>
<td>

`TableName` *extends* [`AuthTableName`](server.mdx#authtablename)

</td>
</tr>
</tbody></table>

<h3 className="nx-font-semibold nx-tracking-tight nx-text-slate-900 dark:nx-text-slate-100 nx-mt-8 nx-text-2xl">Defined in</h3>

[src/server/implementation/types.ts:170](https://github.com/get-convex/convex-auth/blob/main/src/server/implementation/types.ts#L170)

***

## OnUpdateTrigger\<TableName\>


Trigger handler called when a document is updated.
Omit oldDoc to skip reading the old document.
Omit both to skip all reads.

```ts
type OnUpdateTrigger<TableName> =
| ((ctx: MutationCtx) => Promise<void>)
| ((ctx: MutationCtx, newDoc: Doc<TableName>) => Promise<void>)
| ((ctx: MutationCtx, newDoc: Doc<TableName>, oldDoc: Doc<TableName>) => Promise<void>)
```

<h3 className="nx-font-semibold nx-tracking-tight nx-text-slate-900 dark:nx-text-slate-100 nx-mt-8 nx-text-2xl">Type Parameters</h3>

<table className="api_reference_table"><tbody>
<tr>
<th>Type Parameter</th>
</tr>
<tr>
<td>

`TableName` *extends* [`AuthTableName`](server.mdx#authtablename)

</td>
</tr>
</tbody></table>

<h3 className="nx-font-semibold nx-tracking-tight nx-text-slate-900 dark:nx-text-slate-100 nx-mt-8 nx-text-2xl">Defined in</h3>

[src/server/implementation/types.ts:179](https://github.com/get-convex/convex-auth/blob/main/src/server/implementation/types.ts#L179)

***

## OnDeleteTrigger\<TableName\>


Trigger handler called when a document is deleted.
Omit doc to skip reading the document before deletion.

```ts
type OnDeleteTrigger<TableName> =
| ((ctx: MutationCtx, id: GenericId<TableName>) => Promise<void>)
| ((ctx: MutationCtx, id: GenericId<TableName>, doc: Doc<TableName> | null) => Promise<void>)
```

<h3 className="nx-font-semibold nx-tracking-tight nx-text-slate-900 dark:nx-text-slate-100 nx-mt-8 nx-text-2xl">Type Parameters</h3>

<table className="api_reference_table"><tbody>
<tr>
<th>Type Parameter</th>
</tr>
<tr>
<td>

`TableName` *extends* [`AuthTableName`](server.mdx#authtablename)

</td>
</tr>
</tbody></table>

<h3 className="nx-font-semibold nx-tracking-tight nx-text-slate-900 dark:nx-text-slate-100 nx-mt-8 nx-text-2xl">Defined in</h3>

[src/server/implementation/types.ts:188](https://github.com/get-convex/convex-auth/blob/main/src/server/implementation/types.ts#L188)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@convex-dev/auth",
"version": "0.0.90",
"version": "0.0.91",
"description": "Authentication for Convex",
"keywords": [
"authentication",
Expand Down
10 changes: 6 additions & 4 deletions src/server/implementation/mutations/createVerificationCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { EmailConfig, PhoneConfig } from "../../types.js";
import { getAccountOrThrow, upsertUserAndAccount } from "../users.js";
import { getAuthSessionId } from "../sessions.js";
import { LOG_LEVELS, logWithLevel, sha256 } from "../utils.js";
import { createTriggeredCtx } from "../triggeredDb.js";

export const createVerificationCodeArgs = v.object({
accountId: v.optional(v.id("authAccounts")),
Expand All @@ -19,11 +20,12 @@ export const createVerificationCodeArgs = v.object({
type ReturnType = string;

export async function createVerificationCodeImpl(
ctx: MutationCtx,
originalCtx: MutationCtx,
args: Infer<typeof createVerificationCodeArgs>,
getProviderOrThrow: Provider.GetProviderOrThrowFunc,
config: Provider.Config,
): Promise<ReturnType> {
const ctx = createTriggeredCtx(originalCtx, config);
logWithLevel(LOG_LEVELS.DEBUG, "createVerificationCodeImpl args:", args);
const {
email,
Expand All @@ -36,7 +38,7 @@ export async function createVerificationCodeImpl(
} = args;
const existingAccount =
existingAccountId !== undefined
? await getAccountOrThrow(ctx, existingAccountId)
? await getAccountOrThrow(originalCtx, existingAccountId)
: await ctx.db
.query("authAccounts")
.withIndex("providerAndAccountId", (q) =>
Expand All @@ -50,8 +52,8 @@ export async function createVerificationCodeImpl(
| EmailConfig
| PhoneConfig;
const { accountId } = await upsertUserAndAccount(
ctx,
await getAuthSessionId(ctx),
originalCtx,
await getAuthSessionId(originalCtx),
existingAccount !== null
? { existingAccount }
: { providerAccountId: email ?? phone! },
Expand Down
6 changes: 3 additions & 3 deletions src/server/implementation/mutations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export const storeImpl = async (
return signInImpl(ctx, args, config);
}
case "signOut": {
return signOutImpl(ctx);
return signOutImpl(ctx, config);
}
case "refreshSession": {
return refreshSessionImpl(ctx, args, getProviderOrThrow, config);
Expand Down Expand Up @@ -146,10 +146,10 @@ export const storeImpl = async (
);
}
case "modifyAccount": {
return modifyAccountImpl(ctx, args, getProviderOrThrow);
return modifyAccountImpl(ctx, args, getProviderOrThrow, config);
}
case "invalidateSessions": {
return invalidateSessionsImpl(ctx, args);
return invalidateSessionsImpl(ctx, args, config);
}
default:
args satisfies never;
Expand Down
Loading