Skip to content

Implement Image Service using Rust (Axum) #25

Description

@Coderx85

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

  1. Dedicated image upload API — accept multipart uploads, validate content type and size, store to disk / S3-compatible object storage.
  2. On-the-fly transformations — resize, crop, format conversion (WebP, AVIF), quality tuning via URL parameters (?w=200&fmt=webp).
  3. Serving layer — return processed images with proper cache headers (Cache-Control, ETag) and immutability-based caching.
  4. Pluggable storage backend — file system for development, S3/MinIO for production.
  5. 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)

  • Scaffold Rust project with cargo init image-service/
  • Implement Cargo.toml with core dependencies
  • Basic Axum server with health endpoint GET /health
  • Configuration via environment variables (.env or config struct)
  • Add Dockerfile.image-service (multi-stage Rust build)
  • Register service in docker-compose.yaml under collaro-network
  • Wire up tracing with format compatible with the existing Loki pipeline

Phase 2 — Upload & Storage (Week 2)

  • Implement POST /api/images/upload with multipart parsing
  • Validate: allowed MIME types (image/jpeg, image/png, image/webp, image/avif)
  • Validate: max file size (configurable, default 10 MB)
  • Store original to filesystem (dev) / S3 (prod)
  • Return { id, url } response
  • Generate UUID v7 for each upload
  • DELETE /api/images/:id endpoint

Phase 3 — Transforms & Caching (Week 3)

  • Implement query-parameter parsing for w, h, fmt, q (quality)
  • Resize with aspect-ratio preservation (fit=cover|contain|fill)
  • Format conversion: JPEG ↔ PNG ↔ WebP ↔ AVIF
  • Cache transformed images on disk
  • Set Cache-Control: public, max-age=31536000, immutable for cached variants
  • ETag based on image content hash
  • HEAD endpoint returns content-type and dimensions without body

Phase 4 — Integration with Next.js (Week 4)

  • Replace the naive uploadLogo() in src/modules/workspace/class.ts with an HTTP call to the Image Service
  • Create a shared TypeScript client (src/lib/image-client.ts) wrapping fetch requests
  • Wire up avatar upload in user settings (src/app/(root)/(dashboard)/user/settings/)
  • Wire up workspace logo upload in workspace settings (src/app/(root)/(dashboard)/workspace/[slug]/admin/workspace-settings/)
  • Update usersTable and workspacesTable schema to reference image service IDs
  • Add a fallback <img> placeholder when no image is uploaded
  • Update stream/client.ts and stream/class.ts to pass image-service URLs instead of userName

Phase 5 — Production Hardening (Week 5)

  • Rate limiting per IP/user (configurable RPM)
  • Request size limiting middleware
  • S3/MinIO integration for production storage
  • Graceful shutdown (SIGTERM handler)
  • Structured error responses ({ error: string, code: string })
  • Prometheus metrics endpoint (GET /metrics) for Grafana
  • Integration tests with axum-test or reqwest
  • Health check in docker-compose.yaml (like collaro-app already has)

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

  1. A curl to POST /api/images/upload with a JPEG returns 200 and a JSON body containing id and url.
  2. GET /api/images/:id?w=200&h=200&fmt=webp returns a WebP image exactly 200×200 pixels.
  3. GET /api/images/:id returns the original with correct Content-Type.
  4. Uploading a .exe or a 50 MB file returns 400 / 413 with a descriptive error.
  5. DELETE /api/images/:id returns 204 and subsequent GET returns 404.
  6. The transformed image endpoint returns Cache-Control: public, max-age=31536000, immutable.
  7. The service boots in Docker as part of docker compose up and passes its health check.
  8. Next.js can upload a workspace logo through the admin settings UI and the image renders correctly.
  9. 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

  • All five phases implemented and merged to main
  • Integration tests cover upload, transform, delete, and error paths
  • Workspace logo upload & user avatar upload work end-to-end through the UI
  • Docker Compose stack boots cleanly with the new service
  • Grafana dashboard shows image-service metrics (request count, latency, error rate)
  • Documentation updated: README.md, environment variables, API contract

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions