How to Build a Serverless Custom App with Vercel
Storyblok is the first headless CMS that works for developers & marketers alike.
Storyblok allows you to build custom applications or tools that enhance your editor with your custom functionality. For these custom applications to have access to your Storyblok content and to be able to change content with the Management API, we will first need to authenticate the application with an OAuth flow. You can do that with your own server, for example with frameworks like Express, Koa, Hapi, or Fastify, but there is also the option to do it without a server by making use of serverless functions. This allows you to deploy the application statically and authenticate the application with Storyblok through the serverless function. This tutorial will focus on how to use serverless functions to handle OAuth login flows for your Storyblok application. To enable sessions the tutorial uses the Supabase database to store the OAuth session information across serverless functions. For the OAuth flow, the Grant library with its Vercel handler is used to simplify the OAuth process.
Since this tutorial uses the grant library for the OAuth flow, it requires cookies to be accessible in iframes. This is a problem in Safari since they started blocking third-party cookies this year. In order for the app to be functional in Safari, the Website Tracking setting in Safari must be enabled.
You can find the code for the end result of this tutorial in the Github repository: serverless-custom-app-starter
Creating a new Storyblok App
To create a new custom application you need to be signed up as a Partner. Head into the partner portal, click on Apps {1} and then click the New button {2}. As App type select Sidebar {3} and click Create {4}.
To get started we will clone the workflow app starter template: github.com/storyblok/storyblok-workflow-app. This is a basic Nuxt application that will show our logged in Storyblok user and workflow stages.
Let's also already install all the dependencies necessary for this tutorial
Since our OAuth flow will be handled through serverless functions we can remove the @storyblok/nuxt-auth
module in the nuxt.config.js
file.
Creating a Serverless Function
To authenticate our application, we will need a serverless function that handles the OAuth authentication. You can create such functions on different providers like Vercel, Netlify, AWS Lambda, Azure Function, or Google Cloud Functions. We will choose Vercel for this tutorial, but any other provider should work similarly.
To create serverless functions with Vercel, we have to create an api
folder in our project root. If you're using Next.js the api
folder is located in /pages/api
. You can also follow the detailed Vercel docs for more information.
Your first serverless function
Let’s start by creating a Hello World example. Create a file api/hello-world.js
with the following content:
Now let’s deploy this first stage of the project to Vercel. Run the following commands:
If you don't have a project connected yet, Vercel will ask you to set up a new project. After deploying the repository, there is no connection to Storyblok set up yet. But we can already call the API path of the serverless function, we just created and should get a response.
Open https://your-app.name.vercel.app/api/hello-world?name=Homer
and you should see the following response:
Great job, that's already your first serverless deployed function!
Creating a Serverless OAuth Function
The next step is to create a function that can authenticate the application with Storyblok. We will make use of the grant package to do that.
To work with grant, we will need a few different files. Let's create a folder auth
in our root directory to store our configuration and utility files for the OAuth flow. Inside the auth
folder create a file auth/grantconfig.js
. This file stores the configuration for our grant client with the Storyblok OAuth URLs and tokens.
auth/grantconfig.js
When looking at the configuration, you can see that there are a few environment variables. You can store your required environment variables in the .env-template
file by renaming it to .env
Let's get the correct tokens and ids from Storyblok. Head into the partner portal and under Apps {2} and click on your app name. There you will find the client id {3} and client secret {4}.
For the Live URLs, we need to add our Vercel deployment URL with the /connect/storyblok
path for the URL to your app {5} like htttps://my-app.vercel.app/connect/storyblok
.
We also need to add the OAuth callback URL {6} with the /connect/storyblok/callback/
path like htttps://my-app.vercel.app/connect/storyblok/callback
If you want to keep a local version running you can add a local ngrok tunnel URL under development {7}. This allows you to access your localhost app when opening the app with a ?dev=1
parameter.
First, we need to set all these variables on Vercel, so our application can authenticate with Storyblok. Open your Vercel Dashboard by signing in at vercel.com. Open your current project {1} and click on Settings {2} and then on Environment Variables {3}. Add a new secret variable, with the matching name of the Starter, e.g. STORYBLOK_CLIENT_ID
, then create a new reference name and enter the client id we just retrieved from Storyblok {5}. Do the same for the STORYBLOK_CLIENT_SECRET
. We will also need to add a BASE_URL
variable, which can be Plaintext. The BASE_URL
should be URL of your Vercel deployment, e.g. https://my-custom-app.vercel.app
Once we have our environment variables set, we need to create a vercel.json
file in the root of our project to redirect the callback routes to the correct serverless functions.
vercel.json
We want to redirect the connect/storyblok
URL to our serverless function in the api/grant.js
file and the connect/storyblok/callback
to the api/callback.js
function. Next, we need to create those two serverless function for the authentication.
grant.js
Inside the api
folder create a grant.js
file with the following code. First, we import the grant package and the grantconfig
file. We set up the grant library with its Vercel handler with the following configuration. Grant then automatically redirects to our api/callback
function, since we configured that in the callback
property in the grantconfig.js
file.
api/grant.js
callback.js
The callback function will be called when the app authentication was successful with grant and always when we open the app, even if the access was already granted. When the user opens the app in Storyblok, Storyblok will call the OAuth callback URL and send a code
and space_id
parameter like described in the app authentication docs. So our callback needs to take this code parameter and request the access_token
and the refresh token
. If you haven't set up a session store, the default cookie store is used. This is not ideal in terms of security, which is why we will also set up a session store at the end of this tutorial. But let's continue without the session store for now. In our callback function, we will read the code
and space_id
parameters from our req.query
object, that Storyblok sends when calling the OAuth callback. Then we will make use of a helper function getTokenFromCode
to get an access_token
and refresh_token
from Storyblok. Lastly, we will redirect to our index path /
with a space_id
parameter, so our application will know which space to use.
Since we don't have the getTokenFromCode
function yet, we need to create the helper function file util.js
inside the auth
folder with the following code. This sends a post request with the code parameter to Storyblok to retrieve the access_token
and refresh_token
for accessing the Storyblok API.
auth/util.js
auth/util.js
With this set up we should already be able to authenticate our app. Deploy these functions and the Nuxt app by running npm run generate && vercel
. When your app is deployed, open Storyblok and install the App if you haven't already. Then open the App from the Sidebar. When you open the app the first time, you will be asked if you want to give access to this app.
When you approved the application, you should see the Nuxt application loaded inside Storyblok inside an Iframe. Since we haven't set up any loading of content yet in a serverless function, the app will not be able to load the Storyblok content just yet.
Setting up a Session
In order to store the code
and access_tokens
that we got in the api/callback.js
serverless function, we need to set up a session store with a database, so other functions in our app also have access to those tokens and can request content from Storyblok. As a database, you can use Firebase or any other database that allows selecting, updating, or deleting of content. For this tutorial we chose Supabase. You can also check grants firebase store implementation if you would rather use Firebase. First, we need to sign up for a free account for app.supabase.io. Once you're logged in, you can create a new project {1} and then go into the table editor {2}. There we create a new table {3} called session_storage
{4}. Do not include the primary key {5} for now, since we will create it in the next step.
Now in our table {1}, we want to add a new column {2} with the name sid
{3} of type varchar
{4}. This will be our primary key {5}.
Then add another column {1} with the name json
{2} of type json
{3} and click save. That's all the setup we need in the database. Now the last thing is to retrieve our Supabase client id and secret.
To retrieve the access keys, go into Settings {1} and then click on API {2}. There you will find your Supabase URL {3} and an anon public key {4}
We need to add these environment variables in Vercels like we did before with the STORYBLOK_CLIENT_ID
.
Once you added the variables to Vercel under Settings {1}, Environment Variables {2}, you should now have 5 environment variables set up {3}.
store.js
The next step is to add the session_storage
store to grant. Inside of the auth
folder add a new store.js
file with the following code:
auth/store.js
We will also need to install the Supabase client by running this command:
Finally, we adapt auth/grantconfig.js
to use transport-session
and our new store. Open the file and use the following settings:
auth/grantconfig.js
Using the Store in the Serverless Functions
After retrieving the access_token
, the next step is to store the token in our session. Let's open the api/callback.js
file and adapt it. We will set up a session, which retrieves our session cookie and can save content to our Supabase database with the cookie name. If there is already an entry with content in the database, we add the space_id
, code
, access_token
and refresh_token
to the existing entry. Finally, we write to the database with the session.set()
function. With the session.remove()
function, we could remove the entire session from the database, when we don't need it anymore.
api/callback.js
Let's deploy these changes by running vercel
in our command line. If we access our application and reload, there should already be some data written into our Supabase store. Let's see what was written inside our session in app.supabase.io table. The sid will be the name of the grant cookie and the JSON, will be the JSON returned from grant plus the extra entries (space_id
, code
, ... ) from Storyblok we just set with the session.set()
function. If we inspect the JSON field in our session_storage
table {1}, we can see the data that was written by grant, along with the data that we set in the api/callback.js
, the space_id, application_code, access_token and refresh_token.
With the session set up, we can use this session in other serverless functions. If we take a look at the pages/index.vue
file we can see that the application is requesting data in the loadStories
function by calling a GET request to /auth/spaces/${this.spaceId}/stories
.
pages/index.vue
So we need to set up the /auth/
routes with a serverless function that loads and returns content from Storyblok. We set up the route in vercel.json
in the root of our project, so every request that goes to /auth/...
is redirected to our api/storyblok.js
serverless function.
vercel.json
And then create a storyblok.js
file inside of the api
folder to request the content. In the getEndpointUrl
function, we remove the/auth/
part of the URLs and fill in the space_id from the session if it's not yet filled. If the user is requested, we send the request to the oauth/user_info
endpoint, like described in the app auth docs. Then we check if the session contains the access_token
and create a Storyblok Management API Client with the OAuth token. Finally, we request the data from Storyblok and return it as JSON to the client.
api/storyblok.js
Now if we deploy the functions again by running the vercel
command in the command line and then reload our app, we should see that it's working because our app is already showing the logged-in user {1}.
Resource | Link |
---|---|
Github Repository for this tutorial | github.com/storyblok/serverless-custom-app-starter |