Skip to main content

Storage & Upload Pipeline โ€” Frontpics Image Marketplace

Frontpics stores image metadata in PostgreSQL and image files in a runtime-selectable storage backend. The production default is Cloudflare R2, but S3-compatible and local storage are supported.

Storage Backendsโ€‹

The active backend is configured in Redis:

config:storage_backend = r2 | s3 | local

Backends:

  • r2: Cloudflare R2, default production backend.
  • s3: S3-compatible object storage.
  • local: development backend with signed local downloads.

Existing files remain in the backend and storage profile where they were created. Switching the active backend only affects new writes.

Important database fields:

  • ImageFile.cdnUrl
  • ImageFile.fileSizeBytes
  • ImageFile.storageBucket
  • ImageFile.storageKey
  • ImageFile.storageProfileId

Do not introduce legacy aliases such as url or sizeBytes.

File Variantsโ€‹

The pipeline stores an original and generated variants. Public browsing uses thumbnails and watermarked previews. Licensed downloads use signed access to larger/original assets.

Typical variants:

original photographer upload, private
large high-quality processed variant
medium preview/download support variant
thumbnail public browsing image
watermarked public preview image

Upload Flowโ€‹

1. Presigned Upload Requestโ€‹

The web app calls:

POST /api/v1/upload/presigned

The API validates file count, MIME type, and size, then returns upload targets for the active storage backend.

2. Browser Uploadโ€‹

The browser uploads directly to object storage. File bytes do not pass through the API server.

3. Duplicate Checkโ€‹

The upload UI computes SHA-256 hashes and calls:

POST /api/v1/upload/check-duplicates

Duplicates are blocked before confirmation where possible.

4. Upload Confirmationโ€‹

The web app calls:

POST /api/v1/upload/confirm

Each confirmed file includes metadata:

{
"fileId": "uuid",
"metadata": {
"title": "Facade at Aleksanterinkatu",
"description": "Winter facade view",
"isDrone": false,
"basePriceCredits": 15,
"allowRelicense": false,
"tags": ["facade"],
"locationTags": ["kruununhaka", "helsinki"],
"fileHash": "sha256",
"location": {
"addressLine1": "Aleksanterinkatu 1",
"city": "Helsinki",
"postalCode": "00100",
"countryCode": "FI",
"neighborhood": "Kruununhaka",
"latitude": 60.1699,
"longitude": 24.9384
}
}
}

The API creates the Image, Location, ImageTag, and processing queue records.

Address and Coordinate Handlingโ€‹

The upload UI uses:

  • EXIF GPS extraction;
  • reverse geocoding for GPS coordinates;
  • address autocomplete for manually selected locations;
  • forward geocoding for manually entered address fields;
  • manual latitude/longitude fields as a fallback.

Photoshoot requests now follow the same structured lookup behavior for new requests. Photographer job maps require Location.latitude and Location.longitude.

Worker Processingโ€‹

The image processing worker consumes BullMQ jobs and:

  1. reads the uploaded original from storage;
  2. validates MIME type and image content;
  3. generates variants with Sharp;
  4. writes variants through StorageService;
  5. updates ImageFile rows;
  6. moves images into pending_review once processing succeeds.

Queue constants are centralized. Import IMAGE_PROCESS_QUEUE from queue.constants.ts.

Downloadsโ€‹

Buyers purchase images through POST /orders, then access downloads through:

GET /api/v1/images/:id/download/:orderItemId

The response includes a signed download URL and agency branding metadata where available.

The web download flow can:

  • download original/large image;
  • apply centralized agency watermark;
  • place a property pin;
  • use a brand-specific map pin;
  • generate the final branded bitmap client-side.

Agency Brandingโ€‹

Watermarking is resolved from AgencyBrand, usually by email domain or office. The buyer can toggle watermarking and choose its position during download. Do not add per-agent watermark uploads.

Local Backendโ€‹

The local backend is supported for development and signed downloads. It should be treated as a first-class backend, not a test-only shortcut.