Cloudflare Durable Objects
Durable Objects are a new type of serverless compute platform from Cloudflare, and provide a way to run stateful applications in a serverless environment, without needing to manage infrastructure.
What Are Durable Objects?
Each Durable Object can function as a WebSocket server, so it can co-ordinate messages between multiple clients. But importantly, it also has private, transactional and strongly consistent storage attached. Combined with a TinyBase Store
on the client, and using the built-in synchronization and persistence functionality, this gives you an full-stack way to build complex, real-time, multi-device (and even collaborative) apps.
As this guide hopefully shows, this can be done with minimal effort!
Getting Started with Vite
The quickest way to get started with TinyBase and Cloudflare Durable Objects is to use our Vite template. This includes both a client implementation (that by default connects to a demo server we provide) and a server implementation that you can instead use for your own Cloudflare installation.
Install The Client
- Make a copy of the template into a new directory:
npx tiged tinyplex/vite-tinybase-ts-react-sync-durable-object my-tinybase-app
- Go into the client directory:
cd my-tinybase-app/client
- Install the dependencies:
npm install
- Run the application:
npm run dev
- Go the URL shown and enjoy!
Run Your Own Server
This template uses a lightweight socket server on vite.tinybase.cloud
to synchronize data between clients. This is fine for a demo but not intended as a production server for your apps!
If you wish to run your own instance, see the server
directory and start from there.
The vite.tinybase.cloud
server is hosted on Cloudflare (of course), so you should adapt the wrangler.toml
configuration in the server directory. Update it to match your account, domains, and required configuration. In the index.ts
file, you can configure whether data will be stored in the Durable Object or just synchronized between clients.
You will also have to have your client communicate with your new server by configuring the SERVER
constant at the top of the client's App.tsx
file.
How It All Works
Client: Persistence and Synchronization
On the client, we create a simple TinyBase MergeableStore
to encapsulate the data we need for the app. The following are simplified extracts of the code in the App.tsx
file of the Vite template:
export const App = () => {
const store = useCreateMergeableStore(createMergeableStore);
// ...
(Because the template is using React, we use the useCreateMergeableStore
hook so it's not re-created on every render.)
We also create a local Persister
so that if the client goes offline, and the browser window is refreshed, changes will have been cached locally in session storage, with a key 'foo':
// ...
useCreatePersister(
store,
(store) => createLocalPersister(store, 'foo'),
[],
async (persister) => {
await persister.startAutoLoad(/* any initial contents */);
await persister.startAutoSave();
},
);
// ...
More interestingly, we also set up a Synchronizer
that connects to the Cloudflare installation, on a path that also happens to be called 'foo':
// ...
useCreateSynchronizer(store, async (store) => {
const synchronizer = await createWsSynchronizer(
store,
new WebSocket('wss://example.com/foo'),
);
await synchronizer.startSync();
return synchronizer;
});
Those simple lines are enough to have the Store
attempt to synchronize itself with the common Durable Object called /foo
on the server.
Server: Worker and Durable Object
On the server, Cloudflare needs us to configure a worker and Durable Object. It will help if you are familiar with these concepts already - and if not, start with the documentation here.
Following the instructions above, you'll have a wrangler.toml
file containing the configuration for your worker and the Durable Object. In there you'll need to bind a namespace of Durable Objects to a class, something like:
[[durable_objects.bindings]]
name = "TinyBaseDurableObjects"
class_name = "TinyBaseDurableObject"
[[migrations]]
tag = "v1"
new_classes = ["TinyBaseDurableObject"]
(Again, take a look at the contents of the Vite template for the full configuration file.)
In the main worker file, probably called index.js
or index.ts
, you'll need to configure the worker as the default export from the file. TinyBase provides a convenience getWsServerDurableObjectFetch
function that will create a fetch
method that routes WebSocket requests based on the path of the URL:
export default {
fetch: getWsServerDurableObjectFetch('TinyBaseDurableObjects'),
};
In here, the argument is the namespace containing your bound Durable Objects.
Now we need to create the Durable Object itself. This can be as simple as simply extending TinyBase's WsServerDurableObject
class:
export class TinyBaseDurableObject extends WsServerDurableObject {
// ...
}
This sets up synchronization between any clients that connect to this common /foo
path so that their Store
data stays in sync.
But if all your clients disconnect and flush their locally-stored data, it is technically possible to lose it all! So it's also a good idea to have the Durable Object store a synchronized copy too. We do this by overriding the createPersister
method:
// ...
createPersister() {
return createDurableObjectStoragePersister(
createMergeableStore(),
this.ctx.storage,
);
}
// ...
In this method, all we need to do is create a MergeableStore
(that will reside in the Durable Object memory whenever it is running and not hibernated), and indicate how it will be persisted. Almost always you will want to return a DurableObjectStoragePersister
object, which is dedicated to storing a TinyBase Store
in Durable Object storage - as indicated by the this.ctx.storage
argument.
With this in place you now have the full set up! The clients are storing a local copy of the TinyBase data so they can go offline and reload without loss of data; and the server is also storing a copy of the data in Durable Object storage. When online, the clients will connect to the worker, which routes the request to the Durable Object indicated by the URL path, and synchronization between them all keeps them each up-to-date!
Final Notes
Durable Objects have limitations on the data that can be stored in each key of their key-value structure. The DurableObjectStoragePersister
uses one key per TinyBase Value
, one key per Cell
, one key per Row
, and one key per Table
. Mostly this is CRDT metadata, but the main caution is to ensure that each individual TinyBase Cell
and Value
data does not exceed the (128 KiB) limit.
The WsServerDurableObject
is an overridden implementation of the DurableObject class, so you can have access to its members as well as the TinyBase-specific methods. If you are using the storage for other data, you may want to configure a prefix
parameter to ensure you don't accidentally collide with TinyBase data.
Also, always remember to call the super
implementations of the methods that TinyBase uses (the constructor, fetch
, webSocketMessage
, and webSocketClose
) if you further override them.
Finally, the WsServerDurableObject
uses hibernation, which is a Cloudflare feature to minimize the amount of memory used by your Durable Object. After a small amount of time with no WebSocket activity, it will be 'hibernated' even though the WebSockets stay connected. This means that the in-memory TinyBase Store
will be removed (which is a good thing for your Cloudflare usage!) and then re-created with the Persister
when new activity arrives. This lifecycle should be transparent, but should be understood if you want to debug certain Durable Object behaviors.