Setting up Rocicorp's Zero with Ruby on Rails
Published on 2025-02-04
In December 2024, Rocicorp opened sourced their new project Zero. I had been patiently waiting for this to happen and dove right into adding it to a project I had recently started working on. If you've used tools like Linear or Superhuman, they are powered sync engines, which in layman's terms means that data to be displayed in the UI is synced from the database to a client datastore in the browser. In doing so, the UI becomes extremely fast because there is no latency in waiting for API calls to fetch data as you navigate from page to page. There are more than a handful of solutions we as application builders can choose from in this space and if you want to know more about those, I suggest taking a look at The Spectrum of Local First Libraries. However, we're here to talk about Zero and specifically using Ruby on Rails to manage our Zero setup.
One thing to note is that Zero is a library that excels in the Single Page Application space. There are currently two integrations written by Rocicorp, React and Solid, as well as two community implementations for Svelte and Vue. I've been using the React implementation with a Next.js UI and using Rails as a backend to manage the databases and other business logic. I'm not currently familiar enough with vite-ruby to know if Zero would work with that setup, but I may try to explore this route in the future.
With all that said, there won't be a lot of UI code here as there are examples on the Zero Samples documentation page, but there will be a few places where I sprinkle in a bit to tie together why we're setting things up the way we are in Rails.
Databases Setup#
Yes that is plural databases, Zero requires three databases to operate. If you haven't looked at the Rails guide for Multiple Databases with Active Record before I would recommend familiarizing yourself with it before continuing to read here. It covers everything we need to know about setting up the multiple databases needed and then some. The first is the primary database where our Rails migrations and ActiveRecord Models will operate, however Zero refers to it as the upstream database. The second is the Change View Records (CVR) database which contains information about what has been synced to clients. The third and final is the Change Database (CDB) which stores the replication log from the primary database. All we need to do is create the CVR and CDB databases and Zero will handle the rest, but we'll cover a bit more information about them.
One final note is that Zero is only capable of working with PostgreSQL at the moment. The Zero Connecting to Postgres documentation notes this fact.
In the future, Zero will work with many different backend databases. Today only Postgres is supported. Specifically, Zero requires Postgres v15.0 or higher, and support for logical replication.
For now, let's get started on creating the databases.
Primary Database
The following database.yml
is very similar to the default that is generated by Rails for use with PostgreSQL.
I have changed the default username
and password
variables and named the application blog_application
. The
main change here is that we've added the primary
key as a name for this database. If you recall from the Multiple
Databases documention, it noted that primary
is a special key.
If a primary configuration key is provided, it will be used as the "default" configuration. If there is no configuration named primary, Rails will use the first configuration as default for each environment.
We'll interact with this database in the same manner as we have with any other Rails application. We're not going to cover
creating any Models here, but we would create migrations the same way we always have and they will apply to this
primary
database.
CVR Database
As mentioned before, the CVR database holds information about what data has been synced to the clients. We're not going to get into how all that works because it goes over my head. At the end of this section, we'll take a look at the tables that have been created.
There isn't much to it, we use the default
configuration, and change the name to blog_application_cvr
.
Zero will manage the initial setup of the schemas and tables for us so we won't need to make any
migrations at the moment. We do need to set the migrations_paths
or else our migrations in db/migrate
will be applied
to this database. If you recall from the Multiple Databases documentation, it states that we need to tell Rails where to
find the migrations.
Migrations for multiple databases should live in their own folders prefixed with the name of the database key in the configuration.
You also need to set migrations_paths in the database configurations to tell Rails where to find the migrations.
For example the animals database would look for migrations in the db/animals_migrate directory and primary would look in db/migrate.
Change Database
The Change Database holds the replication log from the primary database so that Zero can apply the change deltas to individual rows. Again, we'll skip over exactly what is stored here because it goes over my head.
This setup is exactly the same as the CVR database, just replacing cvr
with cdb
.
Viewing Zero's Tables
Now we're able create all three databases via rails db:create
and ensure that all the
databases have the same username and password to avoid any confusion when trying to connect to them.
Those values will be needed in the next section when we set up our UI to talk to these databases.
Now that we've created the databases, I want to show the tables that Zero will create; all we've done so far is create a blank database, once the UI application starts and runs Zero, the tables will be populated. As an end user of Zero we most likely won't ever be interacting with these tables, but it is nice to know what exists.
PostgreSQL Configuration
The final piece to setting up the databases are a few changes to PostgreSQL itself. Please read and follow the steps in the Zero PostgreSQL Configuration documentation before proceeding.
UI Environment Variables#
The environment variables needed for the UI are shown in the hello-zero README along with explanations for each one. I've copied them and updated the values to represent our setup.
Since we're managing all the databases with Rails, I thought it was important to detail these environment variables since they'll live in a separate portion of the application project, whether that is a different folder for a monorepo or an entirely different repository.
For all the *_DB
values, we're using the default values from database.yml
to fill in the username and password
sections (localuser:localpw
). And then we're binding the address to localhost IP address (127.0.0.1
).
ZERO_AUTH_SECRET
can be any value you wish to use, I've chosen zerosecretkey
which is good for this blog post,
but I wouldn't recommend it for production. All of ZERO_*
environment variables are used by the
Zero Cache server and started via a
command like dev:zero-cache.
The only environment variable we need to expose to our application's UI is where to talk to the Zero
Cache server, which in our case is the default value from Zero of http://localhost:4848
. That port can be
configured, as well as many other options. I've exposed
this value as NEXT_PUBLIC_ZERO_CACHE_PUBLIC_SERVER
since I'm using Next.js, however if you're using
Vite, like hello-zero, then you would use something like VITE_PUBLIC_SERVER
.
We'll reference this environment variable later on in this walk through.
Authentication#
While it is possible to use Zero in an unauthenticated manner, we set up our ZERO_AUTH_SECRET
environment
variable to ensure that the data being accessed and mutated is performed and authorized by an authenticated user.
To that end, I suggest using whatever authentication solution you are most familiar with for Rails. The
application I've been building is using the new Rails 8 authentication generator, which provides session
based authentication. It is important to note that it is session based authentication because Zero
currently has a requirement to use a JWT for authentication. This doesn't mean we need
to swap our Rails authentication from session based to JWT based though; we can generate a JWT from
an API call and encode the currently logged in user.
JWT Creation in Rails
Let's get started by adding the jwt gem to our Gemfile
.
Next we're going to build a class that will take in a User
and create the JWT.
We need to encode a sub
field, as noted in the Zero Authentication
documentation as well as an optional exp
field for when the JWT will expire. Zero has a built in mechanism to
refetch a new JWT when the expiration occurs and we'll cover setting that up after finishing the Rails setup. Finally,
Zero has documentation on Permissions which can read additional
data from the JWT. We could encode things like an organization_id
, or a team_id
, and potentially role information
to indicate what actions the user should be able to perform. I'm not covering any of that as that implementation
will be highly dependent on each application but I wanted to note the ability to do it.
Finally, the encoding process needs a key to generate the encryption. ENV["ZERO_SECRET_KEY"]
needs to be the same
value that we set for ZERO_AUTH_SECRET
in the previous section; in terms of this article that value is zerosecretkey
.
Now that we can generate our JWT, we need to create an endpoint that will return the JWT to the UI.
A few things to note are that the ApplicationController
is enforcing authentication for this route, which is
required because we need access to an authenticated user to properly create the JWT. The second thing is the
use of Current.user
; this is how the Rails 8 authentication allows access to the currently authenticated user,
but if we were using say devise, then it would be current_user
.
That's all we need on the Rails end for our JWT so let's dive into a bit of UI code for fetching this JWT. As noted before, Zero can refresh the token when it expires as per the Authentication Refresh documentation.
In this setup, we have a function getToken
that will call the API we set up get the JWT token and return
the JWT; you should use whichever fetching library you are most familiar with in this case. We pass an
anonymous function () => getToken()
to the auth
arg, which tells Zero to use that function anytime
the it needs to authenticate, whether on first load or JWT expiration. This ties into the optional expiration
that was we set and allows Zero to automatically re-authenticate.
The other requirement is passing in a userId
which matches the sub
field
we encoded into the JWT. In this example, since we require an authenticated user to perform the getToken
call
we will have access to the userId
; how the application provides the userId
to this function is out of
scope for this walk through.
That wraps up setting up the authentication Zero, everything should be set up to start using Zero in our UI.
Conclusion#
I hope the explanations and code samples here helped you get a better understanding of Zero and how it can be set up and managed by Rails. I'm extremely excited to keep working on my project and I'll continue to post updates as I learn new approaches to solving problems. You can find all the posts related to this topic below.
All My Posts About Zero
- Setting up Rocicorp's Zero with Ruby on Rails*
- Using PostgreSQL Functions with Rocicorp's Zero and Ruby on Rails