BTST
PluginsQuickstartDocs
Live Blog

From research to product evaluation

Evaluating a publishing workflow for an app you already own?

See what the BTST Blog plugin adds to an existing React or Next.js app
BTST

Open-source TypeScript features for the React application, data, and deployment you already own.

Released plugins

  • Blog
  • AI Chat
  • CMS
  • Form Builder
  • UI Builder
  • Kanban
  • Comments
  • Media
  • Route Docs
  • OpenAPI
  • Better Auth UI

Resources

  • Quickstart
  • Documentation
  • All plugins
  • Live Blog
  • GitHub (opens in a new tab)
© 2026 BTST. Open source under the MIT License.
AI Chat
September 19, 2026ReactBetter Auth UI

Add Custom Profile Fields to BTST Better Auth UI

Define an editable profile field across storage, server validation, sign-up, and account settings without exposing protected data.

Add Custom Profile Fields to BTST Better Auth UI

A profile field needs a stored schema, a server input policy, and a form that exposes it. BTST Better Auth UI can render additional fields at sign-up and in account settings, but its labels and client validation do not define the server's data rules.

This guide adds an optional department field to an existing auth integration using @btst/better-auth-ui@2.0.1, @btst/stack@3.1.2, and better-auth@1.6.16. The field is user-supplied profile information. It must not determine organization membership, billing entitlements, or access permissions.

Define storage and validation in Better Auth#

Merge this fragment into the existing server's user.additionalFields, preserving other user options and fields:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
import type { BetterAuthOptions } from "better-auth";
import { z } from "zod";

export const profilePolicy = {
  user: {
    additionalFields: {
      department: {
        type: "string",
        required: false,
        validator: {
          input: z.string().trim().max(80),
        },
      },
    },
  },
} satisfies BetterAuthOptions;

Generate and review the corresponding auth schema with the tools for your installed Better Auth version, then migrate through the adapter's normal workflow. An optional field lets existing users continue without a backfill. If you make a field required later, plan how older records and every registration path obtain a valid value first.

The pinned input parser applies the input validator when a value is supplied and uses its parsed result. This example trims surrounding whitespace and rejects values longer than 80 characters. Empty text is allowed, so a user can clear the field without submitting null.

Keep this validator synchronous. The pinned parser rejects asynchronous input validation; a database lookup belongs in an appropriate server hook or application operation. A UI-only validation callback cannot protect direct HTTP requests.

Describe the field and choose where it appears#

Merge these fragments into the existing Stack provider's overrides.auth and overrides.account, respectively:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
import type {
  AccountPluginOverrides,
  AuthPluginOverrides,
} from "@btst/better-auth-ui/client";

export const profileAuthUI = {
  additionalFields: {
    department: {
      label: "Department",
      type: "string",
      required: false,
      placeholder: "Engineering",
      instructions: "Optional; up to 80 characters.",
      validate: async (value: string) => value.trim().length <= 80,
    },
  },
  signUp: {
    fields: ["name", "department"],
  },
} satisfies Partial<AuthPluginOverrides>;

export const profileAccountUI = {
  account: {
    fields: ["name", "department"],
  },
} satisfies Partial<AccountPluginOverrides>;

These arrays are the fields for this example. Retain other existing fields, such as image, if your application already exposes them. The account client plugin must be mounted for account settings, normally /p/account/settings.

Declaring an entry in additionalFields supplies its UI description. The sign-up and account fields arrays decide where that description is used. An entry missing from those arrays does not automatically appear on both screens.

The released account field card and sign-up submit handler both invoke the custom validate callback for string values before sending their mutations. That gives feedback in both forms. Retain the server length bound because a direct HTTP request bypasses both callbacks.

Infer custom fields in your own client code#

If application code also reads or updates the field, add Better Auth's inference plugin to the existing auth client, retaining its other plugins:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
import { createAuthClient } from "better-auth/react";
import { inferAdditionalFields } from "better-auth/client/plugins";
import type { auth } from "@/lib/auth";

export const authClient = createAuthClient({
  plugins: [inferAdditionalFields<typeof auth>()],
});

The import of auth is type-only. Keep the actual server module, database adapter, and secrets out of the browser bundle. For independently built client and server projects, use the upstream additional-field guidance and keep the two schemas aligned.

Inference describes the API to TypeScript. It neither migrates the database nor validates a malicious request. Inspect the authoritative user response after a save when verifying persistence; optimistic UI state alone is insufficient.

Keep protected fields out of self-service updates#

Use this pattern for editable profile data. Do not expose an administrator flag, organization role, or paid-plan status merely by adding it to a field list. Such values need server-controlled writes and permission checks at the operation that uses them.

Better Auth additional fields support input: false for data that clients must not set. Configure that protection on the server and omit the field from self-service UI. A field hidden from a form remains writable through HTTP if the server still accepts it. Conversely, input: false restricts writes; it does not by itself make a field secret or remove it from returned user data.

Test registration, updates, and rejection#

Use a disposable account and test the actual configured adapter:

  1. Sign up without a department, then with a valid one. Both should succeed because the field is optional.
  2. Submit surrounding whitespace and confirm the stored value is trimmed. Submit more than 80 characters and confirm the server rejects it without changing the prior value.
  3. Update from account settings, reload the session, and confirm the saved value. Clear it with an empty string.
  4. Repeat the oversized update by direct HTTP request so the test bypasses the UI callback. Confirm the server still rejects it.
  5. Try an anonymous update and a write to a separately configured input: false field. Confirm neither changes protected data.

The fragments were type-checked, and focused in-memory handler checks cover optional registration, normalization, rejected writes, clearing, and protected-field behavior. Those checks do not run a production migration or the full UI flow. Continue with the BTST auth documentation for wiring and the auth-page customization guide for page-level presentation.

In This Post

Define storage and validation in Better AuthDescribe the field and choose where it appearsInfer custom fields in your own client codeKeep protected fields out of self-service updatesTest registration, updates, and rejection