Overview#
In this page we'll discuss concepts and terminology of r3ply. The goal is to give the reader a big picture understanding of r3ply (Also, documentation that doesn't have a dedicated page should go here).
Swim lanes detailing the flow of data in r3ply
This page is meant to be useful to both future contributors to the codebase, as well as site owners who are using r3ply to receive comments.
Table of Contents
- Comments 💬
Fundamentals#
Here's a discussion of some of terminology and concepts of r3ply.
r3ply#
r3ply in essence is just a library (src ↗). To make a r3ply app you just handle IO, delegating the main logic to the r3ply library. In that sense every r3ply app is just a wrapper around the r3ply library.
Two examples of r3ply apps are:
- The r3ply Cloudflare Worker: accessed publicly, via the internet
- The r3ply CLI (src ↗) -
re: accessed privately, via the local file system
As you can see, the main difference between the two is how they're accessed. Therefore their main responsibilities are handling the particulars of IO specific to their domains.
For example, the cloudflare worker can be accessed via email over the public internet, and it currently powers the r3ply.com service.
On the other hand, the CLI app – re – just receives text from the command line and parses them into arguments.
In both cases the main work they're responsible for is specific to their IO, while the actual logic of processing comments is handled by the underlying r3ply library. Since r3ply app are just IO wrappers around the r3ply library, it's quite easy to build your own r3ply app and extend others.
Sites & Signets#
Central to a r3ply app are the sites they serve, and each r3ply x site pair has a signet. A site is just some domain, like "example.com", and a signet is a 22 character string issued by a r3ply server that allows the site to do business with it. This is not an API key. In fact, signets are never stored and their main purpose is for cryptographically signing things, such as the email addresses of commenters.
A more detailed discussion of anonymization follows below.
Sites and signets are stored in a site's r3ply config as a site entry. Let's take a closer look at a site x r3ply pair and inspect the signet.
[[site]]
# the domain of the site
domain = "example.com"
# the domain of the issuing r3ply app
r3ply = "r3ply.com"
# the actual signet
signet = "iSQIIBcF7ka2UURJpFDkYw"
# the issued date
issued = 2025-08-26
The issued field is used as a key identifier for the signet. This is useful for things like key rotations. Therefore, if you ever decide to get a new signet, you'll be able to track comments that were signed by the old one.
Try changing the domain with different values to see how the signet changes. For more options you can try using re generate signet --help.
Configs#
In the previous section there was mention of [[site]] entries in the site's config, and in fact the main way of using r3ply is by modifying your config. The config is full of different attributes that can be changed to get the behavior that you want.
Both sites and r3ply apps use configs.
A full treatment of the subject is in the config section of the documentation.
Privacy#
Emails addresses of commenters are never shown to site moderators. Instead, they are anonymized to a stable but private identifier. Encryption is also used for forwards compatibility.
r3ply administrators can see email addresses, as this is an unavoidable fact of technology in general. This exists only in the case where there's a problem that needs to be debugged and logs need to be checked to understand what's wrong. That is why the source code is public and fully auditable.
Anonymization#
This section covers in-depth how r3ply's anonymization works. It's not necessary to understand how to use r3ply, but it's documented here nonetheless for people who are curious.
From above:
signets are never stored and their main purpose is for cryptographically signing things
In this context cryptographically signing means to produce some kind of verifiable signature of something without revealing its contents.
Crucially, once something has been signed with a signet it can never be read again, but the same thing signed multiple times will always produce the same signature. In this sense, signets perform a one-way function that is both pseudo-random and deterministic.
(This is why r3ply uses the term pseudonym for authors, since something truly anonymous would be indistinguishable from randomness, but the same author's email will always produce the same pseudonym.)
Importantly, emails signed with signets are still completely secure and practically indistinguishable from total randomness.
Now let's talk more about the specifics of how r3ply's anonymization works. Signets are 22 character strings that are actually key envelopes (wikipedia ↗), storing part of a private key that was produced by a a master key, held by the issuing r3ply app.
To form this key envelope, the site's domain and the signet's issued date are both concatenated, along with the issuing app's r3ply domain, and then signed with the r3ply server's private key.
When a signet is trying to be used this is envelope is re-computed and any deviations from the original signet will be proof the envelope was tampered with. You can see the process in this example code:
// Recompute expected envelope (sanity check)
const site_data_envelope = new TextEncoder().encode(
`${r3ply_domain}:${issued_date}:${site_domain}`,
)
const hmac_bytes = new Uint8Array(
await crypto.subtle.sign('HMAC', master_key, site_data_envelope),
)
const expected_envelope = base64UrlEncode(hmac_bytes.slice(0, 16))
if (expected_envelope !== signet) {
throw new Error('Envelope mismatch — possible tampered config')
}
In other words, a signet can only be used by the site it was issued to, along with the server that issued it.
If the key envelope is recomputed, then the key is recovered from the remaining 16 bytes and used to sign the underlying data with HMAC-SHA256 (wikipedia ↗).
This is how the emails of commenters of are anonymized, i.e. how their pseuodonym is generated.
Encryption#
r3ply also does some encryption. In addition to signing email addresses for anonymization, they are also encrypted. This produces an opaque token that comes with every email comment. Email addresses are padded with null bytes to conceal the length of the original email. For example:
"token": "kktE_W_Nlh95kjQpAbbcDkpOPtTjh8SRJNAdulGWav5Nv0zJNUABG91PMIeTo8K6PyMXkHp8iJsxuR-Qg0rFwKLk3LmZt0NTJ1SNUOLL8-0k0Ik-bNSBWCnH_lRCkWFc7LRpTfPNurZ7ncifRVFGbqgKrFoLhvwGSujQivorr9tNKq_r7C2aTyb-ECmTWJdgWVHaD4lwetqv0tU-tueGkBlbTHWlAR6JUX2UwOrQrTSgzx6Ft3-hb4Q9esLhlN1ffUK43Ov0E8dhGReH-Uy1fj2k_EzyOwLLfZ771mkfC4dMsjPl0jMZTSjDQqP-tK3hiA5xJsC6Aa00S04ZFVXBIZVNHEgds4AbcfUhpZqwOfBLfCXey4scQBW5DZFGkF3Km3_gaBJUYKTaYoYLN71Xd5rjELcpahwzvxUurUoNYQn-D6zt_U-Fbt4SeoA9370ivV1U0HeY6w-5YWrk"
Currently encryption is done mostly for the purpose of future proofing. It will allow things like key rotations to be done more easily. For this reason, it's advised to store the author token alongside the comment even if you don't use it. Otherwise the data would be lost forever, which might be fine for some people.
A 32-byte AES-256 symmetric key is used for encryption.
Data Flow#
The general flow of data is as follows.
Swim lanes detailing the flow of data in r3ply
- The data flow begins when a r3ply app receives a request for comment from a commenting source, e.g. email.
- The r3ply app will check fetch the config from the site for whom the comment request is destined.
- The site responds to the r3ply app with its config.
- The r3ply app resumes processing the comment request according to the site's configuration. The specific details are covered more in depth in the comment processing pipeline docs. When this step is finished the comment request is now a comment.
- The comment is then passed along to the various moderation channels specified in the site's config.
- A response is sent to the original commenter. This is usually but in the case of
re– the r3ply CLI – it might just be printing to the terminal. - TODO: notify the site (see roadmap)
Each comment is abstracted as a file. The file per comment approach allows us to avoid the issue of merge conflicts with version control systems.
Comment Sources#
Each commenting source is a way to begin the r3ply data flow. They are configured below the [config] variable. Currently there is only email as a commenting source.
Each commenting source can specify a filter* variable in their config that filters what [[site]] entries (docs) it accepts comments from. The filter* variable is a list of strings that can be glob patterns reference that site entry's label.
Email#
Commenters can send comment requests via email. Emails must conform to the following requirements:
- The
Tofield of the email must be addressed to the site + r3ply app like<SITE-DOMAIN>@<R3PLY-DOMAIN>. - The
Subjectfield must either be a full URL or a URL path. It is not valid to send an email request with a URL in the subject line that has a different hostname than the local portion of theToaddress. The email address in theFromfield will be anonymized into an author pseudonym. - The body of the email will be the comment. There is a field in the site config to remove the email signature from the body.
It's advised to generate the To, Subject, and Body of the email in advance with a mailto link to ensure that sending comments is a reliable and repeatable process for your users.
Offensive commenters can be blocked by adding their pseudonym to a block list (docs).
r3ply handles receiving and transforming comments according to site configs, however moderation channels are used for ultimately getting the comments to the sites. They can effectively be thought of as a handoff.
The Email Comment Pipeline#
Here are the precise steps of the email comment pipeline. To see the steps clearly yourself try running re simulate email
prescreen: checks made to the email before it's actually opened. These are very basic checks such as ensuring that the email is within the accepted size, or that both the configured site and its r3ply app are enabled and accepting comments. The block list is not checked here. The analogy to the postal service would be asking if your package has anything flammable.receive: an ID and timestamp is assigned to the email comment. This would be similar to the postal service giving you a tracking number.accept: the actual email bytes are parsed. This would be analogous to someone from the post office actually physically picking up your package.deliverable: the email itself is examined for deliverability. Here is where the block list is checked, along with the the Subject line.prepare: the parsed email is prepared into a template context that will provide all the variables that the templating will use later.process: here is when the actual comment is produced by binding the templating context from above with the user's configured template, if any. If there is none then the comment is just the template context.
After these stages the comment request has finished becoming a comment. It will then be sent to any moderation channels that accept it.
Moderation Channels#
Conceptually, moderation channels can often be thought of as destinations for comments. For the purpose of flexibility r3ply allows you to fan-out (wikipedia ↗) a comment to multiple moderation channels. This could be used, for example, to open a pull request with the GitHub Moderation Channel and to send the comment to a Webhook Moderation Channel for delivering a slack notification.
Additionally, each moderation channel allows you to specify an allow list, granting permission to bypass moderation for certain senders. As mentioned above, Block lists are also possible but they are handled further upstream the comment pipeline, in the [comments.email] config section (docs).
Offensive commenters can be blocked by adding their pseudonym to a block list (docs).
There is a subsection in the config documentation where the configuration of each moderation channel is documented.
GitHub Moderation#
GitHub moderation allows r3ply to submit each comment as a file in a pull request. To use it with a private repo you need to give the r3ply GitHub bot permission to access your repo. It can be configured according to the GitHub Moderation config docs.
Webhook Moderation#
The webhook moderation channel is for general purpose integration. It can be configured according to the Webhook Moderation config docs.
Local Moderation#
Local moderation is used by re – the r3ply CLI – to simulate comments. It can be configured according to the Local Moderation config docs.
Comments (0) #
Write Comment Comment [-] Collapse All Collapse [+] Expand All ExpandCommenting Info:
This is a demo of commenting using r3ply. You can leave comments on this website by sending an email.
Your email address will be anonymized, and can never be shared with anyone.
To try it out click the Write Comment button, or draft an email manually 1. Your email client with a template already filled out that looks as follows:
To send an email manually, just compose it exactly like the code snippet above. To respond to a comment append
#<Comment-ID>to the path, e.g.Subject: /docs/getting-started/#abcd1234(please note the trailing slash). You can also respond to text fragments the same way, e.g.Subject: /docs/getting-started/#:~:text=You%20should%20see%20a%20bunch%20of%20text%20representing. ↩Pending Comments (0)
This comment shouldn't actually normally be rendered. It's used to by the base.html template to render one instance of a templat… root / parent .. # . next ⭣ prev. ⭡
This comment shouldn't actually normally be rendered. It's used to by the base.html template to render one instance of a template that's wrapped in
<template>tags. That template is then later cloned and appended to the page for each comment that's pending in the comment cache. To view this comment go to /debug-comment-template/.