How we use Rust, SQLx and Rocket for Oso Cloud
Here at Oso, we like Rust. We wrote the core of Oso, the Polar language, in Rust. Rust is a great language to build a language in, and using Rust made it easy for us to embed Polar in other languages as part of the Oso library.
Recently we've been working on a new way to use Polar. Instead of embedding it in a library, we've moved it to a service that multiple other services can use. This service architecture lets us build new features easily, like the ability to make authorization decisions based on data from multiple services (which is useful for customers’ microservice architectures), better role management, and tools to help build policies. It also means you can use Polar in languages we don’t yet have libraries for, like C# and PHP.
To learn more about how Oso Cloud works, you can check out Oso Cloud’s own documentation.
Rocket Web Framework
The new service uses the same Polar core as the library but combines it with APIs to manage roles, relationships and permissions. This meant we needed to build some new web APIs and also add a data store.
We wanted to use Rust for the API. First, because we all know it well. Also, it would let the Polar core and the new service be a unified code base without an FFI layer between them.
We had built Rust web stuff in the past and, to be honest, the experience wasn’t too great. But a lot had changed since we last looked! For instance, we’d built an old service with Tokio, but that was before async. When writing this new service, we expected to have to write those long chains of futures with
map_err and all kinds of hard-to-follow combinators. Async, thankfully, makes that kind of code a lot easier to read. Async’s improvements made it an easy choice for us to go with Rocket.
Rocket is a web framework that is built on top of Tokio. It uses async Rust and does a lot of our work for us. Rocket has some magical type macros that let us specify the inputs to our request handlers as normal Rust structs. It then uses those types (with some help from the serializing-deserializing library Serde) to take care of all the up-front validation before calling the handler.
This drastically cuts down on the amount of “defensive programming” we have to do. When we say a handler takes a logged-in user and a certain struct, we know that Rocket will make sure all the data is as we’ve described before it calls us. There’s no need to check headers or see if fields are null, or anything like that in our handlers.
There are some downsides. The Rust programming ecosystem is pretty new. There are lots of things you would take for granted in a more mature web ecosystem like Django or Rails that you end up having to do yourself in Rust. We’ve had to build a lot of things ourselves, like Github OAuth. We’ve built custom integrations with Segment.io and Honeycomb.io because there were no existing libraries that covered our needs. This takes time, but hasn’t been a reason to switch away from using Rust.
SQLx Database Library
Oso Cloud stores roles, relations, and permissions. This data is read on every authorize request, which might be every single request in a customer’s app. It gets written to much less frequently. So, read latency and query execution speed are very important to us. We need to be able to make authorization decisions fast, and make those decisions close to where our customer’s applications are.
We decided to go with SQLite as the data store for Oso Cloud. By pairing SQLite with Litestream, we were able to meet our latency requirements. SQLite is embedded in the Rust process so there is very minimal latency between the data and our Polar core. We can also put read replicas in different regions and clouds so authorization requests to Oso Cloud from our customers are as fast as possible.
In Oso Cloud, each tenant gets their own SQLite database. This makes it easy for us to do both single-tenant and multi-tenant deployments of Oso Cloud.
The last thing to decide was how we wanted to talk to SQLite from Rust. There are several Rust SQL libraries we could have used, but we chose SQLx.
SQLx is a SQL interface to SQLite. That means it is not an ORM—instead, you communicate with SQL queries. It has some macros that verify the SQL you are sending and map query results back to types.
The coolest part about SQLx is that it checks the SQL at compile time against a development database. If SQLite gives an error for your query, SQLx turns that error into a Rust error. This means your Rust project won’t compile without valid SQL. One of the great things about Rust is you can make a change to a struct and the compiler will show you all the places in the code that have to be updated. SQLx extends this model to the SQL code too. You can create a new migration, run it on the development database, and SQLx will show you any malformed SQL in your app. It makes writing SQL feel the same way writing Rust does and creates a great development loop.
SQLx is a new-ish library, so there are a few things that aren’t quite right. The way it references a development database to check the queries assumes that there is only one development database. We have per-tenant databases plus a control database that contains information about tenants and users and other operational things. To get SQLx to work for both the tenant and control databases, we had to break them into separate crates and do some tricky stuff with env vars.
SQLx also doesn’t support
in queries, which is a little unfortunate. There is a workaround for Postgres, but we’re hoping they add expansion for
Vec types soon which should make it work.
We've been pretty happy with our choice of database and library, and we have been able to work around all the rough edges so far. Having the whole codebase be end-to-end Rust has made refactors and other changes much easier. If you are interested in Oso Cloud, you can check out the docs, try it out in our sandbox or schedule a demo!