Summary
The owner console (odin-js packages/apps/owner-app) still writes profile attributes by uploading files directly to the ProfileDrive. It does not use the backend profile service (ProfileAttributeService, exposed as PUT /api/v2/profile/attributes and PUT /api/v2/profile/attributes/photo). The owner console must either be migrated onto that API or be decommissioned — two writers for the same data is the source of the divergence described below.
Current state (verified by reading code)
odin-core exposes a server-side write surface:
src/apps/Odin.Hosting/UnifiedV2/Profile/V2ProfileController.cs — PUT /api/v2/profile/attributes, PUT /api/v2/profile/attributes/photo, DELETE /api/v2/profile/attributes/{id}
src/services/Odin.Services/Profile/ProfileAttributeService.cs — all writes funnel here, gated on the ManageProfile permission
src/services/Odin.Services/Profile/ProfilePublishService.cs — republishes sitedata.json, public_image.json (/pub/image) and public_profile.json (/pub/profile) off DriveFileAdded/Changed/Deleted notifications; registered in TenantServices.cs:328
odin-js does not call it. The owner console writes the attribute file itself:
packages/apps/owner-app/src/provider/profile/AttributeData/ManageAttributeProvider.ts → saveProfileAttribute → uploadFile / patchFile against the ProfileDrive; photoAttributeProcessing builds the prfl_key payload plus 250/600px thumbnails client-side
packages/apps/owner-app/src/hooks/profiles/useAttribute.ts → on success calls useStaticFiles().publish(type)
packages/common/common-app/src/hooks/staticFiles/useStaticFiles.ts → publishProfile / publishProfileImage / publishProfileCard → POST /optimization/cdn/{publish,profileimage,profilecard}
There are no /api/v2 profile calls anywhere in js-lib or owner-app; DotYouClient.getEndpoint() resolves to /api/owner/v1 (or /api/apps/v1, /api/guest/v1).
For contrast, contacts have already made this move: odin-js contact writes go through ContactService via ContactsControllerBase (/api/{owner|apps}/v1/contacts), specifically so the server-side merge owns the record. Profile attributes are the remaining client-side writer.
Why this matters
The client-owned publish step is conditional and fails silently:
useStaticFiles.ts:22 gates publishProfileImage/publishProfileCard on dotYouClient.getType() === ApiType.Owner. This matches odin-core, where profileimage/profilecard exist only on OwnerStaticFileContentController — AppStaticFileContentController inherits only publish. So a photo saved from any app context never republishes /pub/image.
ProfileCardManager.ts:245 only publishes a Photo attribute whose ACL is Anonymous and whose section is PersonalInfoSectionId. Anything else publishes nothing and leaves the previously published bytes being served.
- The client decides what the public artifacts contain (image resize, profile-card shape, which attributes are public). That logic is now duplicated between
ProfileCardManager.ts and ProfilePublishService.cs, and the two sets are already documented as deliberately different (see the ProfileCardTriggerTypes comment in ProfilePublishService.cs:84).
Observed symptom that prompted this: saving a profile image through odin-js did not update the public profile image.
Note that ProfilePublishService already covers the direct-upload path server-side (it reacts to any ProfileDrive attribute write, fileType 77, and reuses the client's 250x250 thumbnail), so on current main the publish step is arguably redundant. That makes the duplication a liability rather than a safety net — but it also means the fix is not only "swap the write call", since the client-side publish and the server-side republish can both fire for the same save.
Requested outcome
Pick one and close the gap:
Option A — migrate the owner console.
- Route profile attribute writes through
PUT /api/v2/profile/attributes and PUT /api/v2/profile/attributes/photo
- Drop the client-side
publishProfile / publishProfileImage / publishProfileCard calls from the attribute save path and rely on ProfilePublishService
- Confirm the V2 write surface covers everything the console writes today: Theme attributes live on the HomePageConfigDrive and are explicitly out of scope for
ProfileAttributeService (ProfilePublishService.cs:68-71), and Experience/header-image attributes carry their own payload keys and thumbnail sets — these need either V2 coverage or a documented carve-out
- Decide the fate of
/optimization/cdn/{publish,profileimage,profilecard} once no client calls them
Option B — decommission the owner console in favor of whatever replaces it, and make the replacement a V2-only client from the start.
Open questions
- Is there a target date / replacement for the owner console that makes Option B the real plan?
- Does
SetPhotoAttributeAsync accept the same rendition set the console produces today (250 + 600px), given the server does not resize?
- Do other odin-js consumers (homebase-id-app, homebase-web) write profile attributes directly as well? They were not audited here.
Verification note
Everything under "Current state" was verified by reading odin-core main (e3f78eade) and the local odin-js working copy. The claim that the observed stale public image was caused by one of the specific gates above is inferred, not confirmed — it was not reproduced against a running server, and no tenant logs were inspected for the swallowed Failed to republish public_image.json warning that TryRunAsync would emit.
Summary
The owner console (odin-js
packages/apps/owner-app) still writes profile attributes by uploading files directly to the ProfileDrive. It does not use the backend profile service (ProfileAttributeService, exposed asPUT /api/v2/profile/attributesandPUT /api/v2/profile/attributes/photo). The owner console must either be migrated onto that API or be decommissioned — two writers for the same data is the source of the divergence described below.Current state (verified by reading code)
odin-core exposes a server-side write surface:
src/apps/Odin.Hosting/UnifiedV2/Profile/V2ProfileController.cs—PUT /api/v2/profile/attributes,PUT /api/v2/profile/attributes/photo,DELETE /api/v2/profile/attributes/{id}src/services/Odin.Services/Profile/ProfileAttributeService.cs— all writes funnel here, gated on theManageProfilepermissionsrc/services/Odin.Services/Profile/ProfilePublishService.cs— republishessitedata.json,public_image.json(/pub/image) andpublic_profile.json(/pub/profile) offDriveFileAdded/Changed/Deletednotifications; registered inTenantServices.cs:328odin-js does not call it. The owner console writes the attribute file itself:
packages/apps/owner-app/src/provider/profile/AttributeData/ManageAttributeProvider.ts→saveProfileAttribute→uploadFile/patchFileagainst the ProfileDrive;photoAttributeProcessingbuilds theprfl_keypayload plus 250/600px thumbnails client-sidepackages/apps/owner-app/src/hooks/profiles/useAttribute.ts→ on success callsuseStaticFiles().publish(type)packages/common/common-app/src/hooks/staticFiles/useStaticFiles.ts→publishProfile/publishProfileImage/publishProfileCard→POST /optimization/cdn/{publish,profileimage,profilecard}There are no
/api/v2profile calls anywhere injs-liborowner-app;DotYouClient.getEndpoint()resolves to/api/owner/v1(or/api/apps/v1,/api/guest/v1).For contrast, contacts have already made this move: odin-js contact writes go through
ContactServiceviaContactsControllerBase(/api/{owner|apps}/v1/contacts), specifically so the server-side merge owns the record. Profile attributes are the remaining client-side writer.Why this matters
The client-owned publish step is conditional and fails silently:
useStaticFiles.ts:22gatespublishProfileImage/publishProfileCardondotYouClient.getType() === ApiType.Owner. This matches odin-core, whereprofileimage/profilecardexist only onOwnerStaticFileContentController—AppStaticFileContentControllerinherits onlypublish. So a photo saved from any app context never republishes/pub/image.ProfileCardManager.ts:245only publishes a Photo attribute whose ACL is Anonymous and whose section isPersonalInfoSectionId. Anything else publishes nothing and leaves the previously published bytes being served.ProfileCardManager.tsandProfilePublishService.cs, and the two sets are already documented as deliberately different (see theProfileCardTriggerTypescomment inProfilePublishService.cs:84).Observed symptom that prompted this: saving a profile image through odin-js did not update the public profile image.
Note that
ProfilePublishServicealready covers the direct-upload path server-side (it reacts to any ProfileDrive attribute write, fileType77, and reuses the client's 250x250 thumbnail), so on currentmainthe publish step is arguably redundant. That makes the duplication a liability rather than a safety net — but it also means the fix is not only "swap the write call", since the client-side publish and the server-side republish can both fire for the same save.Requested outcome
Pick one and close the gap:
Option A — migrate the owner console.
PUT /api/v2/profile/attributesandPUT /api/v2/profile/attributes/photopublishProfile/publishProfileImage/publishProfileCardcalls from the attribute save path and rely onProfilePublishServiceProfileAttributeService(ProfilePublishService.cs:68-71), and Experience/header-image attributes carry their own payload keys and thumbnail sets — these need either V2 coverage or a documented carve-out/optimization/cdn/{publish,profileimage,profilecard}once no client calls themOption B — decommission the owner console in favor of whatever replaces it, and make the replacement a V2-only client from the start.
Open questions
SetPhotoAttributeAsyncaccept the same rendition set the console produces today (250 + 600px), given the server does not resize?Verification note
Everything under "Current state" was verified by reading odin-core
main(e3f78eade) and the local odin-js working copy. The claim that the observed stale public image was caused by one of the specific gates above is inferred, not confirmed — it was not reproduced against a running server, and no tenant logs were inspected for the swallowedFailed to republish public_image.jsonwarning thatTryRunAsyncwould emit.