Using invisible divs in GitHub Discussions as a key-value store

slickstef11

Stefan 🚀

Posted on July 31, 2023

Using invisible divs in GitHub Discussions as a key-value store

Everything is a key-value store if you try hard enough. 🤓

We're excited to announce that the Open Previews beta is now available.
In this blog post we will explain how we built Open Previews and how we use divs in GitHub discussions as a key-value store. You can try it out live on https://www.openpreviews.com/, the source code is available on Github.

What is Open Previews?

Open Previews allows you to add commenting functionality to your previews/staging environments, or any other website that you want to collect feedback on, like documentation pages. It's a great way to collect feedback from your non technical team members, customers or other stakeholders.
Preview comments is not a novel idea, services like Vercel and Netlify have supported this for a while now, but we wanted to build something that is open source and can be self hosted.

How we built Open Previews

Before building Open Previews we had a few requirements:

  • No database
  • The backend had to be stateless
  • Utilize WunderGraph as much as possible
  • Embeddable using a single script tag

The solution we came up with is inspired by Giscus, which uses Github discussions to store comments.
This is a great solution and allowed us to build Open Previews without a database.

The Problem of storing meta data in Github Discussions

There was one problem though, we had to store additional information with the comments, like the position, selection, and other meta data.
How do we store this information in Github discussions?
But not only that, the meta data should also not be visible to the user to not distract them,
and we need to be able to retrieve it from the discussion when we render the comments.

The Solution: Using divs in HTML comments as a key-value store

Github comments support basic HTML,
so what if we can simply embed JSON inside a data attribute in an empty DIV?
After a simple experiment we found out that this works great.

Adding JSON directly in a data attribute didn't work though because of the quotes and some other escaping issues,
so we encode the JSON using encodeURIComponent.
Now we had a functional key-value store using divs and could store all the information that we needed. 🥳

Here's how it looks like in the code:

export const constructComment = ({
  body,
  meta,
}: {
  meta: {
    timestamp: number
    x: number
    y: number
    path: string
    href: string
    resolved?: boolean
    selection?: string
  }
  body: string
}) => {
  const jsonMeta = JSON.stringify(meta)

  const encodedJsonMeta = encodeURIComponent(jsonMeta)

  return `${body}

  <div data-comment-meta="${encodedJsonMeta}" />`
}
Enter fullscreen mode Exit fullscreen mode

Now when we retrieve the comments we can simply get the encoded data from the div, decode it, parse the JSON and render the comments on the page.
The possibilities are endless 😎

Why GitHub Discussions is a great key-value store

GitHub Discussions as a key-value store is amazing because it comes with a lot of powerful features out of the box:

  • our application is stateless
  • GitHub Discussions are virtually free and scale infinitely
  • built-in authentication: We can use GitHubs authentication to authenticate users
  • build-in authorization: You can only comment on a discussion if you have access to the repository
  • built-in notifications: Users get notified when someone replies to their comment
  • built-in versioning: You can see the history of a discussion
  • built-in spam protection: GitHub Discussions has spam protection built-in

Stateless authentication

The next challenge was building stateless authentication. Adding authentication with WunderGraph is easy using the built-in Github Oauth provider, but we had to store the access tokens somewhere safely. Since previews are typically hosted on a different domain than the WunderGraph server, we can't use secure cookies either.

You might think that we could store the access tokens in our new Github Discusions KV, but that would be a very bad idea. Instead we came up with a solution that uses JSON Web Encryption to store the access tokens in the browser. The JWT is encrypted using a secret that is only known to the backend. This allows us to verify the JWT and extract the access token from the JWT payload. The access token is then used to authenticate the user with Github.

After logging in, the encrypted JWT is exchanged between the WunderGraph server and the preview. We do this by starting the authentication flow in a popup that allows us to pass the JWT to the preview using the postMessage API. Alternatively a compact token could also be appended to the redirect uri. The JWT is stored in the browser using the localStorage API.

The only limitation of this approach is that the JWT is only valid for a limited time and the user needs to log in again after the JWT has expired.

What's next?

We're still working on Open Previews and we have a lot of ideas for new features. Some feature we'd love to add:

  • Support PR reviews and Github Checks
  • Optimistic updates (make the UI snappier)
  • Like & repy to comments
  • Markdown / Emoji support
  • Upload images

If you have any ideas, feedback, or want to contribute, please let us know on Twitter, Github or Discord.

💖 💪 🙅 🚩
slickstef11
Stefan 🚀

Posted on July 31, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related