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 '@j0nathan-ll0yd/core'
import { getRequiredEnv } from '@j0nathan-ll0yd/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
_KVS_ARNStorage edge-function KeyValueStoremodule.storage_<name>.edge_kvs_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 ​