Skip to content
GitHub Twitter

Building a full-stack Geoguessr Clone

Here I discuss how I built a full-stack Geoguessr clone

Technologies used

  • Next.js
  • Tailwind
  • Google Maps Javascript SDK and @googlemaps/react-wrapper to play nicely with React
  • Prisma (for schema setup only - ORM not used)
  • Planetscale (MySQL) and @planetscale/database serverless driver
  • tRPC (running on the edge)
  • Clerk for authentication

Basic idea

I started off just wanting to let the user play the game without worrying about logging them in or anything like that. For this, the user goes to /game and they are presented with the familiar:

Geoguessr game page

The user may then add panoramas of their choice to the map, by visiting the /map-maker route:

Architecture

The architecture is pretty simple:

  • The Next.js app is deployed to Vercel
  • Edge functions running on Vercel are used to serve the tRPC API
  • Authentication is handled by Clerk (I used the cloud/managed version)
  • Planetscale is used to manage the MySQL database

Google Maps

In order to use Google Maps, I needed to create a Google Cloud project and enable the Maps Javascript API. I then created an API key and added it to the environment variables in Vercel.

Prisma schema

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider          = "mysql"
  url               = env("DATABASE_URL")
  shadowDatabaseUrl = env("SHADOW_URL")
  relationMode      = "prisma"
}

model Map {
  id          Int           @id @default(autoincrement())
  name        String
  createdAt   DateTime      @default(now())
  updatedAt   DateTime      @updatedAt
  MapPanorama MapPanorama[]
  Challenge   Challenge[]
  userId      String
  User        User          @relation(fields: [userId], references: [id])

  @@index([userId])
}

model User {
  id            String          @id
  email         String          @unique
  createdAt     DateTime        @default(now())
  Challenge     Challenge[]
  Map           Map[]
  ChallengeGame ChallengeGame[]
}

model MapPanorama {
  id         Int      @id @default(autoincrement())
  panoramaId String
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt
  Map        Map      @relation(fields: [mapId], references: [id])
  mapId      Int

  @@index([mapId])
  @@index([panoramaId])
}

model Challenge {
  id            Int             @id @default(autoincrement())
  url           String          @unique
  panorama1     String?
  panorama2     String?
  panorama3     String?
  panorama4     String?
  panorama5     String?
  createdAt     DateTime        @default(now())
  Map           Map             @relation(fields: [mapId], references: [id])
  mapId         Int
  userId        String
  User          User            @relation(fields: [userId], references: [id])
  ChallengeGame ChallengeGame[]

  @@index([mapId])
  @@index([userId])
  @@index([url])
}

model ChallengeGame {
  id           Int       @id @default(autoincrement())
  challengeUrl String
  Challenge    Challenge @relation(fields: [challengeUrl], references: [url])
  userId       String
  User         User      @relation(fields: [userId], references: [id])
  guess1       Json?
  guess2       Json?
  guess3       Json?
  guess4       Json?
  guess5       Json?
  createdAt    DateTime  @default(now())
  currentRound Int       @default(0)

  @@index([challengeUrl])
  @@index([userId])
}

YouTube Video

You can embed YouTube videos in your blog posts.

Tweet

You can embed tweets in your blog posts.

CodePen

You can embed codepens in your blog posts.

Ecosystem - Pen in CSS by Ruphaa

GitHub Gist

You can embed GitHub gists in your blog posts.

Lesser Known HTML Elements

abbr

GIF is a bitmap image format.

sub

H2O

sup

Xn + Yn = Zn

kbd

Press CTRL+ALT+Delete to end the session.

mark

Most salamanders are nocturnal, and hunt for insects, worms, and other small creatures.