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.
Requirements
The requirements from a project such as this are not plenty and can be put into bullet points:
- Simple, straightforward experience in creating notes.
- Easily legible notes once rendered without over-the-top styling.
- Notes are to be saved anonymously yet editable to the original author for a limited period of time.
- Notes, like fleeting thoughts, are temporary and are saved for only 24 hours.
- No social aspect - this means no likes, comments, upvotes or otherwise.
Challenges
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:
- Making sure the code is robust enough to handle a large amount of visitors poking, prodding and testing the tool (since registering or logging in is not required to create a note).
- Making sure no PII is stored regarding the note author.
- Making sure the UI/UX is not blocking people during creating or reading notes.
Code
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.
Anonymousness
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:
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.
Nonces
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.
Markdown
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.
Frontend
JavaScript
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.
CSS
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.
Conclusion
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.