← All work

Studio Operations Platform

Replaced a photography company's entire spreadsheet-based operation with a three-portal web app.

Sole engineer, through StackSloth · Jul 2026 – Oct 2026

  • Next.js
  • TypeScript
  • React
  • Tailwind
  • shadcn/ui
  • Supabase
  • Postgres (RLS)
  • Vercel
  • Resend
  • Dropbox API
  • Slack API
  • QuickBooks API
  • Metricool API
  • Claude Code

The client is a commercial real estate photography firm. I've left out its name, and every screenshot here uses seeded demo data.

The problem

Nothing was really going wrong. The whole company ran out of one Google Sheet: hundreds of projects, plus the schedule, editors, invoices, and client communication. It worked, but it was sprawling, and it was holding the company back.

The owner was the one holding it together. They were managing about ten people’s worth of statuses, on top of messaging the team in Slack, talking to clients over email and phone, making Dropbox folders, tracking expenses, and telling everyone when something changed. Staff work from New York and the Philippines, and photographers and editors are contractors, so nearly every hand-off went through the owner. They’d become the company’s coordinator, with no time left to grow the business.

The hard part

The hardest part was walking into a business I didn’t know, learning how they actually work, and finding where it hurt. That meant working with people who do this every day and trying to step into their shoes.

Then I had to make the judgment call: the app had to be familiar enough that people could keep working the way they already did, but different enough to automate the coordination away. That’s harder than any technical challenge I could have faced.

What I built

One Postgres database behind three portals, because there are three kinds of users (staff, contractors, and clients). They all need to see different things, but they share different slices of the same data.

  • Admin portal for staff (admins, sales, schedulers, producers, QC, editors, and social media managers), covering projects, scheduling, QC, delivery, invoicing, financials, and settings.
  • Team portal for contractors, so photographers, videographers, and editors see only their own jobs and earnings.
  • Client portal for brokers and asset managers: passwordless magic-link sign-in, live project status, deliverable previews, revision requests, and acceptance. It’s built and waiting on the client’s launch; clients aren’t on it yet.

Now, as people do their part of a shoot, the project moves forward on its own and the app tells the next person (in Slack and by email) that it’s their turn.

That carries on through QC, all the way to delivery in the client’s portal.

Social content works the same way. An editor uploads a post, it goes through QA, gets sent to the client for approval, gets scheduled, and posts automatically. Before, that meant jumping from Canva to Slack for internal approval, then someone emailing the client for their approval, then scheduling it by hand in the social media suite, with people pinging each other at every step.

It replaced the spreadsheet, and I migrated about 1,000 historical records in.

Under the hood

  • Security. Data access is enforced in Postgres with row-level security and database-driven RBAC, a layer below the API endpoints. If a route has a bug, the database still won’t hand over data that user can’t see. That’s defense in depth. When the policies got slow, I audited them and fixed per-row permission checks across 175 policies.
  • Integrations: Dropbox for deliverable storage, folder provisioning, and upload reconciliation; Slack for assignment DMs; QuickBooks Online for invoicing (built, waiting on the client to connect it); Resend for queued transactional email; and Metricool to publish social posts and pull back metrics.
  • Background work runs on pg_cron and Vercel cron: dwell alerts, reminders, monthly social billing, and integration syncs.

How I built it

Solo, with an agent-first workflow, and it didn’t start out smooth.

Early on, the agents wrote code that wasn’t DRY, and it was hard to catch because every PR looked great on its own. It wasn’t until I spent some time digging through the whole codebase that I found tons of duplicated functionality. So I stopped feature work for a day to refactor, and wrote state-machine docs for the data models so every feature interacts with them through the same contract.

Styling had the same problem: screens drifted from each other. So I set up a style contract the agents have to follow, and made it hard to break:

  • One tone registry for every status color, and a fixed type scale. Raw palette classes and arbitrary sizes are lint errors everywhere except the registry.
  • Shared primitives (chips, callouts, steppers, empty states, tab navigation, deliverable galleries) reused across all three portals.
  • Every date pinned to the studio’s time zone, also lint-enforced, because staff span two continents and the server renders in UTC.

After that, I started running three adversarial reviewers against every PR to hunt for duplication, bugs, and anything that got overlooked. Since then development has gone smoothly, and when something does go wrong, it’s usually something I left out of the request.

That added up to 300+ merged PRs in four months. Every merge deploys to production, and schema changes ship as migrations against live data, with 270+ migrations and no data lost. I also wrote a staff guide with a page per role and per task, with screenshots generated by a script from seeded data so they never go stale.

The engagement ran through StackSloth, so I also ran the business side: discovery with the owner and staff, the statement of work and milestones, recurring software reviews with the client, and a paid scope expansion when the client wanted more.


Dan Slapelis

Written by Dan Slapelis I'm a maker who loves to build things and write about them. Reach out to him at dan@slapelis.com.

© 2026, Dan Slapelis