[GH-ISSUE #4066] Error: column "displayUsername" of relation "users" does not exist in better-auth 1.3.7 when using admin() and username() plugin together #18451

Closed
opened 2026-04-15 16:54:07 -05:00 by GiteaMirror · 8 comments
Owner

Originally created by @slidenerd on GitHub (Aug 18, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/4066

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Create an express JS backend with the config supplied below
  2. Create a sveltekit frontend with the config supplied below
  3. Try to update the username from the sveltekit frontend
  4. It immediately throws an Internal Server Error whose stacktrace looks like this
  5. I clean dropped and recreated the database and then ran migrations to ensure this issue was actually happening
SERVER_ERROR:  error: column "displayUsername" of relation "users" does not exist
    at /Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/pg/lib/client.js:545:17
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async PostgresConnection.executeQuery (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/dialect/postgres/postgres-driver.js:93:49)
    at async /Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/query-executor/query-executor-base.js:37:28
    at async DefaultConnectionProvider.provideConnection (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/driver/default-connection-provider.js:12:20)
    at async DefaultQueryExecutor.executeQuery (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/query-executor/query-executor-base.js:36:16)
    at async UpdateQueryBuilder.execute (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/query-builder/update-query-builder.js:461:24)
    at async UpdateQueryBuilder.executeTakeFirst (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/query-builder/update-query-builder.js:477:26)
    at async withReturning (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/better-auth/dist/shared/better-auth.Bm_lsK_X.cjs:119:13)
    at async Object.update (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/better-auth/dist/shared/better-auth.Bm_lsK_X.cjs:265:16)
    at PostgresConnection.executeQuery (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/dialect/postgres/postgres-driver.js:105:69)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async /Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/query-executor/query-executor-base.js:37:28
    at async DefaultConnectionProvider.provideConnection (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/driver/default-connection-provider.js:12:20)
    at async DefaultQueryExecutor.executeQuery (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/query-executor/query-executor-base.js:36:16)
    at async UpdateQueryBuilder.execute (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/query-builder/update-query-builder.js:461:24)
    at async UpdateQueryBuilder.executeTakeFirst (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/query-builder/update-query-builder.js:477:26)
    at async withReturning (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/better-auth/dist/shared/better-auth.Bm_lsK_X.cjs:119:13)
    at async Object.update (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/better-auth/dist/shared/better-auth.Bm_lsK_X.cjs:265:16)
    at async Object.update (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/better-auth/dist/shared/better-auth.DzBLnNed.cjs:463:19) {
  length: 135,
  severity: 'ERROR',
  code: '42703',
  detail: undefined,
  hint: undefined,
  position: '37',
  internalPosition: undefined,
  internalQuery: undefined,
  where: undefined,
  schema: undefined,
  table: undefined,
  column: undefined,
  dataType: undefined,
  constraint: undefined,
  file: 'analyze.c',
  line: '2536',
  routine: 'transformUpdateTargetList'
}

Current vs. Expected behavior

  • Current behavior is the error
  • Expected behavior is that the username gets updated when you set it

What version of Better Auth are you using?

1.3.7

System info

System:
    OS: macOS 15.6
    CPU: (8) arm64 Apple M1
    Memory: 146.56 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Browsers:
    Safari: 18.6

I tested it on chrome though
Chrome
Version 139.0.7258.128 (Official Build) (arm64)

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

Client, Backend

Auth config (if applicable)


Additional context

Dependencies of the client package.json

	"devDependencies": {
		"@eslint/compat": "^1.3.2",
		"@eslint/js": "^9.33.0",
		"@playwright/test": "^1.54.2",
		"@sveltejs/adapter-node": "^5.3.1",
		"@sveltejs/kit": "^2.31.1",
		"@sveltejs/vite-plugin-svelte": "^6.1.2",
		"@vitest/browser": "^3.2.4",
		"eslint": "^9.33.0",
		"eslint-config-prettier": "^10.1.8",
		"eslint-plugin-svelte": "^3.11.0",
		"globals": "^16.3.0",
		"playwright": "^1.54.2",
		"prettier": "^3.6.2",
		"prettier-plugin-svelte": "^3.4.0",
		"svelte": "^5.38.1",
		"svelte-check": "^4.3.1",
		"typescript": "^5.9.2",
		"typescript-eslint": "^8.39.1",
		"vite": "^7.1.2",
		"vite-plugin-devtools-json": "^1.0.0",
		"vitest": "^3.2.4",
		"vitest-browser-svelte": "^1.1.0"
	},
	"dependencies": {
		"@better-fetch/fetch": "^1.1.18",
		"better-auth": "^1.3.7"
	}

Dependencies of the server package.json

	"dependencies": {
		"@dotenvx/dotenvx": "1.48.4",
		"bcryptjs": "^3.0.2",
		"better-auth": "^1.3.7",
		"bullmq": "^5.58.0",
		"cors": "2.8.5",
		"express": "4.21.2",
		"helmet": "8.1.0",
		"http-terminator": "3.2.0",
		"ioredis": "5.7.0",
		"pg": "^8.16.3",
		"pg-promise": "11.15.0",
		"pino": "9.9.0",
		"pino-http": "10.5.0"
	},
	"description": "An app built using Zupstrap API",
	"devDependencies": {
		"@biomejs/biome": "2.2.0",
		"@commitlint/cli": "19.8.1",
		"@commitlint/config-conventional": "19.8.1",
		"@testcontainers/postgresql": "11.5.1",
		"@testcontainers/redis": "11.5.1",
		"@types/cors": "2.8.19",
		"@types/express": "4.17.21",
		"@types/node": "24.3.0",
		"@types/pg": "^8.15.5",
		"@types/supertest": "6.0.3",
		"@vitest/coverage-v8": "3.2.4",
		"commitizen": "4.3.1",
		"cz-conventional-changelog": "3.3.0",
		"lefthook": "1.12.3",
		"node-pg-migrate": "^8.0.3",
		"pino-pretty": "13.1.1",
		"supertest": "7.1.4",
		"tsx": "4.20.4",
		"typescript": "5.9.2",
		"vitest": "3.2.4"
	},

Environment variables for server config

  • Values of the environment variables used in the config below (Note: My express server runs on 3002)
BASE_URL=http://localhost:3002
COOKIE_HTTP_ONLY=true
COOKIE_PARTITIONED=true
COOKIE_SAME_SITE=None
COOKIE_SECURE=true
CORS_ALLOWED_ORIGINS=http://localhost:4173,http://localhost:5173,https://api.x.com
EMAIL_VERIFICATION_EXPIRES_IN=86400
FACEBOOK_APP_ID=...
FACEBOOK_APP_SECRET=...
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
NODE_ENV=development
PASSWORD_HASH_SALT_ROUNDS=10
RESET_PASSWORD_TOKEN_EXPIRES_IN=86400
SERVER_HOST=127.0.0.1
SERVER_PORT=3002
TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
TWITTER_APP_ID=...
TWITTER_APP_SECRET=...
DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DATABASE}


Server config

  • This is the auth config used on the server
  • It uses admin(), captcha() and username() plugins

src/lib/auth.ts

import bcrypt from "bcryptjs";
import { betterAuth } from "better-auth";
import {
	admin,
	captcha,
	createAuthMiddleware,
	username,
} from "better-auth/plugins";
import { Pool } from "pg";
import {
	BASE_URL,
	BETTER_AUTH_SECRET,
	COOKIE_HTTP_ONLY,
	COOKIE_PARTITIONED,
	COOKIE_SAME_SITE,
	COOKIE_SECURE,
	CORS_ALLOWED_ORIGINS,
	EMAIL_VERIFICATION_EXPIRES_IN,
	FACEBOOK_APP_ID,
	FACEBOOK_APP_SECRET,
	GOOGLE_CLIENT_ID,
	GOOGLE_CLIENT_SECRET,
	PASSWORD_HASH_SALT_ROUNDS,
	RESET_PASSWORD_TOKEN_EXPIRES_IN,
	TURNSTILE_SECRET_KEY,
	TWITTER_APP_ID,
	TWITTER_APP_SECRET,
	USE_SECURE_COOKIES,
} from "../config/env";
import { getConnectionPoolOptions } from "../config/postgres";
import { getRedis } from "./redis";

export const auth = betterAuth({
	account: {
		accountLinking: {
			enabled: true,
		},
		fields: {
			accessToken: "access_token",
			accessTokenExpiresAt: "access_token_expires_at",
			accountId: "account_id",
			createdAt: "created_at",
			idToken: "id_token",
			password: "password",
			providerId: "provider_id",
			refreshToken: "refresh_token",
			refreshTokenExpiresAt: "refresh_token_expires_at",
			scope: "scope",
			updatedAt: "updated_at",
			userId: "user_id",
		},
		modelName: "accounts",
	},
	advanced: {
		cookiePrefix: "ch-api",
		database: {
			generateId() {
				return crypto.randomUUID();
			},
		},
		defaultCookieAttributes: {
			httpOnly: COOKIE_HTTP_ONLY,
			partitioned: COOKIE_PARTITIONED,
			sameSite: COOKIE_SAME_SITE,
			secure: COOKIE_SECURE,
		},
		ipAddress: {
			ipAddressHeaders: ["x-forwarded-for", "x-real-ip", "x-client-ip"],
		},
		useSecureCookies: USE_SECURE_COOKIES,
	},
	appName: "CoinHexa API",
	baseUrl: BASE_URL,
	basePath: "/api/auth",
	database: new Pool(getConnectionPoolOptions()),
	emailAndPassword: {
		autoSignIn: true,
		disableSignUp: false,
		enabled: true,
		maxPasswordLength: 255,
		minPasswordLength: 8,
		onPasswordReset: async ({ user }, _request) => {
			console.log(`Password reset for user: ${user.email}`);
		},
		password: {
			hash(password: string) {
				return bcrypt.hash(password, PASSWORD_HASH_SALT_ROUNDS);
			},
			verify(data: { password: string; hash: string }) {
				return bcrypt.compare(data.password, data.hash);
			},
		},
		requireEmailVerification: true,
		resetPasswordTokenExpiresIn: RESET_PASSWORD_TOKEN_EXPIRES_IN,
		sendResetPassword: async ({ user: _user, url: _url, token: _token }) => {},
	},
	emailVerification: {
		async afterEmailVerification(user, _request) {
			console.log(`${user.email} has been successfully verified!`);
		},
		autoSignInAfterVerification: true,
		expiresIn: EMAIL_VERIFICATION_EXPIRES_IN,
		sendOnSignUp: true,
		sendVerificationEmail: async ({
			user: _user,
			url: _url,
			token: _token,
		}) => {},
	},
	hooks: {
		after: createAuthMiddleware(async (ctx) => {
			console.log("after hook", ctx);
		}),
	},
	plugins: [
		admin(),
		captcha({
			endpoints: [
				"/forget-password",
				"/reset-password",
				"/sign-in/email",
				"/sign-up/email",
			],
			provider: "cloudflare-turnstile",
			secretKey: TURNSTILE_SECRET_KEY,
		}),
		username(),
	],
	onAPIError: {
		throw: true,
		onError: (error, _ctx) => {
			console.error("Auth error:", error);
		},
		errorURL: "/api/auth/error",
	},
	rateLimit: {
		customRules: {
			"/forget-password": {
				max: 3,
				window: 10,
			},
			"/sign-in/email": {
				max: 3,
				window: 10,
			},
			"/sign-up/email": {
				max: 3,
				window: 10,
			},
		},
		enabled: true,
		max: 60,
		storage: "secondary-storage",
		window: 60,
	},
	secret: BETTER_AUTH_SECRET,
	secondaryStorage: {
		get: async (key) => {
			const value = await getRedis().get(key);
			return value ? value : null;
		},
		set: async (key, value, ttl) => {
			if (ttl) await getRedis().set(key, value, "EX", ttl);
			else await getRedis().set(key, value);
		},
		delete: async (key) => {
			await getRedis().del(key);
		},
	},
	session: {
		expiresIn: 60 * 60 * 24 * 7,
		fields: {
			createdAt: "created_at",
			expiresAt: "expires_at",
			impersonatedBy: "impersonated_by",
			ipAddress: "ip_address",
			token: "token",
			updatedAt: "updated_at",
			userAgent: "user_agent",
			userId: "user_id",
		},
		modelName: "sessions",
		updateAge: 60 * 60 * 24,
	},
	socialProviders: {
		facebook: {
			clientId: FACEBOOK_APP_ID,
			clientSecret: FACEBOOK_APP_SECRET,
		},
		google: {
			clientId: GOOGLE_CLIENT_ID,
			clientSecret: GOOGLE_CLIENT_SECRET,
			prompt: "select_account",
		},
		twitter: {
			clientId: TWITTER_APP_ID,
			clientSecret: TWITTER_APP_SECRET,
		},
	},
	telemetry: {
		enabled: false,
	},
	trustedOrigins: CORS_ALLOWED_ORIGINS,
	user: {
		deleteUser: {
			afterDelete: async (user, _request) => {
				console.log(`User deleted: ${user.email}`);
			},
			enabled: true,
			sendDeleteAccountVerification: async (
				{ user: _user, url: _url, token: _token },
				_request,
			) => {},
		},
		fields: {
			banExpires: "ban_expires",
			banReason: "ban_reason",
			banned: "banned",
			createdAt: "created_at",
			displayUsername: "display_username",
			email: "email",
			emailVerified: "email_verified",
			image: "image",
			name: "name",
			role: "role",
			updatedAt: "updated_at",
			username: "username",
		},
		modelName: "users",
	},
	verification: {
		fields: {
			createdAt: "created_at",
			expiresAt: "expires_at",
			identifier: "identifier",
			updatedAt: "updated_at",
			value: "value",
		},
		modelName: "verifications",
	},
});

Environment variables for client config

  • Here is the config for the sveltekit client
  • Values of the environment variables used in the config below (Note: sveltekit runs on 5173)
PUBLIC_SERVER_HOST=127.0.0.1
PUBLIC_SERVER_PORT=3002
PUBLIC_SERVER_PROTOCOL=http

Client Config

src/lib/auth/client.ts

import { adminClient, usernameClient } from 'better-auth/client/plugins';
import { createAuthClient } from 'better-auth/svelte';
import { env } from '$env/dynamic/public';

export const client = createAuthClient({
	/** The base URL of the server (optional if you're using the same domain) */
	baseURL: `${env.PUBLIC_SERVER_PROTOCOL}://${env.PUBLIC_SERVER_HOST}:${env.PUBLIC_SERVER_PORT}`,
	basePath: '/api/auth',
	fetchOptions: {
		throw: true
	},
	plugins: [adminClient(), usernameClient()]
});

src/data/migrations/1748345325030_create-users-table.ts

import type { MigrationBuilder } from "node-pg-migrate";

export const up = (pgm: MigrationBuilder) => {
	pgm.createTable(
		"users",
		{
			id: {
				comment: "Unique identifier for each user",
				primaryKey: true,
				type: "uuid",
			},
			ban_expires: {
				comment: "Timestamp when the ban expires",
				type: "timestamptz",
			},
			ban_reason: {
				comment: "Reason for the user ban",
				type: "text",
			},
			banned: {
				comment: "Whether the user is banned",
				type: "boolean",
			},
			display_username: {
				comment: "Non normalized username of the user",
				type: "text",
			},
			email: {
				comment: "User's email address for communication and login",
				notNull: true,
				type: "text",
			},
			email_verified: {
				comment: "Whether the user's email is verified",
				notNull: true,
				type: "boolean",
			},
			image: {
				comment: "User's image URL",
				type: "text",
			},
			name: {
				comment: "User's chosen display name",
				notNull: true,
				type: "text",
			},
			role: {
				comment: "User's role in the system",
				type: "text",
			},
			username: {
				comment: "The username of the user",
				type: "text",
			},
			created_at: {
				comment: "Timestamp of when the user account was created",
				notNull: true,
				type: "timestamptz",
			},
			updated_at: {
				comment: "Timestamp of the last update to the user's information",
				notNull: true,
				type: "timestamptz",
			},
		},
		{
			comment:
				"Table to store user information, including authentication and profile details.",
			ifNotExists: true,
		},
	);
};

export const down = (pgm: MigrationBuilder) => {
	pgm.dropTable("users", { cascade: true, ifExists: true });
};

src/data/migrations/1748348413644_add-users-indexes.ts

import type { MigrationBuilder } from "node-pg-migrate";

export const up = (pgm: MigrationBuilder) => {
	pgm.createIndex("users", "email", {
		ifNotExists: true,
		method: "btree",
		name: "users_email_idx",
		unique: true,
	});

	pgm.createIndex("users", "username", {
		ifNotExists: true,
		method: "btree",
		name: "users_username_idx",
		unique: true,
	});
};

export const down = (pgm: MigrationBuilder) => {
	pgm.dropIndex("users", "username", {
		cascade: true,
		ifExists: true,
		name: "users_username_idx",
	});

	pgm.dropIndex("users", "email", {
		cascade: true,
		ifExists: true,
		name: "users_email_idx",
	});
};

Screenshots of all tables with their schema after creating them

Accounts

Image

Sessions

Image

Users

Image

Verifications

Image

Client code to update the username in sveltekit

src/routes/(account)/username/+page.svelte

<script lang="ts">
	import { BetterAuthError } from 'better-auth';
	import { BetterFetchError } from '@better-fetch/fetch';
	import { client } from '$lib/auth/client';
	import { getErrorMessage, hasCodeAndMessage, hasOnlyMessage } from '$lib/auth/errors';

	function updateUsername() {
		const displayUsername = 'something';
		const username = 'abracadabra';
		return client.updateUser({ displayUsername, username });
	}

	async function onSubmit() {
		try {
			const response = await updateUsername();
			console.log(response.status, 'success');
		} catch (error) {
			if (error instanceof BetterAuthError) {
				console.log('better auth error', JSON.stringify(error));
			} else if (
				error instanceof BetterFetchError &&
				error.status > 0 &&
				hasCodeAndMessage(error)
			) {
				console.log(
					'better fetch error 1',
					JSON.stringify(error),
					getErrorMessage(error.error.code, 'en')
				);
			} else if (error instanceof BetterFetchError && error.status > 0 && hasOnlyMessage(error)) {
				console.log('better fetch error 2', JSON.stringify(error), error.error.message);
			} else if (error instanceof BetterFetchError && error.status === 0) {
				console.log('network error', error);
			} else {
				console.log('unknown error', error);
			}
		}
	}
</script>

<form onsubmit={onSubmit}>
	<button>Update Profile</button>
</form>

Originally created by @slidenerd on GitHub (Aug 18, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/4066 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Create an express JS backend with the config supplied below 2. Create a sveltekit frontend with the config supplied below 3. Try to update the username from the sveltekit frontend 4. It immediately throws an Internal Server Error whose stacktrace looks like this 5. I clean dropped and recreated the database and then ran migrations to ensure this issue was actually happening ``` SERVER_ERROR: error: column "displayUsername" of relation "users" does not exist at /Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/pg/lib/client.js:545:17 at process.processTicksAndRejections (node:internal/process/task_queues:105:5) at async PostgresConnection.executeQuery (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/dialect/postgres/postgres-driver.js:93:49) at async /Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/query-executor/query-executor-base.js:37:28 at async DefaultConnectionProvider.provideConnection (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/driver/default-connection-provider.js:12:20) at async DefaultQueryExecutor.executeQuery (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/query-executor/query-executor-base.js:36:16) at async UpdateQueryBuilder.execute (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/query-builder/update-query-builder.js:461:24) at async UpdateQueryBuilder.executeTakeFirst (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/query-builder/update-query-builder.js:477:26) at async withReturning (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/better-auth/dist/shared/better-auth.Bm_lsK_X.cjs:119:13) at async Object.update (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/better-auth/dist/shared/better-auth.Bm_lsK_X.cjs:265:16) at PostgresConnection.executeQuery (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/dialect/postgres/postgres-driver.js:105:69) at process.processTicksAndRejections (node:internal/process/task_queues:105:5) at async /Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/query-executor/query-executor-base.js:37:28 at async DefaultConnectionProvider.provideConnection (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/driver/default-connection-provider.js:12:20) at async DefaultQueryExecutor.executeQuery (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/query-executor/query-executor-base.js:36:16) at async UpdateQueryBuilder.execute (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/query-builder/update-query-builder.js:461:24) at async UpdateQueryBuilder.executeTakeFirst (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/kysely/dist/cjs/query-builder/update-query-builder.js:477:26) at async withReturning (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/better-auth/dist/shared/better-auth.Bm_lsK_X.cjs:119:13) at async Object.update (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/better-auth/dist/shared/better-auth.Bm_lsK_X.cjs:265:16) at async Object.update (/Users/vr/Desktop/code/INCOMPLETE/ch_api/node_modules/better-auth/dist/shared/better-auth.DzBLnNed.cjs:463:19) { length: 135, severity: 'ERROR', code: '42703', detail: undefined, hint: undefined, position: '37', internalPosition: undefined, internalQuery: undefined, where: undefined, schema: undefined, table: undefined, column: undefined, dataType: undefined, constraint: undefined, file: 'analyze.c', line: '2536', routine: 'transformUpdateTargetList' } ``` ### Current vs. Expected behavior - Current behavior is the error - Expected behavior is that the username gets updated when you set it ### What version of Better Auth are you using? 1.3.7 ### System info ```bash System: OS: macOS 15.6 CPU: (8) arm64 Apple M1 Memory: 146.56 MB / 16.00 GB Shell: 5.9 - /bin/zsh Browsers: Safari: 18.6 I tested it on chrome though Chrome Version 139.0.7258.128 (Official Build) (arm64) ``` ### Which area(s) are affected? (Select all that apply) Client, Backend ### Auth config (if applicable) ```typescript ``` ### Additional context ## Dependencies of the client package.json ``` "devDependencies": { "@eslint/compat": "^1.3.2", "@eslint/js": "^9.33.0", "@playwright/test": "^1.54.2", "@sveltejs/adapter-node": "^5.3.1", "@sveltejs/kit": "^2.31.1", "@sveltejs/vite-plugin-svelte": "^6.1.2", "@vitest/browser": "^3.2.4", "eslint": "^9.33.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.11.0", "globals": "^16.3.0", "playwright": "^1.54.2", "prettier": "^3.6.2", "prettier-plugin-svelte": "^3.4.0", "svelte": "^5.38.1", "svelte-check": "^4.3.1", "typescript": "^5.9.2", "typescript-eslint": "^8.39.1", "vite": "^7.1.2", "vite-plugin-devtools-json": "^1.0.0", "vitest": "^3.2.4", "vitest-browser-svelte": "^1.1.0" }, "dependencies": { "@better-fetch/fetch": "^1.1.18", "better-auth": "^1.3.7" } ``` ## Dependencies of the server package.json ``` "dependencies": { "@dotenvx/dotenvx": "1.48.4", "bcryptjs": "^3.0.2", "better-auth": "^1.3.7", "bullmq": "^5.58.0", "cors": "2.8.5", "express": "4.21.2", "helmet": "8.1.0", "http-terminator": "3.2.0", "ioredis": "5.7.0", "pg": "^8.16.3", "pg-promise": "11.15.0", "pino": "9.9.0", "pino-http": "10.5.0" }, "description": "An app built using Zupstrap API", "devDependencies": { "@biomejs/biome": "2.2.0", "@commitlint/cli": "19.8.1", "@commitlint/config-conventional": "19.8.1", "@testcontainers/postgresql": "11.5.1", "@testcontainers/redis": "11.5.1", "@types/cors": "2.8.19", "@types/express": "4.17.21", "@types/node": "24.3.0", "@types/pg": "^8.15.5", "@types/supertest": "6.0.3", "@vitest/coverage-v8": "3.2.4", "commitizen": "4.3.1", "cz-conventional-changelog": "3.3.0", "lefthook": "1.12.3", "node-pg-migrate": "^8.0.3", "pino-pretty": "13.1.1", "supertest": "7.1.4", "tsx": "4.20.4", "typescript": "5.9.2", "vitest": "3.2.4" }, ``` ## Environment variables for server config - Values of the environment variables used in the config below (Note: My express server runs on 3002) ``` BASE_URL=http://localhost:3002 COOKIE_HTTP_ONLY=true COOKIE_PARTITIONED=true COOKIE_SAME_SITE=None COOKIE_SECURE=true CORS_ALLOWED_ORIGINS=http://localhost:4173,http://localhost:5173,https://api.x.com EMAIL_VERIFICATION_EXPIRES_IN=86400 FACEBOOK_APP_ID=... FACEBOOK_APP_SECRET=... GOOGLE_CLIENT_ID=... GOOGLE_CLIENT_SECRET=... NODE_ENV=development PASSWORD_HASH_SALT_ROUNDS=10 RESET_PASSWORD_TOKEN_EXPIRES_IN=86400 SERVER_HOST=127.0.0.1 SERVER_PORT=3002 TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA TWITTER_APP_ID=... TWITTER_APP_SECRET=... DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DATABASE} ``` ## Server config - This is the auth config used on the server - It uses admin(), captcha() and username() plugins **`src/lib/auth.ts`** ``` import bcrypt from "bcryptjs"; import { betterAuth } from "better-auth"; import { admin, captcha, createAuthMiddleware, username, } from "better-auth/plugins"; import { Pool } from "pg"; import { BASE_URL, BETTER_AUTH_SECRET, COOKIE_HTTP_ONLY, COOKIE_PARTITIONED, COOKIE_SAME_SITE, COOKIE_SECURE, CORS_ALLOWED_ORIGINS, EMAIL_VERIFICATION_EXPIRES_IN, FACEBOOK_APP_ID, FACEBOOK_APP_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, PASSWORD_HASH_SALT_ROUNDS, RESET_PASSWORD_TOKEN_EXPIRES_IN, TURNSTILE_SECRET_KEY, TWITTER_APP_ID, TWITTER_APP_SECRET, USE_SECURE_COOKIES, } from "../config/env"; import { getConnectionPoolOptions } from "../config/postgres"; import { getRedis } from "./redis"; export const auth = betterAuth({ account: { accountLinking: { enabled: true, }, fields: { accessToken: "access_token", accessTokenExpiresAt: "access_token_expires_at", accountId: "account_id", createdAt: "created_at", idToken: "id_token", password: "password", providerId: "provider_id", refreshToken: "refresh_token", refreshTokenExpiresAt: "refresh_token_expires_at", scope: "scope", updatedAt: "updated_at", userId: "user_id", }, modelName: "accounts", }, advanced: { cookiePrefix: "ch-api", database: { generateId() { return crypto.randomUUID(); }, }, defaultCookieAttributes: { httpOnly: COOKIE_HTTP_ONLY, partitioned: COOKIE_PARTITIONED, sameSite: COOKIE_SAME_SITE, secure: COOKIE_SECURE, }, ipAddress: { ipAddressHeaders: ["x-forwarded-for", "x-real-ip", "x-client-ip"], }, useSecureCookies: USE_SECURE_COOKIES, }, appName: "CoinHexa API", baseUrl: BASE_URL, basePath: "/api/auth", database: new Pool(getConnectionPoolOptions()), emailAndPassword: { autoSignIn: true, disableSignUp: false, enabled: true, maxPasswordLength: 255, minPasswordLength: 8, onPasswordReset: async ({ user }, _request) => { console.log(`Password reset for user: ${user.email}`); }, password: { hash(password: string) { return bcrypt.hash(password, PASSWORD_HASH_SALT_ROUNDS); }, verify(data: { password: string; hash: string }) { return bcrypt.compare(data.password, data.hash); }, }, requireEmailVerification: true, resetPasswordTokenExpiresIn: RESET_PASSWORD_TOKEN_EXPIRES_IN, sendResetPassword: async ({ user: _user, url: _url, token: _token }) => {}, }, emailVerification: { async afterEmailVerification(user, _request) { console.log(`${user.email} has been successfully verified!`); }, autoSignInAfterVerification: true, expiresIn: EMAIL_VERIFICATION_EXPIRES_IN, sendOnSignUp: true, sendVerificationEmail: async ({ user: _user, url: _url, token: _token, }) => {}, }, hooks: { after: createAuthMiddleware(async (ctx) => { console.log("after hook", ctx); }), }, plugins: [ admin(), captcha({ endpoints: [ "/forget-password", "/reset-password", "/sign-in/email", "/sign-up/email", ], provider: "cloudflare-turnstile", secretKey: TURNSTILE_SECRET_KEY, }), username(), ], onAPIError: { throw: true, onError: (error, _ctx) => { console.error("Auth error:", error); }, errorURL: "/api/auth/error", }, rateLimit: { customRules: { "/forget-password": { max: 3, window: 10, }, "/sign-in/email": { max: 3, window: 10, }, "/sign-up/email": { max: 3, window: 10, }, }, enabled: true, max: 60, storage: "secondary-storage", window: 60, }, secret: BETTER_AUTH_SECRET, secondaryStorage: { get: async (key) => { const value = await getRedis().get(key); return value ? value : null; }, set: async (key, value, ttl) => { if (ttl) await getRedis().set(key, value, "EX", ttl); else await getRedis().set(key, value); }, delete: async (key) => { await getRedis().del(key); }, }, session: { expiresIn: 60 * 60 * 24 * 7, fields: { createdAt: "created_at", expiresAt: "expires_at", impersonatedBy: "impersonated_by", ipAddress: "ip_address", token: "token", updatedAt: "updated_at", userAgent: "user_agent", userId: "user_id", }, modelName: "sessions", updateAge: 60 * 60 * 24, }, socialProviders: { facebook: { clientId: FACEBOOK_APP_ID, clientSecret: FACEBOOK_APP_SECRET, }, google: { clientId: GOOGLE_CLIENT_ID, clientSecret: GOOGLE_CLIENT_SECRET, prompt: "select_account", }, twitter: { clientId: TWITTER_APP_ID, clientSecret: TWITTER_APP_SECRET, }, }, telemetry: { enabled: false, }, trustedOrigins: CORS_ALLOWED_ORIGINS, user: { deleteUser: { afterDelete: async (user, _request) => { console.log(`User deleted: ${user.email}`); }, enabled: true, sendDeleteAccountVerification: async ( { user: _user, url: _url, token: _token }, _request, ) => {}, }, fields: { banExpires: "ban_expires", banReason: "ban_reason", banned: "banned", createdAt: "created_at", displayUsername: "display_username", email: "email", emailVerified: "email_verified", image: "image", name: "name", role: "role", updatedAt: "updated_at", username: "username", }, modelName: "users", }, verification: { fields: { createdAt: "created_at", expiresAt: "expires_at", identifier: "identifier", updatedAt: "updated_at", value: "value", }, modelName: "verifications", }, }); ``` ## Environment variables for client config - Here is the config for the sveltekit client - Values of the environment variables used in the config below (Note: sveltekit runs on 5173) ``` PUBLIC_SERVER_HOST=127.0.0.1 PUBLIC_SERVER_PORT=3002 PUBLIC_SERVER_PROTOCOL=http ``` ## Client Config **`src/lib/auth/client.ts`** ``` import { adminClient, usernameClient } from 'better-auth/client/plugins'; import { createAuthClient } from 'better-auth/svelte'; import { env } from '$env/dynamic/public'; export const client = createAuthClient({ /** The base URL of the server (optional if you're using the same domain) */ baseURL: `${env.PUBLIC_SERVER_PROTOCOL}://${env.PUBLIC_SERVER_HOST}:${env.PUBLIC_SERVER_PORT}`, basePath: '/api/auth', fetchOptions: { throw: true }, plugins: [adminClient(), usernameClient()] }); ``` ## Relevant node-pg-migration related to the users table **`src/data/migrations/1748345325030_create-users-table.ts`** ``` import type { MigrationBuilder } from "node-pg-migrate"; export const up = (pgm: MigrationBuilder) => { pgm.createTable( "users", { id: { comment: "Unique identifier for each user", primaryKey: true, type: "uuid", }, ban_expires: { comment: "Timestamp when the ban expires", type: "timestamptz", }, ban_reason: { comment: "Reason for the user ban", type: "text", }, banned: { comment: "Whether the user is banned", type: "boolean", }, display_username: { comment: "Non normalized username of the user", type: "text", }, email: { comment: "User's email address for communication and login", notNull: true, type: "text", }, email_verified: { comment: "Whether the user's email is verified", notNull: true, type: "boolean", }, image: { comment: "User's image URL", type: "text", }, name: { comment: "User's chosen display name", notNull: true, type: "text", }, role: { comment: "User's role in the system", type: "text", }, username: { comment: "The username of the user", type: "text", }, created_at: { comment: "Timestamp of when the user account was created", notNull: true, type: "timestamptz", }, updated_at: { comment: "Timestamp of the last update to the user's information", notNull: true, type: "timestamptz", }, }, { comment: "Table to store user information, including authentication and profile details.", ifNotExists: true, }, ); }; export const down = (pgm: MigrationBuilder) => { pgm.dropTable("users", { cascade: true, ifExists: true }); }; ``` **`src/data/migrations/1748348413644_add-users-indexes.ts`** ``` import type { MigrationBuilder } from "node-pg-migrate"; export const up = (pgm: MigrationBuilder) => { pgm.createIndex("users", "email", { ifNotExists: true, method: "btree", name: "users_email_idx", unique: true, }); pgm.createIndex("users", "username", { ifNotExists: true, method: "btree", name: "users_username_idx", unique: true, }); }; export const down = (pgm: MigrationBuilder) => { pgm.dropIndex("users", "username", { cascade: true, ifExists: true, name: "users_username_idx", }); pgm.dropIndex("users", "email", { cascade: true, ifExists: true, name: "users_email_idx", }); }; ``` ## Screenshots of all tables with their schema after creating them ### Accounts <img width="903" height="321" alt="Image" src="https://github.com/user-attachments/assets/e30af732-aed7-4022-ac0c-b2fdb80f9572" /> ### Sessions <img width="843" height="290" alt="Image" src="https://github.com/user-attachments/assets/16788ce8-0968-4df8-81fc-2b0d93f97978" /> ### Users <img width="962" height="364" alt="Image" src="https://github.com/user-attachments/assets/e7472830-05e8-4643-b8c6-50855a5585ca" /> ### Verifications <img width="794" height="212" alt="Image" src="https://github.com/user-attachments/assets/b5fbbc76-a762-41d3-9b62-5b007e2a3916" /> ### Client code to update the username in sveltekit **`src/routes/(account)/username/+page.svelte`** ``` <script lang="ts"> import { BetterAuthError } from 'better-auth'; import { BetterFetchError } from '@better-fetch/fetch'; import { client } from '$lib/auth/client'; import { getErrorMessage, hasCodeAndMessage, hasOnlyMessage } from '$lib/auth/errors'; function updateUsername() { const displayUsername = 'something'; const username = 'abracadabra'; return client.updateUser({ displayUsername, username }); } async function onSubmit() { try { const response = await updateUsername(); console.log(response.status, 'success'); } catch (error) { if (error instanceof BetterAuthError) { console.log('better auth error', JSON.stringify(error)); } else if ( error instanceof BetterFetchError && error.status > 0 && hasCodeAndMessage(error) ) { console.log( 'better fetch error 1', JSON.stringify(error), getErrorMessage(error.error.code, 'en') ); } else if (error instanceof BetterFetchError && error.status > 0 && hasOnlyMessage(error)) { console.log('better fetch error 2', JSON.stringify(error), error.error.message); } else if (error instanceof BetterFetchError && error.status === 0) { console.log('network error', error); } else { console.log('unknown error', error); } } } </script> <form onsubmit={onSubmit}> <button>Update Profile</button> </form> ```
GiteaMirror added the lockedbug labels 2026-04-15 16:54:08 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Aug 18, 2025):

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

The error happens because the better-auth username plugin uses the field name displayUsername directly in its code, but your database column is named display_username. The plugin does not respect the field mapping you set in your config, so it tries to update a column that doesn't exist in your database schema. This is a known issue in better-auth 1.3.7—SQL queries reference the camelCase field name instead of the actual snake_case column name in your table, causing the error.

A recent fix (PR #3021) was merged to address schema and field mapping mismatches, but it may not be included in 1.3.7. If upgrading is an option, check if a newer version includes this fix.

For a workaround on 1.3.7, you can either:

  • Rename your database column to displayUsername (not recommended for production, as it breaks naming conventions).
  • Patch the plugin or adapter code to use the mapped field name (display_username) when building queries.
  • If possible, upgrade better-auth to a version that includes the mapping fix.

Let me know if you need help with a specific workaround or patch.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

<!-- gh-comment-id:3196300909 --> @dosubot[bot] commented on GitHub (Aug 18, 2025): <!-- Greeting --> Hi @slidenerd! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The error happens because the better-auth username plugin uses the field name `displayUsername` directly in its code, but your database column is named `display_username`. The plugin does not respect the field mapping you set in your config, so it tries to update a column that doesn't exist in your database schema. This is a known issue in better-auth 1.3.7—SQL queries reference the camelCase field name instead of the actual snake_case column name in your table, causing the error. A recent fix ([PR #3021](https://github.com/better-auth/better-auth/pull/3021)) was merged to address schema and field mapping mismatches, but it may not be included in 1.3.7. If upgrading is an option, check if a newer version includes this fix. For a workaround on 1.3.7, you can either: - Rename your database column to `displayUsername` (not recommended for production, as it breaks naming conventions). - Patch the plugin or adapter code to use the mapped field name (`display_username`) when building queries. - If possible, upgrade better-auth to a version that includes the mapping fix. Let me know if you need help with a specific workaround or patch. <!-- 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/d4bfb8d7-d26f-49a5-b8d0-367916ee6a81?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/d4bfb8d7-d26f-49a5-b8d0-367916ee6a81?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/d4bfb8d7-d26f-49a5-b8d0-367916ee6a81?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/d4bfb8d7-d26f-49a5-b8d0-367916ee6a81?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/d4bfb8d7-d26f-49a5-b8d0-367916ee6a81?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/d4bfb8d7-d26f-49a5-b8d0-367916ee6a81?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/d4bfb8d7-d26f-49a5-b8d0-367916ee6a81?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&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/4066)
Author
Owner

@slidenerd commented on GitHub (Aug 18, 2025):

@dosu how to patch the adapter or plugin?

<!-- gh-comment-id:3196510122 --> @slidenerd commented on GitHub (Aug 18, 2025): @dosu how to patch the adapter or plugin?
Author
Owner

@dosubot[bot] commented on GitHub (Aug 18, 2025):

To patch this in better-auth 1.3.7, you can monkey-patch the username plugin's database hooks so that any time it tries to update or create a user, it rewrites the displayUsername property to display_username before the data hits the database. This works because the plugin uses camelCase (displayUsername), but your DB expects snake_case (display_username).

Here's a practical way to do it in your server setup, before you initialize betterAuth:

import { username } from "better-auth/plugins";

// Patch the username plugin's database hooks
function patchedUsername(options) {
  const orig = username(options);
  const origInit = orig.init;
  orig.init = (ctx) => {
    const plugin = origInit(ctx);
    // Patch the databaseHooks for user create/update
    const hooks = plugin.options?.databaseHooks?.user;
    if (hooks) {
      ["create", "update"].forEach((op) => {
        if (hooks[op]?.before) {
          const origBefore = hooks[op].before;
          hooks[op].before = async (user, context) => {
            // Map displayUsername to display_username
            if ("displayUsername" in user) {
              user.display_username = user.displayUsername;
              delete user.displayUsername;
            }
            return origBefore(user, context);
          };
        }
      });
    }
    return plugin;
  };
  return orig;
}

Then, in your plugin list, use patchedUsername() instead of username():

plugins: [
  admin(),
  captcha({ ... }),
  patchedUsername(),
],

This intercepts the user object before it reaches the DB and ensures the correct field name is used. You can remove this patch once you upgrade to a version where the field mapping is handled automatically.

This approach is safe as long as you only need to patch this one field and you keep the patch up to date with any plugin changes. If you have more fields with similar issues, you can generalize the mapping logic.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

<!-- gh-comment-id:3196533781 --> @dosubot[bot] commented on GitHub (Aug 18, 2025): <!-- Answer --> To patch this in better-auth 1.3.7, you can monkey-patch the username plugin's database hooks so that any time it tries to update or create a user, it rewrites the `displayUsername` property to `display_username` before the data hits the database. This works because the plugin uses camelCase (`displayUsername`), but your DB expects snake_case (`display_username`). Here's a practical way to do it in your server setup, before you initialize `betterAuth`: ```js import { username } from "better-auth/plugins"; // Patch the username plugin's database hooks function patchedUsername(options) { const orig = username(options); const origInit = orig.init; orig.init = (ctx) => { const plugin = origInit(ctx); // Patch the databaseHooks for user create/update const hooks = plugin.options?.databaseHooks?.user; if (hooks) { ["create", "update"].forEach((op) => { if (hooks[op]?.before) { const origBefore = hooks[op].before; hooks[op].before = async (user, context) => { // Map displayUsername to display_username if ("displayUsername" in user) { user.display_username = user.displayUsername; delete user.displayUsername; } return origBefore(user, context); }; } }); } return plugin; }; return orig; } ``` Then, in your plugin list, use `patchedUsername()` instead of `username()`: ```js plugins: [ admin(), captcha({ ... }), patchedUsername(), ], ``` This intercepts the user object before it reaches the DB and ensures the correct field name is used. You can remove this patch once you upgrade to a version where the field mapping is handled automatically. This approach is safe as long as you only need to patch this one field and you keep the patch up to date with any plugin changes. If you have more fields with similar issues, you can generalize the mapping logic. <!-- 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/5ff966e4-f320-4295-8ff9-d19a5358caf0?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/5ff966e4-f320-4295-8ff9-d19a5358caf0?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/5ff966e4-f320-4295-8ff9-d19a5358caf0?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/5ff966e4-f320-4295-8ff9-d19a5358caf0?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/5ff966e4-f320-4295-8ff9-d19a5358caf0?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/5ff966e4-f320-4295-8ff9-d19a5358caf0?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/5ff966e4-f320-4295-8ff9-d19a5358caf0?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&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/4066)
Author
Owner

@slidenerd commented on GitHub (Aug 18, 2025):

@dosu can you rewrite the above snippet with appropriate types in typescript?

<!-- gh-comment-id:3197011596 --> @slidenerd commented on GitHub (Aug 18, 2025): @dosu can you rewrite the above snippet with appropriate types in typescript?
Author
Owner

@dosubot[bot] commented on GitHub (Aug 18, 2025):

Here's a TypeScript version of the monkey-patch for the better-auth username plugin. This ensures that any time the plugin tries to update or create a user, it maps displayUsername (camelCase) to display_username (snake_case) before the DB operation. Types are annotated for clarity and safety.

import { username, UsernameOptions } from "better-auth/plugins";

// You can further type the user object if you have a User interface.
// For now, we use Record<string, any> for compatibility with better-auth internals.
type UserObject = Record<string, any>;
type HookContext = Record<string, any>;

export function patchedUsername(options?: UsernameOptions) {
  const origPlugin = username(options);
  const origInit = origPlugin.init;

  origPlugin.init = (ctx: any) => {
    const plugin = origInit(ctx);
    const hooks = plugin.options?.databaseHooks?.user as
      | Record<"create" | "update", { before?: (user: UserObject, context: HookContext) => Promise<{ data: UserObject }> }>
      | undefined;

    if (hooks) {
      (["create", "update"] as const).forEach((op) => {
        const origBefore = hooks[op]?.before;
        if (origBefore) {
          hooks[op]!.before = async (user: UserObject, context: HookContext): Promise<{ data: UserObject }> => {
            // Map displayUsername to display_username for DB compatibility
            if ("displayUsername" in user) {
              user.display_username = user.displayUsername;
              delete user.displayUsername;
            }
            return origBefore(user, context);
          };
        }
      });
    }
    return plugin;
  };

  return origPlugin;
}

Use patchedUsername() in your plugin list instead of username(). If you have more fields with similar mapping issues, you can generalize the mapping logic inside the hook. This approach matches the plugin's internal types and structure as seen in the source code.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

<!-- gh-comment-id:3197024711 --> @dosubot[bot] commented on GitHub (Aug 18, 2025): <!-- Answer --> Here's a TypeScript version of the monkey-patch for the better-auth username plugin. This ensures that any time the plugin tries to update or create a user, it maps `displayUsername` (camelCase) to `display_username` (snake_case) before the DB operation. Types are annotated for clarity and safety. ```typescript import { username, UsernameOptions } from "better-auth/plugins"; // You can further type the user object if you have a User interface. // For now, we use Record<string, any> for compatibility with better-auth internals. type UserObject = Record<string, any>; type HookContext = Record<string, any>; export function patchedUsername(options?: UsernameOptions) { const origPlugin = username(options); const origInit = origPlugin.init; origPlugin.init = (ctx: any) => { const plugin = origInit(ctx); const hooks = plugin.options?.databaseHooks?.user as | Record<"create" | "update", { before?: (user: UserObject, context: HookContext) => Promise<{ data: UserObject }> }> | undefined; if (hooks) { (["create", "update"] as const).forEach((op) => { const origBefore = hooks[op]?.before; if (origBefore) { hooks[op]!.before = async (user: UserObject, context: HookContext): Promise<{ data: UserObject }> => { // Map displayUsername to display_username for DB compatibility if ("displayUsername" in user) { user.display_username = user.displayUsername; delete user.displayUsername; } return origBefore(user, context); }; } }); } return plugin; }; return origPlugin; } ``` Use `patchedUsername()` in your plugin list instead of `username()`. If you have more fields with similar mapping issues, you can generalize the mapping logic inside the hook. This approach matches the plugin's internal types and structure as seen in the [source code](https://github.com/better-auth/better-auth/blob/efa60af1c2d45a735ed397171bfca513b7e202ca/packages/better-auth/src/plugins/username/index.ts). <!-- 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/8cee6a52-5867-4e8e-b103-420d3eebee13?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/8cee6a52-5867-4e8e-b103-420d3eebee13?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/8cee6a52-5867-4e8e-b103-420d3eebee13?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/8cee6a52-5867-4e8e-b103-420d3eebee13?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/8cee6a52-5867-4e8e-b103-420d3eebee13?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/8cee6a52-5867-4e8e-b103-420d3eebee13?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/8cee6a52-5867-4e8e-b103-420d3eebee13?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&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/4066)
Author
Owner

@slidenerd commented on GitHub (Aug 22, 2025):

@dosu

  • The username updates but display_username is still not set in the postgres table
<!-- gh-comment-id:3213390199 --> @slidenerd commented on GitHub (Aug 22, 2025): @dosu - The username updates but display_username is still not set in the postgres table
Author
Owner

@dosubot[bot] commented on GitHub (Aug 22, 2025):

The reason display_username isn't being set is that better-auth expects updates to use camelCase field names like displayUsername, not snake_case. When you patch the plugin or send update payloads, you must set displayUsername (camelCase). The framework will then map it to display_username in the database, based on your config mapping. If you set display_username directly in your patch or update, better-auth will ignore it and it won't reach the DB.

So: update or patch only displayUsername, not display_username, and let better-auth handle the mapping. This will persist the value to the correct column in your Postgres table as long as your config maps displayUsername to display_username, which your config already does.
Reference

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

<!-- gh-comment-id:3213409153 --> @dosubot[bot] commented on GitHub (Aug 22, 2025): <!-- Answer --> The reason display_username isn't being set is that better-auth expects updates to use camelCase field names like displayUsername, not snake_case. When you patch the plugin or send update payloads, you must set displayUsername (camelCase). The framework will then map it to display_username in the database, based on your config mapping. If you set display_username directly in your patch or update, better-auth will ignore it and it won't reach the DB. So: update or patch only displayUsername, not display_username, and let better-auth handle the mapping. This will persist the value to the correct column in your Postgres table as long as your config maps displayUsername to display_username, which your config already does. [Reference](https://github.com/better-auth/better-auth/blob/fe9c5e428b0a11ec3217bf9c66db978cf54f7ad8/packages/better-auth/src/plugins/username/index.ts) <!-- 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/d7b37f1e-84df-43a0-9d16-06253554bac9?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/d7b37f1e-84df-43a0-9d16-06253554bac9?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/d7b37f1e-84df-43a0-9d16-06253554bac9?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/d7b37f1e-84df-43a0-9d16-06253554bac9?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/d7b37f1e-84df-43a0-9d16-06253554bac9?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/d7b37f1e-84df-43a0-9d16-06253554bac9?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/d7b37f1e-84df-43a0-9d16-06253554bac9?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&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/4066)
Author
Owner

@slidenerd commented on GitHub (Aug 23, 2025):

  • Major unresolved issue on better-auth it seems, documentation really needs to mention this
  • as of 1.3.7 you gotta modify the server side plugin like shown below

plugins: [
	admin(),
	username({
		schema: {
			user: {
				fields: {
					displayUsername: "display_username",
				},
			},
		},
	}),
],
<!-- gh-comment-id:3217063352 --> @slidenerd commented on GitHub (Aug 23, 2025): - Major unresolved issue on better-auth it seems, documentation really needs to mention this - as of 1.3.7 you gotta modify the server side plugin like shown below ``` plugins: [ admin(), username({ schema: { user: { fields: { displayUsername: "display_username", }, }, }, }), ], ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#18451