Signet stores three independent JSON objects on each user. Their names and access boundaries match Clerk's public/private/unsafe split:
| Field | Ordinary user/session responses | Browser write | Admin/backend write |
|---|---|---|---|
publicMetadata | yes | no | yes |
privateMetadata | no | no | yes |
unsafeMetadata | yes | yes | yes |
All three are native PostgreSQL jsonb columns. They share an 8192-byte encoded limit per user, must be JSON objects, and are not copied into authentication cookies. Empty objects are omitted from user responses so an account with no metadata retains the stock better-auth user shape.
Browser-owned onboarding data
unsafeMetadata is the only bucket accepted during email sign-up:
{
"email": "reader@example.com",
"password": "correct horse battery staple",
"name": "Reader",
"unsafeMetadata": {
"onboarding": { "step": 1 },
"newsletter": true
}
}An authenticated user can replace it through POST {base_path}/update-user:
{
"unsafeMetadata": {
"onboarding": { "step": 2 }
}
}The object is replaced, not deep-merged. Send {} to clear it; omit the field to leave it unchanged. A browser request containing publicMetadata or privateMetadata is rejected rather than silently ignored. Never use unsafeMetadata for authorization, billing identity, or another decision the user must not control.
Backend-owned data
An admin can set any bucket in the data object accepted by the better-auth admin plugin's POST {base_path}/admin/create-user and POST {base_path}/admin/update-user routes. The same creation contract is available to the instance operator at POST /admin/v1/users with the admin Bearer key.
{
"email": "member@example.com",
"name": "Member",
"data": {
"publicMetadata": { "plan": "pro" },
"privateMetadata": { "billingId": "cus_123" },
"unsafeMetadata": { "onboarding": { "step": 1 } }
}
}Admin responses include non-empty private metadata. Ordinary sign-up, sign-in, session, user, and JWT user shapes never serialize privateMetadata; they include non-empty public and unsafe objects. The server enforces the combined size after applying a replacement, so an update cannot push an existing user's three buckets over the limit.
The metadata limit applies to application metadata, not large documents or file storage. Store a stable identifier here and keep the larger value in the application's own table.