monkeytype Documentation

The most customizable typing website with a minimalistic design and a ton of features. Test yourself in various modes, track your progress and improve your...

20,883 stars TypeScript TypeScript 1 file ยท ~15,272 tokens #monkeytype#typingtest
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy monkeytype on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name monkeytype -p 80:80 --restart always monkeytype/monkeytype-frontend:latest
Configured Services & Ports
โš™๏ธ monkeytype-proxy
image: traefik:v3.7
๐Ÿ”Œ Ports: ${HTTP_PORT:-80}:80, ${HTTPS_PORT:-443}:443
โš™๏ธ monkeytype-frontend
image: monkeytype/monkeytype-frontend:latest
โš™๏ธ monkeytype-backend
image: monkeytype/monkeytype-backend:latest
โš™๏ธ monkeytype-redis
image: redis:6.2.6
โš™๏ธ monkeytype-mongodb
image: mongo:5.0.13
docker-compose.yml
yaml (docker-compose.yml)
services:
  monkeytype-proxy:
    container_name: monkeytype-proxy
    image: traefik:v3.7
    restart: on-failure
    depends_on:
      monkeytype-backend:
        condition: service_healthy
      monkeytype-frontend:
        condition: service_healthy
    ports:
      - "${HTTP_PORT:-80}:80"
      - "${HTTPS_PORT:-443}:443"
      #Dashboard disabled for security (uncomment only for debugging behind VPN)
      #- "8080:8080"
    networks:
      - public
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./letsencrypt:/letsencrypt
    command:
      - "--providers.docker=true"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"

      # enable for HTTPS
      #- "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}"
      #- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
      #- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
      #- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      #- "--entrypoints.web.http.redirections.entrypoint.scheme=https"

      # for debugging only
      #- "--api.insecure=true"
      #- "--log.level=DEBUG"

  monkeytype-frontend:
    container_name: monkeytype-frontend
    image: monkeytype/monkeytype-frontend:latest
    #uncomment if you want to expose the frontend server directly (not recommended)
    #ports:
    #  - "${FRONTEND_PORT:-8080}:80"
    restart: on-failure
    networks:
      - public
    labels:
      - "traefik.http.routers.frontend.rule=PathPrefix(`/`)"
      - "traefik.http.routers.frontend.entrypoints=web,websecure"
      - "traefik.http.routers.frontend.priority=25"
      - "traefik.http.services.frontend.loadbalancer.server.port=80"
      # enable for HTTPS
      #- "traefik.http.routers.frontend.rule=Host(`${DOMAIN}`)"
      #- "traefik.http.routers.frontend.tls=true"
      #- "traefik.http.routers.frontend.tls.certresolver=letsencrypt"
      #- "traefik.http.routers.frontend.tls.domains[0].main=${DOMAIN}"
    env_file:
      - path: ./.env
        required: true
    environment:
      - FIREBASE_APIKEY=${FIREBASE_APIKEY:-}
      - FIREBASE_AUTHDOMAIN=${FIREBASE_AUTHDOMAIN:-}
      - FIREBASE_PROJECTID=${FIREBASE_PROJECTID:-}
      - FIREBASE_STORAGEBUCKET=${FIREBASE_STORAGEBUCKET:-}
      - FIREBASE_MESSAGINGSENDERID=${FIREBASE_MESSAGINGSENDERID:-}
      - FIREBASE_APPID=${FIREBASE_APPID:-}
      - MONKEYTYPE_BACKENDURL=${BASE_URL:-http://localhost}/api
      - RECAPTCHA_SITE_KEY=${RECAPTCHA_SITE_KEY:-}
    healthcheck:
      test: ["CMD", "nc", "-z", "localhost", "80"]
      interval: 5s
      timeout: 10s
      retries: 15
      start_period: 5s

  monkeytype-backend:
    container_name: monkeytype-backend
    image: monkeytype/monkeytype-backend:latest
    #uncomment if you want to expose the backend server (not recommended)
    #ports:
    #  - "${BACKEND_PORT:-5005}:5005"
    networks:
      - public # access to the internet, e.g. google firebase
      - isolated # access to the databases
    restart: on-failure
    environment:
      - DB_NAME=monkeytype
      - DB_URI=mongodb://monkeytype-mongodb:27017
      - REDIS_URI=redis://monkeytype-redis:6379
      - FRONTEND_URL=${BASE_URL:-http://localhost}
      - RECAPTCHA_SECRET=${RECAPTCHA_SECRET:-}
      - EMAIL_HOST=${EMAIL_HOST:-}
      - EMAIL_PORT=${EMAIL_PORT:-}
      - EMAIL_USER=${EMAIL_USER:-}
      - EMAIL_PASS=${EMAIL_PASS:-}
      - EMAIL_FROM=${EMAIL_FROM:-}
    volumes:
      #uncomment to enable the account system, check the SELF_HOSTING.md file
      #- type: bind
      #  source: ./serviceAccountKey.json
      #  target: /app/backend/src/credentials/serviceAccountKey.json
      #  read_only: true
      - type: bind
        source: ./backend-configuration.json
        target: /app/backend/dist/backend-configuration.json
        read_only: true
    depends_on:
      monkeytype-redis:
        condition: service_healthy
      monkeytype-mongodb:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "nc", "-z", "localhost", "5005"]
      interval: 5s
      timeout: 10s
      retries: 15
      start_period: 5s
    labels:
      - "traefik.http.middlewares.backend-prefix.stripprefix.prefixes=/api"
      - "traefik.http.middlewares.backend-headers.headers.customrequestheaders.CF-Connecting-IP="
      - "traefik.http.routers.backend.middlewares=backend-prefix,backend-headers"
      - "traefik.http.routers.backend.rule=PathPrefix(`/api`)"
      - "traefik.http.routers.backend.priority=100"
      - "traefik.http.routers.backend.entrypoints=web"
      - "traefik.http.services.backend.loadbalancer.server.port=5005"
      - "traefik.enable=true"
      - "traefik.docker.network=monkeytype-public"

  monkeytype-redis:
    container_name: monkeytype-redis
    image: redis:6.2.6
    #uncomment if you want to expose the redis server (not recommended)
    #ports:
    #  - "${REDIS_PORT:-6379}:6379"
    networks:
      - isolated
      #uncomment if you want to expose the redis server (not recommended)
      #- public
    restart: on-failure
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
      interval: 5s
      timeout: 10s
      retries: 15
      start_period: 10s

  monkeytype-mongodb:
    container_name: monkeytype-mongodb
    image: mongo:5.0.13
    restart: on-failure
    volumes:
      - mongo-data:/data/db
    #uncomment if you want to expose the mongodb server (not recommended)
    #ports:
    #  - "${MONGO_PORT:-27017}:27017"
    networks:
      - isolated
      #uncomment if you want to expose the mongodb server (not recommended)
      #- public
    healthcheck:
      test: echo 'db.stats().ok' | mongo localhost:27017/test --quiet
      interval: 5s
      timeout: 10s
      retries: 15
      start_period: 10s

volumes:
  mongo-data:
    name: monkeytype_mongo_data
  redis-data:
    name: monkeytype_redis_data

networks:
  isolated:
    name: monkeytype-isolated
    internal: true
  public:
    name: monkeytype-public

Technical Documentation: monkeytypegame/monkeytype

โ„น๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference

Overview (README.md)

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

 
 
 
 

About

Monkeytype is a minimalistic and customizable typing test. It features many test modes, an account system to save your typing speed history, and user-configurable features such as themes, sounds, a smooth caret, and more. Monkeytype attempts to emulate a natural typing experience during a typing test by unobtrusively presenting the text prompts and displaying typed characters in place, providing straightforward, real-time feedback on typos, speed, and accuracy.

Features

  • Minimalistic design, with optional advertisements and focus mode while typing
  • Type what you see, see what you type
  • Live errors, wpm, and accuracy displays
  • A variety of test lengths and languages
  • Punctuation and numbers modes
  • Quotes
  • Themes
  • Smooth caret
  • Account system
  • Challenges and just-for-fun test modifiers
  • And much more

Discord bot

On the Monkeytype Discord server, we added a Discord bot to auto-assign optional roles based on typing performance and challenge completion. You can find its code over at https://github.com/monkeytypegame/monkeytype-bot.

Bug report or Feature request

If you encounter a bug or have a feature request, send us an email, create an issue, create a discussion thread, or join the Discord server.

Want to Contribute?

Refer to CONTRIBUTING.md.

Code of Conduct

Before contributing to this repository, please read the code of conduct.

Security

To report a security vulnerability, please refer to SECURITY.md.

Credits

Montydrei for the name suggestion.

Everyone who provided valuable feedback on the original Reddit post for the prototype of this website.

All of the contributors have helped implement various features, add themes, fix bugs, and more.

Support

If you wish to support further development and feel extra awesome, you can donate, become a Patron, or buy a t-shirt.


AGENT BROWSER (`docs/AGENT_BROWSER.md`)

Browser testing (agents)

Dev server

Assume the frontend dev server is already running at http://localhost:3000. Never start it (a second instance grabs port 3001) and never probe it (lsof, ps, curl โ€” sandboxed curl can't reach localhost anyway).

If a page load fails (ERR_CONNECTION_REFUSED, "This site can't be reached"), stop immediately and ask the user to start the server.

Tabs

New tabs open on chrome://newtab, which rejects javascript_tool and screenshots. Navigate to http://localhost:3000 first, then interact.

Before testing anything animated (page fades, anime.js, carets), check document.visibilityState. In a hidden tab Chrome pauses requestAnimationFrame, so fades never finish and pages look stuck at opacity 0. If it's hidden, ask the user to bring the Chrome window to the front.

`window.__mt` (dev only)

Defined in frontend/src/ts/dev/expose.ts; absent in production builds. Wait for it after load:

js
for (let i = 0; i < 30 && !window.__mt; i++) await new Promise((r) => setTimeout(r, 300));
Member Use
navigate(url) client-side route change
getActivePage() current page name
getConfig / setConfig(key, value) read/write config
showModal(id) open a modal by ModalId
restart() restart the typing test
signals(filter?) list tracked Solid signals {name, source, value}, filter by name/source substring
getSignal(name) / setSignal(name, value) read/write a tracked signal by exact name

Read app state through these members or the DOM. Importing app modules from an injected script (import("/ts/...")) loads a separate module instance with its own state.

Older globals from frontend/src/ts/index.ts (addToGlobal) also exist in all envs: config, snapshot, qs/qsa, lastEventLog(), currentEventLog(), toggleDebugLogs().

Console

read_console_messages with a pattern โ€” logger output is noisy. Errors from utils/logger.ts are prefixed ERR.


CODE OF CONDUCT (`docs/CODE_OF_CONDUCT.md`)

Contributor Covenant Code of Conduct

Our Pledge

We as members, contributors, and leaders pledge to participate in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

Our Standards

Examples of behavior that contributes to a positive environment for our
community includes:

  • Demonstrating empathy and kindness toward other people
  • Being respectful of differing opinions, viewpoints, and experiences
  • Giving and gracefully accepting constructive feedback
  • Accepting responsibility and apologizing to those affected by our mistakes,
    and learning from the experience
  • Focusing on what is best not just for us as individuals, but for the overall
    community

Examples of unacceptable behavior include:

  • The use of sexualized language or imagery, and sexual attention or advances of
    any kind
  • Trolling, insulting or derogatory comments, and personal or political attacks
  • Public or private harassment
  • Publishing others' private information, such as a physical or email address,
    without their explicit permission
  • Other conduct that could reasonably be considered inappropriate in a
    professional setting

Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

Scope

This Code of Conduct applies within all community spaces and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[email protected].
All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.

Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

1. Correction

Community Impact: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

Consequence: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

2. Warning

Community Impact: A violation through a single incident or series of
actions.

Consequence: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.

3. Temporary Ban

Community Impact: A serious violation of community standards, including
sustained inappropriate behavior.

Consequence: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.

4. Permanent Ban

Community Impact: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.

Consequence: A permanent ban from any sort of public interaction within the
community.

Attribution

This Code of Conduct is adapted from the Contributor Covenant,
version 2.1, available at
https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.

Community Impact Guidelines were inspired by
Mozilla's code of conduct enforcement ladder.

For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.


CONTRIBUTING (`docs/CONTRIBUTING.md`)

Contributing

**Table of Contents**

Getting Started

When contributing to Monkeytype, it's good to know our best practices, tips, and tricks. First, Monkeytype is written in JavaScript TypeScript, HTML, and CSS (in order of language usage within the project); thus, we assume you are comfortable with these languages or have basic knowledge of them. Our backend is in NodeJS and we use MongoDB to store our user data. Firebase is used for authentication. Redis is used to store ephemeral data (daily leaderboards, jobs via BullMQ, OAuth state parameters). Furthermore, we use Oxc (Oxfmt and Oxlint) to format and lint our code.

How to Contribute

We have two separate contribution guides based on what you're looking to contribute. If you're simply looking to help us augment our language or quotes data, please refer to CONTRIBUTING_BASIC.md. This guide will go over how to do so easily and without the need to set up a local development server.

If you're looking to make deeper code changes that affect functionality, or will require screenshots of the changes, please refer to CONTRIBUTING_ADVANCED.md.

Standards and Guidelines

Below is a set of general guidelines for different types of changes.

Pull Request Naming Guidelines

We use Conventional Commits for our pull request titles (and commit messages on the master branch) and also include the author name at the end inside parenthesis. Please follow the guidelines below when naming pull requests.

For types, we use the following:

  • feat: A new feature
  • impr: An improvement to an existing feature
  • fix: A bug fix
  • docs: Documentation only changes
  • style: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc)
  • refactor: A code change that neither fixes a bug nor adds a feature, but makes the code easier to read, understand, or improve
  • perf: A code change that improves performance
  • test: Adding missing tests or correcting existing tests
  • build: Changes that affect the build system or external dependencies (example scopes: vite, tsup-node, npm)
  • ci: Changes to our CI configuration files and scripts (example scopes: GitHub Workflows)
  • revert: Reverts a previous commit
  • chore: Other changes that don't apply to any of the above

Examples

  • feat: add new feature (@github_username)
  • impr(quotes): add english quotes (@username)
  • fix(leaderboard): show user rank correctly (@user1, @user2, @user3)

Theme Guidelines

Before submitting a theme make sure...

  • your theme is unique and isn't visually similar to any we already have.
  • the text color is either black or white (or very close to these colors)
  • your theme has been added to the _list file and the textColor property is the theme's main color
  • your theme is clear and readable with both flip test colors and colorful mode enabled and disabled

If you want to contribute themes but don't know how, check THEMES.md

Language Guidelines

  • Do not include expletive words
  • Ensure that your contribution meets JSON standards (no trailing comma at the end of a list)
  • Be sure to add your language to the _list and _groups files
  • Make sure the number of words in the file corresponds to the file name (for example: languageName.json is 200 words, languageName_1k.json is 1000 words, and so on)

If you want to contribute languages but don't know how, check LANGUAGES.md

Quote Guidelines

  • Do not include content that contains any libelous or otherwise unlawful, abusive, or obscene text.
  • Ensure that your contribution meets JSON standards (no trailing comma at the end of a list)
  • Verify quotes added aren't duplicates of any already present
  • Verify the length property is correct (length of the text in characters)
  • Verify the id property is incremented correctly
  • Please do not add extremely short quotes (less than 60 characters)
  • For quotes not in English, please include translations of quotes in the description of your pull request. This assists in the verification process to ensure the integrity of the quotes.
  • Remember to name your pull request properly. For example, if you are adding new quotes for the language French, your pull request should be named impr(quotes): add French quotes (@yourusername).

If you want to contribute quotes but don't know how, check QUOTES.md

Layout Guidelines

If you want to contribute layouts but don't know how, check LAYOUTS.md

Questions

If you have any questions, comments, concerns, or problems let me know on GitHub, Discord in the #development channel, or ask a question on Monkeytype's GitHub discussions and a contributor will be happy to assist you.


CONTRIBUTING ADVANCED (`docs/CONTRIBUTING_ADVANCED.md`)

Contributing - Advanced

**Table of Contents**

Prerequisites

This contribution guide is for cases in which you need to test the functionality of your changes, or if you need to take screenshots of your changes. You will need a computer with a stable internet connection, a text editor, Git, and NodeJS with version 24.11.0. There are some additional requirements depending on what you're looking to contribute, such as Firebase for authentication, and Mongo and Docker for the backend. Read the below sections to understand how to set up each of these tools.

Git

[!WARNING]
If you are on Windows, run git config --global core.autocrlf false before cloning this repo to prevent CRLF errors.

Git is optional but we recommend you utilize it. Monkeytype uses the Git source control management (SCM) system for its version control. Assuming you don't have experience typing commands in the command line, we suggest installing Sourcetree. You will be able to utilize the power of Git without needing to remember any cryptic commands. Using a Git client such as Sourcetree won't give you access to the full functionality of Git, but provides an easy-to-understand graphical user interface (GUI). Once you have downloaded Sourcetree, run the installer. While installing Sourcetree, keep your eyes peeled for the option to also install Git with Sourcetree. This is the option you will need to look for in order to install Git. Make sure to click yes in the installer to install Git with Sourcetree.

NodeJS and PNPM

Currently, the project is using version 24.11.0 LTS.

If you use nvm (if you use Windows, use nvm-windows) then you can run nvm install and nvm use (you might need to specify the exact version eg: nvm install 24.11.0 then nvm use 24.11.0) to use the version of Node.js in the .nvmrc file.

Alternatively, you can navigate to the NodeJS website to download it from there.

For package management, we use pnpm instead of npm or yarn. You can install it by running npm i [email protected]. This will install pnpm globally on your machine.

You can use docker to run the frontend and backend. This will take care of OS-specific problems but might be a bit more resource-intensive. You can download it from the Docker website.

Firebase (optional)

The account system will not let you create an account without a Firebase project. You can skip this if you don't think you will need it (you can always set it up later)

  1. Create a Firebase account if you already haven't done so.

  2. Create a new Firebase project.

    • The project name doesn't matter, but the name monkeytype would be preferred.
    • Google Analytics is not necessary.
  3. Enable Firebase Authentication

    • In the Firebase console, go to Build > Authentication > Sign-in method
    • Click on Email/Password, enable it, and save
    • Click on Google, add a support email, and save
  4. Generate a Firebase Admin private key (optional, only needed if you want to work on the backend)

    • In your Firebase console, go to Project Settings > Service Accounts
    • Click "Generate New Private Key"
    • Save as serviceAccountKey.json inside the backend/src/credentials/ directory.
  5. Run pnpm add firebase-tools to install the Firebase Command Line Interface.

  6. Run firebase login on your terminal to log in to the same Google account you just used to create the project.

  7. Within the frontend directory, duplicate .firebaserc_example, rename the new file to .firebaserc and change the project name to the firebase project id you just created.

    • Run firebase projects:list to find your firebase project ID.
    • If .firebaserc_example does not exist after cloning, create your own with:
    .firebaserc
    {
         "projects": {
             "default": "your-firebase-project-id"
         }
     }

Config file

Within the frontend/src/ts/constants directory, duplicate firebase-config-example.ts, rename it to firebase-config.ts

  • If you skipped the Firebase step, you can leave the fields blank
  • Otherwise:
    1. Navigate to Project Settings > General > Your apps
    2. If there are no apps in your project, create a new web app
    3. In the SDK setup and configuration section, select npm
    4. The Firebase config will be visible below
    5. Paste the config into firebase-config.ts
    6. Ensure there is an export statement before const firebaseConfig

If you want to access the frontend from other machines on your network create a file frontend/.env with this content:

text
BACKEND_URL="http://<Your IP>:5005"

Databases (optional if running frontend only)

Follow these steps if you want to work on anything involving the database/account system. Otherwise, you can skip this section.

  1. Inside the backend folder, copy example.env to .env in the same directory.

    • The backend Docker scripts read port bindings from this file. If 27017, 6379, or 5005 are already in use on your machine, update DOCKER_DB_PORT, DOCKER_REDIS_PORT, and DOCKER_SERVER_PORT before starting Docker.
  2. Setup the database server

Manual Docker (recommended)
  1. Install MongoDB Community Edition
  2. Install Redis
  3. Make sure both are running
  1. Install Docker on your machine
  2. Run npm run docker-db-only from the ./backend directory
  1. (Optional) Install MongoDB-compass. This tool can be used to see and manipulate your database visually.
    • To connect, type mongodb://localhost:27017 in the connection string box and press connect. The Monkeytype database will be created and shown after the server is started.

Building and Running Monkeytype

It's time to run Monkeytype. Just like with the databases, you can run the frontend and backend manually or with Docker.

Dependencies (if running manually)

Run pnpm i in the project root to install all dependencies.

Both Frontend and Backend

Manual:

text
npm run dev

Backend only

Manual Docker
npm run dev-be cd backend && npm run docker

Frontend only

Manual Docker
npm run dev-fe cd frontend && npm run docker

By default, these commands will start a local development website on port 3000 and a local development server on port 5005. They will automatically rebuild the website/server when you make changes in the src/ directory. Use Ctrl+C to stop them.

[!NOTE]
Rebuilding doesn't happen instantaneously and depends on your machine, so be patient for changes to appear.

If you are on a UNIX system and you get a spawn error, run npm with sudo.

Standards and Guidelines

Code formatting and linting is enforced by Oxc (Oxfmt and Oxlint), which automatically runs every time you make a commit.

For guidelines on commit messages, adding themes, languages, or quotes, please refer to CONTRIBUTING.md. Following these guidelines will increase the chances of getting your change accepted.

Questions

If you have any questions, comments, concerns, or problems let me know on GitHub, Discord in the #development channel, or ask a question on Monkeytype's GitHub discussions and a contributor will be happy to assist you.


CONTRIBUTING BASIC (`docs/CONTRIBUTING_BASIC.md`)

Contributing - Basic

**Table of Contents**

Basic Contributions

This file details how to create basic contributions to Monkeytype purely through the use of GitHub's web UI. This means you will not need to set up a local development environment of any kind; all you'll need is a browser that can access GitHub and a GitHub account.

Given the above, you should only be using this guide if you plan on making changes that do not impact the functionality of the website. Examples of such cases would be translation fixes, language additions, or quote additions.

For all other changes, please refer to CONTRIBUTING_ADVANCED.md to learn how to set up the necessary tools to develop on your local environment.

Prerequisites

You must have a browser that can access GitHub, and possess a GitHub account. Once you have those two things, you're ready to move on to making your contribution(s)!

The steps for basic contributions are showcased splendidly in this YouTube video, so it is recommended you watch it.

Contributing

Forking Monkeytype

First, you will have to obtain your own copy of the Monkeytype repository, also known as "forking". Click here to open the fork wizard or go to the top right of your screen and then click the fork button.

This will create a clone of the repository under your own account. Navigate to your profile now and click on the new repository called monkeytype under your profile.

Making a Change

There are two methods for making a change in the code.

If you are not a developer and wish to contribute themes, new languages, or quotes, having a text editor will make contributions much easier. To make complex edits without installing anything, we recommend using GitHub's VS Code web editor. In your fork of Monkeytype (fork it first), go to the Code tab of the repo and press .(the period/dot key). This will open up the repo in an online VS Code instance you can use to edit files in the browser. Once you are done making your changes, go to the Source Control tab in the activity bar with Ctrl/Cmd + Shift + G, click the + next to the files you've changed to stage them, type a brief message summarizing the changes made in the commit, and press Ctrl/Cmd + Enter to commit your changes to your fork.

Once done, move on to the next section to create a pull request.

Option 2 - GitHub Web UI

You're now ready to make a change. Navigate to the file that you're looking to contribute to in your forked repository. Once you navigate to the file, you should see an Edit icon (shaped like a pencil) on the right:

Upon clicking this, you'll have the ability to edit the document itself.

Note however that some files that are too large might not have this option. In these cases, you will need to download the code and create edits outside of the GitHub web UI. Refer to CONTRIBUTING_ADVANCED.md

At this point, you should take a look at CONTRIBUTING.md to view guidelines for theme, language, and quote contributions.

Once you've completed your change, you're ready to commit it. At the bottom of the edit file screen, you will find the commit UI. In the first box, you want to put in a title that describes the change you made. Then in the description field, you can put in any additional detail to supplement your title further.

You will find two radio buttons, one prompts you to commit directly to your current branch, and the other prompts you to create a new branch for your commit and start a pull request. Select the first option to commit the change directly to your current branch.

Click Commit changes once you are ready to proceed.

Creating a Pull Request

You can repeat the steps above for as many changes as needed. Once you are done making all your code changes and you have committed them to your branch, you are ready to make a pull request (PR). Go back to the main page of your forked repository. Ensure that your current branch (which is likely still master at this point) is up to date. You can do so by clicking the following button:

Update branch:

Once up to date, you can click the Contribute button to open a PR.

Create a pull request:

Be sure to add a good description to the PR and that the source and destination branches at the top look correct. The base repository and base branch should be listed as monkeytypegame/monkeytype and master respectively, and on the right of that should be your forked repository and the branch you have your changes on.

Once done, click on Create pull request to officially publish your PR.

Merging a Pull Request

All you have to do now is wait for approval or comments and go from there!

Once your PR is approved, all that is left to do is merge it!

Questions

If you have any questions, comments, concerns, or problems, don't hesitate to let us know via email(to Miodec), on GitHub or on Discord in the #development channel and a contributor will be happy to assist you.


FONTS (`docs/FONTS.md`)

**Table of Contents**

Forking Monkeytype

First, you will have to make a personal copy of the Monkeytype repository, also known as "forking". Go to the Monkeytype repo and then click the "fork" button.

Adding Fonts

Once you have forked the repository you can now add your font. Place the font file in ./frontend/static/webfonts e.g. My-Font.woff2.

[!NOTE]
Your font needs to be in the .woff2 format. Your filename cannot include spaces or start with a number.

Open ./packages/schemas/src/fonts.ts and add the new font at the end of the KnownFontNameSchema list like this:

typescript
const KnownFontNameSchema = z.enum(
  [
    "Roboto_Mono",
    "Noto_Naskh_Arabic",
    ...
    "My_Font",

Call it whatever you want but make sure you replace spaces with underscores and the font does not start with a number.

Then, go to ./frontend/src/ts/constants/fonts.ts and add the following code to the end of the Fonts object near to the very end of the file:

typescript
export const Fonts: Record<KnownFontName, FontConfig> = {
  ...
  My_Font: {
    fileName:  "My-Font.woff2",
  }

Committing Languages

Once you have created your language, you now need to create a pull request to the main Monkeytype repository. Go to the branch where you created your languages on GitHub. Then make sure your branch is up to date. Once it is up to date, click "contribute".

Update branch:

Create a pull request:

Language Guidelines

Make sure your language follows the Language guidelines.


LANGUAGES (`docs/LANGUAGES.md`)

**Table of Contents**

Forking Monkeytype

First, you will have to make a personal copy of the Monkeytype repository, also known as "forking". Go to the Monkeytype repo and then click the "fork" button.

Creating Languages

Once you have forked the repository you can now add your language. Create a new JSON file in ./frontend/static/languages/, named as the language name and the number of words, e.g. language_1k.json. If there are less than 1,000 words, simply name the file after the language (e.g. language.json). Note that a minimum of 200 words are required.

The contents of the file should be as follows:

json
{
  "name": string,
  "rightToLeft": boolean,
  "joiningScript": boolean,
  "orderedByFrequency": boolean,
  "bcp47": string,
  "preferredFont":string,
  "words": string[]
}

It is recommended that you familiarize yourselves with JSON before adding a language. For the name field, put the name of your language.
rightToLeft indicates how the language is written. If it is written right to left then put true, otherwise put false.
joiningScript indicates whether the language requires joining letters to render correctly. Set it to true if characters must join with surrounding characters or if their shapes change based on position in a word (initial, medial, final, or isolated), or if they use connecting marks (matras/vowel signs) that reshape the base characters. Otherwise, set it to false.
For bcp47 put your languages IETF language tag.
If the words you're adding are ordered by frequency (most common words at the top, least at the bottom) set the value of orderedByFrequency to true, otherwise false.
Finally, add your list of words to the words field.
If the language is not rendered correctly, you can provide a preferredFont. The font needs to be one of monkeytypes fonts. Check the fonts documentation.

Then, go to packages/schemas/src/languages.ts and add your new language name at the end of the LanguageSchema enum. Make sure to end the line with a comma. Make sure to add all your language names if you have created multiple word lists of differing lengths in the same language.

typescript
export const LanguageSchema = z.enum([
  "english",
  "english_1k",
  ..."your_language_name",
  "your_language_name_10k",
]);

Then, go to frontend/src/ts/constants/language.ts and add your new language name to the LanguageGroups map. You can either add it to an existing group or add a new one. Make sure to add all your language names if you have created multiple word lists of differing lengths in the same language.

typescript
export const LanguageGroups: Record<string, Language[]> = {
  ...
  your_language_name: [
    "your_language_name",
    "your_language_name_10k",
  ]
};

Committing Languages

Once you have created your language, you now need to create a pull request to the main Monkeytype repository. Go to the branch where you created your languages on GitHub. Then make sure your branch is up to date. Once it is up to date, click "contribute".

Update branch:

Create a pull request:

Language Guidelines

Make sure your language follows the Language guidelines.


LAYOUTS (`docs/LAYOUTS.md`)

**Table of Contents**

Forking Monkeytype

First, you will have to make a personal copy of the Monkeytype repository, also known as "forking". Go to the Monkeytype repo and then click the "fork" button.

Creating Layouts

Once you have forked the repository you can now add your layout. Create a new JSON file in ./frontend/static/layouts/, named as the layout name, e.g. qwerty.json.

The contents of the file should be as follows:

json
{
  "keymapShowTopRow": false,
  "type": "ansi",
  "keys": {
    "row1": [
      ["`", "~"],
      ["1", "!"],
      ["2", "@"],
      ["3", "#"],
      ["4", "$"],
      ["5", "%"],
      ["6", "^"],
      ["7", "&"],
      ["8", "*"],
      ["9", "("],
      ["0", ")"],
      ["-", "_"],
      ["=", "+"]
    ],
    "row2": [
      ["q", "Q"],
      ["w", "W"],
      ["e", "E"],
      ["r", "R"],
      ["t", "T"],
      ["y", "Y"],
      ["u", "U"],
      ["i", "I"],
      ["o", "O"],
      ["p", "P"],
      ["[", "{"],
      ["]", "}"],
      ["\\", "|"]
    ],
    "row3": [
      ["a", "A"],
      ["s", "S"],
      ["d", "D"],
      ["f", "F"],
      ["g", "G"],
      ["h", "H"],
      ["j", "J"],
      ["k", "K"],
      ["l", "L"],
      [";", ":"],
      ["'", "\""]
    ],
    "row4": [
      ["z", "Z"],
      ["x", "X"],
      ["c", "C"],
      ["v", "V"],
      ["b", "B"],
      ["n", "N"],
      ["m", "M"],
      [",", "<"],
      [".", ">"],
      ["/", "?"]
    ],
    "row5": [[" "]]
  }
}

It is recommended that you familiarize yourselves with JSON before adding a layout.

keymapShowTopRow indicates whether to always show the first row of the layout.
type can be ansi or iso.

In keys you need to specify row1 to row5. Add the keys within the row as string-array. The string-array can have up to four character. The character define unshifted, shifted, alt-gr and shifted alt-gr character in this order. For example ["e","E","โ‚ฌ"] defines e on regular key press, E if shift is held and โ‚ฌ if alt-gr is held.

Note: Quote and backslash characters need to be escaped: \" and \\.

For ansi layouts the number of keys need to be exactly thirteen for row1 and row2, eleven for row3, ten for row4 and one or two for row5.

For iso the number of keys need to be exactly thirteen for row1, twelve for row2 and row3, eleven for row4 and one or two for row5.

In addition to the layout file you need to add your layout to the packages/schemas/src/layouts.ts file. Just append your layout name (without the .json) at the end of the LayoutNameSchema. Remember to add a comma like this:

ts
export const LayoutNameSchema = z.enum([
  "qwerty",
  "dvorak",
  "colemak",
  ..."your_layout_name",
]);

Committing Layouts

Once you have created your layout, you now need to create a pull request to the main Monkeytype repository. Go to the branch where you created your layout on GitHub. Then make sure your branch is up to date. Once it is up to date, click "contribute".

Update branch:

Create a pull request:

Make sure your PR title follow the syntax feat(layout): add <YOUR_LAYOUT> layout (@<YOUR_GITHUB_NAME>), e.g. feat(layout): add qwerty layout (@teddinotteddy)

Layout Guidelines

Make sure your layout follows the Layout guidelines.


QUOTES (`docs/QUOTES.md`)

**Table of Contents**

Forking Monkeytype

First you will have to copy the Monkeytype repository also known as forking. Go to the Monkeytype Repo and then click the "fork" button.

Creating Quotes

After you forked the Monkeytype repository you can now add your quotes. (If you haven't already forked the repository, refer to this section.) (Before continuing to the next step make sure the quote's language exists in Monkeytype) Add this code in at the end of the quotes ./frontend/static/quotes/[language].json:

json
{
    "text": "[quote]",
    "source": "[source]",
    "id": [number of the quote],
    "length": [number of characters in quote]
}

If the language does exist in Monkeytype, but there are no quotes for it create a new file for the language.

Committing Quotes

Once you have added your quote(s), you now need to create a pull request to the main Monkeytype repository. Go to the branch where you added your quotes on GitHub. Then make sure your branch is up to date. Once it is up to date, click "contribute".

Update branch:

Create a pull request:

Quote Guidelines

Make sure your quote(s) follows the Quote guidelines.


SECURITY (`docs/SECURITY.md`)

Security Policy

We take the security and integrity of Monkeytype very seriously. If you have found a vulnerability, please report it ASAP so we can quickly remediate the issue.

Reporting a Vulnerability

For vulnerabilities that impact the confidentiality, integrity, and availability of Monkeytype services, please send your disclosure via email. For non-security related platform bugs, follow the bug submission guidelines. Include as much detail as possible to ensure reproducibility. At a minimum, vulnerability disclosures should include:

  • Vulnerability Description
  • Proof of Concept
  • Impact
  • Screenshots or Proof

Submission Guidelines

Do not engage in activities that might cause a denial of service condition, create significant strains on critical resources, or negatively impact users of the site outside of test accounts.


SELF HOSTING (`docs/SELF_HOSTING.md`)

Monkeytype Self Hosting

Table of contents

Prerequisites

  • you need to have docker and docker-compose-plugin installed. Follow the docker documentation on how to do this.

Quickstart

Hosting over the network (HTTPS)

If you plan to access your self-hosted Monkeytype instance over a local network or the internet (not using localhost), you must serve it over HTTPS. Modern browsers restrict key web features, such as crypto.randomUUID, to secure contexts. Accessing the site via HTTP over a network will cause the frontend to crash with errors like Uncaught TypeError: crypto.randomUUID is not a function.

Enable HTTPS

Update the .env file and uncomment these lines and set the values based on your domain.

text
DOMAIN=mydomain.com
BASE_URL=https://mydomain.com
[email protected]

Update the docker-compose.yml and uncomment all lines marked with # enable for HTTPS.

Troubleshooting Frontend Connection Issues

If your reverse proxy is up but you see errors like Looks like the server is experiencing unexpected down time or network errors when fetching resources, your frontend is likely trying to communicate with the backend over unsecure HTTP, causing a Mixed Content block in the browser.

Ensure you configure the frontend to talk to your secure backend URL by following these rules in your .env file:

  1. Update the frontend and backend URL: Set DOMAIN and BASE_URL correctly, usually BASE_URL is https://DOMAIN.
  2. Do not include a trailing slash: Ensure the URL does not end with a / (e.g., use https://yourdomain.com, not https://yourdomain.com/). A trailing slash will cause 404 Not Found errors due to double slashes in the API calls (like //configuration).
  3. Force container recreation: Monkeytype is a Single Page Application (SPA), meaning environment variables are baked into the static JavaScript files during startup. If you change your .env, you must completely recreate the container for the changes to apply:
bash
docker compose up -d --force-recreate

[!TIP]
After updating your configuration and recreating the containers, clear your browser cache or perform a hard reload (Ctrl + F5) to make sure your browser isn't running an old cached version of the frontend.

Security

Do not expose the Monkeytype backend directly to the internet. Instead, place it behind a reverse proxy and configure the backend to only accept connections from the reverse proxy.

The backend's built-in rate limiting is based on the authenticated user's uid or, for unauthenticated requests, the client's IP address.

To determine the client's IP address, the backend checks the following sources in order:

  1. CF-Connecting-IP (when requests are proxied through Cloudflare)
  2. X-Forwarded-For
  3. The source IP address of the HTTP connection

We recommend the following configuration:

  • If you are not using Cloudflare, remove any incoming CF-Connecting-IP header in your reverse proxy before forwarding requests.
  • Configure your reverse proxy to set the X-Forwarded-For header to the client's IP address.
  • Configure the backend to only accept connections from the reverse proxy to prevent clients from spoofing trusted headers.

Sources:

Account System

By default, user sign-up and login are disabled. To enable this, you'll need to set up a Firebase project.
Stop the running docker containers using docker compose down before making any changes.

Setup Firebase

  • create a Firebase account

  • create a new Firebase project.

    • name "monkeytype"
    • uncheck "enable google analytics"
  • enable authentication

    • open the firebase console and open your project
    • go to Authentication > Sign-in method
    • enable Email/Password and save
  • whitelist your domain

    • In the Firebase console, go to Authentication > Sign-in method
    • Scroll to Authorized domains
    • Click Add domain and enter the domain where youโ€™ll host the Monkeytype frontend (e.g. localhost)
  • generate service account

    • go to your project settings by clicking the โš™ icon in the sidebar, then Project settings
    • navigate to the Service accounts tab
    • click Generate new private key to download the .json file.
    • save it as serviceAccountKey.json
    • update docker-compose.yml and uncomment the volume block in the monkeytype-backend container to mount the Firebase service account:
      yaml
      #uncomment to enable the account system, check the SELF_HOSTING.md file
      - type: bind
        source: ./serviceAccountKey.json
        target: /app/backend/src/credentials/serviceAccountKey.json
        read_only: true
  • update the .env file

    • open the firebase console and open your project
    • open the project settings by clicking the โš™ icon on the sidebar and Project settings
    • if your project has no apps yet, create a new Web app (</> icon)
      • nickname monkeytype
      • uncheck set up firebase hosting
      • click Register app
    • select your app and select Config for SDK setup and configuration
    • it will display something like this:
      text
      const firebaseConfig = {
      apiKey: "AAAAAAAA",
      authDomain: "monkeytype-00000.firebaseapp.com",
      projectId: "monkeytype-00000",
      storageBucket: "monkeytype-00000.appspot.com",
      messagingSenderId: "90000000000",
      appId: "1:90000000000:web:000000000000"
      };
    • update the .env file with the values above:
      text
      FIREBASE_APIKEY=AAAAAAAA
      FIREBASE_AUTHDOMAIN=monkeytype-00000.firebaseapp.com
      FIREBASE_PROJECTID=monkeytype-00000
      FIREBASE_STORAGEBUCKET=monkeytype-00000.appspot.com
      FIREBASE_MESSAGINGSENDERID=90000000000
      FIREBASE_APPID=1:90000000000:web:000000000000

Update backend configuration

  • update the backend-configuration.json file and add/modify
    json
    {
      "users": {
        "signUp": true,
        "profiles": {
          "enabled": true
        }
      }
    }

Setup Recaptcha

  • create a new recaptcha token
    • label: monkeytype
    • type: v2
    • domain: the domain of the frontend
  • update the .env file with the site key from the previous step
    text
    RECAPTCHA_SITE_KEY="your site key"
    RECAPTCHA_SECRET="your secret key"

If you host privately you can use these defaults:

text
RECAPTCHA_SITE_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
RECAPTCHA_SECRET=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe

Setup email (optional)

To enable emails for password reset and email verification update the following config in .env file:

text
# email server config
# uncomment below if you want to send emails for e.g. password reset
EMAIL_HOST=mail.myserver   # your mailserver domain
EMAIL_USER=mailuser        # username to authenticate with your mailserver
EMAIL_PASS=mailpass        # password for the user
EMAIL_PORT=465             # port, likely 465 or 587
EMAIL_FROM="Support <noreply@myserver>"

Enable daily leaderboards

To enable daily leaderboards update the backend-configuration.json file and add/modify

json
{
  "dailyLeaderboards": {
    "enabled": true,
    "maxResults": 250,
    "leaderboardExpirationTimeInDays": 1,
    "validModeRules": [
      {
        "language": "english",
        "mode": "time",
        "mode2": "15"
      },
      {
        "language": "english",
        "mode": "time",
        "mode2": "60"
      }
    ]
  }
}
  • language is one of the supported language
  • mode can be time or words
  • mode2 can be 15,30,60 or 120 if you picked mode=time or 10,25,50 or 100 if you picked mode=words.

Configuration files

env file

All settings are described in the example.env file.

serviceAccountKey.json

Contains your firebase config, only needed if you want to allow users to signup.

backend-configuration.json

Configuration of the backend. Check the default configuration for possible values.

[!NOTE]
Configuration changes are applied only on container startup. You must restart the container for your updates to take effect.


THEMES (`docs/THEMES.md`)

**Table of Contents**

Forking Monkeytype

First you will have to make a personal copy of the Monkeytype repository, also known as "forking". Go to the Monkeytype repo and then click the "fork" button.

Creating Themes

Pick a name for your theme. It must be all lowercase, with spaces replaced by underscores.

Go to ./packages/schemas/src/themes.ts and add your new theme name to the end of the ThemeNameSchema enum. Make sure to end the line with a comma.

typescript
export const ThemeNameSchema = z.enum([
  "8008",
  "80s_after_dark",
  ... all existing theme names
  "your_theme_name",
]);

Then, go to ./frontend/src/ts/constants/themes.ts and add the following code to the end of the themes object near to the very end of the file:

typescript
export const themes: Record<ThemeName, Theme>  = {
  ... all existing themes
  your_theme_name: {
    bg: "#ffffff",
    caret: "#ffffff",
    main: "#ffffff",
    sub: "#ffffff",
    subAlt: "#ffffff",
    text: "#ffffff",
    error: "#ffffff",
    errorExtra: "#ffffff",
    colorfulError: "#ffffff",
    colorfulErrorExtra: "#ffffff",
  },
}

Here is an image showing what all the properties correspond to:

If you don't want to add any custom styling you can skip the next section.

Adding custom CSS (optional)

Create a CSS file in ./frontend/static/themes/ matching the name you picked earlier. Update the theme configuration in ./frontend/src/ts/constants/themes.ts and add hasCss: true like this:

typescript
export const themes: Record<ThemeName, Theme>  = {
  ... all existing themes
  your_theme_name: {
    bg: "#ffffff",
    caret: "#ffffff",
    main: "#ffffff",
    sub: "#ffffff",
    subAlt: "#ffffff",
    text: "#ffffff",
    error: "#ffffff",
    errorExtra: "#ffffff",
    colorfulError: "#ffffff",
    colorfulErrorExtra: "#ffffff",
    hasCss: true,
  },
}

Committing Themes

Once you have created your theme(s), you now need to create a pull request to the main Monkeytype repository. Go to the branch where you created your new theme on GitHub. Then make sure your branch is up to date. Once it is up to date, click "contribute".

Update branch:

Create a pull request:

Add some screenshots of your theme to the pull request. Click "create pull request" and if it gets approved then your new theme is available on Monkeytype for everyone to enjoy.

Theme Guidelines

Make sure your theme follows the Theme guidelines.


--- METRICS ---