How Logto Manages User Data: Architecture and Implementation Guide

Logto manages user data through a layered architecture that separates schema definitions, database queries, business logic libraries, and HTTP routes, ensuring type-safe operations with Zod validation and centralized user lifecycle management.

The logto-io/logto repository implements a robust user management system that organizes data handling into distinct layers. This architecture ensures strict type safety, prevents data leakage, and provides a clear separation between database operations, business logic, and API exposure.

Layered Architecture Overview

Logto organizes user data management into four primary layers:

Schema Definitions and Type Safety

User Schema Structure

The foundation of Logto's user data management resides in packages/schemas/src/types/user.ts, which defines the User type using Zod validation. The schema explicitly enumerates safe fields for public API exposure through the userInfoSelectFields array:

export const userInfoSelectFields = Object.freeze([
  'id',
  'username',
  'primaryEmail',
  'primaryPhone',
  'name',
  'avatar',
  'customData',
  'identities',
  'lastSignInAt',
  'createdAt',
  'updatedAt',
  'profile',
  'applicationId',
  'isSuspended',
] satisfies Array<keyof User>);

The userInfoGuard created from this list guarantees that every API response conforms to the declared shape. This prevents accidental leakage of internal columns like password hashes or encryption metadata.

Core Business Logic Library

The createUserLibrary Factory

All high-level user operations live in the user library instantiated by createUserLibrary(tenantId, queries) in packages/core/src/libraries/user.ts. The library receives a Queries object that abstracts the underlying PostgreSQL tables (users, roles, organizations, SSO identities).

Key functions include:

  • insertUser: Generates a short-id, inserts the row, assigns default and custom roles, and emits a developer event for admin-tenant creations
  • checkIdentifierCollision: Validates uniqueness of email, phone, username, and SSO identities before creation or updates
  • verifyUserPassword: Validates supplied passwords against stored hashes, automatically upgrading to Argon2i when needed
  • signOutUser: Revokes all token and session instances for the supplied user ID
  • provisionOrganizations: Automatically adds users to JIT-derived organizations based on email domain, SSO connector, or explicit organization lists after sign-up

The library also encapsulates MFA handling via addUserMfaVerification and SSO identity retrieval through findUserSsoIdentities.

HTTP Routes and API Exposure

The library layer connects to HTTP endpoints that serve the admin console and experience SPA:

Both routes rely on the library's findUserById and updateUserById helpers, ensuring consistent validation and business rules across all entry points.

Data Flow Example: User Sign-Up

The following flow illustrates how user data moves through the architecture during creation:

  1. API Request: The admin console sends POST /api/users with a CreateUser payload
  2. Validation: The route handler calls userLibrary.checkIdentifierCollision to guarantee uniqueness of identifiers
  3. Password Security: encryptUserPassword from packages/core/src/utils/user.ts creates an Argon2i hash, storing passwordEncrypted and passwordEncryptionMethod
  4. Persistence: userLibrary.insertUser writes the new row, assigns roles, and returns the persisted User object
  5. Organization Provisioning: provisionOrganizations executes, adding the user to auto-join organizations based on domain or connector rules
  6. Response Filtering: The route filters results through userInfoGuard before sending safe fields to the client

All steps remain type-checked and validated by Zod guards defined in the schema file, maintaining a stable contract for external callers.

Code Examples

Creating a User via the Library

import { createUserLibrary } from '#src/libraries/user.js';
import type { Queries } from '#src/tenants/Queries.js';

const userLib = createUserLibrary('my-tenant-id', queries);

await userLib.checkIdentifierCollision({
  primaryEmail: 'alice@example.com',
  username: 'alice',
});

const [newUser] = await userLib.insertUser({
  username: 'alice',
  primaryEmail: 'alice@example.com',
  passwordEncrypted: '',
  passwordEncryptionMethod: '',
  name: 'Alice',
});

console.log('Created user id:', newUser.id);

Verifying Password During Authentication

import { createUserLibrary } from '#src/libraries/user.js';
import { queries } from '#src/tenants/queries.js';

const userLib = createUserLibrary('my-tenant-id', queries);
const user = await queries.users.findUserById('user-id');

try {
  const verifiedUser = await userLib.verifyUserPassword(user, 'plainPassword');
  console.log('Login successful for', verifiedUser.id);
} catch (err) {
  console.error('Invalid credentials');
}

Retrieving Current User Profile

import fetch from 'node-fetch';

const response = await fetch('http://localhost:3001/api/me/user', {
  headers: { Authorization: `Bearer ${accessToken}` },
});

const profile = await response.json(); // conforms to UserInfo
console.log(profile.username, profile.primaryEmail);

Summary

  • Logto implements a four-layer architecture (schema, database, library, routes) to manage user data with strict separation of concerns
  • Zod guards in packages/schemas/src/types/user.ts enforce type safety and prevent sensitive field leakage through userInfoSelectFields
  • The createUserLibrary factory in packages/core/src/libraries/user.ts centralizes all business logic including collision detection, password verification, and organization provisioning
  • HTTP routes in packages/core/src/routes-me/user.ts expose only filtered user data, while packages/core/src/routes/user-assets.ts handles binary content
  • All user creation flows enforce Argon2i password hashing and automatic JIT organization assignment before returning sanitized responses

Frequently Asked Questions

How does Logto prevent sensitive user data from leaking in API responses?

Logto uses the userInfoSelectFields array and userInfoGuard Zod schema in packages/schemas/src/types/user.ts to explicitly whitelist only public-safe fields. When routes prepare responses, they filter user objects through these guards, ensuring internal columns like passwordEncrypted and passwordEncryptionMethod never reach clients.

What happens when a user signs up with an email domain that matches an organization?

The provisionOrganizations function in packages/core/src/libraries/user.ts automatically evaluates the user's email domain, SSO connector, or explicit organization list immediately after insertUser completes. If matching JIT (Just-In-Time) provisioning rules exist, the function adds the user to the appropriate organizations before the API returns the created user object.

Where is user password hashing implemented in Logto?

Password hashing logic resides in packages/core/src/utils/user.ts through the encryptUserPassword function, which uses Argon2i by default. During verification, verifyUserPassword in packages/core/src/libraries/user.ts compares supplied passwords against stored hashes and automatically migrates older hash formats to Argon2i when detected.

Can I extend the user schema with custom fields?

Yes, the customData field in the User schema accepts JSON objects for application-specific extensions. The core schema remains stable, but customData provides a flexible storage mechanism without requiring database migrations. Access this data through the standard library functions in createUserLibrary or the /api/me/user endpoint.

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 →