How we built PrivaNote

You may have stumbled upon this page after visiting PrivaNote (privanote.xyz).

PrivaNote is an offline first, end-to-end encrypted note taking solution. It's one of the first applications built on top of Portabella and serves as a reference for other developers looking to build their own apps.

PrivaNote is open source so please take a look at the source code if anything is unclear. We've also released a Chrome extension and a Firefox addon to help you take notes as seamlessly as possible.

With that said, let's dive into how we built this!

Overview#

PrivaNote is a Next.js application with TailwindCSS to make it look pretty. The editor is a reasonably customised Slate instance. Setting up these components to play nicely is a breeze thanks to modern tooling.

Without setting up the encrypted backup PrivaNote will write to your browsers IndexedDB or LocalStorage. After setting up and initialising your Portabella configuration it will read and write from there.

Scaffolding out application#

Our initialisation script looked a little like this:

yarn create next-app # initially creates our project
yarn add tailwindcss@latest postcss@latest autoprefixer@latest # install tailwind dependencies
yarn add slate slate-history slate-hyperscript slate-react @udecode/slate-plugins # install our slate dependencies

We took a lot of inspiration for our editor from one of the examples the slate-plugins library provides.

Reading/writing notes from/to IndexedDB#

After scaffolding our application we needed to ensure it's offline first. That means we should be able to use it without an internet connection and without Portabella. For this we leveraged the localForage library. It tries to use IndexedDB and then falls back to LocalStorage if the browser doesn't have it.

We could have written this layer ourselves but it just made sense to bring in a dependency to make sure we didn't mess anything up.

To read all of our notes it looks a bit like this:

import localForage from 'localforage';
const notesDB = localForage.createInstance({ name: 'privanote/notes' });
const keys = await notesDB.keys();
if (!keys) {
return;
}
const fetched = await Promise.all(
keys.map(async (key) => {
const { updatedAt, text } = await notesDB.getItem(key);
return { id: key, updatedAt, text: safeParseJson(text) };
})
);

Creating a note is as easy as:

const id = uuidv4();
await notesDB.setItem(id, card);

We won't show you the delete operation because, well, you get the gist.

Reading/writing notes from/to Portabella#

Setting up our Portabella integration is a little trickier. We have a few additional constraints here, we need to:

  • save Portabella config in our local database
  • check if they have Portabella config before falling back to the local database storage
  • prompt users to setup their Portabella integration (after 10 seconds to not be annoying).

Let's cover reading and writing data before we get to the administrative tasks. Writing a note to Portabella leveraging the @portabella/sdk is as easy as:

const { ProjectSDK } = require('@portabella/sdk');
const pb = new ProjectSDK(config);
const id = uuidv4();
await pb.addCard({ id, ...card })

Reading all our notes (when we refresh the page for example looks like):

const { cards } = await pb.fetchProject();
const fetched = Object.entries(cards)
.filter(([_, card]) => Boolean((card as any).description))
.map(([id, card]) => ({
id,
text: safeParseJson((card as any).description),
updatedAt: card.updatedAt,
}));

The SDK takes care of decrypting everything for us so we don't need to worry about getting data we can't read.

Prompting for Portabella credentials#

Before we can do the above operations we need to ensure we actually have Portabella config so we can initialise the SDK and make requests.

To do this we want to prompt the user to setup their integration after 10 seconds on PrivaNote. Additionally we don't want to prompt them again if they've already said yes (or no).

Our React useEffect hook looks a bit like this:

useEffect(() => {
let timeout = null;
async function f() {
timeout = setTimeout(async () => {
const hasSeenConfigAfterTimeout = await configDB.getItem(
hasSeenConfigKey
);
if (!hasSeenConfigAfterTimeout) {
setDisplayWelcomeModal(true);
}
}, 10000);
const config = await configDB.getItem(portabellaConfigKey);
if (config) {
await initialisePortabella(config);
}
setPortabellaLoaded(true);
}
f();
return () => {
if (timeout) {
clearTimeout(timeout);
}
};
}, []);

Doing it this way allows for a seamless end user experience.

Packaging it as a browser extension#

The final piece of the puzzle is packaging up the Next.js application for use as a browser extension. Having to manually open privanote.xyz everytime you want to take notes is a bit cumbersome, so the idea is to override the "new tab" page of your browser. That means pressing CMD (or CTRL) + N will open up PrivaNote, allowing you to jott down thoughts without thinking twice about it.

Here's our script we used to do this:

rm -rf .next/ out/;
rm extension.zip;
yarn next build;
yarn next export;
cp manifest.json ./out;
mv ./out/_next ./out/next
cd ./out && grep -rli '_next' * | xargs -I@ sed -i '' 's/_next/next/g' @;
zip -r -FS ../my-extension.zip *;

The most interesting lines are the ones that contain _next and next. Chrome won't let browser extensions import JavaScript files that start with an underscore. So trying to add a <script src="/_next/a.js" /> tag won't work out of the box, we need to replace _next with next (or any string you prefer). The sed command shown here works on Mac OS but you might have to fiddle with it if you're using a different OS.

Conclusion#

Thanks for taking the time to checkout this tutorial, as we said PrivaNote is one of the first applications built on top of Portabella so we'll continue to improve documentation and APIs as we go and discover what's useful to people.

If you have any questions feel free to reach on Twitter to us @portabellaio or via email hello@portabella.io.