← All churts

uBixCore 0.17 — Real-Time Publishing Without a Client Library

uBixCore 0.17 — Real-Time Publishing Without a Client Library

The Problem

A PHP-FPM request ends. That’s the whole point of it, and it’s why PHP scales so boringly well. It’s also why you can’t hold a WebSocket in one. The first product built on uBixCore needs things to happen live: a new post should reach a supporter who’s signed in right now, a direct message should appear without a refresh, a live stream’s chat should move. The specs for all three had quietly written down “a WebSocket endpoint on the API”, which was never going to work, for two reasons:

  1. Nothing holds the socket. Something long-running has to own the connection.
  2. Pods don’t talk to each other. A message posted to API pod A has no way to reach a socket held by pod B.

The answer is a well-worn shape: a small gateway that holds the browser sockets, a message bus between the PHP publishers and the gateway pods, and the database as the source of truth. uBixCore 0.17 ships the framework half of that.

What 0.17 Adds

Everything lives under Ubix\Service\Realtime:

  • RealtimePublisherServiceInterface, the seam a host calls after it has written the row an event is about.
  • NatsRealtimePublisherService, a publish-only NATS client. Publishing is five protocol verbs (INFO, CONNECT, PUB, PING, PONG), so it speaks them itself instead of pulling in a client library whose subscription, request/reply and JetStream machinery a PHP request would never touch. No new dependency. Every publish is followed by a PING and waits for the PONG, because NATS reports a refused publish (a permissions violation) asynchronously. That round trip is what turns “written to a socket” into “accepted by the server”, so a misconfigured account shows up as an exception, not as silence.
  • SubjectGrantService, which signs and verifies short-lived grants: a statement from the host that this user may subscribe to these subjects, for the next 60 seconds.

Grants, Not Sessions

The gateway never sees a session cookie and never touches the database. The host decides who may see what, using the same entitlement checks as everything else, and hands the browser a grant. The gateway verifies it and subscribes to exactly what it names. Nothing else.

That only works if the gateway can be written in something other than PHP, so the token format is deliberately language-neutral:

v1.<base64url JSON claims>.<base64url HMAC-SHA256 signature>

The signature is checked over the received bytes, in constant time, before the payload is decoded, so JSON key order can never matter. Both implementations test against the same fixed token, and that token was computed independently in a third language. If either side drifts, a test fails before a user notices.

The Rules That Keep It Honest

  • The database is the truth. A publish happens after the commit; delivery is at-most-once; a client that reconnects refetches over REST. A missed push costs latency, never data.
  • Who hears about something is decided in PHP. A new post is resolved to the entitled readers by the host, then published once per reader. It is never broadcast to a wide subject for the gateway to filter, because that would move access control into the one component that isn’t supposed to make access decisions.
  • Least privilege on the bus. Publishers may only publish; the gateway may only subscribe. A compromised gateway can’t forge an event.

The Go gateway that verifies these grants lives in the host project, not the framework, because it versions with the API that signs its grants. It’s written to know nothing about the product, so moving it out later is a directory move.

Try It

uBixCore v0.17.0 is on github.com/ubixsys/ubixcore, and ubixsys.com/ubixcore has the full capability list. Upgrading is the usual one-line diff:

composer update ubixsys/ubixcore
git add composer.lock && git commit -m "chore: ubixcore 0.17"
← All churts