Using Custom Profile Fields in Logto for Extended User Data

Logto allows you to extend user profiles with arbitrarily-named, typed custom fields that are defined in the admin console or via the Admin API and automatically collected during sign-up.

Logto is an open-source identity management solution developed by the logto-io/logto repository. Using custom profile fields in Logto for extended user data enables you to capture domain-specific information beyond standard attributes like email and name, with full type safety and validation built into the sign-up flow.

Understanding Custom Profile Field Types

Logto supports nine distinct field types defined in packages/schemas/src/types/custom-profile-fields.ts. Each type includes specific configuration options and validation rules:

  • TextProfileField – Free-form text input with optional placeholder text
  • NumberProfileField – Numeric values with min/max constraints
  • DateProfileField – Date pickers for birthdates or other temporal data
  • CheckboxProfileField – Boolean toggle switches
  • SelectProfileField – Dropdown menus with predefined options
  • UrlProfileField – URL validation for website or portfolio links
  • RegexProfileField – Custom regular expression pattern matching
  • AddressProfileField – Structured address data with sub-parts (street, city, postal code), validated against userProfileAddressKeys using fieldPartGuard
  • FullnameProfileField – Composite name fields

All type definitions use Zod guards compiled with satisfies ToZodObject<...>, ensuring runtime validation matches TypeScript type inference throughout the codebase.

Architectural Overview

Field Definitions and Schema

Custom fields are stored in the custom_profile_fields table and managed through layered abstractions. The custom-profile-fields library in packages/core/src/libraries/custom-profile-fields/index.ts provides CRUD operations, while packages/core/src/queries/custom-profile-fields.ts handles direct database access for inserts, updates, deletes, and reordering operations.

When you create a field via POST /api/custom-profile-fields, the request body validates against Zod guards (customProfileFieldGuard, textProfileFieldGuard, etc.) before persistence.

Sign-in Experience Integration

The FullSignInExperience type in packages/schemas/src/types/sign-in-experience.ts (lines 55-62) maintains two arrays:

  1. customProfileFields – Fields displayed during sign-up
  2. customProfileFieldCatalog – Complete field set used by the Account Center and profile pages

The utility function resolveSignUpCustomProfileFields in packages/core/src/libraries/custom-profile-fields/utils.ts computes the ordered list of fields to collect during registration.

Database and Query Layer

The query layer in packages/core/src/queries/custom-profile-fields.ts provides methods for:

  • Batch ordering via displayOrder column updates
  • Field metadata retrieval
  • Row-level operations for the custom_profile_fields table

Managing Custom Profile Fields via the Admin API

The REST endpoints defined in packages/core/src/routes/custom-profile-fields.ts expose full CRUD capabilities. All examples assume an admin access token stored in ADMIN_TOKEN and a base URL https://logto.example.com/api.

Creating and Updating Fields

To create a required text field for company information:

await fetch(`${BASE_URL}/custom-profile-fields`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${ADMIN_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'company',
    label: 'Company',
    type: 'Text',
    required: true,
    config: { placeholder: 'Acme Corp.' },
  }),
});

To modify a field (making it optional):

await fetch(`${BASE_URL}/custom-profile-fields/company`, {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${ADMIN_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ required: false }),
});

To retrieve the complete catalog:

const catalog = await fetch(`${BASE_URL}/custom-profile-fields`, {
  headers: { Authorization: `Bearer ${ADMIN_TOKEN}` },
}).then(res => res.json());
// Returns CustomProfileField[]

Ordering Fields for Sign-up

Control the display order on the sign-up page using the sie-order endpoint:

await fetch(`${BASE_URL}/custom-profile-fields/properties/sie-order`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${ADMIN_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ 
    fieldNames: ['company', 'nickname', 'birthdate'] 
  }),
});

The order array determines the displayOrder column values in the database.

Deleting Fields

Remove a field permanently:

await fetch(`${BASE_URL}/custom-profile-fields/company`, {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${ADMIN_TOKEN}` },
});

Validating Custom Fields During Registration

During sign-up, the validation flow in packages/core/src/routes/experience/classes/libraries/profile-validator.ts ensures data integrity:

  1. The client fetches FullSignInExperience from /api/sign-in-experience
  2. resolveSignUpCustomProfileFields extracts the ordered list of required fields
  3. The validator checks that all required fields contain values before allowing registration completion
  4. Valid data is stored in the user's customData property as { <fieldName>: <value>, ... }

Reserved keys such as email and phone cannot be used for custom field names, as enforced by validation logic around line 298 in the custom profile fields implementation.

Integration Testing

The end-to-end test suite in packages/integration-tests/src/tests/api/custom-profile-fields.test.ts demonstrates field creation, ordering, and sign-up validation workflows, providing reference implementations for custom field lifecycle management.

Summary

  • Nine field types (Text, Number, Date, Checkbox, Select, URL, Regex, Address, Fullname) provide flexible data collection options
  • Type-safe validation via Zod guards ensures data integrity across the TypeScript codebase
  • Admin API endpoints (/custom-profile-fields/*) support full CRUD operations and field ordering
  • Sign-up integration automatically renders required fields and validates input before registration completion
  • Data storage occurs in customData object within the user profile, separate from standard identity claims

Frequently Asked Questions

What reserved keys cannot be used for custom profile fields?

Keys such as email, phone, and other standard user profile attributes are reserved and cannot be used as custom field names. The validation logic in the custom profile fields module (around line 298 in the source) explicitly blocks these identifiers to prevent conflicts with built-in user properties.

How does Logto validate address fields with multiple parts?

The AddressProfileField type uses fieldPartGuard to validate sub-components like street, city, and postal code against the userProfileAddressKeys definition. Each address part undergoes individual validation while maintaining the overall field structure, allowing complex address data to be collected as a single custom field.

Can I make custom fields optional for some users but required for others?

No. The required flag is set at the field definition level in the custom_profile_fields table and applies universally to all users during sign-up. However, you can update this flag dynamically via the Admin API (PUT /custom-profile-fields/{name}) if your business rules change, affecting all subsequent registrations.

Where does Logto store the values collected from custom profile fields?

Collected values are stored in the customData property of the user object as key-value pairs matching the field names. This structure keeps extended user data separate from standard identity claims (email, phone, username) while remaining accessible through the standard user profile API.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →