Wyatt Kirby
By Cofounder & CTO
black laptop computer turned on on table

Welcome to the inaugural devlog! These posts are here to serve a few different purposes: first and foremost, we fundamentally believe that being open about our design decisions leads to a stronger product. Second, we’ve found lots of value in other founders sharing their own stories about bootstrapping, and a big part of that process is our technical decisions. And lastly, we like to bike shed—a lot—and writing about our own development process gives me an opportunity to do just that.

These posts are going to be fairly casual in tone, and at no regular interval. When we think there’s an interesting technical problem or design decision, we’ll probably write it up. That means these posts will cover, eventually, every part of the development process: bugs, features, systems design, and more bugs.

For this inaugural post, though, I’d like to gesture vaguely at the overall architecture and tech stack behind Ream. That way, in the future, we can point back here instead of re-explaining why the application is the way it is. So let’s dig in.

The Application

At it’s core, Ream the application is three things: a fairly standard CRUD app, an HTML-to-PDF templating and rendering pipeline, and a real-time backend for concurrent WYSIWYG editing. The application, while managed in a monorepo, is also broken up into 3 separate applications, which don’t quite map to the parts above: a Ruby on Rails application, a React client (built on Next.js), and a small sync service running essentially stock Hocuspocus.

Behind these three applications are a slew of the standard AWS TLAs: S3 for file storage, RDS for our PostgreSQL database, ECS for managing our runtime containers, ElastiCache running Redis (for now), SES for delivering emails, and down the line we expect to add SQS and SNS, along with a half dozen other services that get spun up over the lifetime of any long-lived web application.

Nothing about our use of these services is novel or interesting, but I mention it all to provide the foundation for how the application runs in the real world. With that out of the way, let’s talk about the application... well, applications.

The Server

The best place to start is our Ruby backend. We built Ream on Rails, as we have built most applications for the past decade. Rails has fallen out of favor, but mostly because it has become boring, and we view that as one of its core strengths. I can trust that the code we are writing today will run on Rails 5 or 10 years from now because the fundamental design of Rails is sound. If you were to dive into the Gemfile of our Rails application, you’d find a lot of the old standards: Devise, Administrate, Lograge, Rspec.

You’ll also find our own gems which we’ve built and maintained over a decade of services work. I’ll pull out two in particular which drive the design and implementation of the application: Slayer and papers_please. Slayer is our “service layer”, and wraps almost all our core business logic in reusable, composable, easily-tested “commands.” We’ve been building Rails applications on top of Slayer for years now, and we find it makes the process of adding new features really pleasant: add a new command, that command has defined inputs and defined outputs, write tests for that command, then hook that command to the rest of the Rails platform. papers_please, meanwhile, is our own take on RBAC for Ruby, and gives us a strong and efficient mechanism for scoping access in the application.

The Server is also home to our HTML-to-PDF templating and rendering pipeline. We’ll dive into this process in more detail in a future devlog, but under the hood this process pipes data through the Liquid templating engine (n.b. for now—there is a strong desire to move away from liquid for reasons we’ll detail when that migration happens), into Pandoc, and finally to a headless instance of Chromium managed by ferrum. We’ve explored all kinds of HTML-to-PDF options (another future entry in this devlog, I’m sure), but ultimately we find the most predictable results come from trusting a browser to interpret HTML and CSS correctly.

One final note before we move on to the other components of the application: because 2 of 3 components of our application are written in Typescript, there’s a reasonably strong desire to move the server to Typescript as well—but as yet there’s nothing that offers as pleasant a batteries-included experience in the TS ecosystem as Rails. We’ll see which happens first: a really viable Typescript-based alternative to Rails, or a really viable type system we can layer on top of our Ruby code.

The Client

Our client is built with React on top of Next.js. Most of the client code is also boring: being boring is one of our core principles of application development. Where it is probably most interesting is in the two mini applications which constitute our document editing frontends: one for text-based documents built on top of the excellent tiptap (which is in turn built on top of the truly excellent ProseMirror). The particulars of our tiptap implementation likely merit their own entry, but the high points are its extensibility and robust support for bridging between the rest of our React application and the editor instance itself.

Our second editing interface, for file-backed documents, is an entirely in-house solution, backed by HTML drag-and-drop API. Both editing interfaces, though, are wrapped by a shared context, which is synchronized via yjs. That synchronization is managed by the Sync Service.

The Sync Service

The Sync Service is the smallest of the three applications, and farms almost all of its functionality out to Hocuspocus. Out of the box, this gives us CRDT backed content synchronization, and closes the loop between all three applications: updates to the underlying document are posted back to the server for storage in S3, and to expose the “live” content of any given document to the backend.

Final Thoughts

If you’ve stuck with me for this long, congratulations. The technical underpinnings of an application are less interesting, ultimately, than the features that they support—but I think an important foundation for understanding how, why, and when those features get implemented. The stack that I’ve described above could serve as the backing stack for any number of applications, with almost no changes to the fundamentals—but we believe that is one of its greatest strengths; the architecture offers both flexibility and stability, which allows us to build quickly and pivot frequently all without putting our technical future at risk.

If the technical particulars of Ream are interesting to you, I hope you stick around. Now that we’ve laid the ground work I’m excited to dig into some of the more exciting features we’ve worked through: robust document versioning, the pros and cons of WYSIWYG editors, content-specific document metadata, and more.

Until then, we’d love for you to try our beta.