Skip to content
PharmaFlow Knowledgebase
Esc
↑↓navigate↵open⌘Jpreview
On this page

Images and Branding

Upload medicine photos and the org logo through presigned S3 URLs with type and size validation.

You store images in S3 through presigned URLs so the browser uploads bytes directly. The routes live in apps/server/src/features/images/route.ts, the business writes live in apps/server/src/features/images/service.ts, and the S3 helpers live in apps/server/src/core/storage/storage.ts. Every route requires authentication.

Configuring storage

You set AWS_S3_BUCKET, AWS_ENDPOINT_URL_S3, AWS_ACCESS_KEY_ID, and AWS_SECRET_ACCESS_KEY before any upload works, as listed in apps/server/.env.example. You may also set AWS_REGION, which defaults to us-east-2, and AWS_S3_PUBLIC_BASE; without the public base, public URLs fall back to the endpoint plus bucket. Missing config returns 500 with Image storage is not configured.

Accepted types and size limits

You may upload JPEG, PNG, and WebP only. Medicine images cap at 5 MB and branding images cap at 2 MB, enforced in validateImageUpload inside apps/server/src/core/storage/storage.ts. Keys must start with medicines/ or branding/ respectively, carry a matching image extension, and never contain .. or backslashes, checked by isValidMedicineImageKey and isValidBrandingImageKey in the same file.

Uploading a medicine photo

You need the admin, inventory_manager, or branch_manager role. You ask for a presigned URL, PUT the bytes to it, then attach the key to the medicine.

curl -X POST http://localhost:8000/images/medicines/upload-url \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"content_type":"image/jpeg","size_bytes":412000}'

You get key, uploadUrl, and publicUrl. Upload with a plain PUT that matches the declared content type:

curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: image/jpeg" \
  --data-binary @photo.jpg

Then you attach the key. The server verifies the object exists with a HEAD request via getMedicineImageStatus, rejects missing or oversized uploads with 400, and writes image_key plus image_url onto the medicine row.

curl -X POST http://localhost:8000/images/medicines/MEDICINE_ID/image \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"image_key":"medicines/abc123.jpg"}'

Attaching emits a medicines sync change marked global and logs a medicine IMAGE_UPDATE audit row, so every device picks up the new photo on its next pull.

Branding is admin-only. You follow the same two-step flow against the branding prefix, then set the logo with PUT /images/branding/logo. The server checks the object with getBrandingImageStatus and rejects anything missing or over 2 MB.

curl -X POST http://localhost:8000/images/branding/upload-url \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"content_type":"image/png","size_bytes":180000}'
curl -X PUT http://localhost:8000/images/branding/logo \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"image_key":"branding/abc123.png"}'

The logo lands in the single org_settings row with id org, anchored by SETTINGS_ID in apps/server/src/features/settings/queries.ts and created when missing by setBrandLogo inserting through insertOrgSettings in apps/server/src/features/images/mutations.ts. You read the logo back with GET /settings, whose logo_url is resolved by resolveLogoUrl in apps/server/src/features/settings/queries.ts. Every logo change logs an org_settings LOGO_UPDATE audit row.

Handling failures

You retry the attach step when you see Image upload not found or larger than 5 MB or Logo upload not found or larger than 2 MB: the PUT either has not finished or the declared size was wrong. You treat Invalid image key and Invalid branding image key as a client bug, since the key must come from the upload-url response. Presigned URLs expire after 900 seconds, so you request a fresh one when the PUT returns an S3 expiry error.

Next steps

Sync the new image rows to devices in /platform/sync-protocol, trace logo changes in /platform/audit-log, and review the stock behind the photos in /inventory/medicines-and-batches.

Last updated on October 20, 2026