TheRare Visuals

The architecture behind TheRare Visuals

A photography studio's booking platform, end to end: Next.js and Spring Boot on Cloud Run, Postgres, two storage buckets, a job queue, and why each piece is there.

TheRare Engineering18 August 20266 min readBeginner

What we're building

TheRare Visuals is a photography and videography studio. People book us for pre-wedding films, portraits, ring ceremonies, birthdays and baby shoots across Karnataka, Kerala and Goa. That sounds like a business that needs a nice website and a phone number. It turned out to need much more:

  • Bookings with dates, packages, coupons, weekday pricing and a team assigned to each shoot.
  • Payments: an advance to confirm, the balance before delivery, refunds when plans change.
  • Delivery: a single shoot can be thousands of photos and hundreds of gigabytes of 4K video. Customers need to preview, pay, then download or stream all of it, often on a phone.
  • A studio back office: staff roles, audit logs, notifications, analytics, a content system for the portfolio.
  • The fun parts: our own video player, an AI photo studio, and two browser games.

This series explains how it's built. This first post is the map; every later post zooms into one part of it.

The big picture

There are two services, one database, two storage buckets and a job queue:

PieceWhat it doesBuilt with
WebsitePublic pages, customer dashboard, admin panelNext.js 14, React 18, TypeScript, Tailwind
APIAll business logic, auth, payments, mediaSpring Boot 3.2, Java 17
DatabaseBookings, customers, media records, audit logPostgreSQL, Flyway migrations
Private bucketCustomers' photos and videosGoogle Cloud Storage, signed URLs
Public bucketPortfolio images on the websiteGoogle Cloud Storage, public-read
Job queueWatermarked previews, video encodingCloud Tasks and ffmpeg

Both services are Docker containers on Google Cloud Run. Cloud Run runs a container when requests arrive and scales it down to zero when they stop, so a small studio pays for traffic it actually gets rather than for servers sitting idle.

Following one request

The easiest way to understand a system is to follow a single click. Say a customer opens their dashboard to see their portrait photos.

  1. The browser asks the Next.js service for /account. Next.js sends back the page's HTML and JavaScript.
  2. The page's React code calls the API: GET /api/v1/customer/bookings. The browser automatically attaches a session cookie, and the API checks it.
  3. The API reads the customer's bookings from PostgreSQL.
  4. For each photo, the API creates a signed URL: a link to the file in the private bucket that works for a short time and only for that file.
  5. The browser loads the thumbnails directly from Cloud Storage using those links.

Step 5 is the most important design decision in the whole system. The photos never pass through our servers. Our API only hands out permission slips; the heavy bytes go straight from Google's storage to the customer. That keeps the API small and cheap, and it means a 4 GB video download doesn't tie up one of our containers for twenty minutes.

Why the website and the API are separate

We could have written everything in Next.js, which can run server code too. We split them because they do different jobs:

  • Next.js is good at pages. Server rendering makes the public site fast and readable by Google, which matters for a business that lives on "photographer in Mysore" searches.
  • Spring Boot is good at business rules. Payments, refunds, row locks, scheduled jobs, role-based permissions and 99 database migrations are comfortable in a typed backend framework with mature libraries for transactions and security.

The split also gives us one API that any client can call. The upcoming Shootlo mobile app talks to the same kind of API as the website.

Why two storage buckets

Customer photos are private: a customer should see only their own shoot, and only once they're allowed to. Portfolio photos are the opposite: we want everyone to see them, as fast as possible.

We tried to have one bucket with a public "portfolio" folder. Cloud Storage doesn't allow that: public access is granted to a whole bucket, never to part of one. So the portfolio lives in a second, fully public bucket that browsers and caches can read freely, and the private bucket never has a single public file in it. When a platform won't let you do something, it's often telling you the safer design.

Background work

Some work is too slow to do while a customer waits. Making a watermarked preview of a photo takes up to a second; encoding a 20-minute film into four qualities takes many minutes.

The API puts these jobs on a Cloud Tasks queue. The queue calls an encoder service (the same code, deployed with more memory) over HTTP for each job, retries failures with backoff, and limits how many run at once. Part 4 explains why the jobs have to arrive as HTTP requests on Cloud Run, and Part 6 covers the video pipeline itself.

Other services we call

  • Razorpay for payments. The customer pays on Razorpay's checkout; our API verifies the payment signature before marking anything paid.
  • Resend for email over HTTPS. We started with ordinary SMTP email and it failed in a way that took a day to understand; that story is in Part 8.
  • Gemini for RareAI Studio, where customers can edit their delivered photos with a text prompt.

Choices that saved money

A studio isn't a venture-funded startup, so cost shaped the design:

  • Scale to zero. Cloud Run bills per request, so quiet nights cost almost nothing.
  • Managed Postgres that sleeps. We started on Cloud SQL, which bills for a server all day, plus a small virtual machine just to reach it privately: roughly ₹4,700 a month for those two alone. We moved to a serverless Postgres provider that pauses when idle and removed both. Today our whole Google Cloud bill is about ₹1,800 a month. The trade-offs were written down and accepted: about half a second to wake the database after a quiet spell, and a public TLS endpoint with a password instead of a private network.
  • No bytes through the API. Signed URLs mean we never pay to move a file twice.

Region: a lesson in reading the fine print

Our customers are in South India, so Mumbai was the obvious region. We deployed there, then found that Cloud Run's custom-domain mapping isn't offered in that region. Everything moved to Singapore, which is still fast for our customers. The lesson: check that every feature you need exists in your region before you build there.

How to read the rest of the series

  • Part 2 teaches React from basics to advanced using code from our dashboard. Start there if you're new to frontend.
  • Part 3 is the story of galleries crashing phones and how pagination fixed it.
  • Part 4 is the backend design: data model, roles, jobs and concurrency.
  • Part 5 covers uploading and downloading very large files.
  • Part 6 is how we built our own video player, from ffmpeg to the play button.
  • Part 7 is security.
  • Part 8 is the production bugs that cost us a day each, so they don't cost you one.

Found this useful?

Built something with what you learned here, or spotted a better way? Tell us. We work with freelance developers on Shootlo.