Building a Location-Based App with Appwrite

During my time on the Appwrite discord server, I’ve seen a handful of people ask about how to use Appwrite to create an app using GPS coordinates considering Appwrite doesn’t have geo queries yet. Although I had some ideas for how this could be implemented, I found the opportunity to tackle this problem during the Appwrite Hackathon on Dev with Places. Read on for details on how Places is designed and architected. Overview Places has 2 types of users: Unauthenticated Authenticated Unauthenticated users should be able to: View places on the map View comments on a place View photos on a place Authenticated users should be able to do everything unauthenticated users can do as well as: Add a place Add a comment on a place Add a photo on a place Design Collections Given the use cases defined in the Overview, the following Collections are required: Note: Appwrite’s relationship attributes are experimental and can lead to performance problems. To minimize the number of related documents fetched, we only create one-way many-to-one relationships from Comments and Photos to Places. Places: Stores the places on the map using latitude and longitude Users: Stores the name users publicly since Appwrite’s Users’ data is not publicly accessible Comments: Stores comments for a place $createdAt: ISO 8601 formatted timestamp place: one-way many-to-one relationship with Place user: one-way many-to-one relationship with Users text: comment text Photos: Stores photos metadata for a place $createdAt: ISO 8601 formatted timestamp fileId: $id of the file from the Storage API place: one-way many-to-one relationship with Place user: one-way many-to-one relationship with Users text: description of photo Storage 2 buckets are used for the actual photo files: Photo Uploads: Write only bucket that allows users to submit photos Photos: Read only bucket for the photos exposed to users Functions In order to ensure the integrity of the data such that there are no comments for places that don’t exist and a user can’t impersonate another user, the Comments and Photos collections are not publicly writable. Instead, Appwrite Functions validate and auto-populate attributes before creating the document. In addition, event based functions are used for for Users and Photos. Create Comment and Create Photo Each of these functions accept the following input: placeId text Then, they: Validate placeId exists by fetching for the Place by ID Auto populate user using the user ID of the user who executed the function Create document in corresponding collection Create User This function triggers when a user creates an account. The only thing the function does is creates a User document using the user ID and name. Process Photo Because the Storage API only supports files without any other metadata besides name (which is read-only via JavaScript), a Photos collection is used for the metadata (linking the user and place to the photo). An Appwrite function is used to keep the Photo document and file in sync as well as perform any processing on the photo. The flow for uploading a photo is as follows: Create Photo document Create File providing the Photo document ID as the custom ID The Process Photo function, then, triggers and does the following: Exits if bucket is not the Photo Uploads bucket Get user ID from File read permission Validate user ID matches the Photo document’s user This ensures the user who uploaded the file is the same as the user who created the document. Download the uploaded file from Photo Uploads bucket Resize the image so photos are not unnecessarily large Remove location metadata from the photo to protect user privacy Create a new file in Photos bucket Update the fileId of the existing Photos document with the newly created file ID Delete file from Photo Uploads bucket to prevent the bucket from getting too large Front End The front end is built with the following libraries: React because it’s a widely popular front end library TypeScript because the static type checking greatly improves the developer experience by catching errors early and providing informative code completion MUI because it’s a widely popular UI library and makes it quick and easy to build a nice looking, responsive UI React Leaflet because we’re using React and Leaflet is an open source mobile friendly maps library with lots of plugins Fetching GPS Data The magic that makes working with the GPS data so easy is the React Leaflet library. When people first came to the discord server asking how to query the data, I thought it was necessary to use trigonometry to calculate the North-South-East-West bounds given a center point and a radius. Doing the math was one approach, but I soon learned it was totally unnecessary. It turns out the React Leaflet library exposes a map.getBounds()

May 13, 2025 - 07:00
 0
Building a Location-Based App with Appwrite

Photo by Z on [Unsplash](https://unsplash.com/photos/world-map-with-pins-TrhLCn1abMU)

During my time on the Appwrite discord server, I’ve seen a handful of people ask about how to use Appwrite to create an app using GPS coordinates considering Appwrite doesn’t have geo queries yet. Although I had some ideas for how this could be implemented, I found the opportunity to tackle this problem during the Appwrite Hackathon on Dev with Places. Read on for details on how Places is designed and architected.

Overview

Use Case Diagram

Places has 2 types of users:

  1. Unauthenticated
  2. Authenticated

Unauthenticated users should be able to:

  1. View places on the map
  2. View comments on a place
  3. View photos on a place

Authenticated users should be able to do everything unauthenticated users can do as well as:

  1. Add a place
  2. Add a comment on a place
  3. Add a photo on a place

Design

Collections

Given the use cases defined in the Overview, the following Collections are required:

Class Diagram

Note: Appwrite’s relationship attributes are experimental and can lead to performance problems. To minimize the number of related documents fetched, we only create one-way many-to-one relationships from Comments and Photos to Places.

  • Places: Stores the places on the map using latitude and longitude
  • Users: Stores the name users publicly since Appwrite’s Users’ data is not publicly accessible
  • Comments: Stores comments for a place
    • $createdAt: ISO 8601 formatted timestamp
    • place: one-way many-to-one relationship with Place
    • user: one-way many-to-one relationship with Users
    • text: comment text
  • Photos: Stores photos metadata for a place
    • $createdAt: ISO 8601 formatted timestamp
    • fileId: $id of the file from the Storage API
    • place: one-way many-to-one relationship with Place
    • user: one-way many-to-one relationship with Users
    • text: description of photo

Storage

Storage Diagram

2 buckets are used for the actual photo files:

  1. Photo Uploads: Write only bucket that allows users to submit photos
  2. Photos: Read only bucket for the photos exposed to users

Functions

In order to ensure the integrity of the data such that there are no comments for places that don’t exist and a user can’t impersonate another user, the Comments and Photos collections are not publicly writable. Instead, Appwrite Functions validate and auto-populate attributes before creating the document. In addition, event based functions are used for for Users and Photos.

Create Comment and Create Photo

Each of these functions accept the following input:

  • placeId
  • text

Then, they:

  1. Validate placeId exists by fetching for the Place by ID
  2. Auto populate user using the user ID of the user who executed the function
  3. Create document in corresponding collection

Create User

This function triggers when a user creates an account. The only thing the function does is creates a User document using the user ID and name.

Process Photo

Because the Storage API only supports files without any other metadata besides name (which is read-only via JavaScript), a Photos collection is used for the metadata (linking the user and place to the photo). An Appwrite function is used to keep the Photo document and file in sync as well as perform any processing on the photo. The flow for uploading a photo is as follows:

  1. Create Photo document
  2. Create File providing the Photo document ID as the custom ID

The Process Photo function, then, triggers and does the following:

  1. Exits if bucket is not the Photo Uploads bucket
  2. Get user ID from File read permission
  3. Validate user ID matches the Photo document’s user
    • This ensures the user who uploaded the file is the same as the user who created the document.
  4. Download the uploaded file from Photo Uploads bucket
  5. Resize the image so photos are not unnecessarily large
  6. Remove location metadata from the photo to protect user privacy
  7. Create a new file in Photos bucket
  8. Update the fileId of the existing Photos document with the newly created file ID
  9. Delete file from Photo Uploads bucket to prevent the bucket from getting too large

Front End

The front end is built with the following libraries:

  1. React because it’s a widely popular front end library
  2. TypeScript because the static type checking greatly improves the developer experience by catching errors early and providing informative code completion
  3. MUI because it’s a widely popular UI library and makes it quick and easy to build a nice looking, responsive UI
  4. React Leaflet because we’re using React and Leaflet is an open source mobile friendly maps library with lots of plugins

Fetching GPS Data

The magic that makes working with the GPS data so easy is the React Leaflet library. When people first came to the discord server asking how to query the data, I thought it was necessary to use trigonometry to calculate the North-South-East-West bounds given a center point and a radius.

Doing the math was one approach, but I soon learned it was totally unnecessary. It turns out the React Leaflet library exposes a map.getBounds() function that returns a LatLngBounds object containing North-South-East-West bounds of the current view of the map.

map.getBounds()
{
  "_southWest": {
    "lat": 51.46684144864419,
    "lng": -0.15964508056640628
  },
  "_northEast": {
    "lat": 51.5429188223739,
    "lng": -0.020256042480468753
  }
}

So, the only thing left to do was to build a query to filter based on these bounds:

const b = map.getBounds();
const documentList = await sdk.database.listDocuments<Place>(
  Collections.Places,
  [
    Query.greater(Attributes.Places.Latitude, b.getSouth()),
    Query.lesser(Attributes.Places.Latitude, b.getNorth()),
    Query.greater(Attributes.Places.Longitude, b.getWest()),
    Query.lesser(Attributes.Places.Longitude, b.getEast()),
  ]
);

Gotchas

Panning East and West

Since the earth is round, if you pan West, you’ll eventually end up back in the same place as you started. However, how does this translate to longitude in the app? It turns out, Leaflet doesn’t reset the longitude and just continues to decrement or increment. For example, if you start at (40.71, -74.01) and go West around the world twice to the same location again, you’ll end up at (40.71, -794.01). This causes problems when viewing places and adding a place.

Panning when Viewing a Place

The problem with panning when viewing places is if a place was added at (40.71, -74.01), but the user panned to (40.71, -794.01) they wouldn’t see the place.

To handle this, if a user pans past a certain point, the map will reset to stay within a certain range. When using 0 and 360 as the range, I noticed a problem where the map appeared to glitch when crossing the boundary. This can be improved by using -180 and 180 as the boundary. Since this boundary is over the Pacific Ocean, without any land, the glitch was much less visible and may not even be experienced if the user doesn’t pan across the Pacific Ocean.

Panning when adding a place

The problem with the panning when adding a place is if someone pans to (40.71, -794.01) and adds a place there, when someone views the same location, but at (40.71, -74.01), the place won’t appear.

To solve this problem, I modified the longitude right before sending it to Appwrite. While the longitude was less than -180, add 360. While the longitude was greater than 180, subtract 360.

Conclusion

As you can see, it’s perfectly possible to create a location-based app with Appwrite even without geo queries. Places effectively showcases:

  1. How to design related collections
  2. How to ensure integrity of related data via Appwrite Functions
  3. How to store and fetch location data

Although it wasn’t mentioned in this article, Places also makes use of:

  1. Realtime API to fetch asynchronously processed data
  2. Responsive design to build a Progressive Web App

To see a demo of Places, browse to https://places.pages.dev. For the full source code, check out GitHub.