A person's photo
A person can add one photo to their account, frame it on the client, and everyone in their homes sees the same face. The client controls pan and zoom, the server accepts only the square the client framed, and the stored image is re-encoded into a fixed 512 × 512 WebP. The photo is available through a public URL with no token, while account endpoints let the person upload, view, or remove their own image.
Workflow
Section titled “Workflow”The client does the framing. In the console, AvatarEditor lets the person drag to move and slide to zoom. On the phone, the crop sheet does the same job. The upload is the exact square the person framed, sent as raw image bytes.
The server rejects a non-square upload. The error includes the size, such as “send a square: this image is 500 by 400”, and it allows a small two percent tolerance for phone rounding. The server does not crop behind the client’s back, so what the person centres stays centred everywhere.
After upload, the image is decoded and written again as one 512 × 512 WebP. EXIF orientation is applied, and metadata is dropped. That means camera words, location data, and other hidden file content do not leave the person’s phone. The server identifies the file type from the bytes’ own signature, not from a client-chosen header, and refuses a header that disagrees with the bytes.
Routes
Section titled “Routes”| Route | Who | What |
|---|---|---|
PUT /auth/me/avatar | the person | Sends the raw image body. Accepts image/jpeg, image/png, or image/webp, with a 5 MB cap and a 4096 px cap. Returns { avatarUrl, updatedAt }. |
GET /auth/me/avatar | the person | Returns the person’s own image copy, with a strong ETag and private cache handling. |
DELETE /auth/me/avatar | the person | Removes the photo and returns 204. The account goes back to the initial state. |
GET /avatars/:userId/:key.webp | anyone holding the URL | Returns the image with Cache-Control: public, max-age=31536000, immutable and nosniff. |
Only PUT /auth/me/avatar parses a raw body. Every other route in auth-service stays JSON. The gateway sends /avatars to auth-service without authentication, like /auth.
URL behavior
Section titled “URL behavior”The image is served on /avatars/<userId>/<key>.webp with no token. That works because an <img> tag cannot send a bearer, and a phone should not need one. The key is 128 random bits created per upload and removed on deletion, so nobody can guess a person’s photo from the user id alone. When the photo changes, the URL changes too, and the old URL stops answering at once.
A stale key, an unknown person, and “no photo” all return 404. The image response is cached as immutable for a year, because a changed photo gets a changed URL. GET /auth/me answers the current avatarUrl, and so does the members list from phase C.
Storage and account scope
Section titled “Storage and account scope”There is one row per person in UserAvatar, and the stored image is about 30 KB. That fits the database well: one backup, one restore, and the row moves with the account. The photo belongs to the person, not to a home, so the same face appears in every home they belong to.
Client behavior
Section titled “Client behavior”The console uses a 1024 px JPEG output from AvatarEditor. It shows the Photo card on Your settings and the user chip in the top bar. The phone converts HEIC to JPEG before sending, and it sends an exact square.
Limits
Section titled “Limits”- HEIC is not accepted. A browser cannot decode it, and the image pipeline does not have libheif for it. If a person picks HEIC in the console, the client says so before upload.
- There is no moderation and no face detection. A home’s members are the people who chose each other.
- The photo is per person, not per home.
Access boundaries
Section titled “Access boundaries”Household permissions are narrower than staff permissions. DEV credentials act in the developer sandbox. LIVE credentials only work for their own installed connector.
Implementation notes for developers
Section titled “Implementation notes for developers”Use the upload route for the raw image body, then read back avatarUrl and updatedAt from the response. Use GET /auth/me/avatar when you need the person’s own current image copy, and use the public /avatars/:userId/:key.webp URL when you need an image source that an <img> tag can load directly.