Atelier: Art & Community
Live demoAn art community rebuilt to explore data ownership, Redis caching, secure sessions, and recovery across services.

The problem
Sharing artwork looks simple until an upload fails, a cached response contains another person's state, or logout leaves a session active. I rebuilt Atelier to learn how system design handles those failures while keeping the app responsive.
What I built
- The full community workflow. An Angular interface and Express API for discovering artwork, publishing pieces, following artists, and saving favorites.
- A deliberate data split. MongoDB holds artwork documents; PostgreSQL owns accounts and relationships; object storage holds images. Redis caches derived reads and supplies queue hints.
- The infrastructure around those features. Revocable sessions, staged uploads, retryable cleanup, a SQL notification outbox, and CI checks for API, frontend, and database behavior.
Why I built it this way
- Give each store a clear responsibility. Artwork documents and transactional relationships have different access patterns. Explicit ownership keeps a cache or search projection from becoming an accidental source of truth.
- Publish in stages. SQL, MongoDB, and storage cannot share one transaction. Posts move through pending, written, and published states. Public reads hide unfinished work; reconciliation handles ambiguous failures.
- Cache shared content, then add private state. Redis reuses artwork and discovery responses. Likes and ownership flags are added per viewer afterward. Generation checks stop an old read from refilling a newly invalidated cache.
- Make logout a server decision. Short-lived access tokens and rotating refresh secrets remain subject to session revocation. Server-side role, ownership, and CSRF checks enforce access beyond the interface.
- Save side effects with the transaction. A like and its notification job commit together in PostgreSQL. A worker claims leased jobs and retries failures; a lost Redis hint does not lose the durable work.
- Bound work and test failure paths. Small connection pools, indexed queries, signed feed cursors, and request limits fit serverless constraints. CI exercises revocation, cache races, publication recovery, and queue behavior.
What I learned
I learned system design principles by applying them: data ownership, consistency, cache invalidation, authorization, and at-least-once processing. The main lesson was to define what survives a failure. Adding another service is useful only when its responsibility and recovery path are clear.
Evidence & current limits
The live demo and automated checks demonstrate these workflows. Provider quotas still limit capacity; large-scale load testing, automated backups, and dedicated alerts remain future work. Public email delivery is disabled pending sender-domain and webhook setup.
More on caching, security, and recovery
Cache behavior
Public responses normally expire after 60 seconds. Writes change a shared generation; conditional fills reject stale in-flight reads. Concurrent misses share a source read within an API instance. Redis outages fall back to the databases, though failed invalidation can leave public counts stale until expiry.
Security boundaries
Ten-minute access JWTs and rotating refresh secrets use HttpOnly, Secure production, SameSite Strict cookies. SQL stores refresh hashes. Reused secrets revoke the session family. Revocation establishes a Redis fence before SQL writes; failure to establish that boundary returns 503. Parameterized SQL, salted scrypt, CSRF proofs, origin checks, and a restrictive CSP protect the API and browser.
Recoverable work
Signed uploads enter private staging before validation and public promotion. Deletion checkpoints cleanup across stores. SQL job leases, backoff, jitter, dead letters, and stable provider idempotency keys support retryable background work. Delivery is at least once with bounded deduplication; a sleeping worker can delay it.
Inspect the implementation on GitHub ↗More project screens
Explore the interface. Select an image to view it at full size.

