[OIDC Provider] Trusted Clients: Token Endpoint Fails Due to Missing Database Entry for FK Constraint #2496

Open
opened 2026-03-13 09:59:19 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @inducingchaos on GitHub (Dec 9, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

To Reproduce

  1. Configure BetterAuth with OIDC provider plugin
  2. Add a trusted client in the trustedClients array (e.g., for a native/mobile app using PKCE)
  3. Configure database schema with FK constraint: oauth_access_tokens.client_id references oauth_applications.client_id
  4. Attempt to exchange an authorization code for tokens via /oauth2/token endpoint
  5. Observe database FK constraint violation error

Minimal reproduction:

// auth.ts
import { betterAuth } from "better-auth"
import { oidcProvider } from "better-auth/plugins"

export const auth = betterAuth({
    plugins: [
        oidcProvider({
            trustedClients: [
                {
                    clientId: "my-native-app",
                    name: "My Native App",
                    type: "public", // PKCE client
                    clientSecret: "placeholder-secret",
                    redirectUrls: ["myapp://callback"],
                    disabled: false,
                    skipConsent: true
                }
            ]
        })
    ]
})

Database schema (Drizzle):

export const oauthAccessTokens = pgTable("oauth_access_tokens", {
    // ... other fields
    clientId: varchar()
        .notNull()
        .references(() => oauthApplications.clientId) // FK constraint
    // ...
})

Current vs. Expected behavior

Current behavior:
When exchanging an authorization code for tokens, the token endpoint fails with a database foreign key constraint violation:

Error [PostgresError]: insert or update on table "oauth_access_tokens" violates foreign key constraint "oauth_access_tokens_client_id_oauth_applications_client_id_fkey"
detail: 'Key (client_id)=(my-native-app) is not present in table "oauth_applications".'

Root cause:
Looking at the BetterAuth source code (line ~700-720 in packages/better-auth/src/plugins/oidc-provider/index.ts):

  1. The token endpoint calls getClient(client_id.toString(), trustedClients) which reads from the in-memory trustedClients config array (line ~600)
  2. Client validation succeeds because the trusted client exists in the config
  3. However, when creating the access token, adapter.create() is called with clientId: client_id.toString() (line ~700-720)
  4. The database FK constraint requires client_id to exist in oauth_applications table
  5. Since trusted clients bypass normal registration, they never get inserted into the database
  6. Result: FK constraint violation when trying to insert the access token

Expected behavior:
Trusted clients should either:

  • Be automatically synced to the database when tokens are created, OR
  • The FK constraint should be optional/relaxed for trusted clients, OR
  • There should be a hook/option to ensure trusted clients exist in the DB

Workarounds:

  1. Manually insert trusted clients into oauth_applications table on startup
  2. Remove the FK constraint (not ideal for data integrity)
  3. Use dynamic client registration instead of trusted clients

What version of Better Auth are you using?

1.4.5

System info

{                                                       "system": {                                             "platform": "darwin",                                 "arch": "arm64",                                      "version": "Darwin Kernel Version 25.0.0: Wed Sep 17 21:38:03 PDT 2025; root:xnu-12377.1.9~141/RELEASE_ARM64_T8112",                                              "release": "25.0.0",                                  "cpuCount": 8,                                        "cpuModel": "Apple M2",                               "totalMemory": "8.00 GB",                             "freeMemory": "0.16 GB"
  },                                                    "node": {                                               "version": "v22.20.0",                                "env": "development"                                },
  "packageManager": {                                     "name": "pnpm",                                       "version": "10.20.0"                                },
  "frameworks": [                                         {                                                       "name": "next",                                       "version": "16.0.8"                                 },                                                    {                                                       "name": "react",                                      "version": "catalog:react"                          }                                                   ],                                                    "databases": [                                          {                                                       "name": "postgres",                                   "version": "^3.4.7"                                 },                                                    {                                                       "name": "drizzle",                                    "version": "catalog:drizzle"                        }                                                   ],                                                    "betterAuth": {                                         "version": "catalog:better-auth",                     "config": {                                             "appName": "ALTERED",                                 "baseURL": "http://localhost:258",                    "secret": "[REDACTED]",                               "socialProviders": {                                    "google": {                                             "clientId": "[REDACTED]",                             "clientSecret": "[REDACTED]"                        }                                                   },                                                    "plugins": [                                            {                                                       "name": "next-cookies",                               "config": {                                             "id": "next-cookies",                                 "hooks": {                                              "after": [                                              {}                                                  ]                                                   }                                                   }                                                   },                                                    {                                                       "name": "oidc",                                       "config": {                                             "id": "oidc",                                         "hooks": {                                              "after": [                                              {}                                                  ]                                                   },                                                    "endpoints": {},                                      "schema": {                                             "oauthApplication": {                                   "modelName": "oauthApplication",                      "fields": {                                             "name": {                                               "type": "string"                                    },                                                    "icon": {                                               "type": "string",                                     "required": false                                   },                                                    "metadata": {                                           "type": "string",                                     "required": false                                   },                                                    "clientId": {
                    "type": "string",                                     "unique": true                                      },                                                    "clientSecret": {
                    "type": "string",
                    "required": false                                   },                                                    "redirectUrls": {                                       "type": "string"                                    },                                                    "type": {                                               "type": "string"                                    },                                                    "disabled": {
                    "type": "boolean",                                    "required": false,                                    "defaultValue": false                               },                                                    "userId": {                                             "type": "string",                                     "required": false,                                    "references": {                                         "model": "user",                                      "field": "id",                                        "onDelete": "cascade"                               },                                                    "index": true                                       },                                                    "createdAt": {                                          "type": "date"                                      },                                                    "updatedAt": {                                          "type": "date"                                      }                                                   }                                                   },                                                    "oauthAccessToken": {                                   "modelName": "oauthAccessToken",                      "fields": {
                  "accessToken": {                                        "type": "string",
                    "unique": true                                      },                                                    "refreshToken": {
                    "type": "string",                                     "unique": true                                      },                                                    "accessTokenExpiresAt": {                               "type": "date"                                      },                                                    "refreshTokenExpiresAt": {                              "type": "date"                                      },                                                    "clientId": {                                           "type": "string",                                     "references": {                                         "model": "oauthApplication",
                      "field": "clientId",
                      "onDelete": "cascade"                               },
                    "index": true
                  },
                  "userId": {                                             "type": "string",
                    "required": false,                                    "references": {
                      "model": "user",
                      "field": "id",
                      "onDelete": "cascade"
                    },
                    "index": true
                  },
                  "scopes": {                                             "type": "string"
                  },
                  "createdAt": {
                    "type": "date"                                      },
                  "updatedAt": {                                          "type": "date"
                  }
                }
              },
              "oauthConsent": {
                "modelName": "oauthConsent",
                "fields": {                                             "clientId": {
                    "type": "string",                                     "references": {
                      "model": "oauthApplication",
                      "field": "clientId",
                      "onDelete": "cascade"
                    },
                    "index": true                                       },
                  "userId": {                                             "type": "string",
                    "references": {
                      "model": "user",
                      "field": "id",
                      "onDelete": "cascade"
                    },                                                    "index": true
                  },                                                    "scopes": {
                    "type": "string"
                  },
                  "createdAt": {
                    "type": "date"
                  },                                                    "updatedAt": {
                    "type": "date"                                      },
                  "consentGiven": {
                    "type": "boolean"
                  }
                }
              }                                                   },
            "options": {                                            "codeExpiresIn": 600,
              "defaultScope": "openid",
              "accessTokenExpiresIn": 3600,
              "refreshTokenExpiresIn": 604800,                      "allowPlainCodeChallengeMethod": true,
              "storeClientSecret": "[REDACTED]",                    "loginPage": "/sign-in",
              "trustedClients": [
                {
                  "clientId": "[REDACTED]",
                  "name": "ALTERED for Raycast",
                  "type": "public",
                  "clientSecret": "[REDACTED]",
                  "icon": "icon.png",                                   "redirectUrls": [
                    "https://raycast.com/redirect?packageName=Extension"
                  ],
                  "disabled": false,
                  "skipConsent": true,                                  "metadata": {
                    "platform": "raycast"
                  }                                                   }
              ],                                                    "scopes": [
                "openid",                                             "profile",
                "email",
                "offline_access"
              ]
            }
          }
        },
        {
          "name": "oidc-polyfill",                              "config": {
            "id": "oidc-polyfill",                                "endpoints": {}
          }
        }
      ],                                                    "advanced": {                                           "database": {}                                      }
    }
  }
}

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth"
import { drizzleAdapter } from "better-auth/adapters/drizzle"
import { oidcProvider } from "better-auth/plugins"
import { db } from "~/server/data/store"

export const auth = betterAuth({
    database: drizzleAdapter(db, {
        provider: "pg",
        usePlural: true
    }),
    plugins: [
        oidcProvider({
            loginPage: "/sign-in",
            trustedClients: [
                {
                    clientId: "altered-launcher",
                    name: "ALTERED for Raycast",
                    type: "public",
                    clientSecret: "placeholder-secret",
                    redirectUrls: ["https://raycast.com/redirect?packageName=Extension"],
                    disabled: false,
                    skipConsent: true,
                    metadata: { platform: "raycast" }
                }
            ]
        })
    ]
})

Additional context

Design gap identified:

  • Trusted clients are validated from config (in-memory) but not persisted to database
  • Token creation requires database records for FK constraints
  • This creates a mismatch between validation (config-based) and persistence (database-based)

Suggested solution:
Add a hook or automatic sync mechanism that ensures trusted clients exist in the database before token creation. This could be:

  • An onTokenCreate hook that checks and creates the client if missing
  • An automatic sync on plugin initialization
  • A configuration option to auto-sync trusted clients to database

Reference:

Originally created by @inducingchaos on GitHub (Dec 9, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce ## To Reproduce 1. Configure BetterAuth with OIDC provider plugin 2. Add a trusted client in the `trustedClients` array (e.g., for a native/mobile app using PKCE) 3. Configure database schema with FK constraint: `oauth_access_tokens.client_id` references `oauth_applications.client_id` 4. Attempt to exchange an authorization code for tokens via `/oauth2/token` endpoint 5. Observe database FK constraint violation error **Minimal reproduction:** ```typescript // auth.ts import { betterAuth } from "better-auth" import { oidcProvider } from "better-auth/plugins" export const auth = betterAuth({ plugins: [ oidcProvider({ trustedClients: [ { clientId: "my-native-app", name: "My Native App", type: "public", // PKCE client clientSecret: "placeholder-secret", redirectUrls: ["myapp://callback"], disabled: false, skipConsent: true } ] }) ] }) ``` **Database schema (Drizzle):** ```typescript export const oauthAccessTokens = pgTable("oauth_access_tokens", { // ... other fields clientId: varchar() .notNull() .references(() => oauthApplications.clientId) // FK constraint // ... }) ``` ### Current vs. Expected behavior **Current behavior:** When exchanging an authorization code for tokens, the token endpoint fails with a database foreign key constraint violation: ``` Error [PostgresError]: insert or update on table "oauth_access_tokens" violates foreign key constraint "oauth_access_tokens_client_id_oauth_applications_client_id_fkey" detail: 'Key (client_id)=(my-native-app) is not present in table "oauth_applications".' ``` **Root cause:** Looking at the BetterAuth source code (line ~700-720 in `packages/better-auth/src/plugins/oidc-provider/index.ts`): 1. The token endpoint calls `getClient(client_id.toString(), trustedClients)` which reads from the in-memory `trustedClients` config array (line ~600) 2. Client validation succeeds because the trusted client exists in the config 3. However, when creating the access token, `adapter.create()` is called with `clientId: client_id.toString()` (line ~700-720) 4. The database FK constraint requires `client_id` to exist in `oauth_applications` table 5. Since trusted clients bypass normal registration, they never get inserted into the database 6. Result: FK constraint violation when trying to insert the access token **Expected behavior:** Trusted clients should either: - Be automatically synced to the database when tokens are created, OR - The FK constraint should be optional/relaxed for trusted clients, OR - There should be a hook/option to ensure trusted clients exist in the DB **Workarounds:** 1. Manually insert trusted clients into `oauth_applications` table on startup 2. Remove the FK constraint (not ideal for data integrity) 3. Use dynamic client registration instead of trusted clients ### What version of Better Auth are you using? 1.4.5 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 25.0.0: Wed Sep 17 21:38:03 PDT 2025; root:xnu-12377.1.9~141/RELEASE_ARM64_T8112", "release": "25.0.0", "cpuCount": 8, "cpuModel": "Apple M2", "totalMemory": "8.00 GB", "freeMemory": "0.16 GB" }, "node": { "version": "v22.20.0", "env": "development" }, "packageManager": { "name": "pnpm", "version": "10.20.0" }, "frameworks": [ { "name": "next", "version": "16.0.8" }, { "name": "react", "version": "catalog:react" } ], "databases": [ { "name": "postgres", "version": "^3.4.7" }, { "name": "drizzle", "version": "catalog:drizzle" } ], "betterAuth": { "version": "catalog:better-auth", "config": { "appName": "ALTERED", "baseURL": "http://localhost:258", "secret": "[REDACTED]", "socialProviders": { "google": { "clientId": "[REDACTED]", "clientSecret": "[REDACTED]" } }, "plugins": [ { "name": "next-cookies", "config": { "id": "next-cookies", "hooks": { "after": [ {} ] } } }, { "name": "oidc", "config": { "id": "oidc", "hooks": { "after": [ {} ] }, "endpoints": {}, "schema": { "oauthApplication": { "modelName": "oauthApplication", "fields": { "name": { "type": "string" }, "icon": { "type": "string", "required": false }, "metadata": { "type": "string", "required": false }, "clientId": { "type": "string", "unique": true }, "clientSecret": { "type": "string", "required": false }, "redirectUrls": { "type": "string" }, "type": { "type": "string" }, "disabled": { "type": "boolean", "required": false, "defaultValue": false }, "userId": { "type": "string", "required": false, "references": { "model": "user", "field": "id", "onDelete": "cascade" }, "index": true }, "createdAt": { "type": "date" }, "updatedAt": { "type": "date" } } }, "oauthAccessToken": { "modelName": "oauthAccessToken", "fields": { "accessToken": { "type": "string", "unique": true }, "refreshToken": { "type": "string", "unique": true }, "accessTokenExpiresAt": { "type": "date" }, "refreshTokenExpiresAt": { "type": "date" }, "clientId": { "type": "string", "references": { "model": "oauthApplication", "field": "clientId", "onDelete": "cascade" }, "index": true }, "userId": { "type": "string", "required": false, "references": { "model": "user", "field": "id", "onDelete": "cascade" }, "index": true }, "scopes": { "type": "string" }, "createdAt": { "type": "date" }, "updatedAt": { "type": "date" } } }, "oauthConsent": { "modelName": "oauthConsent", "fields": { "clientId": { "type": "string", "references": { "model": "oauthApplication", "field": "clientId", "onDelete": "cascade" }, "index": true }, "userId": { "type": "string", "references": { "model": "user", "field": "id", "onDelete": "cascade" }, "index": true }, "scopes": { "type": "string" }, "createdAt": { "type": "date" }, "updatedAt": { "type": "date" }, "consentGiven": { "type": "boolean" } } } }, "options": { "codeExpiresIn": 600, "defaultScope": "openid", "accessTokenExpiresIn": 3600, "refreshTokenExpiresIn": 604800, "allowPlainCodeChallengeMethod": true, "storeClientSecret": "[REDACTED]", "loginPage": "/sign-in", "trustedClients": [ { "clientId": "[REDACTED]", "name": "ALTERED for Raycast", "type": "public", "clientSecret": "[REDACTED]", "icon": "icon.png", "redirectUrls": [ "https://raycast.com/redirect?packageName=Extension" ], "disabled": false, "skipConsent": true, "metadata": { "platform": "raycast" } } ], "scopes": [ "openid", "profile", "email", "offline_access" ] } } }, { "name": "oidc-polyfill", "config": { "id": "oidc-polyfill", "endpoints": {} } } ], "advanced": { "database": {} } } } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" import { drizzleAdapter } from "better-auth/adapters/drizzle" import { oidcProvider } from "better-auth/plugins" import { db } from "~/server/data/store" export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg", usePlural: true }), plugins: [ oidcProvider({ loginPage: "/sign-in", trustedClients: [ { clientId: "altered-launcher", name: "ALTERED for Raycast", type: "public", clientSecret: "placeholder-secret", redirectUrls: ["https://raycast.com/redirect?packageName=Extension"], disabled: false, skipConsent: true, metadata: { platform: "raycast" } } ] }) ] }) ``` ### Additional context **Design gap identified:** - Trusted clients are validated from config (in-memory) but not persisted to database - Token creation requires database records for FK constraints - This creates a mismatch between validation (config-based) and persistence (database-based) **Suggested solution:** Add a hook or automatic sync mechanism that ensures trusted clients exist in the database before token creation. This could be: - An `onTokenCreate` hook that checks and creates the client if missing - An automatic sync on plugin initialization - A configuration option to auto-sync trusted clients to database **Reference:** - BetterAuth OIDC plugin source: https://github.com/better-auth/better-auth/blob/5f89bfa076dbb71959a2582b43d13e49dddf0856/packages/better-auth/src/plugins/oidc-provider/index.ts - Token endpoint implementation: lines ~600-720 - `getClient()` function: reads from `trustedClients` array - `adapter.create()` for tokens: line ~700-720, requires FK constraint to be satisfied
GiteaMirror added the plugin label 2026-03-13 09:59:19 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Dec 9, 2025):

It appears that the issue stems from the OIDC plugin validating trusted clients from your in‑memory configuration while the access token creation process later requires a matching client record in the database. The error arises because the FK constraint in your oauth_access_tokens table forces the client to exist in oauth_applications, but trusted clients aren’t automatically persisted with the current implementation [1].

A couple of options to resolve this include:
• Automatically synchronizing your in‑memory trusted clients into the oauth_applications table on startup (or during token creation via an on‑token‑create hook).
• Or, as a workaround, manually inserting the trusted client data into the oauth_applications table to satisfy the FK constraint.

Have you already considered one of these approaches in your setup? Tag @better-auth-agent if you would like an answer from the Agent.

If you need more help, tag @better-auth-agent in a comment so I can respond.

DiagramDiscordGitHub

Diagram Join Star

@better-auth-agent[bot] commented on GitHub (Dec 9, 2025): It appears that the issue stems from the OIDC plugin validating trusted clients from your in‑memory configuration while the access token creation process later requires a matching client record in the database. The error arises because the FK constraint in your oauth_access_tokens table forces the client to exist in oauth_applications, but trusted clients aren’t automatically persisted with the current implementation [[1]](https://github.com/better-auth/better-auth/issues/1183). A couple of options to resolve this include: • Automatically synchronizing your in‑memory trusted clients into the oauth_applications table on startup (or during token creation via an on‑token‑create hook). • Or, as a workaround, manually inserting the trusted client data into the oauth_applications table to satisfy the FK constraint. Have you already considered one of these approaches in your setup? Tag @better-auth-agent if you would like an answer from the Agent. _If you need more help, tag @better-auth-agent in a comment so I can respond._ <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/skyvern_ultra_detailed_interactive.html) • [Discord](https://discord.gg/fG2XXEuQX3) • [GitHub](https://github.com/Skyvern-AI/Skyvern) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/skyvern_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/fG2XXEuQX3) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/Skyvern-AI/Skyvern)
Author
Owner

@dosubot[bot] commented on GitHub (Dec 9, 2025):

Hi @inducingchaos! I'm Dosu and I’m helping the better-auth team.

This is a known design gap in BetterAuth's OIDC provider: trustedClients configured in-memory are not automatically inserted into the database, but the token and consent tables enforce a foreign key constraint requiring the client to exist in the oauthApplication table. This causes FK constraint errors when issuing tokens or consents for trusted clients that aren't present in the DB [#4749, #5040, #5468].

The recommended workaround is to manually insert your trusted clients into the oauthApplication table, matching the config fields (clientId, clientSecret, name, redirectURLs, type, etc). You can do this via direct SQL, your ORM (e.g., Prisma), or by enabling dynamic client registration and using the /oauth2/register endpoint. Once the client exists in the table, token and consent flows will work as expected without FK errors [#5040].

There is no built-in auto-sync or hook for this in the current release. An open PR proposes improvements to trustedClients logic, but it's not yet merged or released [PR #4163].

Example (Prisma):

await prisma.oauthApplication.create({
  data: {
    clientId: "my-native-app",
    clientSecret: "placeholder-secret",
    name: "My Native App",
    type: "public",
    redirectURLs: "myapp://callback",
    disabled: false,
    metadata: JSON.stringify({}),
    createdAt: new Date(),
    updatedAt: new Date(),
  }
});

Until an official solution lands, the best practice is to ensure all trusted clients are present in the oauthApplication table to maintain referential integrity. Removing the FK constraint is not recommended unless you fully understand the implications for your data model [#2876].

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

@dosubot[bot] commented on GitHub (Dec 9, 2025): <!-- Greeting --> Hi @inducingchaos! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a known design gap in BetterAuth's OIDC provider: trustedClients configured in-memory are not automatically inserted into the database, but the token and consent tables enforce a foreign key constraint requiring the client to exist in the oauthApplication table. This causes FK constraint errors when issuing tokens or consents for trusted clients that aren't present in the DB [[#4749](https://github.com/better-auth/better-auth/issues/4749), [#5040](https://github.com/better-auth/better-auth/issues/5040), [#5468](https://github.com/better-auth/better-auth/issues/5468)]. The recommended workaround is to manually insert your trusted clients into the oauthApplication table, matching the config fields (clientId, clientSecret, name, redirectURLs, type, etc). You can do this via direct SQL, your ORM (e.g., Prisma), or by enabling dynamic client registration and using the `/oauth2/register` endpoint. Once the client exists in the table, token and consent flows will work as expected without FK errors [[#5040](https://github.com/better-auth/better-auth/issues/5040)]. There is no built-in auto-sync or hook for this in the current release. An open PR proposes improvements to trustedClients logic, but it's not yet merged or released [[PR #4163](https://github.com/better-auth/better-auth/pull/4163)]. Example (Prisma): ```typescript await prisma.oauthApplication.create({ data: { clientId: "my-native-app", clientSecret: "placeholder-secret", name: "My Native App", type: "public", redirectURLs: "myapp://callback", disabled: false, metadata: JSON.stringify({}), createdAt: new Date(), updatedAt: new Date(), } }); ``` Until an official solution lands, the best practice is to ensure all trusted clients are present in the oauthApplication table to maintain referential integrity. Removing the FK constraint is not recommended unless you fully understand the implications for your data model [[#2876](https://github.com/better-auth/better-auth/issues/2876)]. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/2652b70c-b331-44b4-808e-c82e601e292e?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/2652b70c-b331-44b4-808e-c82e601e292e?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/2652b70c-b331-44b4-808e-c82e601e292e?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/2652b70c-b331-44b4-808e-c82e601e292e?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/2652b70c-b331-44b4-808e-c82e601e292e?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/2652b70c-b331-44b4-808e-c82e601e292e?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/2652b70c-b331-44b4-808e-c82e601e292e?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=This%20is%20a%20known%20design%20gap%20in%20BetterAuth%27s%20OIDC%20provider%3A%20trustedClients%20configured%20in-memory%20are%20not%20automatically%20inserted%20into%20the%20database%2C%20but%20the%20token%20and%20consent%20tables%20enforce%20a%20foreign%20key%20constraint%20requiring%20the%20client%20to%20exist%20in%20the%20oauthApplication%20table.%20This%20causes%20FK%20constraint%20errors%20when%20issuing%20tokens%20or%20consents%20for%20trusted%20clients%20that%20aren%27t%20present%20in%20the%20DB%20%5B%5B%234749%5D%28https%3A//github.com/better-auth/better-auth/issues/4749%29%2C%20%5B%235040%5D%28https%3A//github.com/better-auth/better-auth/issues/5040%29%2C%20%5B%235468%5D%28https%3A//github.com/better-auth/better-auth/issues/5468%29%5D.%0A%0AThe%20recommended%20workaround%20is%20to%20manually%20insert%20your%20trusted%20clients%20into%20the%20oauthApplication%20table%2C%20matching%20the%20config%20fields%20%28clientId%2C%20clientSecret%2C%20name%2C%20redirectURLs%2C%20type%2C%20etc%29.%20You%20can%20do%20this%20via%20direct%20SQL%2C%20your%20ORM%20%28e.g.%2C%20Prisma%29%2C%20or%20by%20enabling%20dynamic%20client%20registration%20and%20using%20the%20%60/oauth2/register%60%20endpoint.%20Once%20the%20client%20exists%20in%20the%20table%2C%20token%20and%20consent%20flows%20will%20work%20as%20expected%20without%20FK%20errors%20%5B%5B%235040%5D%28https%3A//github.com/better-auth/better-auth/issues/5040%29%5D.%0A%0AThere%20is%20no%20built-in%20auto-sync%20or%20hook%20for%20this%20in%20the%20current%20release.%20An%20open%20PR%20proposes%20improvements%20to%20trustedClients%20logic%2C%20but%20it%27s%20not%20yet%20merged%20or%20released%20%5B%5BPR%20%234163%5D%28https%3A//github.com/better-auth/better-auth/pull/4163%29%5D.%0A%0AExample%20%28Prisma%29%3A%0A%60%60%60typescript%0Aawait%20prisma.oauthApplication.create%28%7B%0A%20%20data%3A%20%7B%0A%20%20%20%20clientId%3A%20%22my-native-app%22%2C%0A%20%20%20%20clientSecret%3A%20%22placeholder-secret%22%2C%0A%20%20%20%20name%3A%20%22My%20Native%20App%22%2C%0A%20%20%20%20type%3A%20%22public%22%2C%0A%20%20%20%20redirectURLs%3A%20%22myapp%3A//callback%22%2C%0A%20%20%20%20disabled%3A%20false%2C%0A%20%20%20%20metadata%3A%20JSON.stringify%28%7B%7D%29%2C%0A%20%20%20%20createdAt%3A%20new%20Date%28%29%2C%0A%20%20%20%20updatedAt%3A%20new%20Date%28%29%2C%0A%20%20%7D%0A%7D%29%3B%0A%60%60%60%0A%0AUntil%20an%20official%20solution%20lands%2C%20the%20best%20practice%20is%20to%20ensure%20all%20trusted%20clients%20are%20present%20in%20the%20oauthApplication%20table%20to%20maintain%20referential%20integrity.%20Removing%20the%20FK%20constraint%20is%20not%20recommended%20unless%20you%20fully%20understand%20the%20implications%20for%20your%20data%20model%20%5B%5B%232876%5D%28https%3A//github.com/better-auth/better-auth/issues/2876%29%5D.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/6649)
Author
Owner

@matthiastjong commented on GitHub (Dec 11, 2025):

Subscribing to this. I wanted to create a new OIDC flow with the latest better auth but it just does not work out of the box because of this.

@matthiastjong commented on GitHub (Dec 11, 2025): Subscribing to this. I wanted to create a new OIDC flow with the latest better auth but it just does not work out of the box because of this.
Author
Owner

@inducingchaos commented on GitHub (Dec 16, 2025):

Subscribing to this. I wanted to create a new OIDC flow with the latest better auth but it just does not work out of the box because of this.

Hey, in case you missed it I'm currently using a workaround for this and haven't run into any errors. Simply disable the FK reference on the oauth_access_tokens table like this:

export const oauthAccessTokens = pgTable("oauth_access_tokens", {
    id: varchar().primaryKey().notNull().$defaultFn(nanoid),
    accessToken: varchar().notNull(),
    refreshToken: varchar().notNull(),
    accessTokenExpiresAt: timestamp().notNull(),
    refreshTokenExpiresAt: timestamp().notNull(),
    /**
     * @remarks The recommended FK to `oauthApplications.clientId` errors when using trusted OIDC clients without having them as a database entry - disabling the reference works for now.
     *
     * @see https://github.com/better-auth/better-auth/issues/6649
     */
    clientId: varchar().notNull(),
    // .references(() => oauthApplications.clientId),
    userId: varchar()
        .notNull()
        .references(() => users.id),
    scopes: varchar().notNull(),
    createdAt: timestamp().notNull().defaultNow(),
    updatedAt: timestamp()
        .notNull()
        .defaultNow()
        .$onUpdateFn(() => new Date())
})

Note that when using Drizzle relations, I don't think foreign keys are truly necessary for most scenarios - see docs: https://orm.drizzle.team/docs/relations-v2#foreign-keys

And this is what the relations code looks like. Using version 1.0.0-beta.2-86f844e:


export const relations = defineRelations(tables, r => ({
    users: {
        sessions: r.many.sessions({
            from: r.users.id,
            to: r.sessions.userId
        }),
        accounts: r.many.accounts({
            from: r.users.id,
            to: r.accounts.userId
        }),
        oauthApplications: r.many.oauthApplications({
            from: r.users.id,
            to: r.oauthApplications.userId
        }),
        oauthAccessTokens: r.many.oauthAccessTokens({
            from: r.users.id,
            to: r.oauthAccessTokens.userId
        }),
        oauthConsents: r.many.oauthConsents({
            from: r.users.id,
            to: r.oauthConsents.userId
        })
    },

    oauthApplications: {
        oauthAccessTokens: r.many.oauthAccessTokens({
            from: r.oauthApplications.clientId,
            to: r.oauthAccessTokens.clientId
        }),
        oauthConsents: r.many.oauthConsents({
            from: r.oauthApplications.clientId,
            to: r.oauthConsents.clientId
        })
    }
}))

Hopefully this helps. The OIDC flow is really nice when you get it working.

@inducingchaos commented on GitHub (Dec 16, 2025): > Subscribing to this. I wanted to create a new OIDC flow with the latest better auth but it just does not work out of the box because of this. Hey, in case you missed it I'm currently using a workaround for this and haven't run into any errors. Simply disable the FK reference on the `oauth_access_tokens` table like this: ```ts export const oauthAccessTokens = pgTable("oauth_access_tokens", { id: varchar().primaryKey().notNull().$defaultFn(nanoid), accessToken: varchar().notNull(), refreshToken: varchar().notNull(), accessTokenExpiresAt: timestamp().notNull(), refreshTokenExpiresAt: timestamp().notNull(), /** * @remarks The recommended FK to `oauthApplications.clientId` errors when using trusted OIDC clients without having them as a database entry - disabling the reference works for now. * * @see https://github.com/better-auth/better-auth/issues/6649 */ clientId: varchar().notNull(), // .references(() => oauthApplications.clientId), userId: varchar() .notNull() .references(() => users.id), scopes: varchar().notNull(), createdAt: timestamp().notNull().defaultNow(), updatedAt: timestamp() .notNull() .defaultNow() .$onUpdateFn(() => new Date()) }) ``` Note that when using Drizzle relations, I don't think foreign keys are truly necessary for most scenarios - see docs: https://orm.drizzle.team/docs/relations-v2#foreign-keys And this is what the relations code looks like. Using version `1.0.0-beta.2-86f844e`: ```ts export const relations = defineRelations(tables, r => ({ users: { sessions: r.many.sessions({ from: r.users.id, to: r.sessions.userId }), accounts: r.many.accounts({ from: r.users.id, to: r.accounts.userId }), oauthApplications: r.many.oauthApplications({ from: r.users.id, to: r.oauthApplications.userId }), oauthAccessTokens: r.many.oauthAccessTokens({ from: r.users.id, to: r.oauthAccessTokens.userId }), oauthConsents: r.many.oauthConsents({ from: r.users.id, to: r.oauthConsents.userId }) }, oauthApplications: { oauthAccessTokens: r.many.oauthAccessTokens({ from: r.oauthApplications.clientId, to: r.oauthAccessTokens.clientId }), oauthConsents: r.many.oauthConsents({ from: r.oauthApplications.clientId, to: r.oauthConsents.clientId }) } })) ``` Hopefully this helps. The OIDC flow is really nice when you get it working.
Author
Owner

@dvanmali commented on GitHub (Dec 24, 2025):

Hi all, we released the new OAuth Provider Plugin which should fix this issue, trusted clients are now required on the database, cacheable with cachedTrustedClients. Feel free to let us know how it works :)

@dvanmali commented on GitHub (Dec 24, 2025): Hi all, we released the new [OAuth Provider Plugin](https://www.better-auth.com/docs/plugins/oauth-provider) which should fix this issue, trusted clients are now required on the database, cacheable with `cachedTrustedClients`. Feel free to let us know how it works :)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2496