Skip to main content

User Metadata

The User Metadata API is a small key/value store attached to each user, scoped to the calling OAuth client. Reach for it when you need to persist lightweight per-user state — UI preferences, onboarding flags, last-used settings — without standing up a database of your own.

Data Model

Metadata entries live in the user_app_metadata table and are uniquely identified by the triple (user_id, client_id, key):

FieldTypeNotes
keystringURL-safe: must match [a-zA-Z0-9._-]+.
valuestring (TEXT column)Required; max 65535 characters.
expires_atISO-8601 datetime/nullOptional; if set, must be in the future. Expired entries are purged by a scheduled job.

Scoping is per OAuth client, not global. The client_id is taken from the access token used to call the API, so two different applications writing to the key theme for the same user keep completely separate values. There is no way to read another client's metadata.

Values are opaque strings. If you need structured data, serialize to JSON yourself before PUT and parse on read.

Reading Metadata

List every key the calling app has stored for the authenticated user:

curl https://identity.eurofurence.org/api/v2/metadata \
-H "Authorization: Bearer $ACCESS_TOKEN"
{
"data": [
{ "key": "theme", "value": "dark", "expires_at": null },
{ "key": "locale", "value": "en", "expires_at": null }
]
}

Fetch a single key:

curl https://identity.eurofurence.org/api/v2/metadata/theme \
-H "Authorization: Bearer $ACCESS_TOKEN"
{ "key": "theme", "value": "dark", "expires_at": null }

A missing key returns 404 Not Found.

Writing Metadata

PUT is create-or-update (upsert). The URL path carries the key; the JSON body carries the value.

curl -X PUT https://identity.eurofurence.org/api/v2/metadata/theme \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": "dark"}'

Returns 201 Created on first write, 200 OK on subsequent updates. The response body is the stored resource:

{ "key": "theme", "value": "dark", "expires_at": null }

You can also set an optional expiry:

curl -X PUT https://identity.eurofurence.org/api/v2/metadata/onboarding_banner \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": "dismissed", "expires_at": "2026-12-31T23:59:59Z"}'

expires_at must be in the future. Expired entries are removed by a scheduled prune job.

Deleting Metadata

curl -X DELETE https://identity.eurofurence.org/api/v2/metadata/theme \
-H "Authorization: Bearer $ACCESS_TOKEN"

Returns 204 No Content on success, 404 Not Found if the key does not exist for this user and client.

Scopes and Auth

All endpoints require a user-bearing access token on the auth:api guard (i.e. issued via the normal OAuth2 authorization code flow, not raw client credentials — the token must resolve to a user).

OperationRequired scope
GET /metadatametadata.read
GET /metadata/{key}metadata.read
PUT /metadata/{key}metadata.write
DELETE /metadata/{key}metadata.write

Requests missing the required scope are rejected with 403 Forbidden.

Reference

See the API v2 reference for the full endpoint list.