Skip to content

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:

typescript
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_id

The key is the env var name, the value is the config resource name. The env var suffix determines the resource type:

Env var suffixResource typeTerraform output
_BUCKETStorage bucketmodule.storage_<name>.bucket_id
_CLOUDFRONT_DOMAINStorage CDNmodule.storage_<name>.cloudfront_domain_name
_QUEUE_URLSQS queuemodule.queue_<name>.queue_url
_TABLE_NAMEDynamoDB tablemodule.dynamodb_<name>.table_name
_TABLE_ARNDynamoDB tablemodule.dynamodb_<name>.table_arn
_TOPIC_ARNSNS topicaws_sns_topic.<name>.arn
_APPLICATION_ARNSNS platform appaws_sns_platform_application.<name>.arn
_FUNCTION_NAMELambda functionmodule.lambda_<name>.function_name
_FUNCTION_ARNLambda functionmodule.lambda_<name>.function_arn

Resolution order

The generator resolves env vars in this order, stopping at the first match:

  1. Hard-coded — well-known vars like EVENT_BUS_NAME, DSQL_ENDPOINT
  2. Bind — explicit mapping from defineLambda({ bind: { ... } })
  3. Prefix match — env var prefix matches a config resource name (e.g., FILES_BUCKET → storage files)
  4. Bare single-resource — bare suffix like BUCKET resolves if there's exactly one resource of that type
  5. 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:

typescript
// 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 needed

When you need bind

Use bind when the env var name doesn't match the resource name:

typescript
// 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:

typescript
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):

typescript
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:

typescript
// 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:

typescript
// src/lambdas/api/feedly/webhook.post.ts
defineLambda({ bind: { SNS_QUEUE_URL: 'SendPushNotification' } })

S3ObjectCreated — combines bind with DLQ:

typescript
// 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:

typescript
// 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