Do you wonder if Baserow may be a good fit over Airtable? 🤔 Let’s quickly see what it can be used for, in a quick example with a simple use-case of creating your own self-hosted book collection exposed to the Web. 🌐

How to get yourself a book collection with Baserow?

There are several ways to install Baserow: on Heroku, K8S, with Traefik etc…but I took the shortest and easiest way so far: on my local machine. For that one, I recommend that you install Docker on your favorite OS and double-check that your version of Docker is >19 (using docker --version). Then you can follow this guide to have the whole thing installed in no time.

In short, paste the following in your terminal and you’re set. ☑️

docker run \
  -d \
  --name baserow \
  -e BASEROW_PUBLIC_URL=http://localhost \
  -e BASEROW_EXTRA_ALLOWED_HOSTS=http://localhost \
  -v baserow_data:/baserow/data \
  -p 80:80 \
  -p 443:443 \
  --restart unless-stopped \

In case you’re not an expert in Docker and want to kill everything for any reason (eg: some container using the same port), run the following

docker rmi $(docker images -a --filter=dangling=true -q)

Then go to http://localhost and you’ll be able to create yourself an admin profile.

signup form

Then we’ll be logged in to our local self-hosted version of Baserow! 🎊

dashboard admin

There we can go to the sidebar, click on Create new > From template, on the freshly opened modal let’s go to the right part and select Personal > Book Catalog and click on Use this template.

We will then have our nice prefilled table with some books.

new books table

Now it’s time to create a brand new Nuxt3 project. Be careful by checking the prerequisites and run npx nuxi init nuxt3-baserow-books, then yarn && yarn dev.

You should have a cute presentation of the framework! 🏔️

new Nuxt3 app

Let’s edit the /app.vue file to make use of Nuxt’s file-based routing

  <nuxt-page />

And let’s create a basic /pages/index.vue file to try to authenticate with our freshly created admin account and store our JWT.

<script setup>
const jwt = ref('')

async function getJwtToken() {
  const response = await $fetch('http://localhost/api/user/token-auth/', {
    method: 'POST',
    body: {
      // 👇 it's the email here and not "Your name"
      username: '[email protected]',
      password: 'securepassword', // don't copy my password, it's a secure one
  jwt.value = response.token

await getJwtToken()

  <main class="max-w-3xl mx-auto pt-6 pb-16">
    <pre>{{ jwt }}</pre>

jwt token

Thanks to the JWT, we can get our user’s admin group with the following function.

async function groups() {
  const response = await $fetch('http://localhost/api/groups/', {
    headers: {
      Authorization: `JWT ${jwt.value}`,
  console.log('current group of the user', response[0].users[0].group)

kissu's group

I’m not sure why, but it should give you 74. Still, double-check just to be sure. Later on, we can even hard-code that value into our calls, it will probably not change until the end of our quick book tutorial. 🤓

Now, you may think that we could use the JWT to do any kind of operations regarding our tables. But the documentation states otherwise

These endpoints should never be used to show data on your own website because that would mean you have to expose your credentials or JWT token. They should only be used the make changes in your data. You can publicly expose your data in a safe way by creating a database token

That means that we will need to fetch and use a database token for our next usage.

async function getToken() {
  const response = await $fetch('http://localhost/api/database/tokens/', {
    method: 'POST',
    body: {
      name: 'testing-purposes', // not really that important for us
      group: 74, // the current ID of our user "kissu"
    headers: {
      Authorization: `JWT ${jwt.value}`,
  token.value = response.key

Here we go, we have the key as a response! 🗝️

database token

Going to the dashboard, we can get the ID of the table we’re looking for from the URL, in our case, it is 386 (3rd screenshot). Thanks to this ID and our token, we can get a collection of all the books and their fields from Baserow local instance with the following.

async function fetchBooks() {
  const response = await $fetch('http://localhost/api/database/rows/table/386/?user_field_names=true', {
    headers: {
      Authorization: `Token ${token.value}`,
  books.value = response

We can inspect it thanks to the Vue devtools, to have a more readable output.

collection of books

Now, it’s time to add 2 modules to our nuxt.config.ts file: @nuxtjs/tailwindcss and nuxt-icon. Those 2 are one-liners to have a quick and basic styling thanks to Tailwind and some easy-to-use and quick icons to import. More on the latter in one of my previous posts can be found here. 😉

With that out of the way, we can then proceed into writing a bit of styling + the whole looping on elements. The markup can be a bit messy visually because of Tailwind, so I’ll redirect you to the public github repo for that specific one! 👨‍💻

Here is what the whole thing looks like. 📚 You could now edit your Baserow table and see the changes being effective on your Frontend app (or trigger a webhook for it to rebuild as SSG). Pretty much your own self-hosted version of goodreads.

final result

Some areas for improvement:

  • host it on some server, so that we don’t need to have our Docker server running locally
  • make a cleaner presentation with more info regarding our process, our rating etc…
  • handle the database token more properly than what we have now (basically not re-query a new one each time we arrive on the page)