/ Making Notes.cx

A couple of days ago I launched my newest project - Notes.cx, which is a small tool came to mind while I was in the car with my wife. At the time, I was working (and still am) on Diary.by, another project of mine, so I asked my wife what would be the chances that she would find use in something like Diary.by.

My wife mentioned that she is not the kind of person that would keep a diary or a blog, which made me remember another thing that she mentioned not too long ago - that she likes to sometimes write things down to help herself process them better.

I figured it would be pretty neat if there was an outlet or a void you could shout into, anonymously, just to put your feelings in writing. Either this, or just sharing meeting notes, a temporary article or blog entry that would live for a short period of time before disappearing forever.


The requirements from a project such as this are not plenty and can be put into bullet points:


Even though this project is less complex than Diary.by (and is fairly simple in general), the requirements pose a few challenges I had to overcome during the creation of it. To name a few:


Building Blocks

Thankfully, I recently started writing code in a more reusable manner. This practically jump-started the development of this project, as routing and templating at the very least was already dealt with as part of making Saisho and Diary.by.


When a user visits Notes.cx, a temporary random user identifier is created for the author which is hashed using SHA-256 and is saved as a cookie. The key is an MD5 hash of the author's IP address and random_bytes(32) was used to generate the value for this. The result looks a bit something like this:

The cookie that is generated by Notes.cx This hash is then used when creating notes in the database, and looks something like this:

How a note appears in the database

Since the hash is random, if the author removes the cookie that allows the note to be edited, nothing longer ties the author to the note (data-wise. nginx logging still exists), unless the author decides to leave PII on their own accord.


To make sure requests are only sent from the form, I decided to generate a nonce value which is then verified once the note is created. Since nonces are temporary, I didn't necessarily need the same level of randomness as the user hashes, so a simple letter randomizer was used to create nonces:

public static function generateID(int $length = 32, ?int $negative = 0) : string {
    $characterSet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    $id = '';
    if ($negative > 0) {
        $length = random_int($length - $negative, $length);
    for ($i = 0; $i < $length; $i++) {
        $id .= substr($characterSet, random_int(0, strlen($characterSet)), 1);
    return $id;

The randomly generated nonce is tied to the user hash as an additional measure to make sure the author who created the note is actually the one sending it to the database via the form. If the nonce is verified, it is then deleted and the note is created.

Random note ID

The random note identifier is generated using the generateID() method above, like this: generateID(10, 3). This generates a random string between 7 and 10 characters. To make sure no duplicates exist, the generated ID is checked against the database and is generated again in case it already exists.

To make sure users cannot edit a note with a manually entered identifier (i.e. changing the URL to be https://notes.cx/edit/potato, for example), before the user is redirected to the editor form the generated identifier is saved on the server and is then verified post-redirection (that's why you can also spot a PHPSESSID cookie).

There are probably (most likely) better ways to achieve the above, but that's what I could come up with.


I love using Markdown, so I chose to use it with this project as well. As the focus with Notes.cx was it being very lightweight, there is no special editor on the front-end, just a textarea element.

To render Markdown, I decided to use the wildly popular Parsedown (along with the Extra and Extended expansions) on the back-end making sure to enable both setSafeMode and setMarkupEscaped flags to make sure no HTML is being rendered once the content is parsed.

Purging old data

To remove old entries, MySQL events are run each hour to remove old notes and each half an hour to remove old nonces. The database is not being backed up, so there is no way to restore the notes once they have been deleted.



At first I decided against using JS for anything, but it is currently being used to provide users with the ability to copy the note ID on desktop or open the sharing menu on mobile. That's it.


I suck at making CSS files look nice and organized, so the entire thing is a mess. The bottom line is that I've decided against styling things excessively, same as with my own website and as with Diary.by. I refrained from using classes, and instead decided on using semantic HTML and styling the elements to work nicely together.

After all, the point here is the content by the author, not my stylistic choices.


This was a fun little project that I managed to see to fruition and one which I intend to keep operational with the costs associated hopefully being covered by my other projects, namely Diary.by.

I shared it on Hacker News and Product Hunt to gain some feedback and exposure. While there was a lot of poking and prodding, over 1600 notes were written and notes.cx received at least 16.2k unique visitors. According to goaccess, the total transfer for all this was just under 22MB.

Webring Meta Icon