Stacktape
Stacktape


Next.js Website



The nextjs-website resource is designed to deploy Next.js applications to AWS. It leverages serverless technologies like AWS Lambda and Lambda@Edge to run your application efficiently. Stacktape automatically provisions and configures all the necessary AWS resources, so you can focus on writing your application code.

resources:
web:
type: nextjs-web
properties:
appDirectory: ./

Basic use of nextjs-web resource

How it Works

Stacktape uses the open-source project OpenNEXT to build your Next.js application, making it compatible with AWS Lambda. Behind the scenes, Stacktape provisions a set of AWS resources to host your app:

ComponentTypeDescription
Server functionLambda or Lambda@EdgeRuns your Next.js server logic.
Asset BucketS3 bucketStores static assets (like images, CSS) and cached assets.
Image optimization functionLambdaHandles on-the-fly image optimization for the Next.js <Image> component.
CloudFront distributionCDNA CDN that caches and distributes your website content globally for low latency.
Revalidation functionLambdaHandles content revalidation when using Incremental Static Regeneration (ISR).
Revalidation queueSQS QueueA message queue used by the server function to trigger the revalidation function.
Revalidation tableDynamoDB tableA NoSQL database that supports Next.js revalidateTag for on-demand revalidation.
Revalidation inserterLambdaPopulates the revalidation table during deployment.
Warmer function (Optional)LambdaKeeps the server function warm to prevent cold starts (not available for Lambda@Edge).

For more information refer to OpenNext docs.

Architecture overview (credit: OpenNext)

Edge Deployments

To deploy your Next.js application to the edge for lower latency, you can use Lambda@Edge by setting the useEdgeLambda property to true. This deploys your server function to CDN locations around the world.

  • When using edge lambda functions, your server function is executed at the regional cache locations across the globe. This means that the function is executed closer to your user, which results in reduced response time.
  • Downside of using lambda@edge are:
    • slower deployment times compared to using regular lambda,
    • you cannot use warmer (see warmServerInstances property) to keep server function warm.
resources:
web:
type: nextjs-web
properties:
appDirectory: ./
useEdgeLambda: true

Performance Optimization

Mitigating Cold Starts

To mitigate cold starts and ensure your server function responds quickly, you can enable a warmer. This keeps a specified number of function instances active. By default, the warmer is disabled. This feature is not available when using useEdgeLambda.

  • Server function may experience performance issues due to Lambda cold starts (slower response times).
  • To mitigate cold start effects, the server function can be invoked periodically by warmer function. This is done using warmer function which periodically invokes server function to keep it warm.
  • By default, warmer is disabled.

At the moment warmer does not work when using lambda@edge for server function (see useEdgeLambda property)

resources:
web:
type: nextjs-web
properties:
appDirectory: ./
warmServerInstances: 5

Security

Web Application Firewall

You can protect your Next.js application from common web exploits by attaching a web-app-firewall. This can help prevent attacks that could affect your application's availability, compromise security, or consume excessive resources.

resources:
web:
type: nextjs-web
properties:
appDirectory: ./
useFirewall: waf
waf:
type: web-app-firewall
properties:
scope: cdn

Environment variables

You can inject environment variables into the server function. These variables are available at build time and runtime.

Most commonly used types of environment variables:

  • Static - string, number or boolean (will be stringified).
  • Result of a custom directive.
  • Referenced property of another resource (using $ResourceParam directive). To learn more, refer to referencing parameters guide. If you are using environment variables to inject information about resources into your script, see also property connectTo which simplifies this process.
  • Value of a secret (using $Secret directive).
resources:
web:
type: nextjs-web
properties:
appDirectory: ./
environment:
- name: STATIC_ENV_VAR
value: my-env-var
- name: DYNAMICALLY_SET_ENV_VAR
value: $MyCustomDirective('input-for-my-directive')
- name: DB_HOST
value: $ResourceParam('myDatabase', 'host')
- name: DB_PASSWORD
value: $Secret('dbSecret.password')

Custom Domain Names

You can configure custom domains for your Next.js application. Stacktape will automatically provision an SSL certificate and configure DNS routing.

Stacktape allows you to connect your custom domain names to some of your resources (Web Service, Nextjs web, HTTP API Gateways, Application Load Balancers and Buckets with CDNs).

Connecting a custom domain to the resource does 2 things:

  • Creates DNS records:
    • If you use your custom domain with a resource, Stacktape automatically creates a DNS record (during deploy) pointing the specified domain name to the resource.
  • Adds TLS certificates
    • If the origin resource (HTTP API Gateway, Application Load Balancer or CDN) uses HTTPS protocol, Stacktape takes care of issuing and attaching correct (free, AWS-managed) certificate to the resource. This means, you do not have to deal with TLS termination as it is handled by the connected resource.
    • If you want to use your own certificates, you can configure customCertificateArns.

To manage a custom domain, it first needs to be added to your AWS account. This means that a hosted zone (collection of records managed together for a given domain) for your domain exists in your AWS account and your domain registrar's name servers are pointing to it. To learn more, refer to Adding a domain guide.

DomainConfiguration  API reference
domainName
Required
customCertificateArn
disableDnsRecordCreation
resources:
web:
type: nextjs-web
properties:
appDirectory: ./
customDomains:
- domainName: mydomain.com

Accessing Other Resources

By default, most AWS resources are not allowed to communicate with each other. This is a security measure to ensure resource isolation. To allow your Next.js application to access other resources, you need to grant permissions explicitly.

  • IAM Permissions: Access to most AWS services is controlled by IAM (Identity and Access Management). Stacktape automatically manages the necessary permissions for the resources it creates.
  • Relational Databases: Relational Databases use their own access control (a connection string with a username and password) and are not managed by IAM. They are accessible by default unless you configure more restrictive access controls.

If your Next.js application needs to communicate with other resources in your stack, you can grant permissions in two ways:

Using connectTo

The connectTo property is a simplified way to grant access to other Stacktape-managed resources. Stacktape will automatically configure the required IAM permissions and inject connection details as environment variables into your function.

resources:
web:
type: nextjs-web
properties:
appDirectory: ./
connectTo:
- myDatabase
myDatabase:
type: relational-database
properties:
engine:
type: aurora-postgresql
properties:
instances:
- instanceSize: db.t4g.medium

By referencing resources (or services) in connectTo list, Stacktape automatically:

  • configures correct compute resource's IAM role permissions if needed
  • sets up correct security group rules to allow access if needed
  • injects relevant environment variables containing information about resource you are connecting to into the compute resource's runtime
    • names of environment variables use upper-snake-case and are in form STP_[RESOURCE_NAME]_[VARIABLE_NAME],
    • examples: STP_MY_DATABASE_CONNECTION_STRING or STP_MY_EVENT_BUS_ARN,
    • list of injected variables for each resource type can be seen below.

Granted permissions and injected environment variables are different depending on resource type:


Bucket

  • Permissions:
    • list objects in a bucket
    • create / get / delete / tag object in a bucket
  • Injected env variables: NAME, ARN

DynamoDB table

  • Permissions:
    • get / put / update / delete item in a table
    • scan / query a table
    • describe table stream
  • Injected env variables: NAME, ARN, STREAM_ARN

MongoDB Atlas cluster

  • Permissions:
    • Allows connection to a cluster with accessibilityMode set to scoping-workloads-in-vpc. To learn more about MongoDB Atlas clusters accessibility modes, refer to MongoDB Atlas cluster docs.
    • Creates access "user" associated with compute resource's role to allow for secure credential-less access to the the cluster
  • Injected env variables: CONNECTION_STRING

Relational(SQL) database

  • Permissions:
    • Allows connection to a relational database with accessibilityMode set to scoping-workloads-in-vpc. To learn more about relational database accessibility modes, refer to Relational databases docs.
  • Injected env variables: CONNECTION_STRING, JDBC_CONNECTION_STRING, HOST, PORT (in case of aurora multi instance cluster additionally: READER_CONNECTION_STRING, READER_JDBC_CONNECTION_STRING, READER_HOST)

Redis cluster

  • Permissions:
    • Allows connection to a redis cluster with accessibilityMode set to scoping-workloads-in-vpc. To learn more about redis cluster accessibility modes, refer to Redis clusters docs.
  • Injected env variables: HOST, READER_HOST, PORT

Event bus

  • Permissions:
    • publish events to the specified Event bus
  • Injected env variables: ARN

Function

  • Permissions:
    • invoke the specified function
    • invoke the specified function via url (if lambda has URL enabled)
  • Injected env variables: ARN

Batch job

  • Permissions:
    • submit batch-job instance into batch-job queue
    • list submitted job instances in a batch-job queue
    • describe / terminate a batch-job instance
    • list executions of state machine which executes the batch-job according to its strategy
    • start / terminate execution of a state machine which executes the batch-job according to its strategy
  • Injected env variables: JOB_DEFINITION_ARN, STATE_MACHINE_ARN

User auth pool

  • Permissions:
    • full control over the user pool (cognito-idp:*)
    • for more information about allowed methods refer to AWS docs
  • Injected env variables: ID, CLIENT_ID, ARN


SNS Topic

  • Permissions:
    • confirm/list subscriptions of the topic
    • publish/subscribe to the topic
    • unsubscribe from the topic
  • Injected env variables: ARN, NAME


SQS Queue

  • Permissions:
    • send/receive/delete message
    • change visibility of message
    • purge queue
  • Injected env variables: ARN, NAME, URL

Upstash Kafka topic

  • Injected env variables: TOPIC_NAME, TOPIC_ID, USERNAME, PASSWORD, TCP_ENDPOINT, REST_URL

Upstash Redis

  • Injected env variables: HOST, PORT, PASSWORD, REST_TOKEN, REST_URL, REDIS_URL

Private service

  • Injected env variables: ADDRESS

aws:ses(Macro)

  • Permissions:
    • gives full permissions to aws ses (ses:*).
    • for more information about allowed methods refer to AWS docs

Using iamRoleStatements

For more granular control, you can provide raw IAM role statements. This allows you to define precise permissions for accessing any AWS resource, including those not managed by Stacktape.

resources:
web:
type: nextjs-web
properties:
appDirectory: ./
iamRoleStatements:
- Resource:
- $CfResourceParam('NotificationTopic', 'Arn')
Effect: 'Allow'
Action:
- 'sns:Publish'
cloudformationResources:
NotificationTopic:
Type: AWS::SNS::Topic

Known Issues

Here are some known issues and workarounds when deploying Next.js applications with Stacktape.

Bundle Size

Next.js can sometimes include development dependencies in the production bundle, which significantly increases its size. To prevent this, add the following configuration to your next.config.js file:

// next.config.js
module.exports = {
outputFileTracingExcludes: {
'*': [
'@swc/core',
'esbuild',
'uglify-js',
'watchpack',
'webassemblyjs',
'sharp'
],
},
}

Fetch Behavior for ISR

Applies only to Next.js v13.5.1 and later.

A bug in Next.js can cause inconsistent revalidation behavior when using ISR. It may use the lowest revalidate value from all fetch calls on a page, ignoring their individual settings. To fix this, add the following snippet to your root layout component:

// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
const asyncStorage = require("next/dist/client/components/static-generation-async-storage.external");
//@ts-ignore
const staticStore = (fetch as any).__nextGetStaticStore?.() || asyncStorage.staticGenerationAsyncStorage;
const store = staticStore.getStore();
if (store) {
store.isOnDemandRevalidate = store.isOnDemandRevalidate && !(process.env.OPEN_NEXT_ISR === "true");
}
return <>{children}</>;
}

Pricing

All resources provisioned for your Next.js application are pay-per-use. This means you pay nothing for idle resources and only for what you use. As your traffic grows, your costs will increase, but they are generally much lower than managed hosting providers like Vercel.

Cost Breakdown

CloudFront CDN

CloudFront serves as a CDN, caching your content in edge locations close to your users.

  • Data Transfer: ~$0.085 per GB (first 1 TB/month is free)
  • HTTPS Requests: ~$0.0085 per 10,000 requests (first 10,000,000/month are free)

For current pricing, see the AWS CloudFront Pricing page.

AWS Lambda

Costs depend on whether you use regional Lambda or Lambda@Edge.

Regional AWS Lambda

  • Execution Duration: ~$0.0000000167 per millisecond (with 1024MB memory)
  • Requests: $0.20 per 1 million requests

Lambda@Edge

  • Execution Duration: ~$0.00000005001 per millisecond (with 1024MB memory)
  • Requests: $0.60 per 1 million requests

For current pricing, see the AWS Lambda Pricing page.

S3 for Caching

A S3 bucket is used to store the cache for ISR and fetch requests.

  • GetObject: ~$0.0004 per 1,000 requests (reading from cache)
  • PutObject: ~$0.005 per 1,000 requests (writing to cache)

For current pricing, see the AWS S3 Pricing page.

DynamoDB for Cache Tags

A DynamoDB table stores cache tags for on-demand revalidation.

  • Read Requests: ~$0.25 per 1 million read units
  • Write Requests: ~$1.25 per 1 million write units

For current pricing, see the AWS DynamoDB Pricing page.

Example Pricing

The following example is a rough estimate for a website with 300,000 monthly visitors. Actual costs can vary significantly based on your application's caching strategy, dynamic content, and API usage.

Assumptions

  • Monthly Visitors: 300,000
  • Page Views per Visitor: 3
  • Cache Hit Rate: 50% of page views are served from the CDN cache.
  • API Calls per Page: 2 API calls for each page view that hits the server.

This results in:

  • Total Page Views: 900,000
  • Server Page Views: 450,000
  • Total Server Requests: 1,350,000 (450,000 page views + 900,000 API calls)

Monthly Cost Estimate

ServiceCalculationEstimated Cost
CloudFrontWithin free tier limits$0
AWS Lambda675,000 requests, 100ms duration each$1.26
S3 Storage675,000 reads and writes$3.65
DynamoDB675,000 reads and writes$1.01
Total~$5.92 per month

API reference

NextjsWeb  API reference
type
Required
properties.appDirectory
Required
properties.serverLambda
properties.warmServerInstances
properties.useEdgeLambda
properties.buildCommand
properties.fileOptions
properties.environment
properties.customDomains
properties.useFirewall
properties.streamingEnabled
properties.connectTo
properties.iamRoleStatements
overrides
NextjsServerLambdaProperties  API reference
Parent:NextjsWeb
memory
Default: 1024
timeout
Default: 30
logging
joinDefaultVpc
LambdaFunctionLogging  API reference
disabled
retentionDays
Default: 180
logForwarding
EnvironmentVar  API reference
Parent:NextjsWeb
name
Required
value
Required
DirectoryUploadFilter  API reference
Parent:NextjsWeb
includePattern
Required
excludePattern
headers
tags

Contents