Resource Binding
Access the resources in your app in a secure and typesafe way.
Overview
Resource Binding allows you to connect your infrastructure to your frontends and functions. This is done in two steps:
- Bind a resource to your frontend or API through the
bind
prop. - Use the
sst/node
package to access the resource in your function.
Quick start
To see how Resource Binding works, we are going to create an S3 bucket and bind it to a Next.js frontend.
To follow along, you can create a new SST app by running npx create-sst@two
. Alternatively, you can refer to this example repo that's based on the same template.
To create a new bucket, open up
stacks/Default.ts
and add aBucket
construct below theNextjsSite
.stacks/Default.tsconst bucket = new Bucket(stack, "public");
You'll also need to import
Bucket
at the top.import { Bucket } from "sst/constructs";
Then, bind the
bucket
to thesite
.stacks/Default.tsconst site = new NextjsSite(stack, "site", {
path: "packages/next",
+ bind: [bucket],
});Now we can access the bucket's name in our frontend using the
Bucket
helper. Add this topackages/next/pages/index.ts
to generate a presigned URL.packages/next/pages/index.tsimport crypto from "crypto";
import { Bucket } from "sst/node/bucket";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
export async function getServerSideProps() {
const command = new PutObjectCommand({
ACL: "public-read",
Key: crypto.randomUUID(),
Bucket: Bucket.public.bucketName,
});
const url = await getSignedUrl(new S3Client({}), command);
return { props: { url } };
}And install the AWS SDK for this example.
npm install --save @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
That's it!
tip
Since we are dealing with sensitive info, resource binding is only supported in the frontend's server side functions. To access these on the client side, check out the section below.
While we are using the NextjsSite
in this example, resource binding is supported in all SST functions and SSR frontends. With the exception of the RemixSite
, since Remix does not fully support top-level await yet.
Features
Let's take a look at some of the key features of Resource Binding, and how it makes building apps fun and easy again.
Typesafety
In the above example, the Bucket
object that's imported from sst/node/bucket
is typesafe. Your editor should be able to autocomplete the bucket name public
, as well as its property bucketName
.
Behind the scenes
Let's take a look at how this is all wired up.
First, the
sst/node/table
package predefines an interface.export interface BucketResources {}
When SST builds the app, it generates a type file and adds the bucket name to the
BucketResources
interface..sst/types/index.tsimport "sst/node/bucket";
declare module "sst/node/bucket" {
export interface BucketResources {
public: {
bucketName: string;
};
}
}So when the
Bucket
object is imported fromsst/node/bucket
, it has the typeBucketResources
.
Error handling
If you reference a resource that doesn't exist in your SST app, or hasn't been bound to the frontend, you'll get a runtime error.
For example, if you forget to bind the bucket
to the site
, you'll get the following error when the function is invoked.
Cannot use Bucket.public. Please make sure it is bound to this function.
Testing
When testing your code, you can use the sst bind
CLI to bind the resources to your tests.
sst bind vitest run
This allows the sst/node
helper library to work as if it was running inside a Lambda function.
Read more about testing and learn about the sst bind
CLI.
Permissions
When a resource is bound to a Lambda function, the permissions to access that resource are automatically granted to the function.
site.bind([bucket]);
Here, by binding the bucket
to the site
, the frontend is able to perform file download, upload, delete, and other actions against the bucket.
Behind the scenes
An IAM policy is added to the Lambda function's role, allowing it to perform s3:*
actions on the S3 bucket's ARN.
The IAM policy statement looks like:
{
"Action": "s3:*",
"Resource": ["arn:aws:s3:::{BUCKET_NAME}", "arn:aws:s3:::{BUCKET_NAME}/*"],
"Effect": "Allow",
}
To customize the policy statement, specify the desired actions
and resources
during binding:
site.bind([
{ resource: bucket, permissions: [{ actions: ["s3:GetObject"], resources: [`${bucket.arn}/*`] }] }
]);
The IAM policy statement looks like:
{
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::{BUCKET_NAME}/*",
"Effect": "Allow",
}
Construct support
Resource Binding works across all SST constructs. Here are a few more examples.
DynamoDB table name
import { Table } from "sst/node/table";
Table.myTable.tableName;RDS cluster data
import { RDS } from "sst/node/rds";
RDS.myDB.clusterArn;
RDS.myDB.secretArn;
RDS.myDB.defaultDatabaseName;
See the full list of helpers.
Binding other resources
So far we've seen how Resource Binding allows your frontend to access values from other SST constructs. But there are 2 other types of values you might want to access in your frontend.
- Secrets, because you can't define the value of the secrets in your frontend.
- Values from non-SST constructs, for example static values or values from CDK constructs.
For these you can use Config
. Here are a couple of examples.
Binding secrets
To bind a secret to our frontend, start by creating a Config.Secret
construct.
const STRIPE_KEY = new Config.Secret(stack, "STRIPE_KEY");
And continuing with our example, bind it to the site
.
const site = new NextjsSite(stack, "site", {
path: "packages/next",
+ bind: [STRIPE_KEY],
});
Now set the secret value using the SST CLI.
npx sst secrets set STRIPE_KEY sk_test_abc123
And access the value in the function.
import { Config } from "sst/node/config";
Config.STRIPE_KEY;
You can read more about secrets.
Binding CDK resources
Assuming you have an ECS cluster in your app and you need to pass the cluster name to your function.
Since SST doesn't have a construct for ECS, create a Config.Parameter
construct with the cluster name being the value.
const cluster = new ecs.Cluster(stack, "myCluster");
const MY_CLUSTER_NAME = new Config.Parameter(stack, "MY_CLUSTER_NAME", {
value: cluster.clusterName,
});
Then bind it to the site
from our example.
const site = new NextjsSite(stack, "site", {
path: "packages/next",
+ bind: [MY_CLUSTER_NAME],
});
And you can access the value in your function.
import { Config } from "sst/node/config";
Config.MY_CLUSTER_NAME;
Client side access
So far we've looked at how you can use the sst/node
client in your functions or in your frontend's server side functions. But there might be cases where you want to access something on the client side.
To do this, you can pass props from the server functions to the client side. Using the example from above.
export async function getServerSideProps() {
return {
props: {
bucketName: Bucket.public.bucketName,
},
};
}
caution
Be careful not to pass any secrets or sensitive info to the client.
We can read the bucket name in the getServerSideProps
function and pass it as a prop to our component.
export default function Home({ bucketName }: { bucketName: string }) {
// Render component
}
Alternatively, you can set directly set client side environment variables.
Client side environment variables
However, Frontends (like Next.js, Remix, etc.) can read from an environment variable purely on the client side. SST supports setting these client side environment variables as well.
This is useful if you have a completely static frontend and you want to pass in the outputs of other constructs in your SST app. Let's look at how.
Imagine you have an S3 bucket created using the Bucket
construct, and you want to access the name of the bucket in your client side code. You can use the environment
property in your NextjsSite
construct.
const bucket = new Bucket(stack, "Bucket");
new NextjsSite(stack, "Site", {
environment: {
NEXT_PUBLIC_BUCKET_NAME: bucket.bucketName,
},
});
Now you can access the bucket's name in your client side code.
console.log(process.env.NEXT_PUBLIC_BUCKET_NAME);
In Next.js, only environment variables prefixed with NEXT_PUBLIC_
are available in your client side code. Read more about using environment variables.
How it works
When a resource is bound to a Lambda function, the resource values are stored as environment variables for the function. In our example, the bucket name is stored as a Lambda environment variable named SST_Bucket_bucketName_myBucket
.
At runtime, the sst/node/bucket
package uses top-level await to read the value process.env.SST_Bucket_bucketName_myBucket
and make it accessible via Bucket.myBucket.bucketName
.
SST also stores a copy of the bucket name in AWS SSM. In this case, an SSM parameter of the type String
is created with the name /sst/{appName}/{stageName}/Bucket/myBucket/bucketName
, where {appName}
is the name of your SST app, and {stageName}
is the stage. The parameter value is the name of the bucket stored in plain text.
Storing the bucket name in SSM might seem redundant. But it provides a convenient way to fetch all the bound resources in your application. This can be extremely useful for testing. This isn't possible when using Lambda environment variables and we are going to see why.
Binding sensitive values
When binding resources that contain sensitive values, placeholders are stored in the Lambda environment variables. The actual values are stored inside SSM. At runtime, the values are fetched from SSM when the Lambda container first boots up using top-level await. And the values are cached for subsequent invocations. This is similar to how Config.Secret
works.
Typesafety
When running sst build
, sst deploy
, or sst dev
, types are generated for the defined resources and Config
properties in the .sst
directory.
tip
If you are using one of our starters, this should be done automatically for you.
To use these types, place the following sst-env.d.ts
file in any package that needs the types.
/// <reference path="../.sst/types/index.ts" />
Make sure you specify the path to the .sst
directory correctly. With this in place, your IDE should recognize the generated types and autocomplete them.
Client side environment variables
On sst deploy
client side environment variables will first be replaced by placeholder values, ie. {{ NEXT_PUBLIC_BUCKET_NAME }}
, when building the Next.js app. And after the S3 bucket has been created, the placeholders in the HTML and JS files will then be replaced with the actual values.
caution
Since the actual values are determined at deploy time, you should not rely on the values at build time. For example, you cannot reference process.env.NEXT_PUBLIC_BUCKET_NAME
inside getStaticProps()
at build time.
There are a couple of workarounds:
- Hardcode the bucket name
- Read the bucket name dynamically at build time (ie. from an SSM value)
- Use fallback pages to generate the page on the fly
Note that since edge functions don't support Lambda environment variables, the above token replace method is also used.
Working locally
Resource binding works a little differently for the frontend sites because SST does not run them locally. Instead you wrap your frontend local dev command with sst bind
. For example, you run sst bind next dev
for Next.js.
"scripts": {
"dev": "sst bind next dev",
"build": "next build",
"start": "next start"
},
Note that, sst bind
only works if the Next.js app is located inside the SST app or inside one of its subdirectories. For example:
/
sst.config.ts
my-next-app/
There are a couple of things happening behind the scenes here:
- The
sst dev
command generates stores all the bound resources and environment variables in your AWS account as something called the stack metadata. - The
sst bind
CLI loads these environment variables and sets them for your frontend's local development environment. It also gets an IAM role similar to the one that your SSR function will have when deployed. - When you call
sst/node
in your frontend, it'll use the IAM role to fetch the resources you are trying to access from the stack metadata.
Cost
Resource Binding values are stored in AWS SSM with the Standard Parameter type and Standard Throughput. This makes AWS SSM free to use in your SST apps. However when storing a Config.Secret
the value is encrypted by AWS KMS. These are retrieved at runtime in your Lambda functions when it starts up. AWS KMS has a free tier of 20,000 API calls per month. And it costs $0.03 for every 10,000 subsequent API calls. This is worth keeping in mind as these secrets are fetched per Lambda function cold start.
FAQ
Here are some frequently asked questions about Resource Binding.
Resource Binding or Lambda environment variables?
Prior to Resource Binding, people used Lambda environment variables to pass information to their functions.
Aside from the lack of typesafety and error handling, Lambda environment variables have a few drawbacks. Imagine you have a Lambda function that looks like this.
export const handler = async () => {
if (process.env.TOPIC_NAME !== "UserUpdated") {
return;
}
// ...
};
Where TOPIC_NAME
is stored as a Lambda environment variable. You'll need to handle the following:
When testing this function, locally or in your CI, you need to figure out the value for
TOPIC_NAME
and set it as an environment variable.In addition, imagine you have another function that also has a
TOPIC_NAME
Lambda environment variable, but with a different value.packages/functions/src/charged.tsexport const handler = async () => {
if (process.env.TOPIC_NAME !== "InvoiceCharged") {
return;
}
// ...
};What should the
TOPIC_NAME
be in your tests?
With Resource Binding, the value for the topic name is also stored in SSM. When running tests, SST can automatically fetch this from SSM using the sst bind
CLI.
Does this make my Lambda functions slower?
No. The resource values are stored as environment variables for the function. At runtime, reading from environment variables is instantaneous.
For sensitive values, the values are stored in AWS SSM. When the Lambda container first boots up, the values are fetched from SSM and are cached for subsequent invocations.
What if I'm not using Node.js runtime?
For non-Node.js runtimes, you can continue to use Lambda environment variables.
If you want to use Resource Binding, you would need to read the bound values from the Lambda environment variable and AWS SSM directly. Refer to the sst/node
package to see how it is done in Node.js.