@tlj-blocks/feedback-cdk
v0.1.0
Published
AWS CDK construct that receives feedback via a Lambda Function URL and emails it through an SNS topic
Readme
@tlj-blocks/feedback-cdk
An AWS CDK construct that adds a feedback receiver to any CDK stack. It
creates a Lambda behind a public
Function URL
that accepts { type, title, description } posts and publishes them to an SNS
topic. The topic is one the construct creates and emails to an address you
configure, or one you already have.
The Lambda handler is bundled into this package, so you do not need esbuild, Docker, or any bundling setup in your own app. Instantiate the construct and deploy.
Pair it with @tlj-blocks/feedback-client on the frontend, or post
to the endpoint yourself. See HTTP contract.
Install
npm install @tlj-blocks/feedback-cdkaws-cdk-lib and constructs are peer dependencies. Your CDK app already has
them, but if not:
npm install aws-cdk-lib constructsRequires aws-cdk-lib 2.160.0 or later, for lambda.Runtime.NODEJS_22_X, and
constructs 10.x.
Quick start
Add the construct to a stack. The only required prop is feedbackEmail (or
topic, below).
// lib/my-stack.ts
import { Stack, StackProps, CfnOutput } from "aws-cdk-lib";
import { Construct } from "constructs";
import { Feedback } from "@tlj-blocks/feedback-cdk";
export class MyStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const feedback = new Feedback(this, "Feedback", {
feedbackEmail: "[email protected]",
appName: "MyApp",
// Lock CORS down to your real origins in production.
allowedOrigins: ["https://app.example.com", "http://localhost:5173"],
});
// Expose the endpoint so you can hand it to the frontend.
new CfnOutput(this, "FeedbackUrl", { value: feedback.functionUrl.url });
}
}// bin/app.ts
import { App } from "aws-cdk-lib";
import { MyStack } from "../lib/my-stack";
const app = new App();
new MyStack(app, "MyStack");Deploy and confirm
cdk deployThe stack output FeedbackUrl prints the endpoint URL.
AWS then sends a subscription confirmation email to feedbackEmail. Open it and
click "Confirm subscription". SNS delivers nothing until you do.
Submit a test from your frontend, or with the curl below. The feedback should
arrive by email.
Bring your own topic
If your app already has a notification topic, or you want to own the topic's subscribers and lifecycle, pass the topic instead of an address:
import { Topic } from "aws-cdk-lib/aws-sns";
const topic = new Topic(this, "Notifications");
const feedback = new Feedback(this, "Feedback", { topic, appName: "MyApp" });The construct then creates no topic and no subscription. It only publishes
to yours. If you give feedbackEmail as well, the construct subscribes that
address to your topic too. One of the two props is required.
Wire up the frontend
Give the FeedbackUrl to the @tlj-blocks/feedback-client:
import { submitFeedback } from "@tlj-blocks/feedback-client";
await submitFeedback({
endpoint: feedbackUrl, // the CfnOutput value, via a build-time env var
feedback: {
type: "bug", // "bug" or "feature"
title: "Save button does nothing",
description: "Clicking Save on the settings page has no effect.",
},
});HTTP contract
You do not need the client. Any HTTP caller works.
Post to the Function URL with Content-Type: application/json:
{
"type": "bug",
"title": "Save button does nothing",
"description": "Clicking Save on the settings page has no effect.",
"email": "[email protected]",
"system": {
"App version": "1.4.0",
"OS": "macos (aarch64)",
"Settings": "{\n \"theme\": \"dark\"\n}"
}
}type is "bug" or "feature" and is required. title and description are
required non-empty strings. email is optional, and the notification includes
it when present.
system is optional. It holds whatever the app knows about the reporter's
environment, as a flat object of string values. The email prints it after the
description under a --- System --- line, one key: value per entry in the
order given. A value containing line breaks is printed indented under its key,
so a block of JSON reads as JSON. A value that isn't a string is a 400.
The responses are:
| Status | Body | When |
| ------ | ---------------------------------------------------- | -------------------------------- |
| 200 | { "success": true } | Published to SNS |
| 400 | { "error": "ValidationError", "message": "..." } | Missing or invalid body |
| 500 | { "error": "ConfigError" \| "InternalError", ... } | Misconfig or SNS publish failure |
curl -X POST "$FEEDBACK_URL" \
-H "Content-Type: application/json" \
-d '{"type":"feature","title":"Dark mode","description":"Please add a dark theme."}'Props
| Prop | Type | Default | Notes |
| ---------------- | ---------------- | ------------- | ----------------------------------------------------------- |
| feedbackEmail | string | none | Who receives the feedback emails. Required without topic. |
| topic | sns.ITopic | a new topic | Publish to this topic instead of creating one. |
| appName | string | "App" | Topic display name and email subject prefix. |
| topicName | string | generated | Names the SNS topic the construct creates. |
| allowedOrigins | string[] | ["*"] | CORS origins allowed to call the Function URL. |
| runtime | lambda.Runtime | NODEJS_22_X | Lambda runtime for the receiver. |
What the construct exposes
The underlying resources are public so you can extend them.
functionUrl is the lambda.FunctionUrl. Read .url for the endpoint.
topic is the sns.ITopic, either the one you passed in or the one the
construct made. Add more subscribers to it, such as SMS or extra email
addresses.
handler is the lambda.Function. Use it to add alarms, metrics, environment
variables, or log retention.
To notify a second address and alarm on errors:
import { Subscription, SubscriptionProtocol } from "aws-cdk-lib/aws-sns";
const feedback = new Feedback(this, "Feedback", {
feedbackEmail: "[email protected]",
});
// Copy another mailbox.
new Subscription(this, "Cc", {
topic: feedback.topic,
protocol: SubscriptionProtocol.EMAIL,
endpoint: "[email protected]",
});
// Alarm if the receiver starts erroring.
feedback.handler
.metricErrors()
.createAlarm(this, "FeedbackErrors", { threshold: 1, evaluationPeriods: 1 });Resources created
An AWS::SNS::Topic (unless you passed one in), an AWS::SNS::Subscription
of protocol email (when feedbackEmail is given), an
AWS::Lambda::Function with an execution role that can call sns:Publish on
that topic and nothing else, and an AWS::Lambda::Url.
Authentication
The Function URL is public. It uses authType: NONE, so anyone who has the URL
can post feedback, which suits anonymous in-app feedback. The handler reads the
optional email field from the body but does not verify who sent it.
If you need authenticated submissions, put your own authorizer or API Gateway in
front of the handler, restrict allowedOrigins, or extend the construct.
Troubleshooting
If no emails arrive, the SNS subscription is almost certainly unconfirmed. Look in the inbox and the spam folder for the confirmation from AWS and click the link.
A CORS error in the browser means the page's origin is missing from
allowedOrigins.
A 500 ConfigError means the Lambda has no FEEDBACK_TOPIC_ARN. The construct
sets that variable, so this points to someone editing the function's
configuration by hand.
