The problem
I wanted a simple way for friends to build photo albums together. Each album is a set of pages, and each page holds photos and text placed freely on a canvas. One person owns the album, others can join as editors or viewers, and a finished album can be published so anyone can see it.
That sounds like a small CRUD app, but the access rules spread fast. A draft is visible only to its owner and collaborators. Editors may change the album; viewers may not. Only the owner may delete or publish. Photos belong to the person who uploaded them. If these checks live in each endpoint, one forgotten check exposes a private album. The project is private and used by a few friends.
What I did
Split each domain into the same layers
The backend has one module per domain: albums, pages, media, collaborators and users. Each module follows the same shape. Repositories write data, selectors read it, services hold the business rules, and views stay thin. A generic base service gives every module the same basic operations. The trade-off is more files and some boilerplate for an app this size. In return, every rule has an obvious home, and the next module is quick to add.
Keep access rules in the service layer
Every album read goes through one service method that answers “may this user see this album?”. It checks, in order: published albums are open to anyone, otherwise the user must be signed in, then the owner passes, then a collaborator passes. Edit, delete and publish have their own owner or editor checks, and they raise domain errors that the API maps to error responses. The trade-off: the database itself does not enforce these rules, so any code path that skips the service skips the check.
Hand sign-in to an identity provider
I did not want to store passwords. The frontend signs users in through Auth0, and the API verifies the signed token on each request: it picks the right public key, checks the signature, audience, issuer and expiry, then creates a local user on first sign-in, keyed by the provider’s user id. The trade-off is a hard dependency on the provider. My first version also downloads the signing keys on every request, which I cover below.
Soft delete and UUID keys everywhere
All models share a base with UUID primary keys, timestamps and cascading soft delete. Deleting an album hides its pages and links instead of erasing them, so a mistake can be undone. UUIDs keep ids unguessable in shared links. The cost is that deleted rows stay in the database and every query must respect the deleted flag, which the library handles.
Deploy from the repository
A push to main runs a workflow that copies the app’s settings from the repository’s protected environment to the hosting service, then triggers a deploy. Images go to an image CDN; the database stores only links and metadata. The trade-off is that configuration now lives in two places, with the repository as the source of truth.
Result
FotoVerse runs as a React and TypeScript web app on top of the Django REST API, with a drag-and-resize page editor, page-flip viewing, collaborator roles and publishing. A few friends use it. The layered module layout worked well enough that I reused it in a later side project.
What I’d do differently
There are no automated tests yet, and the access rules are exactly what I should have tested first, with one test per owner, editor, viewer and anonymous case. The token check fetches the provider’s signing keys on every request with no cache and no timeout, so a slow provider slows every call; I would cache the keys and refresh them only when an unknown key id appears. A proper README would also help anyone else run it.