---
title: User Data Model
tab: developer
---

## Overview

The `User` object represents an authenticated user in the Business Platform. It extends `TenantEntity` and contains identity, permission, and profile information alongside platform-specific metadata.

## Core Fields

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | Unique identifier for the user |
| `email` | `string` | Canonical email address. Single source of truth — `profileFields.email` only stores privacy metadata |
| `emailVerified` | `boolean` | Better Auth flag indicating the email has been verified |
| `image` | `string \| null` | Profile image URL (from Better Auth / OAuth providers) |
| `displayName` | `string` | Human-readable display name (computed from `profileFields`, not stored) |
| `language` | `string` | Preferred UI language as an ISO 639-1 code (e.g. `"de"`, `"en"`). When set, overrides browser detection and is used by server-rendered content such as emails |
| `permissions` | `string[]` | List of permissions in the format `{TENANT_ID}::{PERMISSION}`, e.g. `67f3b695bb457009cc645282::read` |
| `profileFields` | `UserProfileFields` | Map of custom profile field values |
| `badges` | `EarnedBadge[]` | Array of badges earned by the user (see [Badges](#badges)) |
| `passwordChangeDisabled` | `boolean` | If `true`, the user cannot change their password |
| `projectCreationDisabled` | `boolean` | If `true`, the user cannot create new projects |
| `profileFieldChangeDisabled` | `boolean` | If `true`, the user cannot edit their own profile fields |
| `messagingDisabled` | `boolean` | If `true`, the user cannot create messages or message threads |
| `isPlatformOwner` | `boolean` | If `true`, the user has platform-wide admin access (e.g. can run migrations) |
| `recentProjects` | `Record<string, number>` | Map of `projectId` → last-opened timestamp (ms) |
| `lastSeenReleaseVersion` | `string` | Last release version the user has seen (for multi-device release-notes tracking) |
| `personalAgentPermissions` | `Record<string, Record<string, { read, write }>>` | Per-project, per-type AI tool permissions for the personal agent |
| `optInDecisions` | `UserOptInDecision[]` | Record of the user's opt-in decisions |

## Badges

Users earn badges as they complete key actions on the platform. The `badges` field on the `User` object stores all badges a user has earned.

### EarnedBadge

Each entry in the `badges` array conforms to the `EarnedBadge` interface:

| Field | Type | Description |
|-------|------|-------------|
| `name` | `string` | The unique identifier of the badge (see [Badge Names](#badge-names)) |
| `earnedAt` | `number` | Unix timestamp (milliseconds) indicating when the badge was awarded |

**Example:**

```json
{
  "badges": [
    {
      "name": "projectCreated",
      "earnedAt": 1713800000000
    },
    {
      "name": "firstApplicationCreated",
      "earnedAt": 1713900000000
    }
  ]
}
```

### Badge Names

The following badge names are defined in `BADGE_NAMES`:

| Constant | Value | Awarded When |
|----------|-------|--------------|
| `PROJECT_CREATED` | `"projectCreated"` | User successfully creates their first project |
| `FIRST_ACTION_CREATED` | `"firstActionCreated"` | User creates their first action within a project |
| `FIRST_PROJECT_MEMBER_INVITE` | `"firstProjectMemberInvite"` | User invites the first member to a project |
| `FIRST_DATA_TYPE_CREATED` | `"firstDataTypeCreated"` | User creates their first data type |
| `FIRST_APPLICATION_CREATED` | `"firstApplicationCreated"` | User creates their first application |

### Badge Awarding Behavior

- Badges are awarded **at most once per user**. If a user already holds a badge, subsequent qualifying actions do not re-award it.
- Badge checks occur server-side via the `checkBadge` utility after the relevant resource is successfully created.
- When a badge is newly awarded, the user receives a system notification.
- Badge data is persisted on the `User` document in the `badges` array.

### Viewing Badges

Users can view their earned and locked badges through the **Badges** button in the user profile panel. The modal displays:

- **Earned badges** — shown with a success border, badge icon, description, and the date the badge was earned.
- **Locked badges** — shown in greyscale with a lock indicator, listing what needs to be done to earn them.

The overall progress is shown as a count: *earned / total available*.

## LocalAuthUser

`LocalAuthUser` is a session-scoped subset of `User` used during request handling. The projection lives in `packages/server/src/auth/session-user-freshness.ts` and is an explicit allow-list — only the fields listed here are copied from the `User` document onto the session and become available on the client as `page.data.session.user`.

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | User identifier |
| `createdAt` | `number` | Creation timestamp (ms) |
| `updatedAt` | `number` | Last-update timestamp (ms) |
| `tenantId` | `string` | Tenant the user belongs to |
| `email` | `string` | Canonical email address |
| `permissions` | `string[]` | Effective permissions for the session |
| `profileFields` | `UserProfileFields` | Profile field values |
| `badges` | `EarnedBadge[]` | Earned badges, mirrored from the `User` document |
| `optInDecisions` | `UserOptInDecision[]` | Opt-in decisions |
| `revision` | `number` | Document revision number used for optimistic concurrency |
| `passwordChangeDisabled` | `boolean` | Mirrored flag |
| `projectCreationDisabled` | `boolean` | Mirrored flag |
| `profileFieldChangeDisabled` | `boolean` | Mirrored flag |
| `messagingDisabled` | `boolean` | Mirrored flag |
| `isPlatformOwner` | `boolean` | Mirrored flag |
| `recentProjects` | `Record<string, number>` | Mirrored map of `projectId` → timestamp (ms) |
| `lastSeenReleaseVersion` | `string` | Mirrored last-seen release version |
| `language` | `string` | Preferred UI language (ISO 639-1 code); used by the client to pre-set i18n on session hydration |
| `personalAgentPermissions` | `Record<string, Record<string, { read, write }>>` | Personal-agent tool permissions |

> Fields on `User` that are **not** carried into `LocalAuthUser` (e.g. `image`, `emailVerified`) must be re-fetched server-side when needed.

## Schema Validation

The `UserSchema` validates the `badges` field as an array of objects, each requiring both `name` (string) and `earnedAt` (number) properties. Documents that do not conform to this shape will fail validation.
