Supabase Storage CDN and Transformation with Serverless function (Unofficial)
Tags: javascript, supabase, serverless, webdev
Published: 2021-09-11 02:47:45

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](https://cdn.hashnode.com/res/hashnode/image/upload/v1631279067782/63nFl5KeN.png)
> source: [https://supabase.io/storage](https://supabase.io/storage)

## ⭐🎉🎊
On that note, congratulations Supabase team on raising [$30M](https://techcrunch.com/2021/09/09/supabase-raises-30m-for-its-open-source-insta-backend/) 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.

```javascript
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()](https://supabase.io/docs/reference/javascript/storage-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.
```javascript
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

```javascript
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! 🙌

## 2. Construct new link

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>`
```javascript
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](https://github.com/zernonia/supabase-cdn-transformation) 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

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

Next, create a basic function as such: 
```javascript
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:
```javascript
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.

```javascript
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.

```javascript
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.
```javascript
// 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](https://github.com/zernonia/supabase-cdn-transformation)!


## Showcase


[![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1631288642681/vmrXvTB30.png)](https://www.madewithsupabase.com)

Check out  [Made With Supabase](https://www.madewithsupabase.com/), 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!

{% twitter 1428379387863306241 %}