Nonprofit Platform for At-Risk Youth
An end-to-end learning platform: a secure video LMS with signed-token playback and per-user moving watermarks, progress tracking, donations, certificates, full Hebrew/English RTL support and WCAG AA compliance.
Private repository — source not public
A nonprofit working with at-risk youth needed to move its programme online: video courses, tracked progress, certificates on completion, and donations — in Hebrew and English, accessible, and running on a budget that assumed the answer was "free tier or nothing".
Two requirements pulled against each other. The video content was produced with named professionals and could not be freely redistributable — a link that could be pasted into a group chat was a real problem, not a theoretical one. And the platform had to cost close to nothing to run, which ruled out the DRM products that solve exactly that problem.
A React 19, Vite and Tailwind front end against a Node, TypeScript and Express API on MongoDB.
Nothing is served as a static file URL. Every play requests a grant, and the API mints a short-lived signed token bound to that user and that lesson.
Sequence diagram: the learner opens a lesson, the API checks enrolment and mints a short-lived signed token carrying a watermark seed, the client fetches media segments with that token, overlays a moving watermark, and reports progress checkpoints back to the API which issues a certificate on completion.
const grantPlayback = async (user: IUser, lessonId: string): Promise<IPlaybackGrant> => {
const enrolment = await enrolments.find(user.id, lessonId);
if (!enrolment) {
throw new ForbiddenError('not enrolled in this lesson');
}
const expiresAt = new Date(Date.now() + PLAYBACK_TTL_MS);
return {
url: await provider.signedUrl(lessonId, expiresAt),
watermarkSeed: createHmac('sha256', env.WATERMARK_SECRET).update(`${user.id}:${lessonId}`).digest('hex').slice(0, 16),
expiresAt,
};
};The watermark seed is derived, not stored — the same user and lesson always produce the same seed, and it cannot be computed without the server secret. The client turns the seed into a moving overlay: position and timing come from the seed, so it is different per user and cannot be cropped out by guessing where it sits.
This is deliberately not DRM. A determined person with a screen recorder still wins. What it stops is the actual threat: a URL shared in a group chat, which expires, and a screen recording that carries a watermark tied to the account it came from.
The cost constraint drove the interesting part. Rather than one storage provider, uploads are classified and routed, with a ledger tracking free-tier consumption and health.
Flowchart: an upload is classified as long-form video or as documents and images, video checks free-tier headroom and routes to the managed provider or a secondary provider, everything receives a signed playback token, and a provider ledger with a health check fails over to the secondary provider when needed.
Every provider sits behind one interface, so adding a provider is one file and a configuration entry:
export interface IMediaProvider {
readonly name: string;
supports: (asset: IAssetDescriptor) => boolean;
remainingQuota: () => Promise<number>;
upload: (asset: IAssetDescriptor, stream: Readable) => Promise<IStoredAsset>;
signedUrl: (assetId: string, expiresAt: Date) => Promise<string>;
}Selection asks each provider whether it supports the asset and how much quota it has left, then picks the cheapest that can take it. When a provider reports unhealthy, the ledger already knows where the asset also exists, so failover does not touch lesson data.
Hebrew and English throughout, with logical CSS properties rather than directional ones, so layout follows the writing direction instead of being mirrored after the fact. Accessibility was done as it was built: semantic landmarks, keyboard operation for the whole player including progress, visible focus, labelled controls, and contrast verified in both themes.
Real DRM costs per stream and adds a licence server to the request path. For this threat model — casual redistribution, not commercial piracy — a short-lived signed URL plus an attributable watermark gets most of the protection for none of the cost. The trade-off is explicit: this is deterrence and attribution, not prevention.
A single provider would have been considerably simpler, and I would normally argue for it. Here the requirement was a running cost near zero, and no single free tier covered both long-form video and general object storage. The ledger and the provider interface are the price of that; the benefit is that the platform stayed inside free tiers and gained failover as a side effect.
Storing a seed per user and lesson would have been one fewer moving piece. Deriving it via HMAC means there is no table to keep in sync, no migration when lessons change, and no seed to leak — but it also means rotating WATERMARK_SECRET invalidates every existing watermark's continuity. Acceptable, since nothing depends on a seed being stable across a rotation.
Course structure is a nested document and reads as one. The cost is the same as on Signet: progress updates that would be a single transactional statement in SQL are written as conditional updates instead.
Delivered and running.
What I would revisit: watermark rendering is done in the client, which means a modified client can remove it. Moving the overlay server-side would close that at the cost of transcoding — the right trade only if redistribution actually became a problem in practice.