@storiny/api

Source for REST API endpoints and the real-time collaborative engine

RustPLpgSQLDockerfile MIT archived

About

This is the primary backend service that powers Storiny; handling the core site functionality such as user authentication, CRUD endpoints and the real-time story collaboration.

Tech stack

  • Framework: Actix Web serves our HTTP endpoints, web sockets for real-time story collaboration, and middlewares
  • Database: PostgreSQL paired with SQLX
  • State & caching: Redis (using deadpool-redis) handles our session storage, rate limiting and pub/sub
  • Real-time collaboration: Custom web socket based engine built around yrs (the Rust port of Yjs CRDTs)
  • Internal RPC: gRPC via Tonic for fast & strictly typed internal service communication between micro services
  • Background jobs: Apalis for cron jobs and Lapin for RabbitMQ message processing
  • Media storage: AWS S3 for storing stories and user uploaded media files
  • Email service: AWS SES for dispatching emails
  • Telemetry: Sentry for tracing, monitoring, and capturing errors in production

Realms (collaborative editing)

The heart of this project is Realms: our real-time multi-peer collaborative story editing engine (similar to Google Docs or Notion, this is the system that makes cursor tracking and live story syncing possible across multiple peers).

We built Realms on top of Yjs using the Rust yrs port. It relies on conflict free replicated data types (CRDTs) to guarantee that all connected peers eventually end up with the exact same story state despite facing unavoidable network latency.

Under the hood

  1. Connection & authentication: When a user opens a story/document, their client establishes a web socket connection to the API server. We intercept the handshake, parse the SESSION_COOKIE_NAME, validate it against the user session present in Redis, and then query Postgres to ensure the user has the correct PeerRole (Viewer or Editor) for that specific document.

  2. Realm manager: Once authenticated, the user is placed into a Realm. A Realm is essentially a document loaded into the memory.

    • If the document is just opened, the Realm fetches the latest compressed binary CRDT state from AWS S3 (persistent long term document storage).
    • The Realm maintains a BroadcastGroup that tracks all currently connected peers (tokio::sync::RwLock<HashMap<Uuid, Peer>>).
  3. Sync protocol: An optimized binary protocol over web sockets. It consists of a few core message types:

    • SyncStep1 & SyncStep2: The standard Yjs handshake. When a client joins, they send their StateVector (a summary of their document version). The server replies with any missing updates.
    • Update: As users type, incremental CRDT updates are broadcasted via a tokio::sync::broadcast::channel to all peers in the BroadcastGroup.
    • Awareness: This handles ephemeral data: cursor positions, selection ranges, and “who is currently online”. This is kept out of the core CRDT document to keep the document size small (awareness data is never stored in long term document storage).
  4. Persistence loop: The live document lives in memory (and is constantly being mutated) until there is no peer accessing it. Every PERSISTENCE_LOOP_DURATION (currently 60s), the Realm checks if the StateVector has changed. If it has, it takes a snapshot of the document, compresses it using gzip, and flushes it to S3 for long term persistent storage. When the last peer disconnects, the Realm does one final sync to S3 and then gracefully destroys itself to free up memory.


Steps to launch a local instance of the API server and a list of API endpoints are available in the repository README.