oauthProvider won't work if baseURL is not configured #3032

Open
opened 2026-03-13 10:35:25 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @j-fdion on GitHub (Mar 12, 2026).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

use this config:

//auth.ts
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { betterAuth } from 'better-auth'
import { tanstackStartCookies } from 'better-auth/tanstack-start'

import { db } from '#/db'
import { accessControl, roles } from '#/lib/roles'
import { sendMagicLink } from '#/integrations/better-auth/send-magic-link'
import { admin, magicLink, jwt } from 'better-auth/plugins'
import { oauthProvider } from "@better-auth/oauth-provider"; 

const allowedHosts = ['localhost:3000', '127.0.0.1:3000', '*.railway.app']

export const auth = betterAuth({
  baseURL: {
      allowedHosts,
      protocol: process.env.NODE_ENV === "development" ? "http" : "https",
  },
  advanced: {
    crossSubDomainCookies: {
        enabled: true,
    },
  },
  database: drizzleAdapter(db, {
    provider: 'pg',
  }),
  plugins: [
    jwt(),
    oauthProvider({ 
      loginPage: "/login", 
      consentPage: "/consent", 
      
      validAudiences: ['http://localhost:3000/api'],
    }) ,
    }),
    admin({
      ac: accessControl ,
      roles: roles,
      defaultRole: 'user',
      adminRoles: ['admin', 'superadmin'],
    }),
    tanstackStartCookies(),
  ],
})
`

Current vs. Expected behavior

Current:

jeanf@jfdion:~/work/gcf-paie-app$ npx auth generate
{ issuer: '' }
? The file ./auth-schema.ts already exists. Do you want to overwrite the schema to the file? › (y/N)node:internal/url:819
      href = bindingUrl.parse(input, base, true);
                        ^

TypeError: Invalid URL
    at new URL (node:internal/url:819:25)
    at Object.init (file:///home/jeanf/work/gcf-paie-app/node_modules/.pnpm/@better-auth+oauth-provider@1.5.5_@better-auth+core@1.5.5_@better-auth+utils@0.3.1_@bet_64bc94843570df593e7ac9828ccbf9c8/node_modules/@better-auth/oauth-provider/dist/index.mjs:2724:24)
    at runPluginInit (file:///home/jeanf/work/gcf-paie-app/node_modules/.pnpm/better-auth@1.5.5_@tanstack+react-start@1.166.8_crossws@0.4.4_srvx@0.11.9__react-dom@19_4a5e7525771c9db50800ba64301f2b24/node_modules/better-auth/dist/context/helpers.mjs:15:30)
    at createAuthContext (file:///home/jeanf/work/gcf-paie-app/node_modules/.pnpm/better-auth@1.5.5_@tanstack+react-start@1.166.8_crossws@0.4.4_srvx@0.11.9__react-dom@19_4a5e7525771c9db50800ba64301f2b24/node_modules/better-auth/dist/context/create-context.mjs:222:24)
    at async init (file:///home/jeanf/work/gcf-paie-app/node_modules/.pnpm/better-auth@1.5.5_@tanstack+react-start@1.166.8_crossws@0.4.4_srvx@0.11.9__react-dom@19_4a5e7525771c9db50800ba64301f2b24/node_modules/better-auth/dist/context/init.mjs:11:14) {
  code: 'ERR_INVALID_URL',
  input: ''
}

Node.js v24.14.0

Expected:

Properly executing

What version of Better Auth are you using?

1.5.5

System info

"system": {
    "platform": "linux",
    "arch": "x64",
    "version": "#1 SMP PREEMPT_DYNAMIC Thu Jun  5 18:30:46 UTC 2025",
    "release": "6.6.87.2-microsoft-standard-WSL2",
    "cpuCount": 16,
    "cpuModel": "AMD Ryzen 7 9800X3D 8-Core Processor",
    "totalMemory": "15.15 GB",
    "freeMemory": "9.75 GB"
  },
  "node": {
    "version": "v24.14.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.9.0"
  },
  "frameworks": [
    {
      "name": "react",
      "version": "^19.2.4"
    }
  ],
  "databases": [
    {
      "name": "pg",
      "version": "^8.20.0"
    },
    {
      "name": "drizzle",
      "version": "^0.45.1"
    }
  ],
  "betterAuth": {
    "version": "^1.5.5",
    "config": {
      "baseURL": {
        "allowedHosts": [
          "localhost:3000",
          "127.0.0.1:3000",
          "*.railway.app"
        ],
        "protocol": "https"
      },
      "advanced": {
        "crossSubDomainCookies": {
          "enabled": true
        }
      },
      "user": {
        "additionalFields": {
          "matricule": {
            "type": "string",
            "required": false,
            "nullable": true,
            "input": false,
            "returned": true
          },
          "approvedAt": {
            "type": "date",
            "required": false,
            "nullable": true,
            "input": false,
            "returned": true
          }
        }
      },
      "plugins": [
        {
          "name": "jwt",
          "config": {
            "id": "jwt",
            "endpoints": {},
            "hooks": {
              "after": [
                {}
              ]
            },
            "schema": {
              "jwks": {
                "fields": {
                  "publicKey": {
                    "type": "string",
                    "required": true
                  },
                  "privateKey": {
                    "type": "string",
                    "required": true
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true
                  },
                  "expiresAt": {
                    "type": "date",
                    "required": false
                  }
                }
              }
            }
          }
        },
        {
          "name": "oauth-provider",
          "config": {
            "id": "oauth-provider",
            "options": {
              "codeExpiresIn": 600,
              "accessTokenExpiresIn": 3600,
              "m2mAccessTokenExpiresIn": 3600,
              "refreshTokenExpiresIn": 2592000,
              "allowUnauthenticatedClientRegistration": false,
              "allowDynamicClientRegistration": false,
              "disableJwtPlugin": false,
              "storeClientSecret": "[REDACTED]",
              "storeTokens": "hashed",
              "grantTypes": [
                "authorization_code",
                "client_credentials",
                "refresh_token"
              ],
              "loginPage": "/login",
              "consentPage": "/consent",
              "validAudiences": [
                "http://localhost:3000/api"
              ],
              "scopes": [
                "openid",
                "profile",
                "email",
                "offline_access"
              ],
              "claims": [
                "sub",
                "iss",
                "aud",
                "exp",
                "iat",
                "sid",
                "scope",
                "azp",
                "email",
                "email_verified",
                "name",
                "picture",
                "family_name",
                "given_name"
              ]
            },
            "hooks": {
              "before": [
                {}
              ],
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "schema": {
              "oauthClient": {
                "modelName": "oauthClient",
                "fields": {
                  "clientId": {
                    "type": "string",
                    "unique": true,
                    "required": true
                  },
                  "clientSecret": {
                    "type": "string",
                    "required": false
                  },
                  "disabled": {
                    "type": "boolean",
                    "defaultValue": false,
                    "required": false
                  },
                  "skipConsent": {
                    "type": "boolean",
                    "required": false
                  },
                  "enableEndSession": {
                    "type": "boolean",
                    "required": false
                  },
                  "subjectType": {
                    "type": "string",
                    "required": false
                  },
                  "scopes": {
                    "type": "string[]",
                    "required": false
                  },
                  "userId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "user",
                      "field": "id"
                    }
                  },
                  "createdAt": {
                    "type": "date",
                    "required": false
                  },
                  "updatedAt": {
                    "type": "date",
                    "required": false
                  },
                  "name": {
                    "type": "string",
                    "required": false
                  },
                  "uri": {
                    "type": "string",
                    "required": false
                  },
                  "icon": {
                    "type": "string",
                    "required": false
                  },
                  "contacts": {
                    "type": "string[]",
                    "required": false
                  },
                  "tos": {
                    "type": "string",
                    "required": false
                  },
                  "policy": {
                    "type": "string",
                    "required": false
                  },
                  "softwareId": {
                    "type": "string",
                    "required": false
                  },
                  "softwareVersion": {
                    "type": "string",
                    "required": false
                  },
                  "softwareStatement": {
                    "type": "string",
                    "required": false
                  },
                  "redirectUris": {
                    "type": "string[]",
                    "required": true
                  },
                  "postLogoutRedirectUris": {
                    "type": "string[]",
                    "required": false
                  },
                  "tokenEndpointAuthMethod": {
                    "type": "string",
                    "required": false
                  },
                  "grantTypes": {
                    "type": "string[]",
                    "required": false
                  },
                  "responseTypes": {
                    "type": "string[]",
                    "required": false
                  },
                  "public": {
                    "type": "boolean",
                    "required": false
                  },
                  "type": {
                    "type": "string",
                    "required": false
                  },
                  "requirePKCE": {
                    "type": "boolean",
                    "required": false
                  },
                  "referenceId": {
                    "type": "string",
                    "required": false
                  },
                  "metadata": {
                    "type": "json",
                    "required": false
                  }
                }
              },
              "oauthRefreshToken": {
                "fields": {
                  "token": {
                    "type": "string",
                    "required": true
                  },
                  "clientId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "oauthClient",
                      "field": "clientId"
                    }
                  },
                  "sessionId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "session",
                      "field": "id",
                      "onDelete": "set null"
                    }
                  },
                  "userId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "user",
                      "field": "id"
                    }
                  },
                  "referenceId": {
                    "type": "string",
                    "required": false
                  },
                  "expiresAt": {
                    "type": "date"
                  },
                  "createdAt": {
                    "type": "date"
                  },
                  "revoked": {
                    "type": "date",
                    "required": false
                  },
                  "authTime": {
                    "type": "date",
                    "required": false
                  },
                  "scopes": {
                    "type": "string[]",
                    "required": true
                  }
                }
              },
              "oauthAccessToken": {
                "modelName": "oauthAccessToken",
                "fields": {
                  "token": {
                    "type": "string",
                    "unique": true
                  },
                  "clientId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "oauthClient",
                      "field": "clientId"
                    }
                  },
                  "sessionId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "session",
                      "field": "id",
                      "onDelete": "set null"
                    }
                  },
                  "userId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "user",
                      "field": "id"
                    }
                  },
                  "referenceId": {
                    "type": "string",
                    "required": false
                  },
                  "refreshId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "oauthRefreshToken",
                      "field": "id"
                    }
                  },
                  "expiresAt": {
                    "type": "date"
                  },
                  "createdAt": {
                    "type": "date"
                  },
                  "scopes": {
                    "type": "string[]",
                    "required": true
                  }
                }
              },
              "oauthConsent": {
                "modelName": "oauthConsent",
                "fields": {
                  "clientId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "oauthClient",
                      "field": "clientId"
                    }
                  },
                  "userId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "user",
                      "field": "id"
                    }
                  },
                  "referenceId": {
                    "type": "string",
                    "required": false
                  },
                  "scopes": {
                    "type": "string[]",
                    "required": true
                  },
                  "createdAt": {
                    "type": "date"
                  },
                  "updatedAt": {
                    "type": "date"
                  }
                }
              }
            },
            "rateLimit": [
              {
                "window": 60,
                "max": 20
              },
              {
                "window": 60,
                "max": 30
              },
              {
                "window": 60,
                "max": 100
              },
              {
                "window": 60,
                "max": 30
              },
              {
                "window": 60,
                "max": 5
              },
              {
                "window": 60,
                "max": 60
              }
            ]
          }
        },
        {
          "name": "magic-link",
          "config": {
            "id": "magic-link",
            "endpoints": {},
            "rateLimit": [
              {
                "window": 60,
                "max": 5
              }
            ],
            "options": {}
          }
        },
        {
          "name": "admin",
          "config": {
            "id": "admin",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "$ERROR_CODES": {
              "FAILED_TO_CREATE_USER": {
                "code": "FAILED_TO_CREATE_USER",
                "message": "Failed to create user"
              },
              "USER_ALREADY_EXISTS": {
                "code": "USER_ALREADY_EXISTS",
                "message": "User already exists."
              },
              "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": {
                "code": "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL",
                "message": "User already exists. Use another email."
              },
              "YOU_CANNOT_BAN_YOURSELF": {
                "code": "YOU_CANNOT_BAN_YOURSELF",
                "message": "You cannot ban yourself"
              },
              "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE",
                "message": "You are not allowed to change users role"
              },
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS",
                "message": "You are not allowed to create users"
              },
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS",
                "message": "You are not allowed to list users"
              },
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS",
                "message": "You are not allowed to list users sessions"
              },
              "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS",
                "message": "You are not allowed to ban users"
              },
              "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS",
                "message": "You are not allowed to impersonate users"
              },
              "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS",
                "message": "You are not allowed to revoke users sessions"
              },
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS",
                "message": "You are not allowed to delete users"
              },
              "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD",
                "message": "You are not allowed to set users password"
              },
              "BANNED_USER": {
                "code": "BANNED_USER",
                "message": "You have been banned from this application"
              },
              "YOU_ARE_NOT_ALLOWED_TO_GET_USER": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_GET_USER",
                "message": "You are not allowed to get user"
              },
              "NO_DATA_TO_UPDATE": {
                "code": "NO_DATA_TO_UPDATE",
                "message": "No data to update"
              },
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS",
                "message": "You are not allowed to update users"
              },
              "YOU_CANNOT_REMOVE_YOURSELF": {
                "code": "YOU_CANNOT_REMOVE_YOURSELF",
                "message": "You cannot remove yourself"
              },
              "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE",
                "message": "You are not allowed to set a non-existent role value"
              },
              "YOU_CANNOT_IMPERSONATE_ADMINS": {
                "code": "YOU_CANNOT_IMPERSONATE_ADMINS",
                "message": "You cannot impersonate admins"
              },
              "INVALID_ROLE_TYPE": {
                "code": "INVALID_ROLE_TYPE",
                "message": "Invalid role type"
              }
            },
            "schema": {
              "user": {
                "fields": {
                  "role": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banned": {
                    "type": "boolean",
                    "defaultValue": false,
                    "required": false,
                    "input": false
                  },
                  "banReason": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banExpires": {
                    "type": "date",
                    "required": false,
                    "input": false
                  }
                }
              },
              "session": {
                "fields": {
                  "impersonatedBy": {
                    "type": "string",
                    "required": false
                  }
                }
              }
            },
            "options": {
              "ac": {
                "statements": {
                  "user": [
                    "create",
                    "list",
                    "set-role",
                    "ban",
                    "impersonate",
                    "impersonate-admins",
                    "delete",
                    "set-password",
                    "get",
                    "update"
                  ],
                  "session": [
                    "list",
                    "revoke",
                    "delete"
                  ],
                  "project": [
                    "read",
                    "manage",
                    "delete"
                  ],
                  "validation": [
                    "review",
                    "finalize",
                    "report"
                  ],
                  "app": [
                    "admin-access",
                    "bypass-approval"
                  ],
                  "role": [
                    "manage-sensitive"
                  ]
                }
              },
              "roles": {
                "user": {
                  "statements": {
                    "project": [
                      "read"
                    ]
                  }
                },
                "supervisor": {
                  "statements": {
                    "project": [
                      "read",
                      "manage"
                    ],
                    "validation": [
                      "review",
                      "report"
                    ]
                  }
                },
                "admin": {
                  "statements": {
                    "user": [
                      "create",
                      "list",
                      "set-role",
                      "ban",
                      "impersonate",
                      "delete",
                      "set-password",
                      "get",
                      "update"
                    ],
                    "session": [
                      "list",
                      "revoke",
                      "delete"
                    ],
                    "project": [
                      "read",
                      "manage",
                      "delete"
                    ],
                    "validation": [
                      "review",
                      "finalize",
                      "report"
                    ],
                    "app": [
                      "admin-access",
                      "bypass-approval"
                    ]
                  }
                },
                "superadmin": {
                  "statements": {
                    "user": [
                      "create",
                      "list",
                      "set-role",
                      "ban",
                      "impersonate",
                      "delete",
                      "set-password",
                      "get",
                      "update",
                      "impersonate-admins"
                    ],
                    "session": [
                      "list",
                      "revoke",
                      "delete"
                    ],
                    "project": [
                      "read",
                      "manage",
                      "delete"
                    ],
                    "validation": [
                      "review",
                      "finalize",
                      "report"
                    ],
                    "app": [
                      "admin-access",
                      "bypass-approval"
                    ],
                    "role": [
                      "manage-sensitive"
                    ]
                  }
                }
              },
              "defaultRole": "user",
              "adminRoles": [
                "admin",
                "superadmin"
              ]
            }
          }
        },
        {
          "name": "tanstack-start-cookies",
          "config": {
            "id": "tanstack-start-cookies",
            "hooks": {
              "after": [
                {}
              ]
            }
          }
        }
      ]
    }
  }
}
node:internal/url:819
      href = bindingUrl.parse(input, base, true);
                        ^

TypeError: Invalid URL
    at new URL (node:internal/url:819:25)
    at Object.init (file:///home/jeanf/work/gcf-paie-app/node_modules/.pnpm/@better-auth+oauth-provider@1.5.5_@better-auth+core@1.5.5_@better-auth+utils@0.3.1_@bet_64bc94843570df593e7ac9828ccbf9c8/node_modules/@better-auth/oauth-provider/dist/index.mjs:2724:24)
    at runPluginInit (file:///home/jeanf/work/gcf-paie-app/node_modules/.pnpm/better-auth@1.5.5_@tanstack+react-start@1.166.8_crossws@0.4.4_srvx@0.11.9__react-dom@19_4a5e7525771c9db50800ba64301f2b24/node_modules/better-auth/dist/context/helpers.mjs:15:30)
    at createAuthContext (file:///home/jeanf/work/gcf-paie-app/node_modules/.pnpm/better-auth@1.5.5_@tanstack+react-start@1.166.8_crossws@0.4.4_srvx@0.11.9__react-dom@19_4a5e7525771c9db50800ba64301f2b24/node_modules/better-auth/dist/context/create-context.mjs:222:24)
    at async init (file:///home/jeanf/work/gcf-paie-app/node_modules/.pnpm/better-auth@1.5.5_@tanstack+react-start@1.166.8_crossws@0.4.4_srvx@0.11.9__react-dom@19_4a5e7525771c9db50800ba64301f2b24/node_modules/better-auth/dist/context/init.mjs:11:14) {
  code: 'ERR_INVALID_URL',
  input: ''
}


Even this output is bugged.

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

Backend

Auth config (if applicable)

import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { betterAuth } from 'better-auth'
import { tanstackStartCookies } from 'better-auth/tanstack-start'

import { db } from '#/db'
import { accessControl, roles } from '#/lib/roles'
import { sendMagicLink } from '#/integrations/better-auth/send-magic-link'
import { admin, magicLink, jwt } from 'better-auth/plugins'
import { oauthProvider } from "@better-auth/oauth-provider"; 
export const auth = betterAuth({
  baseURL: {
      allowedHosts,
      protocol: process.env.NODE_ENV === "development" ? "http" : "https",
  },
  advanced: {
    crossSubDomainCookies: {
        enabled: true,
    },
  },
  database: drizzleAdapter(db, {
    provider: 'pg',
  }),
  plugins: [
    jwt(),
    oauthProvider({ 
      loginPage: "/login", 
      consentPage: "/consent", 
      
      validAudiences: ['http://localhost:3000/api'],
    }) ,
    }),
    admin({
      ac: accessControl ,
      roles: roles,
      defaultRole: 'user',
      adminRoles: ['admin', 'superadmin'],
    }),
    tanstackStartCookies(),
  ],
})

Additional context

When baseURL is using the format:

baseURL: {
allowedHosts,
protocol: process.env.NODE_ENV === "development" ? "http" : "https",
},

The code crashes

You need to use the old format (just a string, i.e "http://localhost:3000"

Originally created by @j-fdion on GitHub (Mar 12, 2026). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce use this config: ``` //auth.ts import { drizzleAdapter } from 'better-auth/adapters/drizzle' import { betterAuth } from 'better-auth' import { tanstackStartCookies } from 'better-auth/tanstack-start' import { db } from '#/db' import { accessControl, roles } from '#/lib/roles' import { sendMagicLink } from '#/integrations/better-auth/send-magic-link' import { admin, magicLink, jwt } from 'better-auth/plugins' import { oauthProvider } from "@better-auth/oauth-provider"; const allowedHosts = ['localhost:3000', '127.0.0.1:3000', '*.railway.app'] export const auth = betterAuth({ baseURL: { allowedHosts, protocol: process.env.NODE_ENV === "development" ? "http" : "https", }, advanced: { crossSubDomainCookies: { enabled: true, }, }, database: drizzleAdapter(db, { provider: 'pg', }), plugins: [ jwt(), oauthProvider({ loginPage: "/login", consentPage: "/consent", validAudiences: ['http://localhost:3000/api'], }) , }), admin({ ac: accessControl , roles: roles, defaultRole: 'user', adminRoles: ['admin', 'superadmin'], }), tanstackStartCookies(), ], }) ` ``` ### Current vs. Expected behavior Current: ``` jeanf@jfdion:~/work/gcf-paie-app$ npx auth generate { issuer: '' } ? The file ./auth-schema.ts already exists. Do you want to overwrite the schema to the file? › (y/N)node:internal/url:819 href = bindingUrl.parse(input, base, true); ^ TypeError: Invalid URL at new URL (node:internal/url:819:25) at Object.init (file:///home/jeanf/work/gcf-paie-app/node_modules/.pnpm/@better-auth+oauth-provider@1.5.5_@better-auth+core@1.5.5_@better-auth+utils@0.3.1_@bet_64bc94843570df593e7ac9828ccbf9c8/node_modules/@better-auth/oauth-provider/dist/index.mjs:2724:24) at runPluginInit (file:///home/jeanf/work/gcf-paie-app/node_modules/.pnpm/better-auth@1.5.5_@tanstack+react-start@1.166.8_crossws@0.4.4_srvx@0.11.9__react-dom@19_4a5e7525771c9db50800ba64301f2b24/node_modules/better-auth/dist/context/helpers.mjs:15:30) at createAuthContext (file:///home/jeanf/work/gcf-paie-app/node_modules/.pnpm/better-auth@1.5.5_@tanstack+react-start@1.166.8_crossws@0.4.4_srvx@0.11.9__react-dom@19_4a5e7525771c9db50800ba64301f2b24/node_modules/better-auth/dist/context/create-context.mjs:222:24) at async init (file:///home/jeanf/work/gcf-paie-app/node_modules/.pnpm/better-auth@1.5.5_@tanstack+react-start@1.166.8_crossws@0.4.4_srvx@0.11.9__react-dom@19_4a5e7525771c9db50800ba64301f2b24/node_modules/better-auth/dist/context/init.mjs:11:14) { code: 'ERR_INVALID_URL', input: '' } Node.js v24.14.0 ``` Expected: Properly executing ### What version of Better Auth are you using? 1.5.5 ### System info ```bash "system": { "platform": "linux", "arch": "x64", "version": "#1 SMP PREEMPT_DYNAMIC Thu Jun 5 18:30:46 UTC 2025", "release": "6.6.87.2-microsoft-standard-WSL2", "cpuCount": 16, "cpuModel": "AMD Ryzen 7 9800X3D 8-Core Processor", "totalMemory": "15.15 GB", "freeMemory": "9.75 GB" }, "node": { "version": "v24.14.0", "env": "development" }, "packageManager": { "name": "npm", "version": "11.9.0" }, "frameworks": [ { "name": "react", "version": "^19.2.4" } ], "databases": [ { "name": "pg", "version": "^8.20.0" }, { "name": "drizzle", "version": "^0.45.1" } ], "betterAuth": { "version": "^1.5.5", "config": { "baseURL": { "allowedHosts": [ "localhost:3000", "127.0.0.1:3000", "*.railway.app" ], "protocol": "https" }, "advanced": { "crossSubDomainCookies": { "enabled": true } }, "user": { "additionalFields": { "matricule": { "type": "string", "required": false, "nullable": true, "input": false, "returned": true }, "approvedAt": { "type": "date", "required": false, "nullable": true, "input": false, "returned": true } } }, "plugins": [ { "name": "jwt", "config": { "id": "jwt", "endpoints": {}, "hooks": { "after": [ {} ] }, "schema": { "jwks": { "fields": { "publicKey": { "type": "string", "required": true }, "privateKey": { "type": "string", "required": true }, "createdAt": { "type": "date", "required": true }, "expiresAt": { "type": "date", "required": false } } } } } }, { "name": "oauth-provider", "config": { "id": "oauth-provider", "options": { "codeExpiresIn": 600, "accessTokenExpiresIn": 3600, "m2mAccessTokenExpiresIn": 3600, "refreshTokenExpiresIn": 2592000, "allowUnauthenticatedClientRegistration": false, "allowDynamicClientRegistration": false, "disableJwtPlugin": false, "storeClientSecret": "[REDACTED]", "storeTokens": "hashed", "grantTypes": [ "authorization_code", "client_credentials", "refresh_token" ], "loginPage": "/login", "consentPage": "/consent", "validAudiences": [ "http://localhost:3000/api" ], "scopes": [ "openid", "profile", "email", "offline_access" ], "claims": [ "sub", "iss", "aud", "exp", "iat", "sid", "scope", "azp", "email", "email_verified", "name", "picture", "family_name", "given_name" ] }, "hooks": { "before": [ {} ], "after": [ {} ] }, "endpoints": {}, "schema": { "oauthClient": { "modelName": "oauthClient", "fields": { "clientId": { "type": "string", "unique": true, "required": true }, "clientSecret": { "type": "string", "required": false }, "disabled": { "type": "boolean", "defaultValue": false, "required": false }, "skipConsent": { "type": "boolean", "required": false }, "enableEndSession": { "type": "boolean", "required": false }, "subjectType": { "type": "string", "required": false }, "scopes": { "type": "string[]", "required": false }, "userId": { "type": "string", "required": false, "references": { "model": "user", "field": "id" } }, "createdAt": { "type": "date", "required": false }, "updatedAt": { "type": "date", "required": false }, "name": { "type": "string", "required": false }, "uri": { "type": "string", "required": false }, "icon": { "type": "string", "required": false }, "contacts": { "type": "string[]", "required": false }, "tos": { "type": "string", "required": false }, "policy": { "type": "string", "required": false }, "softwareId": { "type": "string", "required": false }, "softwareVersion": { "type": "string", "required": false }, "softwareStatement": { "type": "string", "required": false }, "redirectUris": { "type": "string[]", "required": true }, "postLogoutRedirectUris": { "type": "string[]", "required": false }, "tokenEndpointAuthMethod": { "type": "string", "required": false }, "grantTypes": { "type": "string[]", "required": false }, "responseTypes": { "type": "string[]", "required": false }, "public": { "type": "boolean", "required": false }, "type": { "type": "string", "required": false }, "requirePKCE": { "type": "boolean", "required": false }, "referenceId": { "type": "string", "required": false }, "metadata": { "type": "json", "required": false } } }, "oauthRefreshToken": { "fields": { "token": { "type": "string", "required": true }, "clientId": { "type": "string", "required": true, "references": { "model": "oauthClient", "field": "clientId" } }, "sessionId": { "type": "string", "required": false, "references": { "model": "session", "field": "id", "onDelete": "set null" } }, "userId": { "type": "string", "required": true, "references": { "model": "user", "field": "id" } }, "referenceId": { "type": "string", "required": false }, "expiresAt": { "type": "date" }, "createdAt": { "type": "date" }, "revoked": { "type": "date", "required": false }, "authTime": { "type": "date", "required": false }, "scopes": { "type": "string[]", "required": true } } }, "oauthAccessToken": { "modelName": "oauthAccessToken", "fields": { "token": { "type": "string", "unique": true }, "clientId": { "type": "string", "required": true, "references": { "model": "oauthClient", "field": "clientId" } }, "sessionId": { "type": "string", "required": false, "references": { "model": "session", "field": "id", "onDelete": "set null" } }, "userId": { "type": "string", "required": false, "references": { "model": "user", "field": "id" } }, "referenceId": { "type": "string", "required": false }, "refreshId": { "type": "string", "required": false, "references": { "model": "oauthRefreshToken", "field": "id" } }, "expiresAt": { "type": "date" }, "createdAt": { "type": "date" }, "scopes": { "type": "string[]", "required": true } } }, "oauthConsent": { "modelName": "oauthConsent", "fields": { "clientId": { "type": "string", "required": true, "references": { "model": "oauthClient", "field": "clientId" } }, "userId": { "type": "string", "required": false, "references": { "model": "user", "field": "id" } }, "referenceId": { "type": "string", "required": false }, "scopes": { "type": "string[]", "required": true }, "createdAt": { "type": "date" }, "updatedAt": { "type": "date" } } } }, "rateLimit": [ { "window": 60, "max": 20 }, { "window": 60, "max": 30 }, { "window": 60, "max": 100 }, { "window": 60, "max": 30 }, { "window": 60, "max": 5 }, { "window": 60, "max": 60 } ] } }, { "name": "magic-link", "config": { "id": "magic-link", "endpoints": {}, "rateLimit": [ { "window": 60, "max": 5 } ], "options": {} } }, { "name": "admin", "config": { "id": "admin", "hooks": { "after": [ {} ] }, "endpoints": {}, "$ERROR_CODES": { "FAILED_TO_CREATE_USER": { "code": "FAILED_TO_CREATE_USER", "message": "Failed to create user" }, "USER_ALREADY_EXISTS": { "code": "USER_ALREADY_EXISTS", "message": "User already exists." }, "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": { "code": "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL", "message": "User already exists. Use another email." }, "YOU_CANNOT_BAN_YOURSELF": { "code": "YOU_CANNOT_BAN_YOURSELF", "message": "You cannot ban yourself" }, "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": { "code": "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE", "message": "You are not allowed to change users role" }, "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": { "code": "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS", "message": "You are not allowed to create users" }, "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": { "code": "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS", "message": "You are not allowed to list users" }, "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": { "code": "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS", "message": "You are not allowed to list users sessions" }, "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": { "code": "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS", "message": "You are not allowed to ban users" }, "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": { "code": "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS", "message": "You are not allowed to impersonate users" }, "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": { "code": "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS", "message": "You are not allowed to revoke users sessions" }, "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": { "code": "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS", "message": "You are not allowed to delete users" }, "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": { "code": "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD", "message": "You are not allowed to set users password" }, "BANNED_USER": { "code": "BANNED_USER", "message": "You have been banned from this application" }, "YOU_ARE_NOT_ALLOWED_TO_GET_USER": { "code": "YOU_ARE_NOT_ALLOWED_TO_GET_USER", "message": "You are not allowed to get user" }, "NO_DATA_TO_UPDATE": { "code": "NO_DATA_TO_UPDATE", "message": "No data to update" }, "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": { "code": "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS", "message": "You are not allowed to update users" }, "YOU_CANNOT_REMOVE_YOURSELF": { "code": "YOU_CANNOT_REMOVE_YOURSELF", "message": "You cannot remove yourself" }, "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE": { "code": "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE", "message": "You are not allowed to set a non-existent role value" }, "YOU_CANNOT_IMPERSONATE_ADMINS": { "code": "YOU_CANNOT_IMPERSONATE_ADMINS", "message": "You cannot impersonate admins" }, "INVALID_ROLE_TYPE": { "code": "INVALID_ROLE_TYPE", "message": "Invalid role type" } }, "schema": { "user": { "fields": { "role": { "type": "string", "required": false, "input": false }, "banned": { "type": "boolean", "defaultValue": false, "required": false, "input": false }, "banReason": { "type": "string", "required": false, "input": false }, "banExpires": { "type": "date", "required": false, "input": false } } }, "session": { "fields": { "impersonatedBy": { "type": "string", "required": false } } } }, "options": { "ac": { "statements": { "user": [ "create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update" ], "session": [ "list", "revoke", "delete" ], "project": [ "read", "manage", "delete" ], "validation": [ "review", "finalize", "report" ], "app": [ "admin-access", "bypass-approval" ], "role": [ "manage-sensitive" ] } }, "roles": { "user": { "statements": { "project": [ "read" ] } }, "supervisor": { "statements": { "project": [ "read", "manage" ], "validation": [ "review", "report" ] } }, "admin": { "statements": { "user": [ "create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update" ], "session": [ "list", "revoke", "delete" ], "project": [ "read", "manage", "delete" ], "validation": [ "review", "finalize", "report" ], "app": [ "admin-access", "bypass-approval" ] } }, "superadmin": { "statements": { "user": [ "create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update", "impersonate-admins" ], "session": [ "list", "revoke", "delete" ], "project": [ "read", "manage", "delete" ], "validation": [ "review", "finalize", "report" ], "app": [ "admin-access", "bypass-approval" ], "role": [ "manage-sensitive" ] } } }, "defaultRole": "user", "adminRoles": [ "admin", "superadmin" ] } } }, { "name": "tanstack-start-cookies", "config": { "id": "tanstack-start-cookies", "hooks": { "after": [ {} ] } } } ] } } } node:internal/url:819 href = bindingUrl.parse(input, base, true); ^ TypeError: Invalid URL at new URL (node:internal/url:819:25) at Object.init (file:///home/jeanf/work/gcf-paie-app/node_modules/.pnpm/@better-auth+oauth-provider@1.5.5_@better-auth+core@1.5.5_@better-auth+utils@0.3.1_@bet_64bc94843570df593e7ac9828ccbf9c8/node_modules/@better-auth/oauth-provider/dist/index.mjs:2724:24) at runPluginInit (file:///home/jeanf/work/gcf-paie-app/node_modules/.pnpm/better-auth@1.5.5_@tanstack+react-start@1.166.8_crossws@0.4.4_srvx@0.11.9__react-dom@19_4a5e7525771c9db50800ba64301f2b24/node_modules/better-auth/dist/context/helpers.mjs:15:30) at createAuthContext (file:///home/jeanf/work/gcf-paie-app/node_modules/.pnpm/better-auth@1.5.5_@tanstack+react-start@1.166.8_crossws@0.4.4_srvx@0.11.9__react-dom@19_4a5e7525771c9db50800ba64301f2b24/node_modules/better-auth/dist/context/create-context.mjs:222:24) at async init (file:///home/jeanf/work/gcf-paie-app/node_modules/.pnpm/better-auth@1.5.5_@tanstack+react-start@1.166.8_crossws@0.4.4_srvx@0.11.9__react-dom@19_4a5e7525771c9db50800ba64301f2b24/node_modules/better-auth/dist/context/init.mjs:11:14) { code: 'ERR_INVALID_URL', input: '' } Even this output is bugged. ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { drizzleAdapter } from 'better-auth/adapters/drizzle' import { betterAuth } from 'better-auth' import { tanstackStartCookies } from 'better-auth/tanstack-start' import { db } from '#/db' import { accessControl, roles } from '#/lib/roles' import { sendMagicLink } from '#/integrations/better-auth/send-magic-link' import { admin, magicLink, jwt } from 'better-auth/plugins' import { oauthProvider } from "@better-auth/oauth-provider"; export const auth = betterAuth({ baseURL: { allowedHosts, protocol: process.env.NODE_ENV === "development" ? "http" : "https", }, advanced: { crossSubDomainCookies: { enabled: true, }, }, database: drizzleAdapter(db, { provider: 'pg', }), plugins: [ jwt(), oauthProvider({ loginPage: "/login", consentPage: "/consent", validAudiences: ['http://localhost:3000/api'], }) , }), admin({ ac: accessControl , roles: roles, defaultRole: 'user', adminRoles: ['admin', 'superadmin'], }), tanstackStartCookies(), ], }) ``` ### Additional context When baseURL is using the format: baseURL: { allowedHosts, protocol: process.env.NODE_ENV === "development" ? "http" : "https", }, The code crashes You need to use the old format (just a string, i.e "http://localhost:3000"
GiteaMirror added the bug label 2026-03-13 10:35:25 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Mar 12, 2026):

This is a known bug reported on March 6, 2026. When using the object-based baseURL format with allowedHosts/protocol, ctx.baseURL is empty during plugin initialization because dynamic URLs are resolved per-request, but the oauth-provider plugin's init function runs before any requests.

The crash happens here in oauth.ts where it tries new URL("").

Workarounds:

  1. Add a fallback URL to your dynamic config:

    baseURL: {
        allowedHosts,
        protocol: process.env.NODE_ENV === "development" ? "http" : "https",
        fallback: "https://your-app.railway.app"
    }
    
  2. Set JWT issuer explicitly:

    jwt({
      issuer: "https://your-app.railway.app"
    })
    
  3. Use a static string baseURL when using oauth-provider

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 (Mar 12, 2026): <!-- Answer --> This is a [known bug reported on March 6, 2026](https://github.com/better-auth/better-auth/issues/8447). When using the object-based `baseURL` format with `allowedHosts`/`protocol`, `ctx.baseURL` is empty during plugin initialization because dynamic URLs are resolved per-request, but the oauth-provider plugin's `init` function runs before any requests. The crash happens [here in oauth.ts](https://github.com/better-auth/better-auth/blob/db5a44424f5747c47a7da347613e42b664e1e734/packages/oauth-provider/src/oauth.ts#L179-L180) where it tries `new URL("")`. **Workarounds:** 1. **Add a `fallback` URL** to your dynamic config: ```javascript baseURL: { allowedHosts, protocol: process.env.NODE_ENV === "development" ? "http" : "https", fallback: "https://your-app.railway.app" } ``` 2. **Set JWT issuer explicitly**: ```javascript jwt({ issuer: "https://your-app.railway.app" }) ``` 3. Use a static string `baseURL` when using oauth-provider <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=brand-link).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/076b8657-99a9-4949-80eb-e5f53e57242a?feedback_type=great_response&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/076b8657-99a9-4949-80eb-e5f53e57242a?feedback_type=irrelevant_answer&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/076b8657-99a9-4949-80eb-e5f53e57242a?feedback_type=incorrect_sources&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/076b8657-99a9-4949-80eb-e5f53e57242a?feedback_type=too_verbose&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/076b8657-99a9-4949-80eb-e5f53e57242a?feedback_type=hallucination&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/076b8657-99a9-4949-80eb-e5f53e57242a?feedback_type=bug_report&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-bug_report) | [Other](https://app.dosu.dev/response-feedback/076b8657-99a9-4949-80eb-e5f53e57242a?feedback_type=other&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-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&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=chat-badge)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=This%20is%20a%20%5Bknown%20bug%20reported%20on%20March%206%2C%202026%5D%28https%3A//github.com/better-auth/better-auth/issues/8447%29.%20When%20using%20the%20object-based%20%60baseURL%60%20format%20with%20%60allowedHosts%60/%60protocol%60%2C%20%60ctx.baseURL%60%20is%20empty%20during%20plugin%20initialization%20because%20dynamic%20URLs%20are%20resolved%20per-request%2C%20but%20the%20oauth-provider%20plugin%27s%20%60init%60%20function%20runs%20before%20any%20requests.%0A%0AThe%20crash%20happens%20%5Bhere%20in%20oauth.ts%5D%28https%3A//github.com/better-auth/better-auth/blob/db5a44424f5747c47a7da347613e42b664e1e734/packages/oauth-provider/src/oauth.ts%23L179-L180%29%20where%20it%20tries%20%60new%20URL%28%22%22%29%60.%0A%0A%2A%2AWorkarounds%3A%2A%2A%0A%0A1.%20%2A%2AAdd%20a%20%60fallback%60%20URL%2A%2A%20to%20your%20dynamic%20config%3A%0A%20%20%20%60%60%60javascript%0A%20%20%20baseURL%3A%20%7B%0A%20%20%20%20%20%20%20allowedHosts%2C%0A%20%20%20%20%20%20%20protocol%3A%20process.env.NODE_ENV%20%3D%3D%3D%20%22development%22%20%3F%20%22http%22%20%3A%20%22https%22%2C%0A%20%20%20%20%20%20%20fallback%3A%20%22https%3A//your-app.railway.app%22%0A%20%20%20%7D%0A%20%20%20%60%60%60%0A%0A2.%20%2A%2ASet%20JWT%20issuer%20explicitly%2A%2A%3A%0A%20%20%20%60%60%60javascript%0A%20%20%20jwt%28%7B%0A%20%20%20%20%20issuer%3A%20%22https%3A//your-app.railway.app%22%0A%20%20%20%7D%29%0A%20%20%20%60%60%60%0A%0A3.%20Use%20a%20static%20string%20%60baseURL%60%20when%20using%20oauth-provider)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=join-discord)&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/8559)
Author
Owner

@j-fdion commented on GitHub (Mar 12, 2026):

What can I do if I have a prod server and a staging server with different urls?
@dosu

@j-fdion commented on GitHub (Mar 12, 2026): What can I do if I have a prod server and a staging server with different urls? @dosu
Author
Owner

@dosubot[bot] commented on GitHub (Mar 12, 2026):

For multiple environments with oauth-provider, you have a couple of options:

Option 1: Use environment variables for a static baseURL per deployment

export const auth = betterAuth({
  baseURL: process.env.BETTER_AUTH_URL, // Set differently in staging vs prod
  plugins: [oauthProvider({ ... })],
})

Set BETTER_AUTH_URL="https://staging.myapp.com" in staging and BETTER_AUTH_URL="https://myapp.com" in production.

Option 2: Per-instance approach (suggested workaround)

const authInstances = new Map<string, Auth>();

export const getAuth = (baseURL = "") => {
  if (!authInstances.has(baseURL))
    authInstances.set(baseURL, betterAuth({ 
      ...authConfig, 
      baseURL,
      plugins: [oauthProvider({ ... })],
    }));
  return authInstances.get(baseURL)!;
};

The dynamic allowedHosts approach unfortunately doesn't work well with oauth-provider due to this bug — the baseURL is empty during plugin initialization.

For now, I'd recommend Option 1 (environment variable per deployment) as the simplest and most reliable solution for your prod/staging setup.

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 (Mar 12, 2026): <!-- Answer --> For multiple environments with oauth-provider, you have a couple of options: **Option 1: Use environment variables for a static baseURL per deployment** ```typescript export const auth = betterAuth({ baseURL: process.env.BETTER_AUTH_URL, // Set differently in staging vs prod plugins: [oauthProvider({ ... })], }) ``` Set `BETTER_AUTH_URL="https://staging.myapp.com"` in staging and `BETTER_AUTH_URL="https://myapp.com"` in production. **Option 2: Per-instance approach** ([suggested workaround](https://github.com/better-auth/better-auth/issues/4151)) ```typescript const authInstances = new Map<string, Auth>(); export const getAuth = (baseURL = "") => { if (!authInstances.has(baseURL)) authInstances.set(baseURL, betterAuth({ ...authConfig, baseURL, plugins: [oauthProvider({ ... })], })); return authInstances.get(baseURL)!; }; ``` The [dynamic `allowedHosts` approach](https://better-auth.com/docs/concepts/dynamic-base-url) unfortunately doesn't work well with oauth-provider due to [this bug](https://github.com/better-auth/better-auth/issues/8447) — the baseURL is empty during plugin initialization. For now, I'd recommend **Option 1** (environment variable per deployment) as the simplest and most reliable solution for your prod/staging setup. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=brand-link).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/d87fc69e-8865-4dd1-baf7-911d2dbe8c39?feedback_type=great_response&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/d87fc69e-8865-4dd1-baf7-911d2dbe8c39?feedback_type=irrelevant_answer&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/d87fc69e-8865-4dd1-baf7-911d2dbe8c39?feedback_type=incorrect_sources&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/d87fc69e-8865-4dd1-baf7-911d2dbe8c39?feedback_type=too_verbose&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/d87fc69e-8865-4dd1-baf7-911d2dbe8c39?feedback_type=hallucination&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/d87fc69e-8865-4dd1-baf7-911d2dbe8c39?feedback_type=bug_report&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-bug_report) | [Other](https://app.dosu.dev/response-feedback/d87fc69e-8865-4dd1-baf7-911d2dbe8c39?feedback_type=other&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-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&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=chat-badge)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=For%20multiple%20environments%20with%20oauth-provider%2C%20you%20have%20a%20couple%20of%20options%3A%0A%0A%2A%2AOption%201%3A%20Use%20environment%20variables%20for%20a%20static%20baseURL%20per%20deployment%2A%2A%0A%60%60%60typescript%0Aexport%20const%20auth%20%3D%20betterAuth%28%7B%0A%20%20baseURL%3A%20process.env.BETTER_AUTH_URL%2C%20//%20Set%20differently%20in%20staging%20vs%20prod%0A%20%20plugins%3A%20%5BoauthProvider%28%7B%20...%20%7D%29%5D%2C%0A%7D%29%0A%60%60%60%0ASet%20%60BETTER_AUTH_URL%3D%22https%3A//staging.myapp.com%22%60%20in%20staging%20and%20%60BETTER_AUTH_URL%3D%22https%3A//myapp.com%22%60%20in%20production.%0A%0A%2A%2AOption%202%3A%20Per-instance%20approach%2A%2A%20%28%5Bsuggested%20workaround%5D%28https%3A//github.com/better-auth/better-auth/issues/4151%29%29%0A%60%60%60typescript%0Aconst%20authInstances%20%3D%20new%20Map%3Cstring%2C%20Auth%3E%28%29%3B%0A%0Aexport%20const%20getAuth%20%3D%20%28baseURL%20%3D%20%22%22%29%20%3D%3E%20%7B%0A%20%20if%20%28%21authInstances.has%28baseURL%29%29%0A%20%20%20%20authInstances.set%28baseURL%2C%20betterAuth%28%7B%20%0A%20%20%20%20%20%20...authConfig%2C%20%0A%20%20%20%20%20%20baseURL%2C%0A%20%20%20%20%20%20plugins%3A%20%5BoauthProvider%28%7B%20...%20%7D%29%5D%2C%0A%20%20%20%20%7D%29%29%3B%0A%20%20return%20authInstances.get%28baseURL%29%21%3B%0A%7D%3B%0A%60%60%60%0A%0AThe%20%5Bdynamic%20%60allowedHosts%60%20approach%5D%28https%3A//better-auth.com/docs/concepts/dynamic-base-url%29%20unfortunately%20doesn%27t%20work%20well%20with%20oauth-provider%20due%20to%20%5Bthis%20bug%5D%28https%3A//github.com/better-auth/better-auth/issues/8447%29%20%E2%80%94%20the%20baseURL%20is%20empty%20during%20plugin%20initialization.%0A%0AFor%20now%2C%20I%27d%20recommend%20%2A%2AOption%201%2A%2A%20%28environment%20variable%20per%20deployment%29%20as%20the%20simplest%20and%20most%20reliable%20solution%20for%20your%20prod/staging%20setup.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=join-discord)&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/8559)
Author
Owner

@j-fdion commented on GitHub (Mar 12, 2026):

@dosubot The problem is that my code is on wsl (localhost:3000) but it redirects to 127.0.0.1 when browsing to the app.

This make it work:
baseURL: {
allowedHosts,
protocol: process.env.NODE_ENV === "development" ? "http" : "https",
fallback: "https://your-app.railway.app"
}

Otherwise it's kinda difficult developing with this current config (Or with multiple apps working at once)

@j-fdion commented on GitHub (Mar 12, 2026): @dosubot The problem is that my code is on wsl (localhost:3000) but it redirects to 127.0.0.1 when browsing to the app. This make it work: baseURL: { allowedHosts, protocol: process.env.NODE_ENV === "development" ? "http" : "https", fallback: "https://your-app.railway.app" } Otherwise it's kinda difficult developing with this current config (Or with multiple apps working at once)
Author
Owner

@dosubot[bot] commented on GitHub (Mar 12, 2026):

Good news — the empty baseURL issue with dynamic config was actually fixed in PR #8455, which makes the fallback URL work properly for plugin initialization. If you're on the latest version, you should be able to use a conditional fallback:

baseURL: {
  allowedHosts: ["localhost:*", "127.0.0.1:*", "*.railway.app"],
  protocol: process.env.NODE_ENV === "development" ? "http" : "https",
  fallback: process.env.NODE_ENV === "development" 
    ? "http://localhost:3000" 
    : "https://your-app.railway.app"
}

This way you get localhost fallback during development and your production URL otherwise.

That said, you're right that this is still awkward for local multi-app setups where you can't rely on a single fallback. It would be better if the oauth-provider plugin could defer its URL construction until request-time. I'd suggest adding a comment to this issue (#8447) explaining your multi-app local development use case — it might help prioritize a more complete fix.

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 (Mar 12, 2026): <!-- Answer --> Good news — the empty baseURL issue with dynamic config was actually [fixed in PR #8455](https://github.com/better-auth/better-auth/pull/8455), which makes the fallback URL work properly for plugin initialization. If you're on the latest version, you should be able to use a conditional fallback: ```typescript baseURL: { allowedHosts: ["localhost:*", "127.0.0.1:*", "*.railway.app"], protocol: process.env.NODE_ENV === "development" ? "http" : "https", fallback: process.env.NODE_ENV === "development" ? "http://localhost:3000" : "https://your-app.railway.app" } ``` This way you get localhost fallback during development and your production URL otherwise. That said, you're right that this is still awkward for local multi-app setups where you can't rely on a single fallback. It would be better if the oauth-provider plugin could defer its URL construction until request-time. I'd suggest adding a comment to [this issue (#8447)](https://github.com/better-auth/better-auth/issues/8447) explaining your multi-app local development use case — it might help prioritize a more complete fix. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=brand-link).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/a76ac6f1-1e79-4881-a121-ba671520bb63?feedback_type=great_response&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/a76ac6f1-1e79-4881-a121-ba671520bb63?feedback_type=irrelevant_answer&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/a76ac6f1-1e79-4881-a121-ba671520bb63?feedback_type=incorrect_sources&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/a76ac6f1-1e79-4881-a121-ba671520bb63?feedback_type=too_verbose&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/a76ac6f1-1e79-4881-a121-ba671520bb63?feedback_type=hallucination&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/a76ac6f1-1e79-4881-a121-ba671520bb63?feedback_type=bug_report&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-bug_report) | [Other](https://app.dosu.dev/response-feedback/a76ac6f1-1e79-4881-a121-ba671520bb63?feedback_type=other&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-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&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=chat-badge)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=Good%20news%20%E2%80%94%20the%20empty%20baseURL%20issue%20with%20dynamic%20config%20was%20actually%20%5Bfixed%20in%20PR%20%238455%5D%28https%3A//github.com/better-auth/better-auth/pull/8455%29%2C%20which%20makes%20the%20fallback%20URL%20work%20properly%20for%20plugin%20initialization.%20If%20you%27re%20on%20the%20latest%20version%2C%20you%20should%20be%20able%20to%20use%20a%20conditional%20fallback%3A%0A%0A%60%60%60typescript%0AbaseURL%3A%20%7B%0A%20%20allowedHosts%3A%20%5B%22localhost%3A%2A%22%2C%20%22127.0.0.1%3A%2A%22%2C%20%22%2A.railway.app%22%5D%2C%0A%20%20protocol%3A%20process.env.NODE_ENV%20%3D%3D%3D%20%22development%22%20%3F%20%22http%22%20%3A%20%22https%22%2C%0A%20%20fallback%3A%20process.env.NODE_ENV%20%3D%3D%3D%20%22development%22%20%0A%20%20%20%20%3F%20%22http%3A//localhost%3A3000%22%20%0A%20%20%20%20%3A%20%22https%3A//your-app.railway.app%22%0A%7D%0A%60%60%60%0A%0AThis%20way%20you%20get%20localhost%20fallback%20during%20development%20and%20your%20production%20URL%20otherwise.%0A%0AThat%20said%2C%20you%27re%20right%20that%20this%20is%20still%20awkward%20for%20local%20multi-app%20setups%20where%20you%20can%27t%20rely%20on%20a%20single%20fallback.%20It%20would%20be%20better%20if%20the%20oauth-provider%20plugin%20could%20defer%20its%20URL%20construction%20until%20request-time.%20I%27d%20suggest%20adding%20a%20comment%20to%20%5Bthis%20issue%20%28%238447%29%5D%28https%3A//github.com/better-auth/better-auth/issues/8447%29%20explaining%20your%20multi-app%20local%20development%20use%20case%20%E2%80%94%20it%20might%20help%20prioritize%20a%20more%20complete%20fix.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=join-discord)&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/8559)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#3032