Persistent PHP objects, inside Cloudflare Durable Objects.

An Atom is a long-lived PHP object running inside a Cloudflare Durable Object. Your PHP app calls it like a normal object - but each Atom controls its own SQLite database and talks directly with clients over WebSockets.

This is an Atom

An Atom is defined as a normal PHP class. It has its own database schema: each Atom instance has its own state separate from your app, and you query it with Eloquent models through atoms/database-illuminate or with plain SQL. An Atom can communicate with your app and directly with clients. Use an Atom to represent a game lobby, a document, a chat room, and more.

join()

Write any method and call it from your PHP monolith. In this example, the main app adds a new player to a game.

onConnect()

Called when a client makes a WebSocket connection.

onMessage()

Called when a message arrives from a connected client.

app/Atoms/GameRoom.php

namespace App\Atoms;

use Atoms\Atom;
use Atoms\DatabaseIlluminate\EloquentBridge;
use Atoms\Websocket\Connection;
use Atoms\Websocket\Message;

class GameRoom extends Atom
{
    public function join(string $player): int
    {
        $db = EloquentBridge::boot($this->db());

        $seat = $db->transaction(function () use ($player): int {
            $taken = Player::query()->count();

            if ($taken >= 4) {
                throw new \DomainException('room_full');
            }

            Player::create(['name' => $player, 'seat' => $taken + 1]);

            return $taken + 1;
        });

        $this->broadcast('room', ['kind' => 'joined', 'player' => $player, 'seat' => $seat]);

        return $seat;
    }

    public function onConnect(Connection $conn, array $params): void
    {
        EloquentBridge::boot($this->db());

        Player::where('name', $params['player'])
            ->update(['connection_id' => $conn->id()]);

        $conn->sendJson([
            'kind'  => 'welcome',
            'board' => Piece::all(['piece', 'square', 'owner'])->toArray(),
        ]);
    }

    public function onMessage(Connection $conn, Message $msg): void
    {
        EloquentBridge::boot($this->db());

        $move  = $msg->json();
        $mover = Player::where('connection_id', $conn->id())->first();

        $moved = Piece::where('piece', $move['piece'])
            ->where('owner', $mover?->name)
            ->update(['square' => $move['square']]);

        if ($moved === 1) {
            $this->broadcast('room', ['kind' => 'moved'] + $move);
        }
    }
}

anywhere in your app

$seat = Atoms::get(GameRoom::class, 'room-42')->join('ada');

Fetch or create an Atom from your monolith

Browsers connect directly to Atoms over WebSockets

Each Atom can communicate directly with clients over WebSockets. A game Atom can accept a player’s move, update the state of the game, and broadcast that new state to all other players.

in the browser

const { url } = await fetch('/api/rooms/room-42/socket', { method: 'POST' })
  .then((res) => res.json());

const ws = new WebSocket(url);
ws.onmessage = (e) => drawBoard(JSON.parse(e.data));

// a player moves a piece
ws.send(JSON.stringify({ piece: 'rook', square: 'd4' }));

The browser requests a connection URL from your PHP app

Authentication

A browser requests a WebSocket connection URL from your PHP app, which sends it with a securely signed ticket using a shared secret. An Atom can use that ticket to identify and authorize the client.

routes/api.php — a normal authenticated route

Route::middleware('auth')->post('/rooms/{room}/socket', function (string $room) {
    return ['url' => Atoms::wsUrl(GameRoom::class, $room, [
        'channels' => ['room'],
        'ticket'   => (string) Atoms::ticket(GameRoom::class, $room, [
            'player' => auth()->user()->name,
        ]),
    ])];
});

How it works

Write your Atoms alongside the rest of your PHP app - but deploy them to Durable Objects.

your repo

Atoms classes are defined in your PHP app. atoms build extracts them into a bundle.

atoms deploy

Run atoms deploy (locally or in CI) to push your bundle to Cloudflare.

your cloudflare account

A Durable Object is created for an Atom instance as soon as your app calls a method on it or a client tries to connect.

Work locally using atoms dev, which runs your Atoms on Cloudflare's open-source workerd runtime.

Why use Atoms?

Offload state

Some applications don't need to store all state centrally. Each Atom class has its own database schema, and each instance has its own persistent SQLite database.

Scoped real-time interactions

In many applications, real-time traffic belongs to one thing in your domain: a game, a chat, a team. Each of those things can be defined as an Atom that defines its own real-time interactions.

Offload compute

Keep your monolith's server small by pushing compute to your Atoms. You could even have your monolith scaled to zero while your users are still interacting with Atoms!

FAQ

Where do my Atoms run?

In your own Cloudflare account. Atoms is an open-source library that you deploy yourself. A Workers Paid plan is required due to the PHP runtime size exceeding free tier limits.

Which PHP versions are supported?

The atoms/* packages require PHP 8.3 or newer in your main app. Atoms themselves currently run on PHP 8.3 compiled to WebAssembly inside the Durable Object.

Which frameworks does Atoms support?

There are adapters for Laravel and Symfony. Any other PHP application can use atoms/client directly - use the framework adapters for inspiration!

Is Atoms ready for production?

Atoms is new and experimental - for now, use it at your own risk.

Is Atoms affiliated with Cloudflare, Laravel, or Symfony?

No. Atoms is a project by Daniel Abernathy (@dabernathy89) and it is neither affiliated with nor endorsed by Cloudflare, Laravel, or Symfony.

What’s on the roadmap?

Future plans include:

  • an Atoms-specific PDO driver to repair some limitations of the current SQLite bridge
  • a custom WebAssembly build of PHP optimized for bundle size and cold boot time

Get started

$ composer require atoms/laravel
$ # If you're working in Laravel:
$ php artisan atoms:install

$ # Scaffold the Worker directory (commit it):
$ npm exec --yes --package=@atomsphp/runtime-cloudflare -- \
    atoms-runtime-cloudflare init atoms-worker
$ # Install JavaScript dependencies:
$ (cd atoms-worker && npm ci)

$ vendor/bin/atoms dev
$ vendor/bin/atoms deploy