marcson909 / gqlforum-rs

Proof of concept writing a monolith BBS using Rust, GraphQL, WASM, and SQL. WILL BE ARCHIVED ONCE PROVEN

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

GraphQL Forum

Important
DO NOT even think about using this in production, lest your sanity be destroyed and credentials lost!

Loosely following the awesome book Zero to Production In Rust, minus the vigorous test cases and battle tested framework.

TL;DR

Summarize backend in one line?

Good performance without even trying, massive pain keeping SQL and code in sync, and huge trouble working around corners of API.

Summarize frontend in one line?

Surprisingly pleasant to write, but documentation is scarce, and code size is large. WASM is probably not ready to be the "main site" yet.

Comment the code quality?

It would be an excellent playground to practice exploits!

How was the experience?

Extremely determined. Damn, full-stack is hard.

Will I do this again?

No, not the full stack. If I want to build anything for real, I would go for something like Preact (for frontend) + Hasura (for GQL and data) + Ory Kratos (for auth). For other backend services that don’t involve dealing with data store, I would still consider Rust Axum, tower stack, and async-graphql to be good.

How is the code quality?

TheDailyWTF kind of quality.

Building and running

Make sure you have trunk installed.

Go to gqlforum-sycamore/, then:

$ trunk build --release

Go to gqlforum-backend/, then:

$ cargo run --release

Go to http://localhost:3000 for the main page. Go to http://localhost:3000/graphql for GraphQL playground.

Admin account is admin, password is admin.

The configs in configuration.toml should be apparent.

Features

The Stack

Backend
  • async-graphql

  • Axum

  • Tower

  • sqlx

  • SQLite

Frontend
  • Sycamore

  • WASM

Backend Functionality

  • ✓ Serves a frontend with the same server

  • ✓ Backend with tracing

  • ✓ List topics

  • ✓ Topic by id

  • ✓ Create topic

  • ✓ Create post

  • ✓ Delete topic

  • ✓ Delete post

Frontend Functionality

  • ✓ Login page

  • ✓ Logout link

  • ✓ List topics

  • ✓ Topic by id

  • ✓ Create topic

  • ✓ Create post

  • Delete topic

  • Delete post

Authentication

  • ✓ Server side session management

    • ✓ Signed secure cookie on client side, hashed secret on server side

  • ✓ Password authentication with Argon2

  • ✓ Login via username/password

  • ✓ Registration (GraphQL only)

  • ✓ Change password

Authorization

  • ✓ Only admins/author can see deleted topics and posts

  • ✓ Regular users can see their own posts/topics and any public posts/topics

  • ✓ Regular users can only delete their own posts

  • ✓ Admin can delete every post

Anti-features

  • ✓ No HTTPS. Therefore, credentials are sent in clear text

    • Admittedly, this is easy to fix.

  • ✓ Frontend crashes all the time. Try refreshing

  • ✓ Horrendous UI design

  • ✓ Despite the backend can paginate, you can only see the latest 10 topics on index page

  • ✓ Despite the backend has this API, you cannot see user profiles at frontend

  • ✓ Despite the backend has this API, you cannot register at frontend

  • ✓ Despite the backend has this API, you cannot delete posts or topics at frontend

  • ✓ Terrible error messages

  • panic!, unwrap, and expect everywhere

  • ✓ Error handling scattered across front end, backend, on different layers of Result/Option, etc.

  • ✓ Barely any defenses

  • ✓ No checks what so ever in user input, but injection is guarded against

  • ✓ Spaghetti code scattered around like crazy

  • ✓ No documentation, no tests, no examples

  • ✓ Monolith repo

  • ✓ A test page that does no good except to verify my graphql implementation

  • ✓ Stale sessions are not cleaned up regularly

  • ✓ Random crashes if redirection goes too quickly

Design Choices

N+1

N+1 is not purposefully avoided. Joins are used to ensure correctness and access control, but not for performance (yet). See: https://www.sqlite.org/np1queryprob.html.

Access Control

Metadata consistency and access control are ensured on SQL queries instead of at application level. Access control comes in form of 4 views: topic_permissions, topic_public, post_permissions, and post_public.

Invariants

  • Posts are never deleted from database.

  • Post number is never changed.

  • Post metadata is always accessible, but contents can only be viewed as permitted.

These invariants are enforced by the SQL query used to access posts.

Experience Report

Warning
DO NOT IMPLEMENT PASSWORD AUTHENTICATION AND SESSIONS YOURSELF!

The Good

  • Great performance without even trying

    • While I don’t have much web experience, the backend feels exceptionally fast

    • With --release, that is

    • 12MB memory use? Yeah, pretty good.

  • Axum comes with a great collection of middleware

  • async-graphql object definition is relatively easy to use…​ once I got the basics

    • I will continue to use it if I need to write a service to do something instead of to retrieve something. The latter is better done with existing solution, like Hasura

  • The compiler is very good at catching mistakes, if I am actually using types properly

  • Trunk sets up WASM output nicely

The Bad

General
  • I have to keep the frontend/backend router in sync, manually.

    • For every route the SPA uses, I need the backend to serve the index.html

  • Cargo workspace does not work well with mixed targets

  • There are these…​ "Context" paradigm which does something like get_context::<Type>() which I don’t like, because they destroy the point of having a statically type-checked language. And they are everywhere.

Backend
  • Really, we are manually doing monad stack here by using all those Context<'_'>, Extension, Layer, …​ except without the nice do syntax Haskell provides

  • async-graphql doesn’t work very well with Axum middleware

    • Cannot use CookieJar because we cannot return extra arguments

      • Ended up rolling my own implementation to sign cookies

    • Repetition in binding middleware (in Axum and async-graphql)

  • sqlx generics are extremely hard to type check, but I managed to use some anyways

  • sqlx macros do not work well with SQLite, because it type checks SQLite bytecode at compile time. This has some bugs, and is an extremely slow process

Frontend
  • There aren’t any Rust GraphQL clients that work under WASM, so I rolled a simple one in a single file.

  • Trunk’s proxy doesn’t work. It just keeps redirecting until the browser refuses to continue.

  • Took me an enormous amount of time to figure out how to do async in WASM

  • Sycamore doesn’t have very good docs. I hacked around with terrible looking code.

  • Sycamore macros don’t work well with formatting

  • Sycamore’s routing seems a bit limited

  • Cannot figure out how to set status code for Sycamore

  • Fight the borrow checker with loads of .as_ref() and .clone()

  • Wasm is quite large, compared to JS libraries. I have practically all optimization turned to max in this project, and the size is still 327kB/129kB(gzipped). Also, it grows fast.

Conclusion

It started with me trying to make a small, monolith, self-contained forum that can run on extremely resource-limited machines. Then my sanity drained as I went along, and I cut features over and over, until I decide "this is going to be a technological study".

No, I will not continue developing this pile of diamonds. If I really want one in production, I am going for something else.

I do, however, think that this "full-stack" project contains some insights on the current ecosystem of Rust in web development, and some snippets that might be helpful to someone else.

Just don’t deploy it in production.

About

Proof of concept writing a monolith BBS using Rust, GraphQL, WASM, and SQL. WILL BE ARCHIVED ONCE PROVEN

License:The Unlicense


Languages

Language:Rust 99.7%Language:HTML 0.3%