Error when adding a customField when items in table #3023

Closed
opened 2026-03-13 10:34:57 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @DarkNasuada on GitHub (Mar 9, 2026).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. fill default table with data
  2. add field to schema with property required, unique and defaultvalue
  3. run npx auth migrate or generate

Current vs. Expected behavior

Current:
default value is not in sql statement causing an error when running migrate on a table already populated with data

Expected:
default value in statement so that table gets populated with default value.

What version of Better Auth are you using?

1.5.4

System info

{
  "system": {
    "platform": "win32",
    "arch": "x64",
    "version": "Windows 11 Pro",
    "release": "10.0.26100",
    "cpuCount": 28,
    "cpuModel": "Intel(R) Core(TM) i7-14700",
    "totalMemory": "31.68 GB",
    "freeMemory": "5.99 GB"
  },
  "node": {
    "version": "v25.6.1",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.9.0"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "^16.1.6"
    },
    {
      "name": "react",
      "version": "^19.2.4"
    }
  ],
  "databases": null,
  "betterAuth": {
    "version": "^1.5.4",
    "config": null
  }
}

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

Backend

Auth config (if applicable)

{
	experimental: { joins: true },
	baseURL: process.env.BETTER_AUTH_URL,
	secret: process.env.BETTER_AUTH_SECRET,
	database: {
		dialect,
		type: 'mssql',
	},
	databaseHooks: {
		session: {
			create: {
				before: async (session, context) => {
					if (!context || session.activeOrganizationId) {
						return
					}

					const adapter = context.context.adapter
					const memberships = await adapter.findMany<{ organizationId: string }>({
						model: 'member',
						where: [
							{
								field: 'userId',
								value: session.userId,
							},
						],
						sortBy: {
							field: 'createdAt',
							direction: 'asc',
						},
						limit: 1,
					})

					const membership = memberships[0]

					let activeTeamId: string | null = null

					const teamMemberships = await adapter.findMany<{ teamId: string; team?: { organizationId?: string | null } | null }>({
						model: 'teamMember',
						where: [
							{
								field: 'userId',
								value: session.userId,
							},
						],
						join: { team: true },
						sortBy: {
							field: 'createdAt',
							direction: 'asc',
						},
					})

					const matchingTeamMembership = teamMemberships.find((teamMembership) => teamMembership.team?.organizationId === membership.organizationId)

					if (matchingTeamMembership) {
						activeTeamId = matchingTeamMembership.teamId
						//add retrieval of team roles here and add to session if found
					}
					return {
						data: {
							...session,
							activeOrganizationId: membership.organizationId,
							activeTeamId: activeTeamId || '',
						},
					}
				},
			},
		},
	},
	trustedOrigins: [process.env.NEXT_PUBLIC_SITE_URL].filter((origin): origin is string => !!origin),
	rateLimit: {
		enabled: true,
	},
	plugins: [
		apiKey({
			configId: 'org-keys',
			defaultPrefix: 'org_',
			references: 'organization', // Owned by organizations
		}),
		nextCookies(),
		admin({
			defaultRole: 'user',
			ac: appAccessControl,
			roles: organizationRoles,
		}),
		organization({
			allowUserToCreateOrganization: false,
			ac: appAccessControl,
			roles: organizationRoles,
			teams: {
				enabled: true,
				allowRemovingAllTeams: false,
			},
			schema: {
				team: {
					additionalFields: {
						customerNumber: {
							type: 'string',
							required: true,
							returned: true,
							unique: true,
						},
						regionColor: {
							type: 'string',
							required: true,
							defaultValue: '#F18C30',
							returned: true,
							unique: true,
						},
						lat: {
							type: 'number',
							returned: true,
						},
						lng: {
							type: 'number',
							returned: true,
						},
					},
				},
			},
		}),
		magicLink({
			disableSignUp: true,
			sendMagicLink: async ({ email, url }, ctx) => {
				const recipientList: Parameters<typeof sendMail>[3] = [{ emailAddress: { address: email } }]
				await sendMail('Aanmelden via magic link', 'html', `<p>Klik op onderstaande link om in te loggen.</p><p><a href="${url}">Inloggen</a></p>`, recipientList)
			},
			expiresIn: 60 * 15,
		}),
	],
	socialProviders: {
		microsoft: {
			clientId: process.env.AZURE_AD_CLIENT_ID,
			clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
			tenantId: process.env.AZURE_AD_TENANT_ID,
			scope: ['openid', 'profile', 'email', 'User.Read', 'offline_access'],
			errorCallbackURL: '/signin?error=social-login-failed',
		},
	},
})

Additional context

Generate result:

alter table "team" add "regionColor" varchar(255) not null unique;

alter table "team" add "lat" integer not null;

alter table "team" add "lng" integer not null;

Originally created by @DarkNasuada on GitHub (Mar 9, 2026). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. fill default table with data 2. add field to schema with property required, unique and defaultvalue 3. run npx auth migrate or generate ### Current vs. Expected behavior Current: default value is not in sql statement causing an error when running migrate on a table already populated with data Expected: default value in statement so that table gets populated with default value. ### What version of Better Auth are you using? 1.5.4 ### System info ```bash { "system": { "platform": "win32", "arch": "x64", "version": "Windows 11 Pro", "release": "10.0.26100", "cpuCount": 28, "cpuModel": "Intel(R) Core(TM) i7-14700", "totalMemory": "31.68 GB", "freeMemory": "5.99 GB" }, "node": { "version": "v25.6.1", "env": "development" }, "packageManager": { "name": "npm", "version": "11.9.0" }, "frameworks": [ { "name": "next", "version": "^16.1.6" }, { "name": "react", "version": "^19.2.4" } ], "databases": null, "betterAuth": { "version": "^1.5.4", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript { experimental: { joins: true }, baseURL: process.env.BETTER_AUTH_URL, secret: process.env.BETTER_AUTH_SECRET, database: { dialect, type: 'mssql', }, databaseHooks: { session: { create: { before: async (session, context) => { if (!context || session.activeOrganizationId) { return } const adapter = context.context.adapter const memberships = await adapter.findMany<{ organizationId: string }>({ model: 'member', where: [ { field: 'userId', value: session.userId, }, ], sortBy: { field: 'createdAt', direction: 'asc', }, limit: 1, }) const membership = memberships[0] let activeTeamId: string | null = null const teamMemberships = await adapter.findMany<{ teamId: string; team?: { organizationId?: string | null } | null }>({ model: 'teamMember', where: [ { field: 'userId', value: session.userId, }, ], join: { team: true }, sortBy: { field: 'createdAt', direction: 'asc', }, }) const matchingTeamMembership = teamMemberships.find((teamMembership) => teamMembership.team?.organizationId === membership.organizationId) if (matchingTeamMembership) { activeTeamId = matchingTeamMembership.teamId //add retrieval of team roles here and add to session if found } return { data: { ...session, activeOrganizationId: membership.organizationId, activeTeamId: activeTeamId || '', }, } }, }, }, }, trustedOrigins: [process.env.NEXT_PUBLIC_SITE_URL].filter((origin): origin is string => !!origin), rateLimit: { enabled: true, }, plugins: [ apiKey({ configId: 'org-keys', defaultPrefix: 'org_', references: 'organization', // Owned by organizations }), nextCookies(), admin({ defaultRole: 'user', ac: appAccessControl, roles: organizationRoles, }), organization({ allowUserToCreateOrganization: false, ac: appAccessControl, roles: organizationRoles, teams: { enabled: true, allowRemovingAllTeams: false, }, schema: { team: { additionalFields: { customerNumber: { type: 'string', required: true, returned: true, unique: true, }, regionColor: { type: 'string', required: true, defaultValue: '#F18C30', returned: true, unique: true, }, lat: { type: 'number', returned: true, }, lng: { type: 'number', returned: true, }, }, }, }, }), magicLink({ disableSignUp: true, sendMagicLink: async ({ email, url }, ctx) => { const recipientList: Parameters<typeof sendMail>[3] = [{ emailAddress: { address: email } }] await sendMail('Aanmelden via magic link', 'html', `<p>Klik op onderstaande link om in te loggen.</p><p><a href="${url}">Inloggen</a></p>`, recipientList) }, expiresIn: 60 * 15, }), ], socialProviders: { microsoft: { clientId: process.env.AZURE_AD_CLIENT_ID, clientSecret: process.env.AZURE_AD_CLIENT_SECRET, tenantId: process.env.AZURE_AD_TENANT_ID, scope: ['openid', 'profile', 'email', 'User.Read', 'offline_access'], errorCallbackURL: '/signin?error=social-login-failed', }, }, }) ``` ### Additional context Generate result: alter table "team" add "regionColor" varchar(255) not null unique; alter table "team" add "lat" integer not null; alter table "team" add "lng" integer not null;
GiteaMirror added the bug label 2026-03-13 10:34:57 -05:00
Author
Owner

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

This is by design - Better Auth's defaultValue is an application-level default, not a database-level default. The type definition explicitly states: "This will not create a default value on the database level. It will only be used when creating a new record."

The migration generator converts required: trueNOT NULL and unique: trueUNIQUE, but doesn't generate DEFAULT clauses (except for date fields with function defaults → CURRENT_TIMESTAMP).

Workarounds for adding fields to existing tables:

  1. Use required: false - Avoids NOT NULL constraint violations on existing rows:

    regionColor: {
      type: 'string',
      required: false,  // Allows NULL for existing rows
      defaultValue: '#F18C30',  // Still applied to new records
      returned: true,
      unique: true,
    }
    
  2. Two-step migration - Add nullable first, then populate:

    -- After migration runs, manually update existing rows:
    UPDATE "team" SET "regionColor" = '#F18C30' WHERE "regionColor" IS NULL;
    
  3. Manually edit generated SQL before running:

    ALTER TABLE "team" ADD "regionColor" varchar(255) NOT NULL DEFAULT '#F18C30' UNIQUE;
    

This is a known gap where required: true creates database constraints but defaultValue only applies at the application layer.

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 9, 2026): <!-- Answer --> This is by design - Better Auth's `defaultValue` is an **application-level default**, not a database-level default. The [type definition explicitly states](https://github.com/better-auth/better-auth/blob/297a1ca5af8b6bb6b8e18fc86434c7bdbdbe42f1/packages/core/src/db/type.ts#L200-L206): "This will not create a default value on the database level. It will only be used when creating a new record." The [migration generator](https://github.com/better-auth/better-auth/blob/297a1ca5af8b6bb6b8e18fc86434c7bdbdbe42f1/packages/better-auth/src/db/get-migration.ts#L427-L470) converts `required: true` → `NOT NULL` and `unique: true` → `UNIQUE`, but doesn't generate `DEFAULT` clauses (except for date fields with function defaults → `CURRENT_TIMESTAMP`). **Workarounds for adding fields to existing tables:** 1. **Use `required: false`** - Avoids `NOT NULL` constraint violations on existing rows: ```typescript regionColor: { type: 'string', required: false, // Allows NULL for existing rows defaultValue: '#F18C30', // Still applied to new records returned: true, unique: true, } ``` 2. **Two-step migration** - Add nullable first, then populate: ```sql -- After migration runs, manually update existing rows: UPDATE "team" SET "regionColor" = '#F18C30' WHERE "regionColor" IS NULL; ``` 3. **Manually edit generated SQL** before running: ```sql ALTER TABLE "team" ADD "regionColor" varchar(255) NOT NULL DEFAULT '#F18C30' UNIQUE; ``` This is a [known gap](https://github.com/better-auth/better-auth/issues/2504) where `required: true` creates database constraints but `defaultValue` only applies at the application layer. <!-- 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/2e74e3b1-3faf-424c-9ef6-d48c85da9ffb?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/2e74e3b1-3faf-424c-9ef6-d48c85da9ffb?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/2e74e3b1-3faf-424c-9ef6-d48c85da9ffb?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/2e74e3b1-3faf-424c-9ef6-d48c85da9ffb?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/2e74e3b1-3faf-424c-9ef6-d48c85da9ffb?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/2e74e3b1-3faf-424c-9ef6-d48c85da9ffb?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/2e74e3b1-3faf-424c-9ef6-d48c85da9ffb?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=This%20is%20by%20design%20-%20Better%20Auth%27s%20%60defaultValue%60%20is%20an%20%2A%2Aapplication-level%20default%2A%2A%2C%20not%20a%20database-level%20default.%20The%20%5Btype%20definition%20explicitly%20states%5D%28https%3A//github.com/better-auth/better-auth/blob/297a1ca5af8b6bb6b8e18fc86434c7bdbdbe42f1/packages/core/src/db/type.ts%23L200-L206%29%3A%20%22This%20will%20not%20create%20a%20default%20value%20on%20the%20database%20level.%20It%20will%20only%20be%20used%20when%20creating%20a%20new%20record.%22%0A%0AThe%20%5Bmigration%20generator%5D%28https%3A//github.com/better-auth/better-auth/blob/297a1ca5af8b6bb6b8e18fc86434c7bdbdbe42f1/packages/better-auth/src/db/get-migration.ts%23L427-L470%29%20converts%20%60required%3A%20true%60%20%E2%86%92%20%60NOT%20NULL%60%20and%20%60unique%3A%20true%60%20%E2%86%92%20%60UNIQUE%60%2C%20but%20doesn%27t%20generate%20%60DEFAULT%60%20clauses%20%28except%20for%20date%20fields%20with%20function%20defaults%20%E2%86%92%20%60CURRENT_TIMESTAMP%60%29.%0A%0A%2A%2AWorkarounds%20for%20adding%20fields%20to%20existing%20tables%3A%2A%2A%0A%0A1.%20%2A%2AUse%20%60required%3A%20false%60%2A%2A%20-%20Avoids%20%60NOT%20NULL%60%20constraint%20violations%20on%20existing%20rows%3A%0A%20%20%20%60%60%60typescript%0A%20%20%20regionColor%3A%20%7B%0A%20%20%20%20%20type%3A%20%27string%27%2C%0A%20%20%20%20%20required%3A%20false%2C%20%20//%20Allows%20NULL%20for%20existing%20rows%0A%20%20%20%20%20defaultValue%3A%20%27%23F18C30%27%2C%20%20//%20Still%20applied%20to%20new%20records%0A%20%20%20%20%20returned%3A%20true%2C%0A%20%20%20%20%20unique%3A%20true%2C%0A%20%20%20%7D%0A%20%20%20%60%60%60%0A%0A2.%20%2A%2ATwo-step%20migration%2A%2A%20-%20Add%20nullable%20first%2C%20then%20populate%3A%0A%20%20%20%60%60%60sql%0A%20%20%20--%20After%20migration%20runs%2C%20manually%20update%20existing%20rows%3A%0A%20%20%20UPDATE%20%22team%22%20SET%20%22regionColor%22%20%3D%20%27%23F18C30%27%20WHERE%20%22regionColor%22%20IS%20NULL%3B%0A%20%20%20%60%60%60%0A%0A3.%20%2A%2AManually%20edit%20generated%20SQL%2A%2A%20before%20running%3A%0A%20%20%20%60%60%60sql%0A%20%20%20ALTER%20TABLE%20%22team%22%20ADD%20%22regionColor%22%20varchar%28255%29%20NOT%20NULL%20DEFAULT%20%27%23F18C30%27%20UNIQUE%3B%0A%20%20%20%60%60%60%0A%0AThis%20is%20a%20%5Bknown%20gap%5D%28https%3A//github.com/better-auth/better-auth/issues/2504%29%20where%20%60required%3A%20true%60%20creates%20database%20constraints%20but%20%60defaultValue%60%20only%20applies%20at%20the%20application%20layer.)&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/8515)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#3023