JUJU Streaming Platform
Media backend for a subscription app that streams video, audio and 'scent & science' content. Sameem built it as direct client work.

Facts about the build and who it was for. Any figure here is shown on a public store listing or site linked on this page — how we present portfolio work.
Project Story
A subscription app serving several media types needs uploads transcoded for mobile, heavy processing kept off the API, and paid content protected.
As direct client work, Sameem built a Node.js backend on AWS S3 and EC2 with Fluent-FFmpeg transcoding, BullMQ job queues, S3 signed URLs and role-based access control.
Uploads are transcoded for mobile, heavy processing runs as background jobs, and paid media is served through signed URLs.
Project Details
JUJU is a subscription app for video, audio and 'scent & science' content. Sameem built its media backend as direct client work (March to October 2025): a Node.js service on AWS EC2 and S3 that transcodes uploads with Fluent-FFmpeg, runs the heavy work as BullMQ background jobs so the API stays responsive, and serves media through S3 signed URLs with role-based access control.
Tech Stack
Where to See It
No public link for this project
Category
Node.js Backend & AWSJUJU is a subscription mobile app for spiritual content that serves video, audio and its own "scent & science" media. JUJU was Sameem's direct client work: from March to October 2025 he built its Node.js media backend: a Fluent-FFmpeg and BullMQ processing pipeline on AWS EC2 and S3, S3 signed URLs for playback, and role-based access control.
What was JUJU's backend for?
JUJU is a mobile app that sells access to spiritual content through subscriptions. The catalogue is not one kind of file: it includes high-definition video, audio and the app's own "scent & science" material, each with different file sizes, processing needs and playback behaviour.
That mix creates three requirements at once. Media has to be ingested and converted without blocking the application. Delivery has to stay fast however large the library becomes. And because access is sold, every asset has to be protected so that a paying member's stream cannot be copied out and shared as a public link.
Those requirements pull against each other. Heavy processing competes with fast responses, and strict access control adds a check to every request. The backend had to satisfy all three together.
How was the backend split between Node.js, EC2 and S3?
The backend is Node.js running on AWS EC2, with AWS S3 as the content store. That split matters: EC2 holds the application and the media-processing work, while S3 holds the finished files and serves them, so the application servers never become the bottleneck for bytes leaving the platform.
Node.js suits this shape of workload because streaming is mostly I/O-bound rather than CPU-bound. Requests for catalogue metadata, entitlement checks and signed playback URLs are short, numerous and concurrent, and an event-driven runtime handles many of them on a small footprint instead of parking a thread per viewer.
The heavy CPU work is deliberately kept off that request path. Transcoding runs as background jobs rather than inside HTTP handlers, which keeps the API responsive while large files are being processed. EC2 gives that work a predictable, tunable environment, and S3 gives every processed rendition a durable home addressable by a single key.
How does the media pipeline handle different content types?
Fluent-FFmpeg sits at the centre of the media pipeline. It wraps FFmpeg in a Node-native API, so the backend composes transcoding commands programmatically instead of assembling fragile command strings, and reacts to progress and error events directly in JavaScript.
That programmability is what makes a mixed catalogue manageable. Video needs re-encoding and container normalisation, audio needs its own codec handling, and the "scent & science" material is a different asset class again. Rather than maintaining one brittle command per format, the pipeline branches on asset type and builds the right invocation from parameters.
Normalising uploads this way also protects playback. Source files arrive in whatever format the contributor exported, and without a conversion step a single unusual codec or container can break the app on someone's phone. Pushing every asset through a consistent FFmpeg stage means what lands in S3 is predictable, and the player only ever sees formats the platform has already checked.
Why was BullMQ used for background media jobs?
BullMQ is the queue that makes the processing pipeline safe to operate. Transcoding is slow, CPU-hungry and failure-prone, which makes it exactly the kind of work that should never run inline with a user request.
When an asset is uploaded, the API records it, enqueues a job and returns immediately. Workers pull jobs from the Redis-backed queue and run the FFmpeg stage at whatever concurrency the EC2 instance can sustain. That gives the platform a natural throttle: a burst of uploads lengthens the queue rather than exhausting the machine.
The operational properties matter as much as the throughput. A queue gives retries with backoff when a transcode fails, so a corrupt source file or a transient storage error does not silently lose content. It shows what is pending, active and failed. And it decouples ingestion from processing, so upload capacity and encoding capacity can be reasoned about, and scaled, separately.
How is paid content protected with signed URLs and RBAC?
Subscription revenue only works if the content behind the paywall stays behind it. JUJU protects files with signed URLs: S3 objects are not publicly readable, and the backend issues a time-limited, cryptographically signed link only after checking that the requesting account is entitled to that asset.
In practice a playback URL becomes a short-lived permission rather than a permanent address. If one is copied out of the app and shared, it expires; it cannot be turned into a lasting public mirror of the library. Authorisation stays on the server, where it can be enforced, instead of being implied by a hard-to-guess path. The mobile app holds no storage credentials: it asks the backend for a playback URL, uses it, and asks again when it expires.
Role-based access control sits above that. RBAC separates what different kinds of account may do, such as a member who watches and listens versus staff who publish and manage content, so permissions attach to roles rather than being scattered through endpoint-level checks. Every signed URL request is a decision made against that model.
How does subscription-based access work?
Access to JUJU is sold through subscription plans, which makes entitlement a first-class concern in the backend rather than a billing detail bolted on at the edge.
Every playback request therefore answers two questions before any media moves: who is this account, and does its current plan cover this asset? Only when both answers are yes does the backend issue a signed URL. Because the check happens on the server for each request, a lapsed or downgraded subscription stops granting access straight away, without waiting for the app to refresh its own state.
Tying entitlement to the RBAC model keeps those rules in one place as the catalogue grows. Adding a plan, or a content type that only some plans include, becomes a change to the access model rather than a change scattered across every endpoint that can hand out a stream.
What load does a streaming backend have to be designed for?
Streaming load is mostly concurrency: many simultaneous sessions, each needing an entitlement check and a freshly signed URL, and each expecting playback to start without hesitation.
Serving bytes from S3 rather than from the application tier is what keeps that manageable. The Node.js layer handles small, fast authorisation requests; the large, sustained transfers are handled by storage. Scaling the API and scaling delivery become separate concerns rather than one coupled problem.
The second pressure sits behind the catalogue. Media processing is bursty by nature, quiet and then suddenly a batch of large uploads, and BullMQ absorbs those spikes so ingestion never competes directly with playback for the same resources. Keeping transcoding asynchronous is what stops a heavy encoding run from degrading playback for anyone watching at the time.
Where can you see JUJU?
There is no public link: JUJU has no store listing or website to point to.
Sameem built JUJU's backend as direct client work, and the sections above set out how it fits together: the processing pipeline, the job queue and the access model.
Why this stack
- Node.js: streaming APIs are I/O-bound, not CPU-bound. The request path is dominated by short, concurrent calls (catalogue metadata, entitlement lookups and signed URL issuance), and an event-driven, non-blocking runtime serves many of those on a modest footprint instead of dedicating a thread per viewer.
- AWS S3: object storage separates byte delivery from the application tier. Finished renditions live under stable keys, durability and read throughput are the storage layer's job rather than the server's, and access to each object can be gated with signed URLs instead of blanket public read.
- AWS EC2: transcoding needs a predictable, tunable machine rather than a short-lived function runtime. EC2 gives the FFmpeg workers controllable CPU, local scratch space for large intermediate files, and long-running processes that a queue can feed continuously.
- Fluent-FFmpeg: FFmpeg is the standard tool for converting mixed video and audio formats, and the Fluent wrapper turns it into a programmable Node API. Commands are composed from parameters per asset type, and progress and failure surface as JavaScript events the job layer can act on.
- BullMQ: encoding is slow and failure-prone, so it belongs behind a Redis-backed queue. Enqueueing on upload keeps the API responsive, worker concurrency caps resource use during upload bursts, and built-in retries with backoff mean a transient failure is retried rather than silently dropping content.
- Signed URLs: they turn a playback link from a permanent address into a short-lived, cryptographically checked permission, so authorisation is enforced on the server per request and a leaked link expires instead of becoming a public mirror of the paid catalogue.
- RBAC: with different kinds of account (members who watch, staff who publish and administer) touching the same media library, roles keep permissions in one coherent model rather than duplicated across endpoints, and every signed URL request is resolved against it.