From 62a2c95863e6eb32840f0147f91fcc38ee57baae Mon Sep 17 00:00:00 2001 From: brunol95 Date: Mon, 11 May 2026 17:13:45 -0400 Subject: [PATCH 1/3] add user.updateMetadata method --- .changeset/user-update-metadata.md | 6 ++++ packages/clerk-js/src/core/resources/User.ts | 8 +++++ .../src/core/resources/__tests__/User.test.ts | 32 +++++++++++++++++++ packages/shared/src/types/user.ts | 11 +++++++ 4 files changed, 57 insertions(+) create mode 100644 .changeset/user-update-metadata.md diff --git a/.changeset/user-update-metadata.md b/.changeset/user-update-metadata.md new file mode 100644 index 00000000000..af887f6aa72 --- /dev/null +++ b/.changeset/user-update-metadata.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': minor +'@clerk/shared': minor +--- + +Add `user.updateMetadata({ unsafeMetadata })` on the `UserResource`. Hits `PATCH /v1/me/metadata` with deep-merge semantics — top-level and nested keys are merged with the existing `unsafeMetadata`, and any key set to `null` is removed. Only `unsafeMetadata` is writable; `publicMetadata` and `privateMetadata` remain Backend-API only. diff --git a/packages/clerk-js/src/core/resources/User.ts b/packages/clerk-js/src/core/resources/User.ts index af80f6704bb..3b96e83f342 100644 --- a/packages/clerk-js/src/core/resources/User.ts +++ b/packages/clerk-js/src/core/resources/User.ts @@ -35,6 +35,7 @@ import type { TOTPJSON, TOTPResource, UpdateMeEnterpriseConnectionParams, + UpdateUserMetadataParams, UpdateUserParams, UpdateUserPasswordParams, UserJSON, @@ -241,6 +242,13 @@ export class User extends BaseResource implements UserResource { }); }; + updateMetadata = (params: UpdateUserMetadataParams): Promise => { + return this._basePatch({ + path: `${this.path()}/metadata`, + body: normalizeUnsafeMetadata(params), + }); + }; + updatePassword = (params: UpdateUserPasswordParams): Promise => { return this._basePost({ body: params, diff --git a/packages/clerk-js/src/core/resources/__tests__/User.test.ts b/packages/clerk-js/src/core/resources/__tests__/User.test.ts index 0dad85bc27e..ec412838809 100644 --- a/packages/clerk-js/src/core/resources/__tests__/User.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/User.test.ts @@ -727,4 +727,36 @@ describe('User', () => { body: params, }); }); + + it('.updateMetadata triggers a PATCH to /me/metadata with stringified unsafe_metadata', async () => { + // @ts-ignore + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: {} })); + + const user = new User({} as unknown as UserJSON); + await user.updateMetadata({ unsafeMetadata: { theme: 'dark', nested: { level: 1 } } }); + + // @ts-ignore + expect(BaseResource._fetch).toHaveBeenCalledWith({ + method: 'PATCH', + path: '/me/metadata', + body: { + unsafeMetadata: JSON.stringify({ theme: 'dark', nested: { level: 1 } }), + }, + }); + }); + + it('.updateMetadata omits unsafe_metadata when not provided', async () => { + // @ts-ignore + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: {} })); + + const user = new User({} as unknown as UserJSON); + await user.updateMetadata({}); + + // @ts-ignore + expect(BaseResource._fetch).toHaveBeenCalledWith({ + method: 'PATCH', + path: '/me/metadata', + body: {}, + }); + }); }); diff --git a/packages/shared/src/types/user.ts b/packages/shared/src/types/user.ts index 43e5aa0a492..ec9488b8cfd 100644 --- a/packages/shared/src/types/user.ts +++ b/packages/shared/src/types/user.ts @@ -108,6 +108,7 @@ export interface UserResource extends ClerkResource, BillingPayerMethods { createdAt: Date | null; update: (params: UpdateUserParams) => Promise; + updateMetadata: (params: UpdateUserMetadataParams) => Promise; delete: () => Promise; updatePassword: (params: UpdateUserPasswordParams) => Promise; removePassword: (params: RemoveUserPasswordParams) => Promise; @@ -187,6 +188,16 @@ type UpdateUserJSON = Pick< export type UpdateUserParams = Partial>; +/** + * Parameters for {@link UserResource.updateMetadata}. Only `unsafeMetadata` + * is end-user-writable on the Frontend API. The submitted value is deep-merged + * with the existing `unsafeMetadata`; keys at any level whose value is `null` + * are removed. + */ +export type UpdateUserMetadataParams = { + unsafeMetadata?: UserUnsafeMetadata; +}; + export type UpdateUserPasswordParams = { newPassword: string; currentPassword?: string; From bdec0ff16052632e61f44599cc3c2de3f9009af3 Mon Sep 17 00:00:00 2001 From: brunol95 Date: Tue, 12 May 2026 14:48:17 -0400 Subject: [PATCH 2/3] unsafe_metadata required on UpdateUserMetadataParams --- .../clerk-js/src/core/resources/__tests__/User.test.ts | 8 +++++--- packages/shared/src/types/user.ts | 8 ++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/clerk-js/src/core/resources/__tests__/User.test.ts b/packages/clerk-js/src/core/resources/__tests__/User.test.ts index ec412838809..4e56eaf1b5d 100644 --- a/packages/clerk-js/src/core/resources/__tests__/User.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/User.test.ts @@ -745,18 +745,20 @@ describe('User', () => { }); }); - it('.updateMetadata omits unsafe_metadata when not provided', async () => { + it('.updateMetadata sends an explicit null patch when a key is being removed', async () => { // @ts-ignore BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: {} })); const user = new User({} as unknown as UserJSON); - await user.updateMetadata({}); + await user.updateMetadata({ unsafeMetadata: { theme: null as unknown as undefined } }); // @ts-ignore expect(BaseResource._fetch).toHaveBeenCalledWith({ method: 'PATCH', path: '/me/metadata', - body: {}, + body: { + unsafeMetadata: JSON.stringify({ theme: null }), + }, }); }); }); diff --git a/packages/shared/src/types/user.ts b/packages/shared/src/types/user.ts index ec9488b8cfd..5f6c392cc93 100644 --- a/packages/shared/src/types/user.ts +++ b/packages/shared/src/types/user.ts @@ -190,12 +190,12 @@ export type UpdateUserParams = Partial>; /** * Parameters for {@link UserResource.updateMetadata}. Only `unsafeMetadata` - * is end-user-writable on the Frontend API. The submitted value is deep-merged - * with the existing `unsafeMetadata`; keys at any level whose value is `null` - * are removed. + * is end-user-writable on the Frontend API and the field is required: the + * submitted value is deep-merged with the existing `unsafeMetadata`, and keys + * at any level whose value is `null` are removed. */ export type UpdateUserMetadataParams = { - unsafeMetadata?: UserUnsafeMetadata; + unsafeMetadata: UserUnsafeMetadata; }; export type UpdateUserPasswordParams = { From 59982f45ef58bc8830ac5168c3ae2ea319f64c8b Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Wed, 13 May 2026 10:39:52 -0700 Subject: [PATCH 3/3] chore: update changeset --- .changeset/user-update-metadata.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/user-update-metadata.md b/.changeset/user-update-metadata.md index af887f6aa72..7b183fb5d29 100644 --- a/.changeset/user-update-metadata.md +++ b/.changeset/user-update-metadata.md @@ -3,4 +3,4 @@ '@clerk/shared': minor --- -Add `user.updateMetadata({ unsafeMetadata })` on the `UserResource`. Hits `PATCH /v1/me/metadata` with deep-merge semantics — top-level and nested keys are merged with the existing `unsafeMetadata`, and any key set to `null` is removed. Only `unsafeMetadata` is writable; `publicMetadata` and `privateMetadata` remain Backend-API only. +Add `user.updateMetadata()` for updating current user's metadata attributes by merging existing values with the provided parameters.