Building DropReady: Early Intercept and Ed25519 Token Verification (Week 2)

Published on 28 July 2026

Building DropReady: Early Intercept and Ed25519 Token Verification (Week 2)

Building DropReady: Early Intercept and Ed25519 Token Verification (Week 2)

In Week 0 I talked about what we're building, in Week 1 about why I picked adaptive polling over WebSockets. This week is different in tone, because I didn't find the problems - dealit.gg did.

Adam and his team ran their own technical code review before rolling out the DropReady integration on their side. They caught two real things: one literal bug that would have blocked 100% of traffic, and one architectural weakness that could have exhausted their PHP-FPM pool at exactly the moment it mattered most. This post isn't "we built it and it looks fine", it's "a real client did an adversarial review and I had homework to do". Honestly, I think that's the better story.

What Early Intercept is

It's literally a name in the code - client-backend/php/composer.json describes the package as "Early Intercept for the queue system". Specifically: a single require at the very top of the client store's public/index.php, before any framework logic, Laravel, WordPress, anything.

It checks a signed token in a cookie and:

  • if valid - does nothing, the request proceeds normally
  • if missing or invalid - header("Location: ...") + exit(), immediately, before the framework even gets to bootstrap

This isn't anti-bot or traffic filtering. It's purely "does this visitor have a valid pass". The point of being "early" is economic - a visitor who isn't let through costs the client's server practically nothing, one redirect, instead of a full request lifecycle through the framework. That's also why the integration works regardless of the client's framework - the hook sits before routing, so it doesn't need to know anything about what's underneath.

Why Ed25519, not HMAC, not RSA

This was a real decision, three overlapping reasons.

Not HMAC, because HMAC is symmetric. The client verifying the token would need to hold the same secret DropReady uses to sign it. That would mean distributing our signing key to every client's server - compromising one client would compromise the ability to forge tokens for all of them. With Ed25519, asymmetric, the client only gets the public key, which is already openly exposed today via GET /api/v1/queue/public-key, no authorization required. Verification alone reveals nothing that would let anyone forge anything.

Not RSA either, also asymmetric, so it would theoretically solve the same problem, but Ed25519 has much smaller keys - 32 raw bytes for the public key versus 256+ for RSA-2048 - and faster verification at a comparable security level. On top of that, fewer ways to shoot yourself in the foot than with RSA, where padding schemes like PKCS1v1.5 have a real history of vulnerabilities.

The most practical argument, though, is the client-side ecosystem. PHP has ext-sodium, Ed25519, built in by default since PHP 7.2, no extra dependency. Both firebase/php-jwt and golang-jwt/jwt/v5 support EdDSA as a named, first-class method. The entire client-side verification is 100% local, zero network call back to DropReady.

Same philosophy as Week 1 - there it was no WebSockets and no held connection state, here it's no shared secret and no network dependency during verification. A different kind of statelessness, same spirit.

Bug #1: double base64_decode

Dealit.gg caught this during a code review before deployment. The code decoded the public key from base64 manually, then passed the already-decoded bytes into JWT::decode(), which decodes internally on its own. Result: double decoding, key truncated to an invalid length.

// Before
$publicKeyBytes = base64_decode($queuePublicKeyBase64);
$decoded = JWT::decode($jwt, new Key($publicKeyBytes, 'EdDSA'));

// After
$decoded = JWT::decode($jwt, new Key($queuePublicKeyBase64, 'EdDSA'));

I verified it live: with the buggy code, 100% of correctly signed tokens ended up failing with sodium_crypto_sign_verify_detached(): Argument #3 must be SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES bytes long. Meaning every real client let through the queue would fall into an infinite redirect loop - the exact opposite of what Early Intercept is supposed to do. After the fix: 0% errors on the same test.

Bug #2: blocking heartbeat, redesigned as fire-and-forget

The second problem was architectural, not a literal bug. The heartbeat, the mechanism checking whether a queue session is still alive, made a blocking network call in the request path. Under enough traffic, with a slow or unreachable endpoint, that could exhaust the PHP-FPM pool at exactly the moment it mattered most for the client's server to stay up.

Fix: fire-and-forget via stream_socket_client(), with a short connection timeout, no reading of the response.

function dropready_send_heartbeat(string $apiBaseUrl, string $tenantId, string $jwt): void
{
    $parts = parse_url($apiBaseUrl);
    if ($parts === false || empty($parts['host'])) {
        return;
    }

    $scheme = $parts['scheme'] ?? 'https';
    $host = $parts['host'];
    $port = $parts['port'] ?? ($scheme === 'https' ? 443 : 80);
    $path = rtrim($parts['path'] ?? '', '/') . '/' . rawurlencode($tenantId) . '/heartbeat';
    $transport = $scheme === 'https' ? 'ssl' : 'tcp';

    $connectTimeoutSeconds = 0.2;
    $socket = @stream_socket_client("$transport://$host:$port", $errno, $errstr, $connectTimeoutSeconds);
    if ($socket === false) {
        return;
    }

    $request = "POST $path HTTP/1.1\r\n"
        . "Host: $host\r\n"
        . "Authorization: Bearer $jwt\r\n"
        . "Content-Length: 0\r\n"
        . "Connection: close\r\n"
        . "\r\n";

    @fwrite($socket, $request);
    fclose($socket);
}

Worth noting - in production this is ssl://, not tcp://. Even fire-and-forget over HTTPS still pays for the TLS handshake, but that cost is bounded by the connection timeout, 0.2s, not by processing time on the other end.

Measured results: the page renders in about 1.1ms, even though the fake heartbeat endpoint deliberately responded with a 1.5 second delay. With a completely unreachable address - a network black hole, not a "connection refused" - the connection timeout capped the wait at around 205ms, against a configured limit of 0.2s.

Token details

The public key is 32 raw bytes, 44 base64 characters. The token itself, an EdDSA JWT, is valid for 20 minutes. It was originally 5 minutes, kept alive solely by the heartbeat - once the heartbeat mechanism was fixed (see above), that dependency went away, so the token got its own, longer lifetime.

Where things stand with dealit.gg

DropReady's infrastructure for dealit.gg - queue, admin panel, licensing - is up and running, verified end to end. What's still in progress is dealit.gg rolling out the interceptor itself into their store, and that's something they're deliberately holding off on until their own review wraps up, not us waiting on them with something broken.

A good, honest part of this story: not "we built it and it looks fine", but "a real client did a thorough code review and we had things to fix".

FAQ

How is Early Intercept different from a standard anti-bot solution?
Early Intercept doesn't filter traffic or judge whether a visitor is a bot. It only checks whether they have a valid, signed admission token from the queue. It's an access control mechanism, not threat detection.

Why does token verification work without a call to the DropReady API?
Because we use an asymmetric signature, Ed25519. The client only has the public key and can verify the signature locally, without asking the DropReady server every time. That reduces latency and network dependency to zero for the verification step itself.

Does Early Intercept require integration with a specific framework?
No. The hook runs before the framework's routing, at the level of index.php itself, so it works regardless of whether the store runs on Laravel, WordPress, or anything else.

Where does the 20-minute token limit come from?
It was originally 5 minutes, kept alive solely by the heartbeat. When we fixed the heartbeat mechanism, it turned out that dependency was too fragile - so the token got its own, longer, independent lifetime.

DropReady

If you're running a store with traffic that can flood it in seconds - drops, launches, promotions - check out DropReady.

Michał Sobczak
Michał Sobczak https://msobczak.pl
Home About Us SaaS Blog Contact Get in Touch