Infrastructure
mantle generate infra reads your src/lambdas/ directory and mantle.config.ts and writes OpenTofu (.tf) files to infra/.
Final result
mantle generate infra
# Creates: main.tf, variables.tf, outputs.tf, api_routes.tf
# lambda_<name>.tf per handler
# storage_<name>.tf, database.tf, eventbridge.tf (if configured)Step 1: Configure your project
Declare infrastructure in mantle.config.ts:
import { defineConfig } from "@j0nathan-ll0yd/core";
export default defineConfig({
name: "my-api",
database: { provider: "aurora-dsql" },
eventbridge: { bus: "my-api-events" },
storage: [{ name: "uploads", cloudfront: true }],
queues: [{ name: "order-processing", visibilityTimeoutSeconds: 60 }],
});Step 2: Structure your Lambda files
Lambda type is determined by which subdirectory under src/lambdas/ the file lives in:
src/lambdas/
api/ # API Gateway routed (filename = <name>.<method>.ts)
eventbridge/ # EventBridge-triggered
scheduled/ # CloudWatch scheduled
standalone/ # Direct invocation
sqs/ # SQS consumer
s3/ # S3 event-triggered
websocket/ # WebSocket API GatewayAPI handler filenames determine routes and HTTP methods:
| File | Route | Method | Lambda name |
|---|---|---|---|
api/items/index.get.ts | /items | GET | ItemsGet |
api/items/index.post.ts | /items | POST | ItemsPost |
api/items/[id]/index.delete.ts | /items/{id} | DELETE | ItemsIdDelete |
api/ping.get.ts | /ping | GET | Ping |
Use index.<method>.ts when the handler responds at the directory path itself.
Step 3: Run the generator
mantle generate infraGenerated files include a header:
# This file is generated by Mantle CLI
# To customize, remove the first line and this file will be skipped on re-generation.To eject a file (take manual control), remove the first header line. The generator skips ejected files on subsequent runs.
Module structure
All modules accept name_prefix and tags from the core module:
module "core" {
source = "../node_modules/@j0nathan-ll0yd/cli/modules/core"
project_name = "my-api"
environment = var.environment
}
module "api" {
source = "../node_modules/@j0nathan-ll0yd/cli/modules/api-gateway"
name_prefix = module.core.name_prefix
tags = module.core.common_tags
}
module "lambda_get_items" {
source = "../node_modules/@j0nathan-ll0yd/cli/modules/lambda"
name_prefix = module.core.name_prefix
function_name = "GetItems"
region = module.core.region
account_id = module.core.account_id
assume_role_policy = module.core.lambda_assume_role_policy
xray_policy_arn = module.core.lambda_xray_policy_arn
tags = module.core.common_tags
}Available modules:
| Module | Purpose |
|---|---|
modules/core | Naming prefix, common tags, IAM assume-role policies, X-Ray policy |
modules/lambda | Lambda function, IAM role, log group, optional triggers |
modules/api-gateway | REST API with stages, authorizer, usage plans |
modules/queue | SQS queue + DLQ + CloudWatch alarm |
modules/storage | S3 bucket with encryption, versioning, optional CloudFront |
modules/idempotency | DynamoDB table for Lambda Powertools idempotency |
modules/observability | CloudWatch dashboards, alarms, SNS topics |
modules/eventbridge | EventBridge custom event bus |
modules/dynamodb | DynamoDB table with GSIs, TTL, PITR |
modules/websocket-api | WebSocket API Gateway |
modules/database/aurora-dsql | Aurora DSQL cluster |
Resources follow the naming convention ${environment}-${project_name}-ResourceName (PascalCase for logical resources, kebab-case for S3/SQS).
Storage edge functions
A storage bucket with cloudfront: true can attach a viewer-request CloudFront Function backed by a CloudFront KeyValueStore — runtime-togglable request gating (feature flags, privacy gates, maintenance modes) that takes effect within seconds, before the cache:
storage: [{
name: 'data',
cloudfront: true,
edgeFunction: {
source: 'src/edge/focus-gate.js', // cloudfront-js-2.0, relative to project root
seedKeys: [{ key: 'hiding', value: '0' }], // provisioned once; runtime writes win afterwards
},
}],The generator emits the function (cloudfront-js-2.0, published), the KeyValueStore, the viewer-request association on the bucket's distribution, and the seeded keys. Seeded key values are drift-ignored, so a Lambda toggling a flag at runtime is never reverted by a later apply. Lambdas that write flags (via @aws-sdk/client-cloudfront-keyvaluestore) get a scoped CloudFrontKvsWrite inline policy automatically; expose the store ARN with a *_KVS_ARN env var (see Resource Binding). Note the KVS data plane requires SigV4A request signing.
Environment variable auto-wiring
The generator scans handler files for getRequiredEnv() / getOptionalEnv() calls and:
- Wires standard env vars (
API_BEARER_TOKEN,DSQL_ENDPOINT, etc.) from project config - Creates Terraform variables for non-standard env vars
- Resolves
*_BUCKETand*_FUNCTION_NAMEpatterns to module outputs instead of variables
# Auto-generated for getOptionalEnv('LOCATIONIQ_API_KEY', '')
variable "locationiq_api_key" {
description = "LOCATIONIQ_API_KEY environment variable"
type = string
sensitive = true
default = ""
}Variables containing key, token, or secret are automatically marked sensitive = true.
*_BUCKET env vars also generate an inline S3Access IAM policy. *_FUNCTION_NAME env vars generate an Invoke<Name> IAM policy.
Lambda configuration
For EventBridge and scheduled handlers, pass config directly to defineEventBridgeHandler / defineScheduledHandler. Use defineLambda() only for standalone handlers:
import { defineLambda } from "@j0nathan-ll0yd/core";
defineLambda({
timeout: 60,
memorySize: 256,
deadLetterQueue: true,
retryAttempts: 2,
bind: { DATA_BUCKET: "files" },
});| Option | Type | Default | Description |
|---|---|---|---|
timeout | number | — | Timeout in seconds |
memorySize | number | 128 | Memory in MB |
reservedConcurrency | number | — | Reserved concurrent executions |
ephemeralStorage | number | 512 | Ephemeral storage in MB |
deadLetterQueue | boolean | — | true = auto-generate SQS DLQ. See Dead Letter Queues. |
retryAttempts | number | — | Max retry attempts (0–2) |
bind | Record<string, string> | — | Map env var names to config resource names. See Resource Binding. |
Ejected files
If you eject main.tf and add new Lambdas, manually add their invoke_arn to the redeployment_trigger hash — the generator silently skips ejected files.
Path parameter directories ([id]) become {id} in the API Gateway path_part and have brackets stripped from Terraform resource identifiers.
Next steps
- Deployment —
mantle buildandmantle deploy - Database — Drizzle schema and Aurora DSQL setup
- Entity Queries —
@RequiresTableandmantle generate permissions