Collaro Image Service (Rust / Axum)
Status: Draft
Priority: High
Labels: backend, rust, microservice, infrastructure
Related: src/modules/workspace/class.ts, src/modules/workspace/interface.ts, docker-compose.yaml
Problem
Collaro currently handles images in a rudimentary way:
- The workspace
uploadLogo() method accepts a raw string URL and stores it directly in the database — there is no actual file upload, validation, compression, or storage logic.
- User/workspace avatars and logos (see
public/avatar-*.png, public/images/) are served as static Next.js assets, not user-uploaded.
- There is no dedicated image pipeline: no resizing, no format optimisation (WebP/AVIF), no CDN integration, no content-type validation, and no malware scanning.
- As the platform grows, users will need to upload profile photos, workspace logos, meeting banners, and screenshots — the current architecture cannot support this at scale.
A purpose-built Image Service — written in Rust (Axum) and deployed as a sidecar microservice — will offload all image processing from the Next.js main application, giving us performance, type safety, and a clean API boundary.
Goals
- Dedicated image upload API — accept multipart uploads, validate content type and size, store to disk / S3-compatible object storage.
- On-the-fly transformations — resize, crop, format conversion (WebP, AVIF), quality tuning via URL parameters (
?w=200&fmt=webp).
- Serving layer — return processed images with proper cache headers (
Cache-Control, ETag) and immutability-based caching.
- Pluggable storage backend — file system for development, S3/MinIO for production.
- Dockerised sidecar — runs alongside the existing
collaro-app container in docker-compose.yaml, integrates with the collaro-network.
Non-Goals
- No facial recognition, NSFW detection, or ML-based image analysis (future work).
- No video processing (handled by Stream.io).
- No migration of existing static assets in
public/ — new service handles only user-uploaded content.
Proposed Architecture
┌──────────────┐ upload /serve ┌──────────────────┐
│ Next.js App │ ──────────────────▶ │ Image Service │
│ (collaro) │ ◀────────────────── │ (Rust / Axum) │
└──────────────┘ └────────┬─────────┘
│
┌──────────┴──────────┐
│ Storage Backend │
│ (FS / S3 / MinIO) │
└─────────────────────┘
Service API Design
POST /api/images/upload Multipart upload → returns { id, url }
GET /api/images/:id Serve original image
GET /api/images/:id?w=200&h=200 Resize + serve
GET /api/images/:id?fmt=webp Format conversion
DELETE /api/images/:id Remove image
HEAD /api/images/:id Metadata only (type, size, dimensions)
Rust Cargo Dependencies (Tentative)
| Crate |
Purpose |
axum |
HTTP framework |
tokio |
Async runtime |
tower-http |
Middleware (CORS, auth, tracing) |
image |
Image decoding/encoding, resize, crop |
image-webp / image-avif |
Modern format output |
uuid |
Unique image IDs |
serde / serde_json |
JSON serialisation |
tracing / tracing-subscriber |
Structured logging (Pino-compatible) |
aws-sdk-s3 or rust-s3 |
S3-compatible storage |
bytes |
Streaming uploads |
mime_guess |
Content-type detection |
sha2 / hex |
ETag hashing |
anyhow / thiserror |
Error handling |
dotenvy |
Dev environment config |
Storage Layout
/uploads/
{env}/
{first-2-chars-of-uuid}/
{uuid}.{ext} ← original
{uuid}_200x200.webp ← cached transform
{uuid}_640x480.jpg ← cached transform
Implementation Plan
Phase 1 — Skeleton Service (Week 1)
Phase 2 — Upload & Storage (Week 2)
Phase 3 — Transforms & Caching (Week 3)
Phase 4 — Integration with Next.js (Week 4)
Phase 5 — Production Hardening (Week 5)
Integration Points
Docker Compose Changes (docker-compose.yaml)
image-service:
build:
context: ./image-service
dockerfile: Dockerfile.image-service
container_name: collaro-image
restart: unless-stopped
ports:
- "4001:4001"
environment:
- IMAGE_SERVICE_PORT=4001
- STORAGE_BACKEND=fs
- UPLOAD_DIR=/data/uploads
- MAX_FILE_SIZE=10485760
- ALLOWED_MIME_TYPES=image/jpeg,image/png,image/webp,image/avif
volumes:
- image-data:/data/uploads
networks:
- collaro-network
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4001/health"]
interval: 30s
timeout: 5s
retries: 3
volumes:
image-data:
Next.js Integration
A new client module src/lib/image-client.ts:
// Pseudocode — TBD in Phase 4
export const imageService = {
baseUrl: process.env.IMAGE_SERVICE_URL || "http://image-service:4001",
async upload(file: File): Promise<{ id: string; url: string }> { … },
async getUrl(id: string, opts?: TransformOptions): Promise<string> { … },
async delete(id: string): Promise<void> { … },
};
DB Schema Changes
usersTable: add avatar_image_id text
workspacesTable: replace logo text with logo_image_id text
- New table
images (optional, if we need to track metadata server-side)
Acceptance Criteria
- A
curl to POST /api/images/upload with a JPEG returns 200 and a JSON body containing id and url.
GET /api/images/:id?w=200&h=200&fmt=webp returns a WebP image exactly 200×200 pixels.
GET /api/images/:id returns the original with correct Content-Type.
- Uploading a
.exe or a 50 MB file returns 400 / 413 with a descriptive error.
DELETE /api/images/:id returns 204 and subsequent GET returns 404.
- The transformed image endpoint returns
Cache-Control: public, max-age=31536000, immutable.
- The service boots in Docker as part of
docker compose up and passes its health check.
- Next.js can upload a workspace logo through the admin settings UI and the image renders correctly.
tracing logs appear in Loki and are visible in Grafana.
Risks & Mitigations
| Risk |
Mitigation |
| Rust learning curve for the team |
Start with a minimal skeleton; leverage axum examples and community patterns |
| Image processing CPU cost under load |
Cache aggressively; consider libvips bindings (libvips crate) for high-throughput in the future |
| Storage fills up |
Enforce max file size + per-user quota; add a TTL for orphaned images |
| Breaking image URLs after migration |
Use UUID-based paths with no sequential IDs; URL scheme is designed to be permanent |
| CORS misconfiguration blocks uploads |
tower-http CORS layer mirrors existing Next.js allowed origins |
Definition of Done
Collaro Image Service (Rust / Axum)
Status: Draft
Priority: High
Labels:
backend,rust,microservice,infrastructureRelated:
src/modules/workspace/class.ts,src/modules/workspace/interface.ts,docker-compose.yamlProblem
Collaro currently handles images in a rudimentary way:
uploadLogo()method accepts a rawstringURL and stores it directly in the database — there is no actual file upload, validation, compression, or storage logic.public/avatar-*.png,public/images/) are served as static Next.js assets, not user-uploaded.A purpose-built Image Service — written in Rust (Axum) and deployed as a sidecar microservice — will offload all image processing from the Next.js main application, giving us performance, type safety, and a clean API boundary.
Goals
?w=200&fmt=webp).Cache-Control,ETag) and immutability-based caching.collaro-appcontainer indocker-compose.yaml, integrates with thecollaro-network.Non-Goals
public/— new service handles only user-uploaded content.Proposed Architecture
Service API Design
Rust Cargo Dependencies (Tentative)
axumtokiotower-httpimageimage-webp/image-avifuuidserde/serde_jsontracing/tracing-subscriberaws-sdk-s3orrust-s3bytesmime_guesssha2/hexanyhow/thiserrordotenvyStorage Layout
Implementation Plan
Phase 1 — Skeleton Service (Week 1)
cargo init image-service/Cargo.tomlwith core dependenciesGET /health.envor config struct)Dockerfile.image-service(multi-stage Rust build)docker-compose.yamlundercollaro-networktracingwith format compatible with the existing Loki pipelinePhase 2 — Upload & Storage (Week 2)
POST /api/images/uploadwith multipart parsingimage/jpeg,image/png,image/webp,image/avif){ id, url }responseDELETE /api/images/:idendpointPhase 3 — Transforms & Caching (Week 3)
w,h,fmt,q(quality)fit=cover|contain|fill)Cache-Control: public, max-age=31536000, immutablefor cached variantsETagbased on image content hashHEADendpoint returns content-type and dimensions without bodyPhase 4 — Integration with Next.js (Week 4)
uploadLogo()insrc/modules/workspace/class.tswith an HTTP call to the Image Servicesrc/lib/image-client.ts) wrapping fetch requestssrc/app/(root)/(dashboard)/user/settings/)src/app/(root)/(dashboard)/workspace/[slug]/admin/workspace-settings/)usersTableandworkspacesTableschema to reference image service IDs<img>placeholder when no image is uploadedstream/client.tsandstream/class.tsto pass image-service URLs instead ofuserNamePhase 5 — Production Hardening (Week 5)
{ error: string, code: string })GET /metrics) for Grafanaaxum-testorreqwestdocker-compose.yaml(likecollaro-appalready has)Integration Points
Docker Compose Changes (
docker-compose.yaml)Next.js Integration
A new client module
src/lib/image-client.ts:DB Schema Changes
usersTable: addavatar_image_id textworkspacesTable: replacelogo textwithlogo_image_id textimages(optional, if we need to track metadata server-side)Acceptance Criteria
curltoPOST /api/images/uploadwith a JPEG returns200and a JSON body containingidandurl.GET /api/images/:id?w=200&h=200&fmt=webpreturns a WebP image exactly 200×200 pixels.GET /api/images/:idreturns the original with correctContent-Type..exeor a 50 MB file returns400/413with a descriptive error.DELETE /api/images/:idreturns204and subsequentGETreturns404.Cache-Control: public, max-age=31536000, immutable.docker compose upand passes its health check.tracinglogs appear in Loki and are visible in Grafana.Risks & Mitigations
axumexamples and community patternslibvipsbindings (libvipscrate) for high-throughput in the futuretower-httpCORS layer mirrors existing Next.js allowed originsDefinition of Done
mainREADME.md, environment variables, API contract