FrankenPHP is pretty badass, especially when used with Laravel Octane to make your Laravel app really fly. But what about real-time feedback to the frontend?
As it happens, FrankenPHP ships with Mercure support out of the box. But configuration can be a little challenging. Having just fought with it for longer than I'd like to admit, I thought I'd share some of the useful tidbits I learned, in hopes of saving someone else the brain damage.
For my purposes, I'm running the ServerSideUp FrankenPHP Docker image, the first challenge was deciphering just how to go about getting it setup at all because the docs there only mention that it's available, but not how to configure.
At first I just tried setting environment variables in my docker config, which seemed to .. sort of work, but I just continually received 403 errors. Then I decided to try the CADDY_SERVER_EXTRA_DIRECTIVES env var, and define the mercure module config:
CADDY_SERVER_EXTRA_DIRECTIVES: |
mercure {
publisher_jwt !mySuperSecretKey!
anonymous
}After that change, I was getting a 401 response.. progress, but still no joy.
I'll be brief; the jwt secret needs to be surrounded by ! if it's a hash (base64, for instance) but if it's hex, you need to omit the ! or it'll misinterpret the key and your properly signed request will be treated as invalid.
So, tl;dr which doesn't seem to be documented very well anywhere:
openssl rand -base64 48Produces something like:
7/SrS1rBcVyw2NK2sj2VD2guaK6PNlfnIDIHse6mBpWE7N2jTt1pXldMnKsmA
Surround it with !
openssl rand -hex 48Produces something like:
483b2395ceaee186816bfa6972d4a8a8d7025c838220c4fe2e3dfa43e907ee7
Do NOT surround it with !
The other issue I ran into was that it complained that it could not open the boltDB for writing, that was easily solved by explicitly defining the transport path to a writeable directory, in my case one I mounted from my host so it persists if I remove the container, and more important so I could just easily see that it created the file... 😅
My final env entry to get it working looks like this (not my actual keys):
CADDY_SERVER_EXTRA_DIRECTIVES: |
# Mercure Hub for real-time Server-Sent Events
mercure {
# Publisher JWT secreA
publisher_jwt 123abc HS256
# Subscriber JWT Secret
subscriber_jwt 456xyz HS256
# Allow anonymous listeners
anonymous
# Allow CORS from anywhere
cors_origins *
# Allow Publishing from anywhere (or maybe just your server)
publish_origins *
# Tell it where to write the bolt db, somewhere it CAN write
transport bolt {
path /opt/frankenphp/mercure.db
}
}All the available mercure directives can be used here.
So, why go to the trouble? After all I could just use Reverb, right? Well websockets are inherently bi-directional, and I have no need for that, where as mercure is really intended for sending to client-side subscribers, and for mobile users, is easier on the battery/etc. It's a pretty cool protocol that I anticipate using more often moving forward. And, bonus - no need for another docker container or hoop jumping to run Reverb in my frankenphp container, which means I have a few more resources to spread around the other services, and one less container to orchestrate and manage.