Skip to content

Commit 99148d3

Browse files
NiallJoeMaherclaude
andcommitted
feat(infra): DynamoDB-backed rate limiting + CDK table
Per the AWS-native stack (matches SES). server/lib/rateLimit.ts now has one async API over two backends: - DynamoDB (when RATE_LIMIT_TABLE is set): atomic fixed-window counters with native TTL; fail-open on errors so a misconfigured table can't take the site down. AWS SDK clients added (@aws-sdk/client-dynamodb + lib-dynamodb). - in-memory sliding window for local dev / fallback. Helper `enforceRateLimit({ key, limit, windowMs, message })` throws TOO_MANY_REQUESTS; search / content.create / sponsor.submit now await it. CDK: StorageStack provisions the rate-limit DynamoDB table (pk + ttl, PAY_PER_REQUEST, retain on prod) and publishes its name to SSM /env/rate-limit-table + a CfnOutput. App env needs RATE_LIMIT_TABLE = that name and dynamodb:UpdateItem on the table. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent abc89a2 commit 99148d3

7 files changed

Lines changed: 883 additions & 167 deletions

File tree

cdk/lib/storage-stack.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as s3 from "aws-cdk-lib/aws-s3";
44
import * as rds from "aws-cdk-lib/aws-rds";
55
import * as ec2 from "aws-cdk-lib/aws-ec2";
66
import * as ssm from "aws-cdk-lib/aws-ssm";
7+
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
78
import * as lambda from "aws-cdk-lib/aws-lambda";
89
import * as backup from "aws-cdk-lib/aws-backup";
910
import * as events from "aws-cdk-lib/aws-events";
@@ -20,6 +21,7 @@ export class StorageStack extends cdk.Stack {
2021
public readonly bucket: s3.Bucket;
2122
public readonly db: rds.DatabaseInstance;
2223
public readonly vpc: ec2.Vpc;
24+
public readonly rateLimitTable: dynamodb.Table;
2325

2426
constructor(scope: Construct, id: string, props?: Props) {
2527
super(scope, id, props);
@@ -30,6 +32,32 @@ export class StorageStack extends cdk.Stack {
3032

3133
const { vpc } = this;
3234

35+
// ── Rate-limit counters (DynamoDB) ──
36+
// Fixed-window counters keyed by `pk`, auto-expired via the `ttl`
37+
// attribute. On-demand billing — pay only per request, no capacity to plan.
38+
// The app reads the table name from RATE_LIMIT_TABLE (see SSM param below);
39+
// its IAM principal needs dynamodb:UpdateItem on this table.
40+
this.rateLimitTable = new dynamodb.Table(this, "RateLimitTable", {
41+
partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING },
42+
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
43+
timeToLiveAttribute: "ttl",
44+
removalPolicy: props?.production
45+
? cdk.RemovalPolicy.RETAIN
46+
: cdk.RemovalPolicy.DESTROY,
47+
});
48+
49+
// Publish the generated table name so the app env (RATE_LIMIT_TABLE) can
50+
// resolve it without hardcoding.
51+
new ssm.StringParameter(this, "RateLimitTableNameParam", {
52+
parameterName: "/env/rate-limit-table",
53+
stringValue: this.rateLimitTable.tableName,
54+
});
55+
56+
new cdk.CfnOutput(this, "RateLimitTableName", {
57+
value: this.rateLimitTable.tableName,
58+
description: "Set RATE_LIMIT_TABLE to this in the app environment",
59+
});
60+
3361
// S3 bucket
3462
const bucketName = ssm.StringParameter.valueForStringParameter(
3563
this,

0 commit comments

Comments
 (0)