Deploying Next.js on the edge with SST — Is SST the game-changer it’s claimed to be?

Kolby Sisk
Udacity Eng & Data
Published in
11 min readMay 22, 2023

--

For as far back as I can trace, I’ve been trying to find a suitable solution for hosting Next.js apps at Udacity. It could be mistakenly considered as a straightforward endeavor, but until recently there were no solutions that supported all of the Next.js features, and fit into our existing platform infrastructure.

Next.js Edge Rendering

Udacity has always sought cutting-edge performance and invested in deploying React on the edge early. When React apps were all client-side SPAs, caching the static files on the edge was the optimal solution. But over the years the meta for rendering a React app has evolved from client-side, to server-side, and now to a hybrid model capable of rendering on both the client and server. While server-side rendering has many benefits, it felt like a step back for our existing edge optimized infrastructure. However, using Next.js static export left a lot of features on the table.

Fortunately, in June 2022 the Next.js landscape had a big shift when Next.js introduced their Edge Runtime. Since then there have been a handful of solutions that support edge rendering, but none that fit well in our existing platform infrastructure.

OpenNext & SST

Udacity’s platform infrastructure is built on AWS, so when I came across OpenNext, I was immediately interested.

Modern SSR applications like Next.js are made up of two parts.

1. The infrastructure necessary to run your app. Typically this includes things like serverless functions, an S3 bucket, a CDN, and serverless edge functions.

2. The code, called the adapter, that wraps around your Next.js app and translates function requests and responses to your app.

OpenNext is the adapter.

As the OpenNext doc says, it is only half of the solution. The other half is the infrastructure. Udacity uses Terraform for our infrastructure as code framework, but sadly OpenNext doesn’t support Terraform yet. There is however another solution: SST — an open source framework for deploying serverless apps. SST created OpenNext, and uses it in their NextjsSite Construct. SST claims to be able to deploy a Next.js app to AWS with a single command after a simple setup process.

Does SST work well for a large scale app? That’s what I wanted to learn. The following documents my journey deploying a large scale Next.js app with SST.

Next.js Platform Infrastructure with SST

One of my goals when building out our Next.js platform infrastructure was to provide developers with all of the Next.js features without needing to use non-standard solutions.

The second goal was to hide the implementation details from developers. In a perfect world developers would only need the Next.js documentation, and to follow the team’s git workflow. CI/CD should handle all interactions with SST.

📚 Adding SST to our Next.js project

I recommend following the SST guide to ensure your setup is up to date. I can assure you that I won’t be keeping any of this up to date.😄

In your Next.js project simply run:

This will create the sst.config.ts file where all the magic happens. Your infrastructure is described here as Stacks. Under the hood, SST is using CDK, an AWS infrastructure as code framework. Under that hood, CDK is ultimately creating an AWS CloudFormation Stack — a single unit that scopes all of the AWS resources defined by your code. Deleting a stack, either through AWS or by running the sst remove command, will remove (almost) all of the resources provisioned in the scope of your stack.

📚 Create an IAM User & Policy

Next, you’ll need to create an IAM User & Policy with the required permissions. After following the guide above and using the policy provided, I still ran into some permission problems when trying to deploy. This was an easy fix though. I simply ran deploy, read the error, found the missing permission (ex: ecr:CreateRepository), and updated my policy to include it.

📚 Deploying

Now simply run npx sst deploy, and after a bit of a wait your application will be deployed on AWS resources. Honestly, this is awesome! It was super easy to get a simple POC deployed.

📚 Live Lambda Development?

While setting up this infrastructure, I thought a lot about the environment that would be best to develop in. SST provides a dev environment that can be started with npx sst dev. This environment allows you to debug and test your Lambda functions locally. It will provision and invoke AWS resources by proxying the request from your AWS account to your local machine.

In my opinion, Live Lambda Development is crucial if you’re using SST to deploy services in your tech stack — not to mention the SST Console dashboard is rad! — but is it necessary for developing just a Next.js application?

Since one of my goals was to hide SST implementation details, I chose to avoid the SST dev environment.

🤔 Opinion: A developer shouldn’t need to provision AWS resources to run a dev environment for a Next.js application.

This decision wouldn’t come without its downsides though:

(Foreshadowing)

📚 CI/CD

The final piece of the SST puzzle was to automate managing the environments. We can do this using a CI/CD tool such as Circle CI, Travis CI, or GitHub Actions. CI/CD allows for seamless integration and deployment of code changes. By incorporating a CI/CD tool into our workflow, we can automate the process of managing environments, ensuring consistency and efficiency throughout the software development lifecycle.

CI/CD pipelines are different project-to-project, but here’s what a typical SST pipeline might include:

  1. Spin up a preview environment when a PR is created.
    CI/CD will run npx sst deploy --stage pr-branch-name
  2. Clean up that environment when the PR is closed.
    CI/CD will run npx sst remove --stage pr-branch-name
  3. Deploy to staging when the PR is merged.
    CI/CD will run npx sst deploy --stage staging
  4. Deploy to production after a hold is approved.
    CI/CD will run npx sst deploy --stage prod

The Challenges

🚩 Env Variables Problem

The first problem I ran into was figuring out how to set env variables when SST builds. Most large scale apps will have different env variables for different environments. In our project we use env-cmd to set the env variables when running the app locally, but how should we handle this when deploying to AWS with SST?

💡 Env Variables Solution

This problem was pretty easy to figure out. The SST CLI provides a way to set the environment for each command — they call this a stage. When we run the deploy command we can provide a stage like this: npx sst deploy --stage prod. And in our sst.config we can reference that stage value. Next we can use that stage name + dotenv + a little bit of logic to get the env variables for the given environment. Finally, we throw those into the stack’s environment prop and we’re good to go!

🚩 Secrets Problem

The next problem I ran into was managing secrets. Locally we could use the Next.js standard and add them as env variables without the NEXT_PUBLIC prefix, but how do we provide the secrets to our deployed code.

First attempt to fix the problem — I thought maybe I could just use the environment prop for secrets too. I thought since Vercel is able to prevent env variables not prefixed with NEXT_PUBLIC from being added to the client, maybe SST will too. However, after doing a quick test I learned this is not the case.

⚠️ Do not put secrets in your Nextjs Site Stack’s environment prop. This is only for client env variables.

Second attempt to fix the problem — I found an SST Construct named Config.Secret. This seemed like .. a solution. Sadly it removes the Vercel experience of using process.env.SECRET_NAME, but I’m just looking for a solution at this point.

I followed the doc to set everything up. I ran npx sst secrets set STRIPE_KEY sk_test_abc123, which adds the secret to AWS SSM Parameters.

Then to access the secret in your code you do this:

There is some magic that happens here. At runtime when you import the Config package it fetches and decrypts secrets from SSM, and then adds them to the Config object.

First problem with Config.Secret that I faced is that Config requires running the sst dev environment. This is a result of the magic that happens when you import the Config package. Just by importing the package, it will attempt to add the secret to Config, but without the sst dev environment running you will get an error: Cannot find the SST_APP environment variable. I was able to fix this using some … creative thinking. I conditionally dynamic-imported Config based on an env variable that I set in the SST environment prop — idea being that it would only be imported when sst build runs.

Second problem with Config.Secret that I faced is that it requires adding sst bind to my build command in sst.config. This shouldn’t be a problem, as far as I can tell, and Dax (Core Team at SST) agreed: “sst bind should not have a dep on sst dev running looks like an issue”. But nonetheless, when I ran npx sst deploy I would get this message in the console: Make sure sst dev is running….

💡 Secrets Solution

I had no idea what to try at this point, so I turned to the SST Discord for support. The Discords MVP — AlcaponeYou — recommended I try using the AWS SDK to fetch the secrets from SSM.

After a little refactor, I had it working!

This solution works well, but it does add some DSL that developers will need to learn. It also requires developers to authenticate with AWS locally. But it works!

🔥 Pro Tip: Use 1Password to manage your environment variables!

🚩 Monitoring Problem

The next problem I faced was setting up monitoring with Datadog. I had 2 goals I wanted to achieve for quality monitoring: The first was getting application console logs forwarded from AWS to Datadog. The second was to add end to end tracing in Datadog. Both without adding any latency to my Lambda functions.

Forwarding logs to Datadog seemed like it would be pretty straightforward. Just add the AWS integration to Datadog, configure it to automatically forward logs, and it should just work. And it did…

After setting up the integration I did a test. An API route in my project with a console log. Deployed it, hit it, checked for the log… nothing. Not only did it not show up in Datadog, I couldn’t even find it in CloudWatch. At this point I didn’t know what to think — everything I read said that console logs from Lambda functions should show up in CloudWatch. Back to the Discord where, again, AlcaponeYou comes to the rescue.

Lambda functions log to the region the user is closest to, not the region the Lambda is created in. CloudWatch will only show logs for the region AWS Console is currently using. To see the log, you need to select the right region in AWS Console.

Not only that, but the Datadog integration was configured to only forward logs from a single region, which is why I wasn’t seeing the log in Datadog.

Adding tracing proved to be a far more difficult task than I expected. I found SST’s monitoring guide and figured this would be the solution. Actually, it probably is the solution, but according to the guide every Lambda handler needs to be wrapped in a datadog-lambda-js function. There’s a problem though. Lambda handlers are built by OpenNext by transforming the Next.js build, so I don’t have direct access to them.

So again, I turned to Discord. Unfortunately there was no solution to be found this time. After some engaging discussion it was recommended that I open a feature request in the OpenNext repo that requests a way to wrap the handlers created by OpenNext.

💡 Monitoring Solution

Logging to AWS works out of the box, but keep in mind that you’ll need to change your AWS region to view logs in different regions. I was able to get logs forwarding to Datadog by simply adding the AWS integration. Unfortunately, I still haven’t found a solution for tracing. When I do, I’ll be sure to add an update.

🚩 UPDATE_IN_PROGRESS Problem

After setting up our CI/CD process we quickly noticed a problem. When we create a PR, CI/CD calls sst deploy to create a preview environment for the PR. When a new commit is pushed to the branch, CI/CD will run sst deploy again so we can see the update. The problem occurs when a developer pushes a commit while SST is still deploying a previous commit. When this happens we get the error UPDATE_IN_PROGRESS. This stops the deployment process for the commit, and the preview environment will not be updated.

💡 UPDATE_IN_PROGRESS Solution

Solving this problem proved to be quite challenging. Initially, we considered the possibility of utilizing a workflow queue. However, while this solution would work, we didn’t like the idea of waiting for every commit to finish deploying. The goal is to deploy only the most recent commit, disregarding any others.

Instead, we ended up using CircleCI’s auto-cancel redundant workflows feature to prevent queuing. Next, in our deploy script we check if there is an existing deployment in progress before running the deploy command. Unfortunately SST doesn’t have a command to check the status of a stack, so we turned to the AWS SDK again. Using the CloudFormationClient we check the status of the stack that we want to deploy. If it is currently in an IN_PROGRESS state, we wait until it’s not. Once it’s not, we run sst deploy.

This is pseudo-code. You get the idea.

🙏 Thanks to AlcaponeYou, Dax, and the SST Discord for helping me out. SST has an amazing community of intelligent and kind people. It’s encouraging to know there is an active channel for support.

Conclusion

SST is truly an awesome framework. Not much more to say other than it just works. I did run into a few hiccups, mostly self-induced, but the community was able to provide surprisingly-quick-support when I needed it.

In just a few days I was able to deploy my Next.js app to AWS with all of the Next.js features I wanted. I tested each feature and they all just work.

Here is the list of features provided by OpenNext today:

  • API routes
  • Dynamic routes
  • Static site generation (SSG)
  • Server-side rendering (SSR)
  • Incremental static regeneration (ISR)
  • Middleware
  • Image optimization
  • NextAuth.js
  • Running at edge

Users in the Discord even have some working examples of using the new Next.js App Router with React Server Components. 🙌

I’m ecstatic to finally have a solid solution for hosting our Next.js apps and I’m looking forward to using all the latest and greatest that Next.js has to offer. Thanks SST 🧡

If you want to keep up with the latest in React & Next.js make sure to follow me here and on Twitter @Kolbysisk. 🙏

--

--

Builder of software with a passion for learning. I specialize in web development and user experience design. www.kolbysisk.com