Back to blog

Writing a Discovery-Focused Notes App from Scratch

Creating a notes app which allows for moments of serendipity with your own writing.

DiscoveryNote takingWriting

Foreward: Some of the code snippets, specifically the ones used to interact with the API, are out-of-date.

In my previous post, I outlined a concept for a "discovery focused" note taking app. Rather than requiring the user to search for information, the app would be constantly doing searches in the background, looking to proactively surface relevant information as the end-user is typing. In a way, this concept for a note taking app would allow a users' knowledge base to "come alive", meaning they're actively working alongside the user (analogus to a co-worker) to help construct or back up new ideas.

In this post, we're going to walk through the implementation of a proof-of-concept discovery-focused note taking app which we built with the Operand API. If you want to follow along, here's the source code.

In our simplistic note-taking app, there are two key structures which store all of the application data. To store this data, we chose to use Prisma to interact with a MySQL database, hosted on Railway. The first structure, a User, is the top-level object for an end-user. Each user has a set of Note objects, which store the content of each note and some additional metadata. The Prisma schema is as follows:

// A top-level user object.
model User {
  id         String   @id @default(cuid())
  email      String   @unique
  password   String
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt
  operandSet String   @unique

  // some stuff omitted...
}

// A single note.
model Note {
  id         String   @id @default(cuid())
  title      String
  content    String   @db.MediumText // 16,777,215 bytes (~16MB)
  User       User     @relation(fields: [userId], references: [id])
  userId     String
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt
  operandDoc String?  @unique
}

You'll notice two special columns here which are important for searching: User.operandSet and Note.operandDoc. These columns correspond directly with the Document Set and Document objects within the Operand API. In this case, we're using a document set on a per-user basis, which means we can easily scope searches to the contents of a particular users' notes.

When the user is created, we instantly create a corresponding document set for the user within Operand. To do this, we make a quick request to the API, passing in the name of our document set and a small description (this is mainly to keep things organized), which returns the object itself (including the sets' ID). The code for this is simple:

// Creates a document set for a new user.
export async function createDocumentSetForUser(email) {
  let response = await fetch(endpoint + "/v1/sets", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: process.env.OPERAND_API_KEY,
    },
    body: JSON.stringify({
      name: email,
      description: "Notes User",
    }),
  });
  return await response.json();
}

Once we have the ID of a set, we're free to do normal CRUD (create, read, update, delete) operations on Document objects. For example, whenever a user makes a new note, we can fire the corresponding create operation within Operand (to ensure our note app and the Operand API remain in sync). Here's the code for that one:

// Create a new document within a document set.
export async function createDocument(set, title, url) {
  let response = await fetch(endpoint + "/v1/documents", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: process.env.OPERAND_API_KEY,
    },
    body: JSON.stringify({
      set: set,
      tags: [],
      properties: {
        title: title,
        url: url,
        lang: "en",
      },
      type: "text",
      content: "",
    }),
  });
  return await response.json();
}

We can do similar operations whenever a document is updated or deleted.

Before we get to the really fun part, let's take a brief minute to talk about this type of note taking app from a UI/UX perspective. As the user is typing, we want to be showing search results from across their knowledge base (and perhaps other knowledge bases, more on this later). To do this, it seems like the most natural place to do this is in the sidebar, slightly off to the side of the users vision (but importantly, still in view as they're writing a note). Our UI for the proof of concept isn't fantastic, but I think the idea is there:

Discovery-Focused UI/UX

The most important thing here is that as the user is typing, we're nearly constantly firing searches, aiming to get the most up-to-date results possible. To do this, if the user stops typing for as little as 250ms, we take the last one (or two) sentences from the input box and perform the search with the API. Here's the code for choosing when to fire the search, and here's the code which actually does the search:

export async function search(set, query, docs, snippets) {
  let response = await fetch(endpoint + "/v1/search", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: process.env.OPERAND_API_KEY,
    },
    body: JSON.stringify({
      sets: [set],
      query: query,
      options: {
        documents: docs,
        snippets: snippets,
      },
    }),
  });
  return await response.json();
}

Note here that as part of our query, we're passing in an array of document set identifiers to search over (yes, you can search over multiple simulaneously) as well as the query itself. More information on this particular operation within Operand's API can be found here.

Frankly, that's sort of... it. The whole thing took just over 3 hours to build from start to finish, mainly because the bulk of the operations were contained inside the Operand API itself. Our operand.js library file had 5 functions, totalling less than 100 lines of code. Better yet, we don't have to worry about anything: infrastructure, reliability, scaling - that's Operand's job!

Obviously this app was largely a proof-of-concept, and there are huge opportunities to improve on it. Here's a list that we came up with, I'm sure you can figure out some more:

  • Better structure of notes themselves, perhaps with folders and/or categorization.
  • Note sharing between users, or perhaps allowing end-users within a team to discover ideas written in team members notebooks.
  • Index users' bookmarks across the web, meaning users could discover ideas and information contained within those as they are typing.
  • Add common, shared knowledge bases which are accessible by all users. For example, a user could have Wikipedia, Hacker News, even GitHub at their fingertips.
  • Importing data from other relevant sources, for example, the users' emails, iMessages, Notion Graph, etc.

All of the ideas described above, combined, form my personal ideal notes app. After chatting with a bunch of founders of the big existing note taking apps, very few of them are focused on this sort of symbiotic (or serendipity) focused aspect of knowledge bases / note taking. If you're a founder of one an existing note taking app wanting to focus on discovery, or are looking to start a company around some of these ideas, please get in touch (we can get you setup on Operand within a day or two).

As a side note, this blog post itself was written in the note taking app it's describing. Since I had also imported the previous post into the app, as I was typing, I was able to instantly reference the ideas I had written about in that post. Overall, it felt fantastic, and frankly, one step closer to feeling like I was living in a science-fiction movie.