Overview of My Submission
useMindmap
is a mind mapping application backed by Appwrite. It leverages Users/Teams, Database, Realtime, and Functions to provide a basic mind mapping service for yourself or a team.
Live: https://usemindmap.app
- Personal and Team workspaces for your mind maps
- Collaborative mind mapping powered by Appwrite Realtime
- Mind mapping functionality built on top of React Flow
- Sign up/sign in, forgot password, and team invites
I recently came across the React Flow project and wanted to try building a team-oriented mind mapping app with it, but didn't quite have the backend experience necessary for the collaborative feature the app needed. So when I saw that Appwrite offered Realtime features that could subscribe to database events, I knew I had a potential killer combination on my hands. The hackathon gave me the perfect excuse and motivation to make it a reality, however, I was wholly unprepared for how truly excellent Appwrite is. Let's dive in.
Building useMindmap
The first step was to deploy Appwrite. I've used Digital Ocean for some time and I love their single-click app marketplace. This got me a cheap droplet running Appwrite in about 5min. Using an existing domain for testing purposes I set up a subdomain for the Appwrite instance, played around with the admin console and thought "Hell yeah, this is awesome!"
I created a Web project, configured environment variables, and did all the housekeeping necessary to prepare for full-scale development. After two days getting acquainted with Appwrite I was ready to build.
FRONTEND
I started the frontend with a simple React project template (webpack, babel, eslint, typescript) and blocked out the page routes I would need:
- Home (
/
) - Auth (
/sign-in
,/sign-up
, etc) - Dashboard (
/~
,/~/profile
,/~/teams
, etc) - Mindmap (
/~/maps
)
Using inspiration from Chakra UI Pro and Tailwind UI I then created the sign-up/sign-in pages and integrated with the Appwrite SDK to create a user and a logged in session.
The Dashboard followed with the sidebar and content:
- Logged in user details
- Main "activity" view
- Profile and Teams view
- "Workspaces" list and view
From some tinkering around with Appwrite's Database and SDK, I settled on making each workspace a Collection and each Mindmap a Document with attributes:
- name: string
- description: string
- nodes: string[]
- edges: string[]
- tags: string[]
After a couple weeks fleshing out the UI and getting intimate with the Docs and SDK, I was finally tackling the MIIINDMAAAP (*spooky music*)
When a user creates a mindmap for a given workspace, a document is created in the associated collection and the user is launched into the /~/maps/:id
path. Once inside the Mindmap view, the React Flow instance is used to create nodes, moved them around, save a label, attach edges, and delete them.
React Flow represents its nodes and edges as objects with attributes like id
, position
, width
, height
, source
, target
, there are many many more properties but these ones are the important bits so they can be saved/restored and propagated to other connected clients.
Each one of these events triggers a Function execution with a payload of the new value (a node move event sends the updated position
, a label update sends a new data
object, etc).
From this point, it's up to Appwrite Functions to resolve the mutation on the mindmap document. Onward, to the Backend!
BACKEND
I wanted the concept of a group of mindmaps to be something the user or team doesn't even need to manage. To support this, using Appwrite Functions, every user.create
or teams.create
system event creates a new collection only that user/team has access to. In effect, this produces a "scoped workspace" of sorts for the user/team to call their own and is entirely automatic.
(As a side note, I also use a Function to generate a gravatar
URL for each user upon registration)
When a user opens a mindmap, the client fetches the initial state of the document while also subscribing to document changes with the Realtime endpoint. Realtime changes received by the client are then merged to the React Query cache to keep them in sync (honestly, React Query isn't even needed to push Realtime data from the callback response to React Flow, but it's just so damn easy to use for handling server state that I can't not use it)... moving on.
With clients listening to documents for changes, what's left is for Appwrite Functions to resolve updates to nodes and edges. To do this, I created two Functions to handle nodes
and edges
, whose responsibilities are:
- Accept the incoming payload from a HTTP event
- Pull the most up-to-date document from Database
- Insert/merge/remove the changed attribute into/from the original attribute
- Save the new document attributes back to Database
- Exit gracefully stage right, grab a coffee
Given the schema of the mindmap document (where nodes and edges are arrays of strings), each incoming node and edge object gets stringified before written to the Database and parsed when read by the client. The challenge is that some events may conflict if mutating the same attributes before an existing mutation has been persisted to the document. In these cases, it's simply whichever event arrives last that is most likely to be written. While a true transactional database would help alleviate this issue it's not too much of a concern for the relatively slow pace of events in a mindmap.
This was a conscious choice with an acceptable level of risk for the scale of the app, compared to the added complexity of using a Collection per Mindmap and treating nodes and edges as individual documents. That approach would involve mapping each React Flow object property to a document attribute, increase Database operations when setting up a new mindmap and reading a list of mindmaps from a "workspace", and also when cleaning up (e.g. user deletes their account/team and all associated mindmap documents). Not to mention the query complexity, and creating document indexes for improved throughput performance.
WRAPPING UP
This all seems like an oversimplification, and while it's rather straightforward once the steps are laid out like this, I did get stuck inside my own head for about a week as I considered all sorts of funky solutions like CRDTs, custom WebSocket servers with Yjs, even using a separate service like Pusher! Ultimately, Appwrite provided the right tools I needed to get useMindmap up and running as envisioned without needing to go outside the box. Kudos to the Appwrite team, I'm excited to see what else they have planned.
I'd like to shout out the invaluable support offered by the Appwrite Team in the Office Hours channel on Discord, particularly @Brandon
and @VincentGe
, you guys rule.
Submission Category:
Web2 Wizards
Link to Code
Proudly open source on GitHub: https://github.com/benweier/use-mind-map
Additional Resources / Info
Tech Stack
- Appwrite (duh)
- React
- Digital Ocean (1x droplet + 1x static site app)
Key Dependencies:
Future Plans
Obviously this is a pretty quick'n'dirty mind mapper, but I loved the experience of combining Appwrite with React Flow that I plan to take this further and develop it into a fully-fledged app.
The mindmap experience right now is basic and rough, and there's not a lot of expressive features to make a mindmap your own. Having options like colours and shapes for the nodes at least, as well as making editing seamless - like opening the node edit panel above the selected node, shortcut keys to add a pre-linked node, an automatic layout engine with elkjs
- would all go a long way.
Top comments (2)
Excellently written. I love hearing the thoughts going through your mind while working on the project. This is definitely something I'd like to see grow with newer releases of Appwrite *hint hint, because as Appwrite grows in features and performance, so much can be unlocked with this app, too.
Cheers~
Awesome work. Works pretty well. The look and feel of the website is amazing.