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.cdnUrlImageFile.fileSizeBytesImageFile.storageBucketImageFile.storageKeyImageFile.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:
- reads the uploaded original from storage;
- validates MIME type and image content;
- generates variants with Sharp;
- writes variants through
StorageService; - updates
ImageFilerows; - moves images into
pending_reviewonce 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.