Brutalist Twitter

See, Elon? Twitter isn't so hard.

Hey there, how's it going? Good? Probably not, given you had enough interest to start reading an article called "Brutalist Twitter." That's fine though, you might also be wondering "What is this all about?" Well, if you'd stop asking questions I'd tell you;

Brutalist Twitter is about providing the base functionality of Twitter, without all the crap that makes Twitter bad. No, we're not banning parody accounts or adding a subscription model -- we're just cutting out the extra javascript, tracking, css, ads, pretty much everything I can think of so that I can make this as quickly and effectively as possible.

I'll layout how I built it, so hopefully you can build your own, and you can see that it's not so hard. Maybe you'll learn something along the way.

Without further ado, let's make some twitter.

The Setup

We'll be using shuttle.rs as our hosting provider for this, and thruster as our web framework. That makes it easy for us to set up a new project using:

mkdir brutalist-twitter
cargo shuttle init --thruster

We're going to need a ton of dependencies too, so just add these to Cargo.toml now.

argon2 = "0.4.1" # For password hashing
askama = "0.11.1" # For templating
chrono = "0.4.22" # For time stuff
form_urlencoded = "1.1.0" # For decoding x-www-form-encoded
log = "0.4.17" # For teh logz
serde = { version = "1.0", features = ["derive"] } # For serializing/deserializing
serde_urlencoded = "0.7.1" # For url encoding
sha2 = "0.10.6" # Because sometimes you gotta hash things
shuttle-aws-rds = { version = "0.7.2", features = ["postgres"] } # For storing data
shuttle-service = { version = "0.7.2", features = ["web-thruster"] } # For deploying easily
sqlx = { version = "0.6.2", features = ["runtime-tokio-native-tls", "postgres", "chrono", "uuid"] } # For dealing with postgres
thruster = { version = "1.3.0", features = ["hyper_server"] } # For http framework stuff
tokio = { version = "1.20.1", features = ["macros"] } # For an async runtime
uuid = { version = "1.2.1", features = ["v4", "serde"] } # For ids

The Schema

Schemas are simple, the ideas behind twitter are simple, we'll probably never need to make a change to this, so here's the schema; throw it in schema.sql.

Next, let's make some quick models for everything. These models will also have the methods to get and update the content, so we'll throw each of them in their own files, and all those files into a src/models folder. Don't forget to add the files as modules in src/models/mod.rs, that'll get you every time.

I'm going to just list the models in here with the file name at the top because this is mostly straightforward boilerplate. If there are questions about individual parts, feel free to ask a question and I'll get more specific, but come on, this is Brutal Twitter, we're just plowing ahead.

Great. That's a lot of random sql queries and boilerplate for our models. None of it is really terribly important -- if we were a bit fancier maybe we'd have something like prisma that could provide all this functionality for us without the need for this boilerplate, but this is Brutal Twitter -- we're going ugly.

The Controllers

Now, we're not trying to build some "mobile first" API bullshit here, we're building a website. You want to use it on your phone? Open up your web browser.

In light of that, our controllers are going to serve up some good old fashioned HTML justice. To do that, we're using askama to staple together a bunch of templates so we can reuse code. For all you React based web devs, it's like using "components" but it's "not shit."

The relevant pages we'll need here are:

  • Sign up

  • Sign in

  • Home

  • View a tweet

Starting with the users, we make a src/controllers/users.rs and throw the imports we need to the top of the file:

It'll be much clearer why we need these things as we fill in the middleware functions, so just trust me for now.

For starters, let's make the sign up function. Basically this will get called as a POST whenever someone submits the signup form.

Note: assume all of these functions are still in the users controller until I say otherwise.

Now, creating a user is great, but we need to let users sign in as well.

These next two bits are specific to how thruster is structured, so I'll describe a bit before the code this time. Thruster works by chaining together functions, but not as much in a linear fashion as much as a nested fashion. So if there are three pieces of middleware, a, b, and c, where c is the "last" in the chain, i.e. it doesn't call next, then the execution would look something like

This lets us do some neat things around execution and timing. It also means that the same context object gets continuously passed to the different middleware functions.

So, with that, here's some code to take the session cookie and get the user from it, and then another one to stop execution and return an error if there's no valid user on the context.

Before we jump into the endpoints for creating tweets and retweets, let's make the static pages first. Now we make another controller, src/controllers/pages.rs, and throw in these imports:

We'll go through the same process as the form endpoints: sign up, sign in.

Now it's time to add our first templates. I made:

  • A base template that has common headers, footers, styles

  • A navbar template

  • A sign up template

  • A sign in template

This is all html/askama, I'll add a few comments but it's pretty self expanatory.

Great, but where are the tweets?? Yes, the tweets. Let's build the home page. Starting with the pages controller

Now, we need to build the home page itself. Ideally we'd have a form to create a new tweet embedded as well. Let's throw that in a template since we'll use it more than once.

Notice that it calls a dynamic route? That's so we can set a special route if it's a reply. Later. Foresight. Okay, now the home template.

Also a reusable feed (again, foresight.)

Aaand the reusable tweet template

Cool cool cool. So that tweet template has a few things we haven't figured out yet. It has replies, likes, and retweets. We need the actual controllers for those endpoints, so we make a new rust file for tweets.

Okay, so what happens when a user clicks on some individual tweet action? In original twitter, "fancy" twitter, "more refined" twitter, you go to the tweet and see all its replies. We can do that too, pretty simple too -- add another template.

And one more function in the pages controller

Alright. At this point, we have models, we have controllers, we have templates, but we have no routes. We have to tell thruster what routes lead to what. That means we need to make a new file. src/app.rs. This holds all of our routes and special middleware (like logging!)

Okay, we're almost done. Now update your lib.rs file to actually make the app.

If only there was an easy, brutally efficient way to deploy this masterpiece. RIGHT -- we made this based on shuttle! All you have to do is run

This will provision a new postgres instance as well as create an edge server to run the code. When it's done, it'll print out a url that you can visit your brand spankin' new site.

Wrap Up

See, Twitter isn't so complicated. Enjoy this ad-free, javascript-free, tracking-free version of twitter. Also we didn't have to fire a bunch of people, then attempt to rehire them, then say they all have to work in the office, so I guess net win!

Last updated

Was this helpful?