diff --git a/infrastructure/aws/README.md b/infrastructure/aws/README.md
index c52d8c9..41a7fd9 100644
--- a/infrastructure/aws/README.md
+++ b/infrastructure/aws/README.md
@@ -11,6 +11,7 @@
| Name | Version |
|------|---------|
+| [archive](#provider\_archive) | n/a |
| [aws](#provider\_aws) | 6.14.1 |
| [infisical](#provider\_infisical) | n/a |
@@ -22,10 +23,28 @@ No modules.
| Name | Type |
|------|------|
+| [aws_api_gateway_deployment.branch_deployment](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/api_gateway_deployment) | resource |
+| [aws_api_gateway_integration.lambda_integrations](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/api_gateway_integration) | resource |
+| [aws_api_gateway_method.lambda_methods](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/api_gateway_method) | resource |
+| [aws_api_gateway_resource.lambda_resources](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/api_gateway_resource) | resource |
+| [aws_api_gateway_rest_api.branch_api](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/api_gateway_rest_api) | resource |
+| [aws_api_gateway_stage.branch_stage](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/api_gateway_stage) | resource |
| [aws_cognito_user_pool.branch_user_pool](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/cognito_user_pool) | resource |
| [aws_cognito_user_pool_client.branch_client](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/cognito_user_pool_client) | resource |
| [aws_db_instance.branch_rds](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/db_instance) | resource |
+| [aws_iam_role.lambda_role](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/iam_role) | resource |
+| [aws_iam_role_policy_attachment.lambda_basic](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/iam_role_policy_attachment) | resource |
+| [aws_lambda_function.functions](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/lambda_function) | resource |
+| [aws_lambda_permission.api_gateway_permissions](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/lambda_permission) | resource |
+| [aws_s3_bucket.lambda_deployments](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_bucket) | resource |
| [aws_s3_bucket.reports_bucket](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_bucket) | resource |
+| [aws_s3_bucket_policy.reports_bucket_policy](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_bucket_policy) | resource |
+| [aws_s3_bucket_public_access_block.reports_bucket_public_access](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_bucket_public_access_block) | resource |
+| [aws_s3_bucket_server_side_encryption_configuration.lambda_deployments](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_bucket_server_side_encryption_configuration) | resource |
+| [aws_s3_bucket_versioning.lambda_deployments](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_bucket_versioning) | resource |
+| [aws_s3_object.lambda_placeholder](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_object) | resource |
+| [archive_file.lambda_placeholder](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source |
+| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/data-sources/caller_identity) | data source |
| [infisical_secrets.rds_folder](https://registry.terraform.io/providers/infisical/infisical/latest/docs/data-sources/secrets) | data source |
## Inputs
@@ -40,9 +59,9 @@ No modules.
| Name | Description |
|------|-------------|
-| [cognito\_client\_id](#output\_cognito\_client\_id) | Cognito User Pool Client ID |
+| [api\_gateway\_url](#output\_api\_gateway\_url) | The URL of the API Gateway |
| [cognito\_region](#output\_cognito\_region) | AWS Region for Cognito |
| [cognito\_user\_pool\_arn](#output\_cognito\_user\_pool\_arn) | Cognito User Pool ARN |
| [cognito\_user\_pool\_endpoint](#output\_cognito\_user\_pool\_endpoint) | Cognito User Pool Endpoint |
-| [cognito\_user\_pool\_id](#output\_cognito\_user\_pool\_id) | Cognito User Pool ID |
+| [reports\_bucket\_name](#output\_reports\_bucket\_name) | Name of the S3 bucket for generated reports |
diff --git a/infrastructure/aws/api_gateway.tf b/infrastructure/aws/api_gateway.tf
new file mode 100644
index 0000000..f333514
--- /dev/null
+++ b/infrastructure/aws/api_gateway.tf
@@ -0,0 +1,109 @@
+# API Gateway for Lambda functions
+resource "aws_api_gateway_rest_api" "branch_api" {
+ name = "branch-api"
+ description = "API Gateway for Branch Lambda functions"
+
+ endpoint_configuration {
+ types = ["REGIONAL"]
+ }
+}
+
+# Define supported HTTP methods per Lambda function based on handlers
+# NOTE: Must be kept in sync with actual Lambda handlers in apps/backend/lambdas/*/openapi.yaml
+locals {
+ lambda_methods = {
+ auth = ["GET", "POST"]
+ donors = ["GET"]
+ expenditures = ["GET", "POST"]
+ projects = ["GET", "POST"]
+ reports = ["GET"]
+ users = ["GET", "POST", "DELETE", "PATCH"]
+ }
+}
+
+# Create a resource for each Lambda function
+resource "aws_api_gateway_resource" "lambda_resources" {
+ for_each = local.lambda_functions
+
+ rest_api_id = aws_api_gateway_rest_api.branch_api.id
+ parent_id = aws_api_gateway_rest_api.branch_api.root_resource_id
+ path_part = each.key
+}
+
+# Create methods for each resource based on supported methods
+resource "aws_api_gateway_method" "lambda_methods" {
+ for_each = merge([
+ for lambda, methods in local.lambda_methods : {
+ for method in methods :
+ "${lambda}-${method}" => {
+ lambda = lambda
+ method = method
+ }
+ }
+ ]...)
+
+ rest_api_id = aws_api_gateway_rest_api.branch_api.id
+ resource_id = aws_api_gateway_resource.lambda_resources[each.value.lambda].id
+ http_method = each.value.method
+ authorization = "COGNITO_USER_POOLS"
+}
+
+# Create Lambda integrations
+resource "aws_api_gateway_integration" "lambda_integrations" {
+ for_each = merge([
+ for lambda, methods in local.lambda_methods : {
+ for method in methods :
+ "${lambda}-${method}" => {
+ lambda = lambda
+ method = method
+ }
+ }
+ ]...)
+
+ rest_api_id = aws_api_gateway_rest_api.branch_api.id
+ resource_id = aws_api_gateway_resource.lambda_resources[each.value.lambda].id
+ http_method = aws_api_gateway_method.lambda_methods[each.key].http_method
+
+ integration_http_method = "POST"
+ type = "AWS_PROXY"
+ uri = aws_lambda_function.functions[each.value.lambda].invoke_arn
+}
+
+# Allow API Gateway to invoke Lambda functions
+resource "aws_lambda_permission" "api_gateway_permissions" {
+ for_each = local.lambda_functions
+
+ statement_id = "AllowAPIGatewayInvoke"
+ action = "lambda:InvokeFunction"
+ function_name = aws_lambda_function.functions[each.key].function_name
+ principal = "apigateway.amazonaws.com"
+
+ # Grant permission for the specific API Gateway and stage
+ source_arn = "${aws_api_gateway_rest_api.branch_api.execution_arn}/*/*"
+}
+
+# Create deployment
+resource "aws_api_gateway_deployment" "branch_deployment" {
+ depends_on = [
+ aws_api_gateway_integration.lambda_integrations
+ ]
+
+ rest_api_id = aws_api_gateway_rest_api.branch_api.id
+
+ lifecycle {
+ create_before_destroy = true
+ }
+}
+
+# Create stage
+resource "aws_api_gateway_stage" "branch_stage" {
+ deployment_id = aws_api_gateway_deployment.branch_deployment.id
+ rest_api_id = aws_api_gateway_rest_api.branch_api.id
+ stage_name = "prod"
+}
+
+# Output the API Gateway URL
+output "api_gateway_url" {
+ description = "The URL of the API Gateway"
+ value = aws_api_gateway_stage.branch_stage.invoke_url
+}
\ No newline at end of file
diff --git a/infrastructure/aws/lambda.tf b/infrastructure/aws/lambda.tf
new file mode 100644
index 0000000..4aa36d0
--- /dev/null
+++ b/infrastructure/aws/lambda.tf
@@ -0,0 +1,108 @@
+# IAM role for Lambda functions
+resource "aws_iam_role" "lambda_role" {
+ name = "branch-lambda-role"
+ assume_role_policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [{
+ Action = "sts:AssumeRole"
+ Effect = "Allow"
+ Principal = { Service = "lambda.amazonaws.com" }
+ }]
+ })
+}
+
+# Attach basic execution policy for CloudWatch Logs
+resource "aws_iam_role_policy_attachment" "lambda_basic" {
+ role = aws_iam_role.lambda_role.name
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
+}
+
+# Get AWS account ID for unique bucket naming
+data "aws_caller_identity" "current" {}
+
+resource "aws_s3_bucket" "lambda_deployments" {
+ bucket = "branch-lambda-deployments-${data.aws_caller_identity.current.account_id}"
+}
+
+resource "aws_s3_bucket_versioning" "lambda_deployments" {
+ bucket = aws_s3_bucket.lambda_deployments.id
+
+ versioning_configuration {
+ status = "Enabled"
+ }
+}
+
+resource "aws_s3_bucket_server_side_encryption_configuration" "lambda_deployments" {
+ bucket = aws_s3_bucket.lambda_deployments.id
+
+ rule {
+ apply_server_side_encryption_by_default {
+ sse_algorithm = "AES256"
+ }
+ }
+}
+
+# Define all Lambda functions
+locals {
+ lambda_functions = toset([
+ "auth",
+ "donors",
+ "expenditures",
+ "projects",
+ "reports",
+ "users",
+ ])
+}
+
+# Minimal placeholder that will be replaced by GitHub Actions on first deployment
+data "archive_file" "lambda_placeholder" {
+ type = "zip"
+ output_path = "${path.module}/lambda-placeholder.zip"
+ source {
+ content = "exports.handler = async () => ({ statusCode: 200, body: JSON.stringify({ message: 'Placeholder - will be replaced by CI/CD' }) });"
+ filename = "handler.js"
+ }
+}
+
+# This allows Terraform to create the Lambda functions initially
+resource "aws_s3_object" "lambda_placeholder" {
+ for_each = local.lambda_functions
+
+ bucket = aws_s3_bucket.lambda_deployments.id
+ key = "${each.key}/initial.zip"
+ source = data.archive_file.lambda_placeholder.output_path
+
+ content_type = "application/zip"
+}
+
+# Create all Lambda functions with a single resource block
+resource "aws_lambda_function" "functions" {
+ for_each = local.lambda_functions
+
+ function_name = "branch-${each.key}"
+ runtime = "nodejs20.x"
+ handler = "handler.handler"
+ timeout = 30
+ memory_size = 256
+ role = aws_iam_role.lambda_role.arn
+
+ # Use S3 for deployment (initial placeholder, replaced by GitHub Actions)
+ s3_bucket = aws_s3_bucket.lambda_deployments.id
+ s3_key = aws_s3_object.lambda_placeholder[each.key].key
+
+ # Prevent Terraform from reverting code deployments made by GitHub Actions
+ lifecycle {
+ ignore_changes = [s3_key]
+ }
+
+ environment {
+ variables = {
+ NODE_ENV = "production"
+ DB_HOST = aws_db_instance.branch_rds.address
+ DB_USER = data.infisical_secrets.rds_folder.secrets["username"].value
+ DB_PASSWORD = data.infisical_secrets.rds_folder.secrets["password"].value
+ DB_PORT = try(data.infisical_secrets.rds_folder.secrets["db_port"].value, "5432")
+ DB_NAME = try(data.infisical_secrets.rds_folder.secrets["db_name"].value, aws_db_instance.branch_rds.db_name)
+ }
+ }
+}
\ No newline at end of file