imageUrl joined live from identityIn-content member avatars (Add-Matter team step, the Settings members table, member pickers) were initials-only because FirmMemberListItem carried no image. The audit feed already solved the identical problem for actors: auditEventView.actorImageUrl — "Acting human's current avatar URL (joined live from identity at read time); null for agent/system or users without a photo." Members deserved the same treatment.
The avatar is already synced into identity.users.image_url by the Clerk webhook — the same source that feeds the audit actor join. So this Story is a minimal expose / join, not new sync infrastructure.
| Piece | Change |
|---|---|
DTO FirmMemberListItem | Adds image_url: str | None (serialized imageUrl via the camelCase alias generator). Description mirrors the audit field: "Member's current avatar URL (joined live from identity at read time); null for users without a photo." |
Service AppFirmService._member_item | Populates image_url=user.image_url. Every call path (list, firm-role assign, reactivate, deactivate) already passes the full User entity, so one line covers the list endpoint and every member-detail response. |
| Read path | No new query. list_members already JOINs identity.users and selects the full User row; image_url rides that existing join — identical pattern to the audit query's User.image_url.label("actor_image_url") outerjoin. |
| Contract | make openapi regenerated; openapi/app-api.json carries imageUrl on the schema so pramaan-app regenerates src/gen. admin-api.json unchanged (app-only surface). |
identity.users.image_url is str | None, so a member without a Clerk photo surfaces imageUrl: null — never an empty string, never a missing key. The app renders the initials fallback on null. The integration test exercises both states in one request: a member with an avatar and the caller without one.Integration: real request against a real (empty-seeded) firm DB — GET /app/firm/members returns [caller, member]; imageUrl == [null, member_avatar]. Member's identity.users.image_url set via SQL; caller left null. Full-shape round-trip (model_dump by_alias == body) guards the whole contract. Contract: make openapi regenerated + idempotent; imageUrl present on FirmMemberListItem schema in openapi/app-api.json. 33 firm routes + dtos + contract tests pass. ruff clean. 0 new mypy errors (image_url str|None matches User.image_url).
<img> + initials fallback on the Add-Matter team step, members table, and pickers, consuming this contract.