Lens.ke is a Django backend for a Kenyan photo marketplace. It lets contributors upload high-resolution images, generates protected watermarked previews asynchronously, exposes a searchable public catalog, and issues secure download links for licensed or free images.
The backend is deployed on DigitalOcean App Platform with a web service and a Celery worker.
https://excalidraw.com/#room=d6bd34a5c32394ccda62,m2WHVeUTYRaovbv91ACChQ
- Contributors can upload high-resolution photos and track upload/earnings activity.
- Buyers can browse the catalog, view watermarked previews, and download original images after authorization or payment.
- The platform applies watermarks, stores protected originals separately from public previews, and records revenue splits.
- Low-latency browsing for mobile users on 3G/4G networks.
- Strict IP protection: original high-resolution files must not be publicly accessible.
- Strong transaction consistency for payments, licenses, and contributor payouts.
- 1,000 contributors uploading 10 images each month.
- About 10,000 new images per month.
- Average original image size: 15 MB.
- Original storage growth: about 150 GB per month.
- Watermarked preview target: about 150 KB per image.
- Preview storage growth: about 1.5 GB per month.
- Expected workload is read-heavy, approximately 100 reads per write.
Client / Frontend
|
| HTTPS API requests
v
DigitalOcean App Platform Web Service
|
| Django + DRF API
v
PostgreSQL
Contributor Upload Flow:
Contributor -> Django API -> Presigned S3/Spaces upload -> Private raw bucket
-> Finalize endpoint -> Redis queue -> Celery worker
-> Pillow processing -> Public preview bucket -> PostgreSQL status update
- Client tier: optimized for mobile browsing; CDN such as Cloudflare can sit in front of API/static assets.
- API tier: Django REST-style endpoints for authentication, catalog browsing, contributor uploads, profiles, and secure downloads.
- Ingestion pipeline: presigned upload intent, metadata finalization, image processing queue, watermarking, compression, and preview publishing.
- Catalog service: photo listing, filtering, search by tags, county filters, popularity sorting, and detail lookup.
- Payment/licensing foundation: transaction, license, and revenue split models are present. Payment gateway webhook integration is a planned extension point.
- Storage: original files are stored in a private S3-compatible bucket; watermarked WebP previews are stored in a public previews bucket.
- Worker: Celery consumes Redis messages and processes images with Pillow.
- Database: PostgreSQL stores users, photos, transactions, licenses, and refresh tokens.
- Python
- Django 6
- Django REST Framework
- drf-spectacular for OpenAPI/Swagger docs
- PostgreSQL
- Redis
- Celery
- Pillow
- boto3 for S3/DigitalOcean Spaces
- Gunicorn
- DigitalOcean App Platform
Core entities:
- User: buyer, contributor, or admin account.
- Photo: uploaded asset metadata, raw object key, public preview URL, tags, county, price, license type, and processing status.
- Transaction: payment record with ACID constraints for contributor/platform revenue split.
- License: issued download/license record linked to a transaction, buyer, and photo.
- RefreshToken: hashed refresh token records for login sessions.
Important database choices:
- PostgreSQL is used because payment and payout data require consistency.
- Photo tag fields use PostgreSQL arrays and GIN indexes for efficient filtering.
- Original files are referenced by object keys, not stored directly in the database.
When the app is running:
- Swagger UI:
/api/docs/ - ReDoc:
/api/redoc/ - OpenAPI schema:
/api/schema/
Main API groups:
POST /api/v1/auth/registerPOST /api/v1/auth/loginPOST /api/v1/auth/refreshPOST /api/v1/auth/logoutGET /api/v1/auth/meGET /api/v1/photosGET /api/v1/photos/<photo_id>POST /api/v1/photos/upload-intentPOST /api/v1/photos/<photo_id>/finalizeGET /api/v1/contributor/dashboardGET /api/v1/licenses/download-token/<photo_id>
- A contributor authenticates and calls
POST /api/v1/photos/upload-intent. - The API creates a pending
Photorecord and returns a presigned upload form for the private raw bucket. - The client uploads the original image directly to S3/DigitalOcean Spaces.
- The client calls
POST /api/v1/photos/<photo_id>/finalizewith tags, county, license type, price, and description. - The API marks the photo as
processingand queues a Celery task. - The worker downloads the private original, creates a compressed watermarked WebP preview, uploads it to the public preview bucket, and marks the photo as
active.
Create and activate a virtual environment, then install dependencies:
pip install -r requirements.txtStart Redis locally:
docker compose up -d redisRun migrations:
python manage.py migrateRun the Django API:
python manage.py runserverRun the Celery worker in a second terminal:
celery -A config worker --loglevel=infoGenerate static files:
python manage.py collectstatic --noinputRequired or commonly configured values:
DATABASE_URL=postgres://...
JWT_SECRET_KEY=...
JWT_ACCESS_TOKEN_MINUTES=30
JWT_REFRESH_TOKEN_DAYS=30
CELERY_BROKER_URL=redis://localhost:6379/0
CELERY_RESULT_BACKEND=redis://localhost:6379/1
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_S3_REGION_NAME=fra1
AWS_S3_ENDPOINT_URL=https://fra1.digitaloceanspaces.com
AWS_PRIVATE_RAW_BUCKET=lenske-private-raw
AWS_PUBLIC_PREVIEWS_BUCKET=lenske-public-previews
AWS_PUBLIC_PREVIEWS_BASE_URL=
AWS_UPLOAD_INTENT_EXPIRES_SECONDS=600
AWS_DOWNLOAD_URL_EXPIRES_SECONDS=600
LENSKE_WATERMARK_TEXT=LENS.KEDigitalOcean App Platform currently provides storage-related variables in app.yml. Database, Redis, and secret values should be configured as encrypted environment variables in the DigitalOcean dashboard.
The App Platform spec is in app.yml.
Deployed components:
- Web service:
lenske-api- Runs migrations.
- Starts Gunicorn with
config.wsgi:application.
- Worker:
maji-pipeline-worker- Runs
celery -A config worker --loglevel=info. - Processes image watermarking and preview generation jobs.
- Runs
The buildpack runs:
python manage.py collectstatic --noinputSTATIC_ROOT is configured as BASE_DIR / "staticfiles" so DigitalOcean can collect Django static files during deployment.
- Raw high-resolution files go into a private bucket.
- Public previews are generated after upload and stored separately.
- Download access uses short-lived presigned URLs.
- The backend never exposes private raw object URLs directly.
- Payment webhook handling is not yet exposed as an API endpoint.
- License PDF generation is represented by the
Licensemodel but not generated in this codebase yet. DEBUGandALLOWED_HOSTSshould be hardened before production traffic beyond staging/demo use.- CDN/rate-limiting/API-gateway concerns are part of the broader design but are not implemented inside this Django app.