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.
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); } } }
app/Atoms/GameRoom.php
namespace App\Atoms; use Atoms\Atom; use Atoms\Database; use Atoms\Websocket\Connection; use Atoms\Websocket\Message; class GameRoom extends Atom { public function join(string $player): int { $seat = $this->db()->transaction(function (Database $db) use ($player): int { $taken = (int) $db->query('SELECT COUNT(*) c FROM players')[0]['c']; if ($taken >= 4) { throw new \DomainException('room_full'); } $db->execute('INSERT INTO players (name, seat) VALUES (?, ?)', [$player, $taken + 1]); return $taken + 1; }); $this->broadcast('room', ['kind' => 'joined', 'player' => $player, 'seat' => $seat]); return $seat; } public function onConnect(Connection $conn, array $params): void { $this->db()->execute( 'UPDATE players SET connection_id = ? WHERE name = ?', [$conn->id(), $params['player']] ); $conn->sendJson([ 'kind' => 'welcome', 'board' => $this->db()->query('SELECT piece, square, owner FROM board'), ]); } public function onMessage(Connection $conn, Message $msg): void { $move = $msg->json(); $moved = $this->db()->execute( 'UPDATE board SET square = ? WHERE piece = ? AND owner = (SELECT name FROM players WHERE connection_id = ?)', [$move['square'], $move['piece'], $conn->id()] ); 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
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
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, ]), ])]; });
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.
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.
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.
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!
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.
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.
There are adapters for Laravel and Symfony. Any other PHP application can use
atoms/client directly - use the framework adapters for inspiration!
Atoms is new and experimental - for now, use it at your own risk.
No. Atoms is a project by Daniel Abernathy (@dabernathy89) and it is neither affiliated with nor endorsed by Cloudflare, Laravel, or Symfony.
Future plans include:
$ 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