Zernonia

Zernonia

Supabase Storage CDN and Transformation with Serverless function (Unofficial)

Supabase Storage CDN and Transformation with Serverless function (Unofficial)

Subscribe to my newsletter and never miss my upcoming articles

This tutorial is just a temporary alternative, while Supabase team is working hard to ship more and more features, where Storage CDN and Transformation is in their pipeline.

image.png

source: https://supabase.io/storage

ā­šŸŽ‰šŸŽŠ

On that note, congratulations Supabase team on raising $30M as an open source backend-as-a-service startup!!


Get started!

Take note āš :

  1. We will be using Vercel Serverless function to make this magic happens, the code might be different but the logic is the same.
  2. We will be serving and transforming Public bucket only. If you wish to see how to implement these magic with Supabase Auth for RLS, remember to follow me for more tutorial.

With that said, we will go through just a few simple steps to implement this magic on our Supabase Storage's images.

1. Getting Image bucket & name

We will be using bucket_name and file_name variable to call the serverless function, instead of the full public url. If not, your image link would be super-duper long, and unneccessary.

Here are some of the way you could prepare the bucket_name and/or file_name.

1.If you are allowing your users to upload static content to Public bucket, then take note of the bucket_name and file_name users keyed-in.

const bucket_name = 'static'    // your bucket name
const file_name = 'avatar.png'    // name for the file

const avatarFile = event.target.files[0]
const { data, error } = await supabase
  .storage
  .from('avatars')
  .upload(`${ bucket_name }/${ file_name }`, avatarFile, {
    cacheControl: '3600',
    upsert: false
  })

2.You can get use from.list() to retrieve the images you want in a bucket.

In this case, I will simply just list everything in my bucket_name bucket.

const { data, error } = await supabase.storage.from(bucket_name).list()
const file_names = data.map(item => item.names)

3.If you already have the public URL fetch together in another query, with link such as https://asdasaeipbvsvnr.supabase.co/storage/v1/object/public/static/avatar.png, then you can quickly get the bucket_name and file_name using

let link = 'https://asdasaeipbvsvnr.supabase.co/storage/v1/object/public/static/avatar.png'
let [ bucket_name, file_name ] = link.split('public/')[1].split('/')

Alright, now we have our appropriate variable, we can start construct our new link to slot into <img> tag! šŸ™Œ

Because we are using Vercel serverless function, we need to wrap our img url around the api route.

If you are using Vercel for your current project, you can simply use the following code to generate new link for your <img>

const params = new URLSearchParams({
    f: file_name,
    b: bucket_name,
    // params we haven't mentioned...
})
const new_link =  window.location.origin + "/api/resize?" + params.toString()

If you are not using, Vercel as deployment, you can easily forked this repo that I created for this tutorial. You just have to follow the steps and setup your .env on Vercel. If you wanted to learn more on how this function works, continue follow along!

Serverless function

This part is where the magic happens, let's create a new file in your project root, named api/resize.ts (be default Vercel will convert all files in api folder into serverless function).

Then, you have to install a few packages

I'm using yarn and typescript, you can use npm, and plain Javascript if you like

yarn add sharp axios
yarn add -D @vercel/node @types/sharp

Next, create a basic function as such:

import { VercelRequest, VercelResponse } from "@vercel/node"
import sharp from "sharp"
import axios from "axios"

export default async (req: VercelRequest, res: VercelResponse) => {
  res.end("Hi")
}

To quickly test out the api, run vercel dev to spin up Vercel Development Server. Then visit http://localhost:3000/api/resize, it should response with 'Hi'.

After that , replace the function with this:

export default async (req: VercelRequest, res: VercelResponse) => {
  const {
    query: { w, h, f, b, q },
  } = req

  // this tricks to deconstruct all the nested query into it's own variable.
  // parameters
  //   w: width   (pixel)
  //   h: height   (pixel)
  //   f: file_name
  //   b: bucket_name
  //   q: quality  (0 to 100)

  res.end("Hi")
}

Remember we have created a new link for the image just now?? Now we have to construct it back to original url, then convert it to Buffer. Thankfully, axios make this job so easy.

export default async (req: VercelRequest, res: VercelResponse) => {
   ...

  // check if `bucket_name` and `file_name` are available, else return error
  if (f && b) {
    const url = `${ process.env.SUPABASE_URL }/storage/v1/object/public/${ b }/${ f }`
    const buffer = (await axios({ url, responseType: "arraybuffer" })).data as Buffer

     res.statusCode = 200
     res.setHeader("Content-Type", "image/png")
     res.end(buffer)
  } else {
    res.statusCode = 500
    res.setHeader("Content-Type", "text/html")
    res.end("<h1>Internal Error</h1><p>Sorry, there was a problem</p>")
  }
}

You can now test this api endpoint as such http://localhost:3000/api/resize?f=avatar.png&b=static (Of course you need to have the image in your bucket) to see if your image is generated. If it works, let continue on the longest script in this tutorial, where we use sharp to transfrom our image to the desire width, height or quality.

export default async (req: VercelRequest, res: VercelResponse) => {
   ...

  if (f && b) {
     ...

   // here we create a new_params object to convert string to number, and also set default value
    const new_params  = {
      w: +w || 800,  // set default 800px
      h: +h || null,    // set to null if not provided, so that Sharp automatically keep the aspect ratio
      q: +q || 80      // set default 80% quality
    }

    // here's where the Transformation happens
    sharp(buffer)
      .resize(new_params.w, new_params.h)
      .jpeg({quality: new_params.q})     // change to .webp() if you want to serve as webp
      .toBuffer()
      .then((data) => {
        // here's where set the cache
        // I set to cache the media for 1 week, 60seconds * 60minutes * 24hours * 7days
        // remove setHeader('Cache-Control') if you wish not to cache it
        res.statusCode = 200
        res.setHeader("Cache-Control", `public, immutable, no-transform, s-maxage=604800, max-age=604800`)  
        res.setHeader("Content-Type", "image/jpeg")
        res.end(data)
      })

  } else {
    res.statusCode = 500
    res.setHeader("Content-Type", "text/html")
    res.end("<h1>Internal Error</h1><p>Sorry, there was a problem</p>")
  }
}

That's it! Just a few line of codes and you have your own CDN and Transformation for Supabase Storage ready to go!!!! But! Don't forget the new_link we created at our frontend.

Lastly!

This is the last step for this tutorial, we generated new_link previously, but now it is ready to add more parameter.

// Set a few width so that cache is more efficient, and need not to create so many cache when different browser visit your website.
let windowWidth = 0
if(window.innerWidth >= 1200) {
  windowWidth = 1000
} else if (window.innerWidth >= 800) {
  windowWidth = 800
} else {
  windowWidth = 600
}

const params = new URLSearchParams({
    f: file_name,
    b: bucket_name,
    w: windowWidth,
    h: null,    // set to null to keep image's aspect ratio
    q: 0.8      
})

const new_link =  window.location.origin + "/api/resize?" + params.toString()

// set the src to new link
document.getElementById("myImg").src = new_link;

And we are DONE!!! All source code for this tutorial can be found here!

Showcase

image.png

Check out Made With Supabase, and inspect the <img>, you will see the similar code there, with slight minor change.

What is Made With Supabase? It is a collection of projects that made with Supabase! Feel free to submit your Supabase project, share the awesome-ness of Supabase with the world!

Before you go

If you find this tutorial helpful, and wish to lean more, then follow me here, and follow my Twitter!

Ā 
Share this