DEV Community

Vova Durcoin
Vova Durcoin

Posted on

I built a media host in one PHP file. Here's what 900 lines can do.

TL;DR: One index.php file. Zero dependencies. Zero database.
Scales to 500k files. MIT licensed. Code on GitHub.

The itch I was scratching

I have a folder of MP3s I wanted to share with a small audience.
Standard options were:

  • Plex / Jellyfin → hours of Docker and configuration
  • S3 + CloudFront → 40 AWS IAM tabs later I give up
  • SoundCloud / YouTube → not self-hosted, algorithmic control
  • WordPress + plugin → bloat, database, security patches forever

I just wanted: "upload files, visitors see them, done." So I wondered
— how minimal can a media server actually be?

The constraints I gave myself

  • Single .php file
  • No database
  • No composer.json or any package manager
  • No build step (no webpack, no TypeScript compilation)
  • Must handle 500k+ files without choking
  • Must work on $3/month shared hosting

Six weeks later: 900 lines of PHP, and it actually does all of that.

Architecture: JSON index cache

The trick to scale a single-file script is avoiding filesystem scans
on every request. On first load:

function rebuildIndex() {
    $files = [];
    $dh = opendir('.');
    while (($f = readdir($dh)) !== false) {
        if (!is_file($f) || $f[0] === '.') continue;
        $files[] = [
            'n' => $f,
            's' => filesize($f),
            'c' => categorize(pathinfo($f, PATHINFO_EXTENSION))
        ];
    }
    shuffle($files);
    file_put_contents('_index.cache.json', json_encode($files));
    return $files;
}
Enter fullscreen mode Exit fullscreen mode

Subsequent requests read _index.cache.json instead. Cache TTL is
1 hour. Tested with 500k dummy files — response times stay under 50ms.

The hardest part: Ed25519 verification in PHP

I wanted users to authenticate via Waves blockchain wallet
(Keeper extension). Waves uses Curve25519 public keys but
signs with Ed25519. You can't verify Ed25519 signatures with
a Curve25519 key directly.

The birational map between the two curves:

y = (u - 1) / (u + 1) mod p
Enter fullscreen mode Exit fullscreen mode

Where u is the Montgomery x-coordinate (Curve25519) and y is
the Edwards y-coordinate (Ed25519). Plus reconstructing the sign bit
from byte 63 of the signature.

function curve25519ToEd25519(string $curvePk, int $signBit): ?string {
    $p = gmp_init('2^255 - 19');
    $u = gmp_mod(gmp_init(bin2hex(strrev($curvePk)), 16), $p);
    $uPlus1 = gmp_mod(gmp_add($u, 1), $p);
    $inv = gmp_invert($uPlus1, $p);
    $y = gmp_mod(gmp_mul(gmp_sub($u, 1), $inv), $p);
    // ... encode back to 32 bytes, set sign bit
}
Enter fullscreen mode Exit fullscreen mode

Then feed to sodium_crypto_sign_verify_detached(). Works reliably
once you get the byte-order and sign-bit handling right.

Other things I crammed into 900 lines

  • HTTP byte-range streaming for audio/video (Accept-Ranges: bytes)
  • MediaSession API so mobile lock-screen controls work
  • Infinite scroll via IntersectionObserver with cancelable fetches
  • 10 language translations including RTL (Arabic)
  • Dark + grayscale light themes
  • Mobile-first responsive (row layout on phones, grid on desktop)
  • Shareable URLs that auto-start playlists: #play=audio&list=a.mp3|b.mp3&from=a.mp3

What I'd do differently

  1. Start with constraints, not features. The "one file" rule
    forced every decision to justify itself in bytes.

  2. HTTP-range streaming is underrated. MediaSession + byte-range
    on a static file gives you 90% of what "proper" streaming services do.

  3. JSON cache > database for read-heavy workloads. For this use
    case, filesystem metadata in JSON is faster than any SQL query
    because there's no query — just json_decode() once.

What this is NOT

  • Not a Plex replacement. It doesn't transcode, scrape metadata, manage libraries.
  • Not for shared multi-user servers. One deployment = one creator.
  • Not secure by itself — put it behind Cloudflare or nginx with rate limiting.

Try it

MIT licensed. Fork it, break it, rewrite it. If you find a cleaner
way to do the Ed25519 dance in PHP — please tell me.


Building minimalist tools is a discipline. Every feature has to
justify its bytes. Turns out: most web apps can be 10× smaller than
they are. Maybe yours too.

Top comments (0)