Resource Binding
Mantle's infrastructure generator automatically wires environment variables to Terraform module outputs based on naming conventions. When a Lambda calls getRequiredEnv('FILES_BUCKET'), the generator resolves FILES_BUCKET to module.storage_files.bucket_id because the FILES_ prefix matches the storage bucket named files.
This works well when env var names match resource names. When they don't, use defineLambda({ bind }) to declare the mapping explicitly.
The problem bind solves
Consider a storage bucket named files and a Lambda that accesses it via getRequiredEnv('DATA_BUCKET'). The generator sees the _BUCKET suffix and looks for a storage resource whose name matches DATA — but the bucket is named files. Without guidance, the generator falls back to creating a Terraform variable var.data_bucket, which requires manual wiring.
Using bind
Declare the mapping in defineLambda(), right next to the code that uses the env var:
import { defineLambda } from '@mantleframework/core'
import { getRequiredEnv } from '@mantleframework/env'
defineLambda({ bind: { DATA_BUCKET: 'files' } })
const bucket = getRequiredEnv('DATA_BUCKET')
// Generator resolves DATA_BUCKET → module.storage_files.bucket_idThe key is the env var name, the value is the config resource name. The env var suffix determines the resource type:
| Env var suffix | Resource type | Terraform output |
|---|---|---|
_BUCKET | Storage bucket | module.storage_<name>.bucket_id |
_CLOUDFRONT_DOMAIN | Storage CDN | module.storage_<name>.cloudfront_domain_name |
_QUEUE_URL | SQS queue | module.queue_<name>.queue_url |
_TABLE_NAME | DynamoDB table | module.dynamodb_<name>.table_name |
_TABLE_ARN | DynamoDB table | module.dynamodb_<name>.table_arn |
_TOPIC_ARN | SNS topic | aws_sns_topic.<name>.arn |
_APPLICATION_ARN | SNS platform app | aws_sns_platform_application.<name>.arn |
_FUNCTION_NAME | Lambda function | module.lambda_<name>.function_name |
_FUNCTION_ARN | Lambda function | module.lambda_<name>.function_arn |
Resolution order
The generator resolves env vars in this order, stopping at the first match:
- Hard-coded — well-known vars like
EVENT_BUS_NAME,DSQL_ENDPOINT - Bind — explicit mapping from
defineLambda({ bind: { ... } }) - Prefix match — env var prefix matches a config resource name (e.g.,
FILES_BUCKET→ storagefiles) - Bare single-resource — bare suffix like
BUCKETresolves if there's exactly one resource of that type - Terraform variable — fallback creates
var.<snake_case_name>(requires manual tfvars entry)
Bind takes precedence over prefix matching, so you can override the default resolution for specific Lambdas without affecting others.
When you don't need bind
If your env var names follow the naming convention, bind is unnecessary:
// Storage bucket named 'files'
getRequiredEnv('FILES_BUCKET') // ✓ prefix 'files' matches — no bind needed
// SQS queue named 'DownloadQueue'
getRequiredEnv('DOWNLOADQUEUE_QUEUE_URL') // ✓ prefix match — no bind needed
// Single storage bucket
getRequiredEnv('BUCKET') // ✓ bare suffix resolves to the only bucket — no bind neededWhen you need bind
Use bind when the env var name doesn't match the resource name:
// Storage bucket named 'files', but Lambda uses DATA_BUCKET
defineLambda({ bind: { DATA_BUCKET: 'files' } })
getRequiredEnv('DATA_BUCKET')
// SQS queue named 'SendPushNotification', but Lambda uses SNS_QUEUE_URL
defineLambda({ bind: { SNS_QUEUE_URL: 'SendPushNotification' } })
getRequiredEnv('SNS_QUEUE_URL')Multiple bindings
A single defineLambda() call can bind multiple env vars:
defineLambda({
bind: {
DATA_BUCKET: 'files',
SNS_QUEUE_URL: 'SendPushNotification',
}
})IAM policy generation
Bind-resolved env vars generate the same IAM policies as convention-resolved ones. A _BUCKET binding generates an S3 access policy; a _QUEUE_URL binding generates an SQS send policy. The infra generator handles this automatically.
Production examples
OfflineMediaDownloader
OMD has a storage bucket named files and an SQS queue named SendPushNotification. Several Lambdas use non-conventional env var names.
Config (mantle.config.ts):
export default defineConfig({
name: 'mantle-offlinemediadownloader',
storage: [{ name: 'files', cloudfront: true }],
queues: [{ name: 'SendPushNotification' }],
// No envAlias needed — bind lives in the handlers
})DevTools Lambda — binds DATA_BUCKET to storage files:
// src/lambdas/mcp/DevTools/index.ts
defineLambda({
timeout: 30,
memorySize: 512,
env: ['DATA_BUCKET', 'EVENT_BUS_NAME'],
bind: { DATA_BUCKET: 'files' }
})Webhook Lambda — binds SNS_QUEUE_URL to queue SendPushNotification:
// src/lambdas/api/feedly/webhook.post.ts
defineLambda({ bind: { SNS_QUEUE_URL: 'SendPushNotification' } })S3ObjectCreated — combines bind with DLQ:
// src/lambdas/s3/S3ObjectCreated/index.ts
defineLambda({
deadLetterQueue: true,
bind: { SNS_QUEUE_URL: 'SendPushNotification' }
})LifegamesPortal
LP has a storage bucket named data. Since DATA_BUCKET prefix-matches data, no bind is needed:
// Config
export default defineConfig({
name: 'lifegames-portal',
storage: [{ name: 'data', cloudfront: true }],
// DATA_BUCKET resolves via prefix match: 'data' === 'data'
})LP required no bind entries — all env vars follow naming conventions.
Next steps
- Dead Letter Queues — resilience for async Lambda failures
- Infrastructure — full generator reference
- SQS Queues — queue configuration and consumer handlers
- S3 Triggers — S3 event-triggered Lambdas