Yaldi.chat Emotes: Architecture and an API-First Mindset
Yaldi.chat started with annoyance and discomfort. It's a bit cringe but I do enjoy watching a handful of creators of Twitch.tv. Twitch, in the pursuit of turning a profit, has lost its sense of direction and community. This includes attempting to turn every community feature into a revenue generator, including simple chat emotes. To offset this, third parties like 7TV have popped up but they too have become profit hungry and imposed limitations as well as regularly struggle with outages and a highly unstable CDN. I also saw all of these features decoupled and jumbled: chat bots, overlays, emotes, etc.
So, I decided to try and build something better: Yaldi.chat. Yaldi features a customizable chatbot with an LLM integration, file sharing, chat translations, short link generator, and a higly robust emote system with a Chrome extension. It currently houses close to half a million emotes and several million chat messages and events. It's slow growth but the project is growing every month. In this post, I'll focus mostly on how I built the emote platform.
The What I Actually Wanted to Solve
On the surface, emotes are meme images. In reality, they are:
- Latency-sensitive assets rendered in real time
- Shared state across thousands of concurrent clients
- User-generated content requiring moderation and versioning
- Performance hazards inside an unfriendly DOM (Twitch chat)
- A cost problem if handled naïvely
Most platforms optimize for rapid growth and feature velocity first, then pay down the architectural debt later. That often leads to large always-on infrastructure, tight coupling between systems, and expensive rewrites.
I intentionally took the opposite approach.
Constraints as an Architectural Tool
From the beginning, I wanted to impose some guardrails/constraints on Yaldi since I knew I didn't want to operate with a huge cost that I would have to offset somehow:
- No Kubernetes
- No frontend JavaScript frameworks
- No always-on backend servers
- No paid services unless they clearly earned their place
- No premature abstractions
These constraints helped force clarity;
A Boring Architecture by Design
Yaldi’s architecture is intentionally boring. At a high level, the system consists of five components:
- A PHP-based public website hosted on AWS EC2
- A stateless API running on AWS Lambda via Function URLs
- A relational database hosted on Neon (PostgreSQL)
- An image pipeline using Cloudflare R2 and the Cloudflare CDN
- A custom Chrome extension responsible for real-time chat rendering
Each component has a narrow responsibility and communicates with the others through explicit interfaces. No component “knows” more than it needs to.
If the extension fails, the platform doesn’t go down. If the website changes, the API doesn’t care. That separation is intentional.
Stateless by Default
The API layer is fully stateless. Every request contains all the information required to process it. This enables:
- Horizontal scaling without coordination (using Neon for my database helped a lot here)
- Safe retries and idempotent operations
- Simpler operational reasoning
- Lower cost at idle
State lives in exactly two places: the database and the client. Anywhere else is a liability.
Images Belong at the Edge
One of the most important early decisions was treating images as immutable assets, not application data. Emote images are stored in Cloudflare R2 and served directly through the Cloudflare CDN using on-the-fly resizing via /cdn-cgi/image. The backend never streams images, resizes images, or proxies image requests.
This single decision removes an entire class of scaling, latency, and (debatebly) cost problems. Cloudflare Images does cost slightly more but I serve most through R2 direcly without resizing which actually saves money (compared to S3, for example). So it's overall better and faster.
Yaldi Is 100% API-First
Yaldi was designed API-first from day one—even before there were external consumers. Neither the website nor Chrome extension talk to the database at all. Nothing bypasses the API. If something deserves to exist, it exists as an API endpoint. If it doesn’t deserve an endpoint, it probably doesn’t deserve to exist at all.
This discipline prevents “internal shortcuts” that quietly accumulate technical debt.
REST Over GraphQL (For Now)
GraphQL is powerful, but power comes with complexity. REST was chosen because it:
- Maps cleanly to resource-oriented domains
- Plays well with caching layers
- Is operationally simpler
- Matches the access patterns of this problem space
That choice may evolve in the future but REST is solving my problems now and I haven't had anything really justify the need to shift to GraphQL yet.
Designing for Futures I Can’t Predict
I don’t know what Yaldi will look like in two years but an API-first architecture keeps options open: Twitch bots, OBS overlays, analytics dashboards, third-party integrations, or entirely new clients. None of those require rethinking the system if the core boundaries are already in place.
What’s Next
This post is intentionally foundational. In upcoming posts, I’ll dig into:
- Why serverless won (and where it didn’t)
- Cost-driven infrastructure decisions
- The Chrome extension’s DOM and performance model
- Moderation, abuse handling, and trust boundaries
- Preparing for scale without premature optimization
Yaldi isn’t finished—and it never will be. But it is one of the most "intentionally designed" projects I've ever built.
Comments
No comments yet.
Leave a comment