Skip to content


Switch branches/tags

Latest commit


Git stats


Failed to load latest commit information.
Latest commit message
Commit time


💥 Attention: This project needs your help! Message me if you're interested in becoming a contributor.

👨‍💻 Currently: Working on migrating from Firebase to the open-source Supabase alternative.

About Tutorbook

Tutorbook is the best way to manage online tutoring and mentoring programs.

It's an online app used by organizations (i.e. nonprofits, K-12 schools) to:

  • Match students with tutors and mentors (e.g. by subjects, availability, languages spoken).
  • Manage and track those matches (e.g. via a communications timeline and tags).

Students use Tutorbook to:

  • Search their school's tutors/mentors themselves (instead of having an admin match them up).
  • Keep track of appointments and availability (e.g. via the schedule view).

Parents and teachers use Tutorbook to:

  • Request tutors/mentors for their students (those requests are then fulfilled by an admin who matches the student with the appropriate tutor/mentor).
  • Track their student's matches (e.g. via the communications timeline).

Terminology and Data Model

This is a high-level overview of the various resources ("things") manipulated by and created through the app. Resources may also specify "tags" (filterable attributes) that are totaled daily for our in-app analytics.

Note: This section is not a complete technical definition of our data model. Instead, please refer to lib/model for always up-to-date Typescript data model definitions.


A user is a person. This person could be a tutor, mentor, student, admin or all of them at the same time. Those roles are not inscribed on each user but rather implied by role-specific properties (e.g. a mentor will have subjects specified in their mentoring.subjects property).


  • Vetted - Has at least one verification. Set when the user is created, updated, or deleted.
  • Matched - In at least one match. Set whenever a match is created, updated, or deleted.
  • Meeting - Has at least one meeting. Set whenever a meeting is created, updated, or deleted.

Users also have role-based tags (tutor/tutee/mentor/mentee) that are set when:

  1. The user is created or updated. Ex: If a user has tutoring.subjects, they are a tutor and thus get the tutor tag.
  2. A match is created or updated. Ex: If a user is a tutor in at least one match at some point in time, they are a tutor and thus get the tutor tag.

Role tags are never removed: once a user has been a tutor at least once, they will always be considered a tutor.


An org is a school, nonprofit, or other business entity that is using TB to manage tutoring and/or mentoring programs.


A match is a pairing of people (typically between a single student and a single tutor/mentor, but there can be group pairings as well). Matches are simply containers for meetings.

  • Students create matches when they "send a request" to a tutor/mentor from the search view.
  • Admins can directly create matches (e.g. when migrating from an existing system, admins know who's matched with whom).


  • Meeting - Has at least one meeting. Set whenever a meeting is created, updated, or deleted.


A meeting is exactly that: a meeting between the people in a match with a specific time and venue (e.g. a specific Zoom link). In order to support complex recurrence rules, a meeting's time consists of:

  • From: The start time of this particular meeting instance.
  • To: The end time of this particular meeting instance.
  • Recur: The time's recurrence rule as defined in the iCalendar RFC. This is used server-side by rrule to calculate individual meeting instances that are then sent to the client. It is manipulated client-side when users select a recurrence rule or choose to add an exception to a recurring meeting.
  • Last: The last possible meeting end time. If a meeting is recurring, this will be the end time of the last meeting instance in that recurring range. Or, if the recurring range is infinite, I use Firestore's max date (Dec 31 9999) which is more than sufficient. This is calculated and assigned server-side using rrule. It is completely ignored client-side (in favor of the to property).

Upon creation, Tutorbook sends an email to all of the people in the new meeting's match with the meeting time, venue, and everyone's contact info.


  • Recurring - Is recurring (has an rrule). Set when the meeting is created, updated, or deleted.

Design Specifications

Summarized here are descriptions of common data flow patterns and design specs. These are some of the front-end design guidelines that TB follows in order to maintain consistency and display predictable behavior.

Recurring Meetings

Recurring events are always a struggle to implement. There are many resources available that are meant to make implementing such recurrence rules easier.

TB's entire recurrence stack is quite simple:

  1. Meetings specify complex RRULE recur rules with support for event exceptions and everything else supported by rrule.
  2. At index time, the last possible end date is stored in our Algolia index to make querying data more efficient.
  3. When a meeting range is requested, our API parses the recur rules for meetings within the requested range (i.e. both the start date and recur end date are within the requested date range) and sends the client individual meeting instances.
  4. When availability is requested, our API again parses the recur rules for meetings within the requested availability range and excludes the resulting individual meeting instances from the user's weekly availability.

Editing and updating recurring meetings is intuitive:

  • When a user updates a single event instance (choosing not to update all recurring events), an exception is added to the recurring event's RRULE and a new regular (i.e non-recurring) meeting is created.
  • When a user deletes a single event instance (choosing not to delete all recurring events), an exception is added to the recurring event's RRULE.


TB uses Segment to collect analytics from both the client and the server. When defining events, I use Segment's recommended object-action framework. Each event name includes an object (e.g. Product, Application) and an action on that object (e.g. Viewed, Installed, Created).

TB also has in-app analytics features for orgs to use. It collects a few totals every day (based on Algolia tags) and stores them in a Firestore subcollection (/orgs/<orgId>/analytics). Those totals are constantly being updated as API requests come in (e.g. when a new user is created, TB increments the "Total Users" statistic by one) and thus are always up-to-date. All of those totals are based on filterable tags (e.g. "Total Users Matched") which allows admins to view all the users/matches/meetings that have certain tags, answering questions like:

  • Who are the students or volunteers that aren't matched? Why aren't they?
  • Who doesn't have meetings? Why don't they?
  • Which students aren't donating money? Why aren't they?

Forms and Data Mutation

There are two types of data entry forms used throughout TB:

  1. Single update forms. These are forms that are explicitly submitted by the user upon completion (think Google Forms; must be submitted to be saved).
    • Includes inputs, submission button, loading overlay, and error message.
    • Upon submission, these forms:
      1. Show a loading state that prevents further user input.
      2. Immediately mutate local data (to start any expensive re-rendering).
      3. Update remote data with a POST or PUT API request.
      4. If the server sends an error, reset local data and show error message. Otherwise, mutate local data with the server's response.
      5. Hide the loading state. Data has been updated or an error has occurred.
    • Ex: New request form, edit user form (in people dashboard), sign-up form.
  2. Continuous update forms. These are forms that continually receive user input, mutate local data, and update remote data at set intervals (think Google Docs; continually auto-saves user input).
    • Includes inputs (shows error message via a snackbar).
    • Upon update, these forms:
      1. Immediately mutate local data (unless such a mutation would cause too much expensive re-rendering delaying further user input).
      2. Set a timeout to update the remote data (e.g. after 5secs of no change, update the remote). Clear any existing timeouts.
      3. Update remote data with a POST or PUT API request.
      4. If the server sends an error, show an error message via a snackbar and retry the request. Local data stays mutated. Otherwise, mutate local data with the server's response.
    • Ex: Org settings form, profile form, query/search form.


Do the following (preferably in order):

  1. Join our Slack workspace.
  2. Message #introductions with who you are and how you can help (and what you'll find the most interesting to work on).
  3. Check the #development channel pins for more information on how you can help out.
  4. Read through the links included below to become familiar with our current tech stack.
  5. Contribute:

Also feel free to check out our recently added tutorials/ directory for additional information detailing different aspects of this project (e.g. tests, deployment workflows, CI/CD, etc).

This project uses (please ensure that you're familiar with our tech stack before trying to contribute; it'll save your reputation and a lot of time):



  • React - As our front-end framework.
  • Next.js - To easily support SSR and other performance PWA features.
  • SWR - Used to manage global state. SWR fetches data from our back-end, stores it in a global cache, and allows local mutations of that cache (with or without automatic revalidation).


  • Yarn - To manage dependencies much faster than NPM (and for better community support, advanced features, etc).
  • ESLint - For code linting to avoid common mistakes and to enforce styling. Follow these instructions to install it in the text editor of your choice (such that you won't have to wait until our pre-commit hooks fail to update your code).
  • Cypress for integration, UI, and some unit tests. Cypress is like Selenium; but built from the ground-up with the developer in mind. Cypress runs alongside your code in the browser, enabling DOM snapshots, time travel, and overall faster test runs.


Commit Message Format

I have very precise rules over how Git commit messages in Tutorbook's repository must be formatted. This format leads to easier to read commit history.

Please refer to the following documentation for more info:

Commit Message Header

Commit messages that do not adhere to the following commit style will not be merged into develop:

<type>(<scope>): <short summary>
  │       │             │
  │       │             └─⫸ Summary in present tense. Not capitalized. No period at the end.
  │       │
  │       └─⫸ Commit Scope: The page, API route, or component modified.
  └─⫸ Commit Type: ci|docs|feat|fix|perf|refactor|test|deps|chore

The <type> and <summary> fields are mandatory, the (<scope>) field is optional.


Must be one of the following:

  • ci: Changes to our CI configuration files and scripts.
  • docs: Documentation only changes.
  • feat: A new feature.
  • fix: A bug fix.
  • perf: A code change that improves performance.
  • refactor: A code change that neither fixes a bug nor adds a feature.
  • test: Adding missing tests or correcting existing tests.
  • deps: A change in dependencies.
  • chore: A code change in utility scripts, build configurations, etc.


The scope should refer to the page, API route, or component modified. This can be flexible however (e.g. the scope for a docs: commit may be the README).


Use the summary field to provide a succinct description of the change:

  • Use the imperative, present tense: "change" not "changed" nor "changes".
  • Don't capitalize the first letter.
  • No dot (.) at the end.

Development Environment

To setup a development environment for and to contribute to the TB website:

  1. Follow these instructions to install nvm (our suggested way to use Node.js) on your machine. Verify that nvm is installed by running:
$ command -v nvm
  1. (Optional) If you use Vim as your preferred text editor, follow these instructions on setting up Vim for editing JavaScript.
  2. Run the following command to install Node.js v16.13.0 (our current version):
$ nvm i 16.13.0
  1. (Optional) Run the following command to set Node.js v16.13.0 as your default Node.js version (useful if you have multiple Node.js versions installed and don't want to have to remember to switch to v16.13.0):
$ nvm alias default 16.13.0
  1. Ensure that you have recent versions of Node.js and it's package manager npm by running:
$ node -v
$ npm -v
  1. (Optional) Install the Cypress system dependencies if you plan on running our integration tests locally.
$ sudo apt-get install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
  1. Clone and cd into this repository locally by running:
$ git clone && cd tutorbook/
  1. Follow these instructions to install yarn (our dependency manager for a number of reasons):
$ npm i -g yarn
  1. Then, install of our project's dependencies with the following command:
$ yarn
  1. Follow the instructions included below (see "Available Scripts") to start a Next.js development server (to see your updates affect the app live):
$ yarn dev
  1. Message me (DM @nicholaschiang on Slack) once (not if) you get the following error (I have to give you some Firebase API keys to put in the .env file):
Error [FirebaseError]: projectId must be a string in FirebaseApp.options
  1. Finally, cd into your desired component or lib utility, make your changes, commit them to a branch off of develop, push it to a fork of our repository, and open a PR on GitHub.

Available Scripts

All of the below scripts come directly from Next.js. In the project directory, you can run:

yarn dev

This command runs two scripts concurrently:

  1. Runs next dev with the Node.js --inspect flag on (useful for debugger statements) to start the Next.js development server.
  2. Runs firebase emulators:start to start the Firebase Emulator Suite.

Open to view the app in the browser (note that TB uses instead of the default localhost for Intercom support. The page will hot-reload if you make edits. You will also see any lint errors in the console.

Open http://localhost:4000 to view the (locally-running) Firebase development console. Here, you can manually seed Firestore data and view GCP Function logs.

yarn build

Builds TB's service worker and runs next build which builds the application for production usage.

yarn start

Runs next start which starts a Next.js production server. I have no use for this right now because I'm deploying to Vercel NOW which handles that for me.

yarn analyze

Runs the build to generate a bundle size visualizer.

yarn lint

Runs all of ESLint tests. This should rarely be necessary because you should have ESLint integrated into your IDE (and thus it should run as you edit code) and I have Husky running pretty-quick before each commit (which should take care of the styling that ESLint enforces).

yarn style

Runs our code styling Husky pre-commit hook. TB uses Prettier to enforce consistent code formatting throughout the codebase.

A pre-commit hook is used to format changed files found on commit, however it is still recommended to install the Prettier plugin in your code editor to ensure consistent code style.