Building an Internal Dashboard for 47 Places

We're growing over at 47 places. When I launched the site back in February 2023 All the data was stored in a json file. To add a place I needed to jump into the file on vscode and edit it. The push the changes up to GitHub to trigger a redeploy of the site. It wasn't elegant but it worked.

Since then the dashboard has evolved and this article covers what it took to get to the current V0.1 of the dashboard that we're using now.

When Ly joined me, I taught her how to add places into the json file but clearly that wasn't very efficient because to date she only added a grand total of 1 place ๐Ÿ’€. It also was a little dangerous having her edit files that could potentially break the site. And that possibility also added overhead because for each change she made I had to go through it line by line to ensure that everything was formatted correctly and no unintended changes were made.

I then moved things over to Supabase. Life became a little bit better with the supabase dashboard. But that only applied to me. To add a single place you needed to edit multiple tables and add images into a storage bucket. Again, not the best UX. We actually ended up not even adding a single place at this point.

Then more recently, about 3 weeks ago, I started working on a custom dashboard that would make adding an managing places as simple as filling in a form.

The stack of choice was a Next.js 13 app using the `app` dir and PostgreSQL (supabase).

The initial requirements were we needed to be able to:

  1. View all the places we have.
  2. control which places are published and which ones are in draft mode
  3. manage data about places
  4. add new places
  5. only allow authorized people write access to place data

I had seen on the interwebs that supabase row level security (RLS) policies were really good for "authorization" and wanted to use them.

So step 1 was disconnecting the main site 47 Places from the database. I don't know how to do development locally with supabase and run migrations successfully and didn't want to figure it out just yet. So I needed the main site to stay up as I made changes to the database schema and developed the dashboard.

Previously, I had RLS off meaning that anyone who had access to the site via api could do anything. Moving forward we can't have that so I switched on RLS.

But now the main site needed read access to be able to display the site to the general public. So I figured out the way to allow read access to everyone and switched that on.

With that setup, I then went ahead and started building the dashboard. I used the supabase guide Supabase Auth with the Next.js App Router to figure that out. The youtube video playlist accompanying the docs was super useful too.

Combining auth + read-only access gave me:

Okay cool, we can now see all the places when logged into the dashboard. Now I needed to make it presentable. I created a basic table and made the sign-out button an actual button that's easier to click on.

I spent a stupid amount of time making the table have rounded corners cause apparently, it's easier to make wooden tables that have rounded corners than html tables with rounded corners.


My solution was to wrap the whole table in a div and then make the div have rounded corners cause any time I tried applying the corners to the table itself, some weird behaviors popped up. But that's besides the point. Onward!

I also had a little hover state going on there to make it more fun.

As I worked on this, the db schema evolved to meet some upcoming requirements. Like previously, there was no way for us to have places that were in draft mode. So I added a colum with a boolean that would tell me if a place was published or not. And we that we had drafts folks. Ly and I can add a place that we don't have complete information on, spend a few days to gather the info, then once it's all good publish the place which will then tell the main 47 Places site to show it to the public.

Once I got that working. I now needed to load up a page for each place. I used dynamic routes to pass the slug of each page to a route that would then house the forms to manage the place data. I passed down the slug of the place to the dynamic route and displayed it. Light work ๐Ÿ˜ค.

Now here is where the heavy lifting started. Remember, I switched on Row Level Security and then set a policy that allows Read Only access. But now I needed to create a page that was able to Update place data.

After a little research, I came across the idea that I could control access to my database using custom claims. Custom claims are little pieces of information that you can embed in a users authentication token that can then be checked at the database level to gate keep actions like updating and deleting data. The beautiful thing about custom claims is because the auth information is stored in the auth token, I don't need to query the database to get the user's role meaning that for each database call, that's one less query. Saving me money in the long run. Lol, me who's on the free tier with a grand total of 2 users. Anyways.

Mark Burggraf from the Supabase team did a magnificent write up on Supabase Custom Claims. And from there I was able to implement a policy that made it possible for Ly and I to everything. That is Read, Insert, Update and Delete from our tables in the db.

So now:

  1. Everyone can Read (so that the 47 places site works as usual)
  2. Only Ly and I can Insert, Update and Delete stuff.


To test this, I created a button on the front end to toggle the published state of an article.

Moving it from published:true to published: false and vice versa. The idea here was, if I can get that small part of the ui working and interacting with the database properly I would have essentially made sure that I had setup RLS properly and that I could now go on to build the rest of the form that we would use to manage places

And after a few tries, I was able to confirm that ly and I could update the publish state, but any other user who tried to got an unauthorized error.

From here it all becomes very frontend developer-ey so I'll just do a quick rundown for the one or two who might be interested in that kind thing.

  1. Built the form to manage places with Formik
  2. Separated the place data into two:
  3. place data: stuff like location, google maps link, etc...
  4. place details data: stuff like booking details, price, open times etc...
  5. Created routes to handle updating the two groups of data. Learnt how to use supabase Upserts. Thanks Mungai.Learnt how upsert uses on-conflict to figure out how to handle updating rows in the db vs creating new rows.
  6. Used alerts to give success or error messages. Fast shipping says that we don't have time to build custom toast components.
  7. Did a lot of refactoring as my components changed shape.
  8. Added the add a new place button and the accompanying page with the add a new place form
  9. Pushed to production and the requests broke
  10. Learnt that PUT requests are a little problematic in prod. It has something to do with the fetch web api option mode so I switched to POST and called it a day
  11. Fixed other small bugs like the creation of new place details data every time instead of updating existing place details data (would have been crazy to fix if I didn't catch this๐Ÿ’€)

There's probably a few things I've missed but that covers the big parts. In the end here's a little demo of what v0.1 of the 47 places dashboard looks like:

So now we can quickly add places and the details about the places, then store them as drafts, fact check them and when everything looks good, publish places for you on 47 Places dot com.

And that's it for today folks ๐Ÿค .