Build a Pixieset clone with Nhost as your backend!

Bring your own frontend. This tutorial is to demonstrate building the backend, with image uploads and permissions. We will show you how to integrate a frontend (and provide a Vue-based example), but Nhost works with any frontend js framework!

0. You will need

  • 1 × Nhost account (set yours up here)
  • 1 × Frontend (example coming soon, or build your own)

1. Set up your project

Pixieset gives you a password-protected web page showing the image gallery. To make our clone, we will treat each gallery as a user account, so you will be able to log in with a password and see all the images in that gallery. This gives us access to configuring permissions, as each user ID can be used whens setting up access controls.

2. Create the database tables

We will need to create a database to store the image URLs related to each gallery.

Images table:

Set up a table called images with the following fields:

Column Type Default
id UUID gen_random_uuid()
user_id UUID
url Text
liked Boolean false
order Integer
What's that order field? If you want to customise how your images are sorted, you can put a number in here and sort the data by this field when you query the GraphQL endpoint.

You will need to set up a foreign key constraint from the user_id to the id field in the users table.

Add a foreign key constraint between images and users

3. Adding galleries

In this example, we are treating users as image galleries.

Configuring roles

We want to only let users see photos in the current gallery, which will mean we will need to configure the roles in the roles table. We will need to configure the permissions for the default user role. You can follow detailed instructions in the documentation.

'Images' table permissions

Field insert select update delete
id none with check none none
user_id none none none none
url none with check none none
liked none with check with check none
order none with check none none

The select permissions for file, type, and order should check that the image is associated with the current gallery.

{
  "user_id": {
    "_eq": "X-Hasura-User-Id"
  }
}

You may also want to limit the number of images a user can request at once – this would require pagination to ensure the rest of the images in the gallery are loaded afterwards.

Images select permissions for the 'user' account

Update permissions

You should use the same custom check as select for updating only the liked column. This will allow users signed in to a gallery to toggle whether they like an image.

Setting 'update' permissions for 'user' role

Creating new galleries

Since we are using user accounts as galleries, you will need to use the user registration URL to create a new gallery.

Post to https://backend-xxxxxxxx.nhost.io/auth/local/register with:

{
  "email": "<email address>",
  "username": "<url safe path>",
  "password": "<secure password>"
}
You can provide a password-only login to the gallery by using the username as a browser slug, for example my-gallery.com/gallery/my-first-gallery

It is a good idea to disable 'auto-registration' in the Nhost console - this means that you can set up your galleries and add images before people can log in to the gallery.

You can also toggle the active flag in the users table to disable the gallery.

If you would like to allow users to see all the available galleries, you will need to configure a public users view. This will allow all users to see specific rows in the table (in this case, the display_name and gallery_name).

Run the following SQL query in the Hasura console to create a view:

CREATE OR REPLACE VIEW "public"."public_galleries" AS 
  SELECT users.display_name,
    users.gallery_name,
    images.url AS image_url
  FROM (users
    JOIN images ON ((users.featured_image_id = images.id)));
Everyone will be able to see the url, name, and featured image of our gallery

This will create a new view called public_galleries, which can have special permissions. You should then add permissions to the view, to allow all roles to select the display_name , gallery_name, and image_url:

Everyone can select the rows in this view
Why are we creating a view, rather than adding permissions to the users table? We already have permissions configured for the 'user' role on the users table, which only allows users to see one table. We need to make a public_galleries view so everybody can see all the galleries, whether they are logged in or not.

You can now query the public_galleries table for a list of all the galleries:

query GuestGetGalleries {
  public_galleries {
    display_name
    gallery_name
    image_url
  }
}

4. Image upload flow

Note! Only accounts with the admin role can upload images. You can configure permissions to allow other roles (for example a 'manager' role) to upload images too.

We need to make sure an image are only accessible in its own gallery. To do this, we will store each image in a folder, with the gallery id as the folder name. This will let us set up permissions so each gallery can only access images in its own folder.

Min.io browser with one gallery set up

Uploading a single image

In order to upload your photos, you will need to make a POST request to the following endpoint. Below is an example using axios, but you can use any HTTP client. Notice the X-Path header, which is used to specify which gallery the file should be in.

async function upload (file, fileName, galleryId) {
  const formData = new FormData()
  formData.append('file', file)

  const { data } = await axios.post(`${BACKEND_URL}/storage/upload`, formData, {
    headers: {
      'Content-Type': 'multipart/form-data',
      'X-Hasura-Admin-Secret': HASURA_ADMIN_SECRET,
      'X-Path': `/${galleryId}/${fileName}`,
    }
  }

  return data
}
File upload function

You should recieve the following JSON response on a successful upload:

{
    "originalname": "image.jpg",
    "mimetype": "image/jpeg",
    "encoding": "7bit",
    "key": "1ad47d71-1b3a-43b0-98f0-2b1aa16a8bd9/filename.jpg",
    "extension": "jpeg",
    "token": "78defeda-8360-4c14-8eab-f8685bedc3f7"
}
Example response when uploading an image

The image URL will be made of the key with the token as a query parameter:

https://backend-xxxxxxxx.nhost.io/storage/file/1ad47d71-1b3a-43b0-98f0-2b1aa16a8bd9/filename.jpg?token=78defeda-8360-4c14-8eab-f8685bedc3f7

You can now store the image data in the images table in your Nhost database using the following GraphQL mutation:

mutation InsertSingleImage (
  $url: String
  $userId: String
) {
  insert_images (
    objects: {
      url: $url,
      user_id: $userId,
    }
  ) {
    returning {
      id
      url
    }
  }
}
GraphQL mutation to insert an image

Uploading multiple images

Most of the time, you will be importing a folder of images to share. You can only upload one image at a time to the file storage, so you could iterate over the images or use Promise.all to upload them if you are using javascript. Once you have a list of the image upload responses, you can bulk upload them to Hasura. See the documentation on uploading arrays of data.

5. Image download flow

In order to view your images, you will need to download a list of image URLs. Here we are sorting the images by the order field, and also checking if the image has a 'like' or not. We need to get the image id too, so we can add or remove 'likes' from the image.

query GetImages {
  images (
    order_by: {
      order: desc_nulls_last
    }
  ) {
    id
    url
    liked
  }
}

6. Image management

Deleting images

You will need to delete the images from the Postgres database with a GraphQL mutation, and from the backend.

Reordering images

If you have specified an order field in the images table, you can query the images and sort by this field. You can put images with a null order value at the end of the query, so you don't need to order all the images. To change the order, you will need to update this value, and also any conflicting values in the order field.

Image likes

Anyone with a password for the gallery can like or unlike an image, due to the permissions we configured above. Here is an example mutation. You will need to add the image id and a boolean value indicating whether the image is liked or not:

mutation UpdateImageLike (
  $imageId: uuid!
  $likeImage: Boolean!
) {
  update_images (
    _set: {liked: $likeImage},
    where: {id: {_eq: $imageId}},
  ) {
    returning {
      id
      url
      liked
    }
  }
}

Congratulations!

Well done! You now have a password-protected image gallery which is fully extensible and will let you add unlimited features! Here's some ideas for improving your app:

  • Store image metadata, so people can see where and when the picture was taken
  • Add comment functionality
  • Add an image processing CDN, so you can query images at different sizes or with watermarks

This post was written by Max Reynolds. github, dev.to