Axelot
A modern, fast, and secure collaborative document/story platform
Overview
I started developing Axelot because I wanted a collaborative document/story platform that actually feels modern, fast, and secure. I was initially inspired by how https://gist.github.com/ works. Most existing tools either lack real-time editing features, have clunky authentication, or don’t scale well to edge runtimes.
The goal was to combine Next.js, Firebase, and Yjs to build something that s both powerful and future-proof. Many features in this project were ported from Kanjou and adjusted to be compatible with Material UI (MUI).
Authorization
Authentication begins with Auth.js (NextAuth-style flows) to support OIDC and social providers. After sign-in we mint a Firebase custom token server-side and hand it to the client. The client exchanges the custom token for a Firebase SDK session and uses the UID provided by that token for Firestore access.
All Firestore rules reference request.auth.uid so reads/writes are validated at the database layer. Session handling is stateless and compatible with Edge runtimes (no in-memory caches or long-running processes required).
Collaboration in Real-Time
Yjs handles CRDT state for documents and presence (awareness). WebRTC is used for peer-to-peer replication between collaborators, while Firestore is used for signaling (offer/answer exchange). All updates are encrypted on the client and merged using Yjs, so conflicts are deterministic and predictable.
We use y-protocols/awareness to sync cursors and presence metadata (name, color, current selection). The system falls back to Firestore realtime updates when WebRTC peers cannot be established.
Server-rendered non-user-specific sections
A practical example of when server rendering helps is the project dashboard. Some sections (for example, a list of recently published stories) are global and do not depend on the currently-authenticated user. Rendering these parts on the server avoids an unnecessary client round-trip and reduces perceived load time.
Server-rendered approach (before / after):
The DashboardShell can render the recentlyAdded list directly and mount lightweight client-only widgets for personalized sections (notifications, user drafts, presence) after the shell is visible.
Why this helps:
- The server-rendered
recentlyAddedlist is cached at the edge, so all clients receive HTML immediately and avoid an extra client fetch. - Personalized widgets remain client-only, so sensitive data or session-bound requests are still retrieved securely on the client.
Multi-Layer Caching Strategy
Public API endpoints like /api/stories/discover implement a three-tier caching approach to minimize Firestore reads and improve response times:
- Client-side: Browser caches responses via
Cache-Controlheaders (10 min for trending, 2 min for fresh content) - Server-side: Next.js
unstable_cachememoizes Firestore queries with tag-based invalidation - Edge/CDN: Responses cached at edge nodes with
stale-while-revalidatefor instant delivery
Implementation:
Impact: ~95% reduction in Firestore reads during peak traffic, sub-50ms response times for cached hits vs 200-500ms cold queries.