Showing preview only (398K chars total). Download the full file or copy to clipboard to get everything.
Repository: aws-samples/serverless-full-stack-webapp-starter-kit
Branch: main
Commit: e4b703eba018
Files: 81
Total size: 373.6 KB
Directory structure:
gitextract_lfz83ak7/
├── .github/
│ └── workflows/
│ ├── build.yml
│ ├── commitlint.yml
│ ├── needs-triage.yml
│ ├── release-please.yml
│ ├── stale.yml
│ └── update_snapshot.yml
├── .prettierrc
├── .release-please-manifest.json
├── .serverless-full-stack-webapp-starter-kit/
│ └── design/
│ └── DESIGN_PRINCIPLES.md
├── AGENTS.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── cdk/
│ ├── .gitignore
│ ├── README.md
│ ├── bin/
│ │ └── cdk.ts
│ ├── cdk.json
│ ├── jest.config.js
│ ├── lib/
│ │ ├── constructs/
│ │ │ ├── async-job.ts
│ │ │ ├── auth/
│ │ │ │ ├── .gitignore
│ │ │ │ ├── index.ts
│ │ │ │ └── prefix-generator.js
│ │ │ ├── cf-lambda-furl-service/
│ │ │ │ ├── edge-function.ts
│ │ │ │ ├── lambda/
│ │ │ │ │ └── sign-payload.ts
│ │ │ │ └── service.ts
│ │ │ ├── database.ts
│ │ │ ├── event-bus/
│ │ │ │ ├── handler.mjs
│ │ │ │ └── index.ts
│ │ │ └── webapp.ts
│ │ ├── main-stack.ts
│ │ └── us-east-1-stack.ts
│ ├── package.json
│ ├── test/
│ │ ├── __snapshots__/
│ │ │ ├── serverless-fullstack-webapp-starter-kit-without-domain.test.ts.snap
│ │ │ └── serverless-fullstack-webapp-starter-kit.test.ts.snap
│ │ ├── serverless-fullstack-webapp-starter-kit-without-domain.test.ts
│ │ ├── serverless-fullstack-webapp-starter-kit.test.ts
│ │ └── snapshot-plugin.ts
│ └── tsconfig.json
├── compose.yaml
├── release-please-config.json
└── webapp/
├── .dockerignore
├── .env.local.example
├── .gitignore
├── Dockerfile
├── README.md
├── components.json
├── eslint.config.mjs
├── job.Dockerfile
├── next.config.ts
├── package.json
├── postcss.config.mjs
├── prisma/
│ └── schema.prisma
├── run.sh
├── src/
│ ├── app/
│ │ ├── (root)/
│ │ │ ├── actions.ts
│ │ │ ├── components/
│ │ │ │ ├── CreateTodoForm.tsx
│ │ │ │ └── TodoItem.tsx
│ │ │ ├── page.tsx
│ │ │ └── schemas.ts
│ │ ├── api/
│ │ │ ├── auth/
│ │ │ │ └── [slug]/
│ │ │ │ └── route.ts
│ │ │ └── cognito-token/
│ │ │ └── route.ts
│ │ ├── auth-callback/
│ │ │ └── page.tsx
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ └── sign-in/
│ │ └── page.tsx
│ ├── components/
│ │ ├── Header.tsx
│ │ └── ui/
│ │ └── sonner.tsx
│ ├── hooks/
│ │ └── use-event-bus.ts
│ ├── jobs/
│ │ ├── async-job/
│ │ │ └── translate.ts
│ │ ├── async-job-runner.ts
│ │ └── migration-runner.ts
│ ├── lib/
│ │ ├── amplifyServerUtils.ts
│ │ ├── auth.ts
│ │ ├── events.ts
│ │ ├── jobs.ts
│ │ ├── prisma.ts
│ │ ├── safe-action.ts
│ │ └── utils.ts
│ └── proxy.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/build.yml
================================================
permissions:
contents: read
name: Build
on:
push:
branches:
- main
workflow_dispatch:
pull_request:
jobs:
Build-and-Test-CDK:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: '22.x'
- run: |
npm ci
npm run format:check
working-directory: ./cdk
name: Install dependencies and run static analysis
- run: |
npm run build
npm run test
working-directory: ./cdk
name: build and test
Build-and-Test-Webapp:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: '22.x'
- run: |
npm ci
npm run format:check
working-directory: ./webapp
name: Install dependencies and run static analysis
- run: |
cp .env.local.example .env.local
npm run build
working-directory: ./webapp
name: build
================================================
FILE: .github/workflows/commitlint.yml
================================================
name: PR Title
on:
pull_request_target:
types: [opened, edited, synchronize]
permissions:
pull-requests: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/needs-triage.yml
================================================
name: Add needs-triage label
on:
issues:
types: [opened]
permissions:
issues: write
jobs:
add-label:
if: ${{ !endsWith(github.event.issue.user.login, '[bot]') }}
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['needs-triage']
});
================================================
FILE: .github/workflows/release-please.yml
================================================
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
name: release-please
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
================================================
FILE: .github/workflows/stale.yml
================================================
name: Close stale issues and PRs
on:
schedule:
- cron: '0 0 * * *'
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
days-before-stale: 60
days-before-close: 14
stale-issue-label: stale
stale-pr-label: stale
stale-issue-message: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs within 14 days.
stale-pr-message: >
This PR has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs within 14 days.
exempt-issue-labels: 'priority: high'
exempt-pr-labels: 'priority: high'
================================================
FILE: .github/workflows/update_snapshot.yml
================================================
name: Update snapshot
on:
workflow_dispatch:
push:
branches:
- 'dependabot/**'
issue_comment:
types: [created]
permissions:
contents: write
jobs:
update:
if: >
github.event_name == 'push' ||
github.event_name == 'workflow_dispatch' ||
(github.event.issue.pull_request && contains(github.event.comment.body, '/update-snapshot'))
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.event_name == 'issue_comment' && format('refs/pull/{0}/head', github.event.issue.number) || '' }}
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: "22.x"
- run: |
npm ci
npm run test -- -u
working-directory: ./cdk
- name: Add & Commit
uses: EndBug/add-and-commit@v7.2.0
with:
add: "cdk/test/__snapshots__/."
message: "update snapshot"
================================================
FILE: .prettierrc
================================================
{
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"printWidth": 120
}
================================================
FILE: .release-please-manifest.json
================================================
{
".": "2.1.0"
}
================================================
FILE: .serverless-full-stack-webapp-starter-kit/design/DESIGN_PRINCIPLES.md
================================================
# Design Principles
This document is for kit maintainers. If you copied this kit to build your own app, you can safely delete the `.serverless-full-stack-webapp-starter-kit/` directory.
## What this kit is
A template you **copy** (not fork) and grow into your own app. It is not a framework — users are expected to read, understand, and modify every file.
## Quality standards
As an aws-samples project:
- Correctness is the top priority — users learn patterns from this code.
- Reproducibility — following the README must produce a working deployment.
- Readability — code should be understandable by developers new to serverless.
- One-command deploy — `npx cdk deploy --all` must be the only deployment step.
The litmus test for any PR: "After this merges, will a developer who copies the kit build their app on a correct understanding?"
## Design decisions
### Template, not framework
- Breaking changes have low impact — users copy and diverge. Major versions can be bumped without a lengthy deprecation cycle.
- The sample app exists solely to prove all kit components work together. Users will delete it. Do not expand sample app features beyond what is needed for this proof.
- Avoid over-abstraction. Readability and modifiability matter more than DRY.
### What to include
- Patterns that every serverless full-stack webapp needs (auth, DB, async jobs, real-time).
- Operational essentials (migration, logging, cost-optimized defaults).
- Only what cannot be trivially added later.
### What to exclude
- App-specific business logic.
- Dependencies on specific AI models or services.
- Patterns needed by fewer than half of expected users.
### Technology choices
| Choice | Rationale |
|--------|-----------|
| `prisma db push` over `prisma migrate` | Simpler default for starter-kit scope. Users can switch to `prisma migrate` when they need migration history. |
| NAT Instance over NAT Gateway | ~$30/month savings. Acceptable trade-off for a starter kit. |
| Single Lambda for all async jobs | Reduces cold starts and simplifies deployment. `cmd` parameter selects the entry point. |
| `proxy.ts` over Next.js middleware | Runs inside Lambda handler, avoiding cold-start CPU starvation from JWKS fetch in middleware. |
| `output: 'standalone'` | Required for Lambda deployment via Docker image. |
| Lambda Web Adapter | Enables response streaming with CloudFront + Lambda Function URL. |
### Architecture Decision Records (ADR)
ADRs are not used yet. Introduce `design/adr/` when a major technology decision is made (e.g., ORM migration, database engine change) that requires recording the context, alternatives considered, and rationale.
### Migration guides
When a breaking change is introduced, write a migration guide alongside the ADR. Place it in `docs/migration/` (e.g., `docs/migration/v3-migration-prompt.md`).
A migration guide is not a complete step-by-step procedure for users. It is an input for an AI coding agent — a meta-prompt that the agent reads, compares against the user's codebase, and uses to build a project-specific migration plan. Write it with that consumer in mind: describe what changed, why, and what patterns in user code are affected, rather than prescribing exact commands.
To surface the guide in release notes, include a link in the `BREAKING CHANGE:` commit footer:
```
feat!: replace ORM from Prisma to Drizzle
BREAKING CHANGE: ORM has been replaced. See [migration guide](docs/migration/v3-migration-prompt.md) for details.
```
release-please will carry this into the Breaking Changes section of the GitHub Release.
================================================
FILE: AGENTS.md
================================================
# AGENTS.md
## Commands
```bash
# webapp
cd webapp && npm ci
cd webapp && npm run dev # starts on port 3010
cd webapp && npm run build
cd webapp && npm run lint
cd webapp && npm run format
# cdk
cd cdk && npm ci
cd cdk && npm run build
cd cdk && npm test
cd cdk && npm run format
cd cdk && npx cdk deploy --all
cd cdk && npx cdk diff
# local development (requires Docker)
docker compose up -d # PostgreSQL on port 5432
cd webapp && npx prisma db push # sync schema to local DB
cd webapp && npm run dev
```
## Development guide
### Authentication
All server-side mutations must go through `authActionClient` (defined in `lib/safe-action.ts`). It validates the Cognito session via Amplify server-side auth and injects `ctx.userId`. Never call Prisma directly from a Server Action without this middleware.
`proxy.ts` handles route protection (redirect to `/sign-in` for unauthenticated users). It is NOT a Next.js middleware file — it runs inside the Lambda handler. There is no `middleware.ts` in this project.
### Async jobs
The dispatch flow is: Server Action → `runJob()` (Lambda async invoke) → `async-job-runner.ts` (discriminated union dispatch) → job handler → `sendEvent()` (AppSync Events) → client `useEventBus` hook.
To add a new job:
1. Add a Zod schema with a `type` literal to the discriminated union in `async-job-runner.ts`
2. Implement the handler in `src/jobs/async-job/`
3. Add the case to the switch statement
All job types share a single Lambda function via `job.Dockerfile`. The CDK `cmd` parameter selects the entry point.
### Database migration
`prisma db push` is used for schema sync by default. The migration runner Lambda is invoked automatically during `cdk deploy` via CDK Trigger. For manual invocation, use the `MigrationCommand` from CDK outputs.
Schema changes: edit `prisma/schema.prisma` → run `npx prisma db push` locally → commit. The `zod-prisma-types` generator auto-generates Zod schemas from the Prisma schema. If you switch to `prisma migrate`, update the migration runner accordingly.
### Lambda environment
The webapp runs on Lambda behind CloudFront via Lambda Web Adapter (response streaming). `next.config.ts` uses `output: 'standalone'`. Build-time env vars (prefixed `NEXT_PUBLIC_`) are injected via CDK `ContainerImageBuild` build args — they cannot be changed at runtime.
### Real-time notifications
Server → client push uses AppSync Events. Server-side: `sendEvent(channelName, payload)` with IAM SigV4 signing. Client-side: `useEventBus` hook with Cognito user pool auth. The channel namespace is `event-bus/`.
## Documentation policy
- Do not document what can be derived from code. An agent can read the codebase.
- Enforce verifiable constraints with tests and linters, not prose.
- Code comments explain "why not" only — the non-obvious reason something was done a certain way.
- Before adding a line to this file, ask: "If I remove this line, will an agent make a mistake?" If no, don't add it. If a root cause is fixed, remove the corresponding line.
## Conventions
- PR titles and code comments in English.
- Issues and discussions in English or Japanese.
- PR titles follow [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `chore:`, etc.).
- UI components: use [shadcn/ui](https://ui.shadcn.com/). Do not introduce alternative component libraries.
- Logs: use JSON structured output.
- Dependencies: esbuild and Next.js bundle everything, so only packages with native binaries needed at Lambda runtime belong in `dependencies`. Everything else goes in `devDependencies`.
## Do not
- Do not bypass `authActionClient` for any mutation. No raw Prisma calls from Server Actions.
- Do not add `middleware.ts`. Route protection is handled by `proxy.ts` inside the Lambda runtime.
- Do not use `prisma migrate` commands unless you have explicitly switched from `prisma db push`. The default setup uses `prisma db push`.
- Do not hardcode AWS region or account IDs. Use CDK context or environment variables.
- Do not add `NEXT_PUBLIC_` env vars to `.env.local` for deployed builds — they must be set as CDK build args in `webapp.ts`.
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## [2.1.0](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/compare/v2.0.0...v2.1.0) (2026-03-22)
### Features
* add /update-snapshot comment trigger to update_snapshot workflow ([764a4fa](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/764a4fa0808b7fb11307f393208449588daa8b3c))
* add CloudWatch LogGroup with retention policy to Lambda functions ([#117](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/117)) ([53877bb](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/53877bb31b5af7cfbb5e80903be076e8ce1c38d6)), closes [#103](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/103)
* **database:** enable Data API and connection logging ([#123](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/123)) ([e32dc7a](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/e32dc7ad5ceb0c36fd287c18e64177e92f0c5ff0))
* increase webapp Lambda memory from 512MB to 1024MB ([#116](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/116)) ([03c5a00](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/03c5a007e141c25b9631a3b38680f62dfe22320f)), closes [#101](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/101)
### Bug Fixes
* add lambda:InvokeFunction permission for CloudFront OAC ([#83](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/83)) ([3cc66bf](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/3cc66bfb775f3a086f231783955b258136ddd266))
* **auth:** improve auth error handling and fix Link CORS issue ([#120](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/120)) ([84be605](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/84be6057d7416c7dd34a7eec3422144bce2f964c))
* disable Cognito self sign-up by default ([#115](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/115)) ([9396e6f](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/9396e6fbefe2feadb5e2eea4ccc03aa2a1c0888e)), closes [#106](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/106)
* prevent CloudFront cache poisoning for Next.js RSC responses ([#119](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/119)) ([70cddda](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/70cddda904ee816a763c53a7a36ce1ea183ff941))
* **prisma:** add retry for Aurora Serverless v2 connection errors ([#121](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/121)) ([7c05dfb](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/7c05dfb61949caf8b8f79a56ec7c2c1e88a04839))
* support Amazon Linux 2023 for NAT instance ([#81](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/81)) ([0c41aa8](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/0c41aa8ec1946883f397126c8e6ae91c0e96b1b0))
## 2.0.0 (2026-03-18)
### Features
* invalidate cloudfront caches when lambda configuration changes ([#63](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/63)) ([b76d122](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/b76d1223d1e00a6b77ee5f89b4d7f9678b01232a))
### Bug Fixes
* pass useNatInstance prop to MainStack ([#71](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/71)) ([23c9e31](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/23c9e31a73d44254cf45d84015bc6cd9c045880b))
* Workflow does not contain permissions ([#59](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/59)) ([34be1de](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/34be1deaf7e02320e097861b989e1e046bfa8488))
* Workflow does not contain permissions ([#60](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/60)) ([6fb8901](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/6fb89018d26428c069679a1443e2010cb4bb4fc5))
================================================
FILE: CODE_OF_CONDUCT.md
================================================
## Code of Conduct
This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
opensource-codeofconduct@amazon.com with any additional questions or comments.
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing Guidelines
Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
documentation, we greatly value feedback and contributions from our community.
Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
information to effectively respond to your bug report or contribution.
## Language policy
- PR titles and code comments: English
- Issues and discussions: English or Japanese
## Commit and PR conventions
PR titles follow [Conventional Commits](https://www.conventionalcommits.org/) (Angular style). Enforced by CI. Releases are automated via [Release Please](https://github.com/googleapis/release-please).
## Reporting Bugs/Feature Requests
We welcome you to use the GitHub issue tracker to report bugs or suggest features.
When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
* A reproducible test case or series of steps
* The version of our code being used
* Any modifications you've made relevant to the bug
* Anything unusual about your environment or deployment
## Contributing via Pull Requests
Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
1. You are working against the latest source on the *main* branch.
2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
To send us a pull request, please:
1. Fork the repository.
2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
3. Ensure local tests pass.
4. Commit to your fork using clear commit messages.
5. Send us a pull request, answering any default questions in the pull request interface.
6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
[creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
## Finding contributions to work on
Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start.
## Code of Conduct
This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
opensource-codeofconduct@amazon.com with any additional questions or comments.
## Security issue notifications
If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
## Licensing
See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
================================================
FILE: LICENSE
================================================
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
# Serverless Full Stack WebApp Starter Kit
[](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/actions/workflows/build.yml)
[](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/releases)
A serverless full-stack web app template you **copy and grow into your own app**. Not a framework — you own every file.
Copy, deploy with a single command, then replace the sample todo app with your own features.
## What you get
1. **Working sample app** — A todo app with authentication, DB CRUD, async jobs, and real-time notifications wired end-to-end. Designed as a readable reference for AI coding agents and humans alike.
2. **End-to-end type safety** — Types flow from Prisma ORM through Zod schemas and Server Actions to React components in a single chain.
3. **Serverless from day one** — Fully serverless architecture starting under $10/month that scales without operational overhead.
4. **Integrated DB migration** — Schema migration is integrated into the CDK deploy process via CDK Trigger, providing a development-to-production path out of the box.
You can refer to [the blog article](https://tmokmss.github.io/blog/posts/serverless-fullstack-webapp-architecture-2025/) for more details (also [Japanese version](https://tmokmss.hatenablog.com/entry/serverless-fullstack-webapp-architecture-2025)).
## Sample app
The kit includes a simple todo app to demonstrate how all components work together.
<img align="left" width="300" src="./.serverless-full-stack-webapp-starter-kit/docs/imgs/signin.png">
Sign in/up page redirects to Cognito Managed Login.
<br clear="left"/>
<img align="left" width="300" src="./.serverless-full-stack-webapp-starter-kit/docs/imgs/top.png">
After login, you can add, delete, and manage your todo items. The translate button triggers an async job and pushes a real-time notification to refresh the page.
<br clear="left"/>
## Architecture

| Service | Role |
|---------|------|
| [Aurora PostgreSQL Serverless v2](https://aws.amazon.com/rds/aurora/serverless/) | Relational database with Prisma ORM |
| [Next.js App Router](https://nextjs.org/docs/app) on [Lambda](https://aws.amazon.com/lambda/) | Unified frontend and backend |
| [CloudFront](https://aws.amazon.com/cloudfront/) + Lambda Function URL | Content delivery with response streaming |
| [Cognito](https://aws.amazon.com/cognito/) | Authentication (email by default, OIDC federation supported) |
| [AppSync Events](https://docs.aws.amazon.com/appsync/latest/eventapi/event-api-welcome.html) + Lambda | Async jobs and real-time notifications |
| [EventBridge](https://aws.amazon.com/eventbridge/) | Scheduled jobs |
| [CloudWatch](https://aws.amazon.com/cloudwatch/) + S3 | Access logging |
| [CDK](https://aws.amazon.com/cdk/) | Infrastructure as Code |
Fully serverless — high cost efficiency, scalability, and minimal operational overhead.
## Getting started
Prerequisites:
* [Node.js](https://nodejs.org/) (>= v20)
* [Docker](https://docs.docker.com/get-docker/)
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) with a configured IAM profile
### 1. Copy the kit
Use the GitHub template ("Use this template" button) or clone and copy:
```sh
git clone https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit.git my-app
cd my-app
rm -rf .git && git init
# Record the kit version in your initial commit for future reference
git add -A && git commit -m "Initial commit from serverless-full-stack-webapp-starter-kit vX.Y.Z"
```
### 2. Customize (optional)
- Update the application name (stack name, tags) in [`cdk/bin/cdk.ts`](cdk/bin/cdk.ts)
- Set a custom domain in `cdk/bin/cdk.ts`
- Remove `cdk.context.json` from `cdk/.gitignore` and commit it (recommended for your own project)
- Switch from `prisma db push` to `prisma migrate` if you need migration history
### 3. Deploy
```sh
cd cdk
npm ci
npx cdk bootstrap
npx cdk deploy --all
```
Initial deployment takes about 20 minutes. After success, you'll see:
```
✅ ServerlessWebappStarterKitStack
Outputs:
ServerlessWebappStarterKitStack.FrontendDomainName = https://web.example.com
ServerlessWebappStarterKitStack.DatabaseSecretsCommand = aws secretsmanager get-secret-value ...
ServerlessWebappStarterKitStack.DatabasePortForwardCommand = aws ssm start-session ...
```
Open the `FrontendDomainName` URL to try the sample app.
> **Note:** `DatabasePortForwardCommand` establishes a local connection to your RDS database, and `DatabaseSecretsCommand` retrieves database credentials from Secrets Manager.
### 4. Add your own features
See [`AGENTS.md`](./AGENTS.md) for development guide — local development setup, authentication patterns, async job setup, DB migration, and coding conventions.
To add social sign-in (Google, Facebook, etc.), see [Add social sign-in to a user pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-configuring-federation-with-social-idp.html).
## Maintenance policy
This kit follows [Semantic Versioning](https://semver.org/). Since users copy (not fork) this kit, breaking changes are introduced as new major versions without a lengthy deprecation cycle.
## Cost
Sample cost breakdown for us-east-1, one month, with cost-optimized configuration:
| Service | Usage Details | Monthly Cost [USD] |
|---------|--------------|-------------------|
| Aurora Serverless v2 | 0.5 ACU × 2 hour/day, 1GB storage | 3.6 |
| Cognito | 100 MAU | 1.5 |
| AppSync Events | 100 events/month, 10 hours connection/user/month | 0.02 |
| Lambda | 1024MB × 200ms/request | 0.15 |
| Lambda@Edge | 128MB × 50ms/request | 0.09 |
| VPC | NAT Instance (t4g.nano) × 1 | 3.02 |
| EventBridge | Scheduler 100 jobs/month | 0.0001 |
| CloudFront | Data transfer 1kB/request | 0.01 |
| **Total** | | **8.49** |
Assumes 100 users/month, 1000 requests/user. Costs could be further reduced with [Free Tier](https://aws.amazon.com/free/).
## Clean up
```sh
cd cdk
npx cdk destroy --force
```
## Maintainers
* [Kenji Kono (konokenj)](https://github.com/konokenj)
### Core contributors
* [Masashi Tomooka (tmokmss)](https://github.com/tmokmss) — original author
* [Kazuho Cryer-Shinozuka (badmintoncryer)](https://github.com/badmintoncryer)
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
Contributors (human and AI) **must** read [`.serverless-full-stack-webapp-starter-kit/design/DESIGN_PRINCIPLES.md`](./.serverless-full-stack-webapp-starter-kit/design/DESIGN_PRINCIPLES.md) before making changes. It defines the design decisions and constraints that govern this kit.
## Security
See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information.
## License
This library is licensed under the MIT-0 License. See the LICENSE file.
================================================
FILE: cdk/.gitignore
================================================
*.js
!jest.config.js
!lib/constructs/cf-lambda-furl-service/cf-function/*.js
*.d.ts
node_modules
# CDK asset staging directory
.cdk.staging
cdk.out
# Ignoring this file only because it is an OSS sample solution.
# It is generally recommended to store the cdk.context.json in git.
cdk.context.json
================================================
FILE: cdk/README.md
================================================
# Infrastructure as Code (AWS CDK)
This is the IaC project written in AWS Cloud Development Kit (CDK).
## Useful commands
* `npx cdk deploy`: deploy the infrastructure
* `npx cdk deploy --require-approval never`: deploy without confirmation (useful for automation)
* `npx cdk deploy --hotswap`: deploy with hotswap feature enabled (useful for development)
* `npx cdk watch`: watch your code and deploy every time changes are detected
* `npx cdk destroy`: delete the infrastructure
================================================
FILE: cdk/bin/cdk.ts
================================================
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { MainStack } from '../lib/main-stack';
import { UsEast1Stack } from '../lib/us-east-1-stack';
const app = new cdk.App();
interface EnvironmentProps {
account: string;
/**
* Custom domain name for the webapp and Cognito.
* You need to have a public Route53 hosted zone for the domain name in your AWS account.
*
* @default No custom domain name. When not specified, the stack automatically generates
* a random prefix for the Cognito domain (e.g., webapp-abc123def4.auth.us-west-2.amazoncognito.com)
* and uses the CloudFront default domain (e.g., d1234567890.cloudfront.net) for the webapp.
*/
domainName?: string;
/**
* Use a NAT instance instead of NAT Gateways.
* @default true
*/
useNatInstance?: boolean;
}
const props: EnvironmentProps = {
account: process.env.CDK_DEFAULT_ACCOUNT!,
// domainName: 'FIXME.example.com',
useNatInstance: true,
};
const virginia = new UsEast1Stack(app, 'ServerlessWebappStarterKitUsEast1Stack', {
env: {
account: props.account,
region: 'us-east-1',
},
crossRegionReferences: true,
domainName: props.domainName,
});
new MainStack(app, 'ServerlessWebappStarterKitStack', {
env: {
account: props.account,
region: process.env.CDK_DEFAULT_REGION,
},
crossRegionReferences: true,
sharedCertificate: virginia.certificate,
domainName: props.domainName,
useNatInstance: props.useNatInstance,
signPayloadHandler: virginia.signPayloadHandler,
});
cdk.Tags.of(app).add('Application', 'ServerlessFullStackWebappStarterKit');
// import { Aspects } from 'aws-cdk-lib';
// import { AwsSolutionsChecks } from 'cdk-nag';
// Aspects.of(app).add(new AwsSolutionsChecks());
================================================
FILE: cdk/cdk.json
================================================
{
"app": "npx ts-node --prefer-ts-exts bin/cdk.ts",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": [
"aws",
"aws-cn"
],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
"@aws-cdk/aws-redshift:columnId": true,
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true
}
}
================================================
FILE: cdk/jest.config.js
================================================
module.exports = {
testEnvironment: 'node',
roots: ['<rootDir>/test'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.tsx?$': 'ts-jest'
},
snapshotSerializers: ['<rootDir>/test/snapshot-plugin.ts']
};
================================================
FILE: cdk/lib/constructs/async-job.ts
================================================
import { Construct } from 'constructs';
import { CfnOutput, Duration, RemovalPolicy, TimeZone } from 'aws-cdk-lib';
import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs';
import { Architecture, DockerImageCode, DockerImageFunction, IFunction } from 'aws-cdk-lib/aws-lambda';
import { Platform } from 'aws-cdk-lib/aws-ecr-assets';
import { Database } from './database';
import { EventBus } from './event-bus';
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { join } from 'path';
import { Schedule, ScheduleExpression, ScheduleTargetInput } from 'aws-cdk-lib/aws-scheduler';
import { LambdaInvoke } from 'aws-cdk-lib/aws-scheduler-targets';
export interface AsyncJobProps {
readonly database: Database;
readonly eventBus: EventBus;
}
export class AsyncJob extends Construct {
readonly handler: IFunction;
constructor(scope: Construct, id: string, props: AsyncJobProps) {
super(scope, id);
const { database, eventBus } = props;
const handler = new DockerImageFunction(this, 'Handler', {
code: DockerImageCode.fromImageAsset(join('..', 'webapp'), {
cmd: ['async-job-runner.handler'],
platform: Platform.LINUX_ARM64,
file: 'job.Dockerfile',
}),
memorySize: 256,
timeout: Duration.minutes(10),
architecture: Architecture.ARM_64,
environment: {
...database.getLambdaEnvironment('main'),
EVENT_HTTP_ENDPOINT: eventBus.httpEndpoint,
},
vpc: database.cluster.vpc,
// limit concurrency to mitigate any possible EDoS attacks
reservedConcurrentExecutions: 1,
logGroup: new LogGroup(this, 'HandlerLogs', {
retention: RetentionDays.ONE_WEEK,
removalPolicy: RemovalPolicy.DESTROY,
}),
});
handler.connections.allowToDefaultPort(database);
eventBus.api.grantPublish(handler);
handler.addToRolePolicy(
new PolicyStatement({
actions: ['translate:TranslateText', 'comprehend:DetectDominantLanguage'],
resources: ['*'],
}),
);
new CfnOutput(this, 'HandlerArn', { value: handler.functionArn });
this.handler = handler;
// you can add scheduled jobs here.
this.addSchedule(
'SampleJob',
ScheduleExpression.cron({ minute: '0', hour: '0', day: '1', timeZone: TimeZone.ETC_UTC }),
);
}
public addSchedule(jobType: string, schedule: ScheduleExpression, payload?: any) {
return new Schedule(this, jobType, {
schedule,
target: new LambdaInvoke(this.handler, {
input: ScheduleTargetInput.fromObject({ jobType, payload }),
retryAttempts: 5,
}),
});
}
}
================================================
FILE: cdk/lib/constructs/auth/.gitignore
================================================
!prefix-generator.js
================================================
FILE: cdk/lib/constructs/auth/index.ts
================================================
import { UpdateUserPoolClientCommandInput } from '@aws-sdk/client-cognito-identity-provider';
import { CfnOutput, CfnResource, CustomResource, Duration, RemovalPolicy, Stack } from 'aws-cdk-lib';
import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager';
import { CfnManagedLoginBranding, ManagedLoginVersion, UserPool, UserPoolClient } from 'aws-cdk-lib/aws-cognito';
import { Code, Runtime, SingletonFunction } from 'aws-cdk-lib/aws-lambda';
import { CnameRecord, IHostedZone } from 'aws-cdk-lib/aws-route53';
import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from 'aws-cdk-lib/custom-resources';
import { Construct } from 'constructs';
import { readFileSync } from 'fs';
import { join } from 'path';
export interface AuthProps {
/**
* Route 53 hosted zone for custom domain.
*
* @default No custom domain. A random prefix will be automatically generated for the Cognito domain.
*/
readonly hostedZone?: IHostedZone;
/**
* ACM certificate for custom domain (must be in us-east-1 for Cognito).
*
* @default No custom domain.
*/
readonly sharedCertificate?: ICertificate;
}
export class Auth extends Construct {
readonly userPool: UserPool;
readonly client: UserPoolClient;
readonly domainName: string;
private callbackUrlCount = 0;
constructor(scope: Construct, id: string, props: AuthProps) {
super(scope, id);
const { hostedZone } = props;
const subDomain = 'auth';
let domainPrefix = '';
if (!hostedZone) {
// When we do not use a custom domain, we must make domainPrefix unique in the AWS region.
// To avoid a collision, we generate a random string with CFn custom resource.
// This allows the stack to work without requiring a custom domain setup.
const generator = new SingletonFunction(this, 'RandomStringGenerator', {
runtime: Runtime.NODEJS_22_X,
handler: 'index.handler',
timeout: Duration.seconds(5),
lambdaPurpose: 'RandomStringGenerator',
uuid: '11e9c903-f11a-4989-833c-985dddef5eb2',
code: Code.fromInline(readFileSync(join(__dirname, 'prefix-generator.js')).toString()),
});
const domainPrefixResource = new CustomResource(this, 'DomainPrefix', {
serviceToken: generator.functionArn,
resourceType: 'Custom::RandomString',
properties: { prefix: 'webapp-', length: 10 },
serviceTimeout: Duration.seconds(10),
});
domainPrefix = domainPrefixResource.getAttString('generated');
}
this.domainName = hostedZone
? `${subDomain}.${hostedZone.zoneName}`
: `${domainPrefix}.auth.${Stack.of(this).region}.amazoncognito.com`;
const userPool = new UserPool(this, 'UserPool', {
passwordPolicy: {
requireUppercase: true,
requireSymbols: true,
requireDigits: true,
minLength: 8,
},
// Set to true to allow self sign-up.
// When false, administrators must create users via the Cognito console or API.
selfSignUpEnabled: false,
signInAliases: {
username: false,
email: true,
},
removalPolicy: RemovalPolicy.DESTROY,
});
const client = userPool.addClient(`Client`, {
idTokenValidity: Duration.days(1),
authFlows: {
userPassword: true,
userSrp: true,
},
oAuth: {
flows: {
authorizationCodeGrant: true,
},
callbackUrls: ['http://localhost/dummy'],
logoutUrls: ['http://localhost/dummy'],
},
});
this.client = client;
this.userPool = userPool;
const domain = userPool.addDomain('CognitoDomain', {
...(hostedZone && props.sharedCertificate
? {
customDomain: {
domainName: this.domainName,
certificate: props.sharedCertificate,
},
}
: {
cognitoDomain: {
domainPrefix,
},
}),
managedLoginVersion: ManagedLoginVersion.NEWER_MANAGED_LOGIN,
});
if (hostedZone) {
new CnameRecord(this, 'CognitoDomainRecord', {
zone: hostedZone,
recordName: subDomain,
domainName: domain.cloudFrontEndpoint,
});
}
new CfnManagedLoginBranding(this, 'Branding', {
userPoolId: this.userPool.userPoolId,
clientId: client.userPoolClientId,
useCognitoProvidedValues: true,
});
new CfnOutput(this, 'UserPoolId', { value: userPool.userPoolId });
new CfnOutput(this, 'UserPoolClientId', { value: client.userPoolClientId });
new CfnOutput(this, 'UserPoolDomainName', { value: this.domainName });
}
public addAllowedCallbackUrls(callbackUrl: string, logoutUrl: string) {
const resource = this.client.node.defaultChild;
if (!CfnResource.isCfnResource(resource)) {
throw new Error('Expected CfnResource');
}
resource.addPropertyOverride(`CallbackURLs.${this.callbackUrlCount}`, callbackUrl);
resource.addPropertyOverride(`LogoutURLs.${this.callbackUrlCount}`, logoutUrl);
this.callbackUrlCount += 1;
}
public updateAllowedCallbackUrls(callbackUrls: string[], logoutUrls: string[]) {
// Lambda depends on userPoolClientId but userPoolClient depends on the CloudFront domain name (callback URL) which depends on Lambda (fURL).
// To avoid the circular dependency, we update the callback URL after a userPoolClientId is created.
// We only use this when custom domain is not used.
new AwsCustomResource(this, 'UpdateCallbackUrls', {
onUpdate: {
service: '@aws-sdk/client-cognito-identity-provider',
action: 'updateUserPoolClient',
parameters: {
ClientId: this.client.userPoolClientId,
UserPoolId: this.userPool.userPoolId,
AllowedOAuthFlows: ['code'],
AllowedOAuthFlowsUserPoolClient: true,
AllowedOAuthScopes: ['profile', 'phone', 'email', 'openid', 'aws.cognito.signin.user.admin'],
ExplicitAuthFlows: ['ALLOW_USER_PASSWORD_AUTH', 'ALLOW_USER_SRP_AUTH', 'ALLOW_REFRESH_TOKEN_AUTH'],
CallbackURLs: callbackUrls,
LogoutURLs: logoutUrls,
SupportedIdentityProviders: ['COGNITO'],
TokenValidityUnits: {
IdToken: 'minutes',
},
IdTokenValidity: 1440,
} satisfies UpdateUserPoolClientCommandInput,
physicalResourceId: PhysicalResourceId.of(this.userPool.userPoolId),
},
policy: AwsCustomResourcePolicy.fromSdkCalls({
resources: [this.userPool.userPoolArn],
}),
});
}
}
================================================
FILE: cdk/lib/constructs/auth/prefix-generator.js
================================================
const response = require('cfn-response');
const crypto = require('crypto');
exports.handler = async function (event, context) {
try {
console.log(event);
if (event.RequestType == 'Delete') {
return await response.send(event, context, response.SUCCESS);
}
const prefix = event.ResourceProperties.prefix ?? '';
const length = event.ResourceProperties.length ?? '8';
const generate = () => {
const random = crypto.randomBytes(parseInt(length)).toString('hex');
return `${prefix}${random.slice(0, length)}`;
};
if (event.RequestType == 'Create') {
const generated = generate();
return await response.send(event, context, response.SUCCESS, { generated }, generated);
}
if (event.RequestType == 'Update') {
const current = event.PhysicalResourceId;
if (current.startsWith(prefix)) {
return await response.send(event, context, response.SUCCESS, { generated: current }, current);
}
const generated = generate();
return await response.send(event, context, response.SUCCESS, { generated }, generated);
}
} catch (e) {
console.log(e);
await response.send(event, context, response.FAILED);
}
};
================================================
FILE: cdk/lib/constructs/cf-lambda-furl-service/edge-function.ts
================================================
import { Stack } from 'aws-cdk-lib';
import { PolicyStatement, ServicePrincipal, Role } from 'aws-cdk-lib/aws-iam';
import { Runtime, IVersion, Version, Architecture } from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
import { AwsCustomResource, PhysicalResourceId, AwsCustomResourcePolicy } from 'aws-cdk-lib/custom-resources';
import { Construct } from 'constructs';
// L@E can only be deployed to us-east-1 region.
const StackRegion = 'us-east-1';
export interface EdgeFunctionProps {
entryPath: string;
}
export class EdgeFunction extends Construct {
private readonly functionVersionParameter: StringParameter;
constructor(scope: Construct, id: string, props: EdgeFunctionProps) {
super(scope, id);
if (Stack.of(this).region !== StackRegion) {
throw new Error('EdgeFunction can only be deployed to us-east-1 region.');
}
const handler = new NodejsFunction(this, 'Handler', {
runtime: Runtime.NODEJS_22_X,
entry: props.entryPath,
});
handler.currentVersion;
this.functionVersionParameter = new StringParameter(this, 'FunctionVersion', {
stringValue: handler.currentVersion.edgeArn,
});
const statement = new PolicyStatement();
const edgeLambdaServicePrincipal = new ServicePrincipal('edgelambda.amazonaws.com');
statement.addPrincipals(edgeLambdaServicePrincipal);
statement.addActions(edgeLambdaServicePrincipal.assumeRoleAction);
(handler.role as Role).assumeRolePolicy!.addStatements(statement);
}
public versionArn(scope: Construct) {
const id = `VersionArn${this.functionVersionParameter.node.addr}`;
const existing = Stack.of(scope).node.tryFindChild(id) as IVersion;
if (existing) {
return existing;
}
const lookup = new AwsCustomResource(Stack.of(scope), `Lookup${id}`, {
onUpdate: {
// will also be called for a CREATE event
service: 'SSM',
action: 'getParameter',
parameters: {
Name: this.functionVersionParameter.parameterName,
},
// it is impossible to know when the parameter is updated.
// so we need to get the value on every deployment.
physicalResourceId: PhysicalResourceId.of(`${Date.now()}`),
region: StackRegion,
},
policy: AwsCustomResourcePolicy.fromSdkCalls({
resources: [this.functionVersionParameter.parameterArn],
}),
});
return Version.fromVersionArn(Stack.of(scope), id, lookup.getResponseField('Parameter.Value'));
}
}
================================================
FILE: cdk/lib/constructs/cf-lambda-furl-service/lambda/sign-payload.ts
================================================
import type { CloudFrontRequestHandler } from 'aws-lambda';
import { createHash } from 'crypto';
const hashPayload = (payload: Buffer) => {
return createHash('sha256').update(payload).digest('hex');
};
export const handler: CloudFrontRequestHandler = async (event) => {
const request = event.Records[0].cf.request;
const body = request.body?.data ?? '';
const hashedBody = hashPayload(Buffer.from(body, 'base64'));
request.headers['x-amz-content-sha256'] = [{ key: 'x-amz-content-sha256', value: hashedBody }];
// LWA replaces authorization2 to authorization again
// if (request.headers['authorization'] != null) {
// request.headers['authorization2'] = [{ key: 'authorization2', value: request.headers['authorization'][0].value }];
// delete request.headers['authorization'];
// }
return request;
};
================================================
FILE: cdk/lib/constructs/cf-lambda-furl-service/service.ts
================================================
import { Construct } from 'constructs';
import { Aws, Duration } from 'aws-cdk-lib';
import { FunctionUrlAuthType, Function, InvokeMode, CfnPermission } from 'aws-cdk-lib/aws-lambda';
import {
AllowedMethods,
CacheCookieBehavior,
CacheHeaderBehavior,
CachePolicy,
CacheQueryStringBehavior,
Distribution,
Function as CfFunction,
FunctionCode as CfFunctionCode,
FunctionEventType,
FunctionRuntime,
LambdaEdgeEventType,
OriginRequestPolicy,
SecurityPolicyProtocol,
} from 'aws-cdk-lib/aws-cloudfront';
import { FunctionUrlOrigin } from 'aws-cdk-lib/aws-cloudfront-origins';
import * as path from 'path';
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
import { ARecord, IHostedZone, RecordTarget } from 'aws-cdk-lib/aws-route53';
import { CloudFrontTarget } from 'aws-cdk-lib/aws-route53-targets';
import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { EdgeFunction } from './edge-function';
import { AwsCustomResource, PhysicalResourceId, AwsCustomResourcePolicy } from 'aws-cdk-lib/custom-resources';
export interface CloudFrontLambdaFunctionUrlServiceProps {
/**
* Subdomain name for the service. If not specified, the root domain will be used.
*
* @default use root domain
*/
subDomain?: string;
handler: Function;
/**
* This should be unique across the app
*/
serviceName: string;
/**
* @default basic auth is disabled
*/
basicAuthUsername?: string;
basicAuthPassword?: string;
/**
* Route 53 hosted zone for custom domain.
*
* @default No custom domain. CloudFront's default domain will be used.
*/
hostedZone?: IHostedZone;
/**
* ACM certificate for custom domain (must be in us-east-1 for CloudFront).
*
* @default No custom domain.
*/
certificate?: ICertificate;
signPayloadHandler: EdgeFunction;
accessLogBucket: Bucket;
}
export class CloudFrontLambdaFunctionUrlService extends Construct {
public readonly urlParameter: StringParameter;
public readonly url: string;
public readonly domainName: string;
constructor(scope: Construct, id: string, props: CloudFrontLambdaFunctionUrlServiceProps) {
super(scope, id);
const { handler, serviceName, subDomain, hostedZone, certificate, accessLogBucket, signPayloadHandler } = props;
let domainName = '';
if (hostedZone) {
domainName = subDomain ? `${subDomain}.${hostedZone.zoneName}` : hostedZone.zoneName;
}
const furl = handler.addFunctionUrl({
authType: FunctionUrlAuthType.AWS_IAM,
invokeMode: InvokeMode.RESPONSE_STREAM,
});
const origin = FunctionUrlOrigin.withOriginAccessControl(furl, {
connectionTimeout: Duration.seconds(6),
readTimeout: Duration.seconds(60),
});
const cachePolicy = new CachePolicy(this, 'SharedCachePolicy', {
queryStringBehavior: CacheQueryStringBehavior.all(),
headerBehavior: CacheHeaderBehavior.allowList(
// CachePolicy.USE_ORIGIN_CACHE_CONTROL_HEADERS_QUERY_STRINGS contains Host header here,
// making it impossible to use with API Gateway
'authorization',
'Origin',
'X-HTTP-Method-Override',
'X-HTTP-Method',
'X-Method-Override',
// Hashed Next.js RSC headers set by the CloudFront Function below.
// See cf-function/cache-key.js for details.
'x-nextjs-cache-key',
),
defaultTtl: Duration.seconds(0),
cookieBehavior: CacheCookieBehavior.all(),
enableAcceptEncodingBrotli: true,
enableAcceptEncodingGzip: true,
});
// CloudFront Function to hash Next.js RSC headers into a single cache key header.
// This prevents cache poisoning between HTML and RSC flight responses while
// staying within CloudFront's 10-header limit on Cache Policies.
const cacheKeyFunction = new CfFunction(this, 'CacheKeyFunction', {
runtime: FunctionRuntime.JS_2_0,
code: CfFunctionCode.fromFile({ filePath: path.join(__dirname, 'cf-function', 'cache-key.js') }),
});
const distribution = new Distribution(this, 'Resource', {
comment: `CloudFront for ${serviceName}`,
defaultBehavior: {
origin,
cachePolicy,
allowedMethods: AllowedMethods.ALLOW_ALL,
originRequestPolicy: OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
functionAssociations: [
{
function: cacheKeyFunction,
eventType: FunctionEventType.VIEWER_REQUEST,
},
],
edgeLambdas: [
{
functionVersion: signPayloadHandler.versionArn(this),
eventType: LambdaEdgeEventType.ORIGIN_REQUEST,
includeBody: true,
},
],
},
// errorResponses: [{ httpStatus: 404, responsePagePath: '/', responseHttpStatus: 200 }],
logBucket: accessLogBucket,
logFilePrefix: `${serviceName}/`,
...(hostedZone ? { certificate: certificate, domainNames: [domainName] } : {}),
minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021,
});
// Starting October 2025, new function URLs require both lambda:InvokeFunctionUrl
// and lambda:InvokeFunction permissions for CloudFront OAC.
// CDK's FunctionUrlOrigin.withOriginAccessControl only adds lambda:InvokeFunctionUrl,
// so we explicitly add lambda:InvokeFunction here.
// See: https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html
new CfnPermission(this, 'InvokeFunctionPermission', {
action: 'lambda:InvokeFunction',
functionName: handler.functionArn,
principal: 'cloudfront.amazonaws.com',
sourceArn: `arn:${Aws.PARTITION}:cloudfront::${Aws.ACCOUNT_ID}:distribution/${distribution.distributionId}`,
});
if (hostedZone) {
new ARecord(this, 'Record', {
zone: hostedZone,
target: RecordTarget.fromAlias(new CloudFrontTarget(distribution)),
recordName: subDomain,
});
} else {
domainName = distribution.domainName;
}
// Invalidate CloudFront when Lambda function version changes
new AwsCustomResource(this, 'CloudFrontInvalidation', {
onUpdate: {
service: 'cloudfront',
action: 'createInvalidation',
parameters: {
DistributionId: distribution.distributionId,
InvalidationBatch: {
CallerReference: `${handler.currentVersion.version}`,
Paths: {
Quantity: 1,
Items: ['/*'],
},
},
},
physicalResourceId: PhysicalResourceId.of('invalidation'),
},
policy: AwsCustomResourcePolicy.fromSdkCalls({
resources: [distribution.distributionArn],
}),
});
this.url = `https://${domainName}`;
this.domainName = domainName;
}
}
================================================
FILE: cdk/lib/constructs/database.ts
================================================
import { CfnOutput, Stack, Token } from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import { Construct } from 'constructs';
interface DatabaseProps {
vpc: ec2.IVpc;
}
export class Database extends Construct implements ec2.IConnectable {
readonly cluster: rds.DatabaseCluster;
readonly secret: secretsmanager.ISecret;
readonly connections: ec2.Connections;
constructor(scope: Construct, id: string, props: DatabaseProps) {
super(scope, id);
const vpc = props.vpc;
const engine = rds.DatabaseClusterEngine.auroraPostgres({ version: rds.AuroraPostgresEngineVersion.VER_16_6 });
const cluster = new rds.DatabaseCluster(this, 'Cluster', {
engine,
writer: rds.ClusterInstance.serverlessV2('Writer', {
enablePerformanceInsights: true,
autoMinorVersionUpgrade: true,
}),
serverlessV2MinCapacity: 0,
vpc,
vpcSubnets: vpc.selectSubnets({ subnets: vpc.isolatedSubnets.concat(vpc.privateSubnets) }),
storageEncrypted: true,
// Exclude some more special characters from password string to avoid from URI encoding issue
// see: https://www.prisma.io/docs/orm/reference/connection-urls#special-characters
credentials: rds.Credentials.fromUsername(engine.defaultUsername ?? 'admin', {
excludeCharacters: ' %+~`#$&*()|[]{}:;<>?!\'/@"\\,=^',
}),
enableDataApi: true,
cloudwatchLogsExports: ['postgresql'],
cloudwatchLogsRetention: logs.RetentionDays.ONE_WEEK,
parameterGroup: new rds.ParameterGroup(this, 'ParameterGroup', {
engine,
parameters: {
// Close idle connection after 60 seconds for Aurora auto-pause
idle_session_timeout: '60000',
log_connections: '1',
log_disconnections: '1',
},
}),
});
this.cluster = cluster;
this.secret = cluster.secret!;
this.connections = this.cluster.connections;
const host = new ec2.BastionHostLinux(this, 'BastionHost', {
vpc,
machineImage: ec2.MachineImage.latestAmazonLinux2023({ cpuType: ec2.AmazonLinuxCpuType.ARM_64 }),
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.NANO),
blockDevices: [
{
deviceName: '/dev/sdf',
volume: ec2.BlockDeviceVolume.ebs(8, {
encrypted: true,
}),
},
],
});
this.connections.allowDefaultPortFrom(host);
new CfnOutput(this, 'PortForwardCommand', {
value: `aws ssm start-session --region ${Stack.of(this).region} --target ${
host.instanceId
} --document-name AWS-StartPortForwardingSessionToRemoteHost --parameters '{"portNumber":["${
cluster.clusterEndpoint.port
}"], "localPortNumber":["5433"], "host": ["${cluster.clusterEndpoint.hostname}"]}'`,
});
new CfnOutput(this, 'DatabaseSecretsCommand', {
value: `aws secretsmanager get-secret-value --secret-id ${cluster.secret!.secretName} --region ${
Stack.of(this).region
}`,
});
}
public getConnectionInfo() {
return {
// We use direct reference for host and port because using only secret here results in failure of refreshing values.
// Also refer to: https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/369
host: this.cluster.clusterEndpoint.hostname,
port: Token.asString(this.cluster.clusterEndpoint.port),
engine: this.secret.secretValueFromJson('engine').unsafeUnwrap(),
username: this.secret.secretValueFromJson('username').unsafeUnwrap(),
password: this.secret.secretValueFromJson('password').unsafeUnwrap(),
};
}
public getLambdaEnvironment(databaseName: string) {
const conn = this.getConnectionInfo();
// connection_limit=1: Each Lambda instance handles one request at a time
// connect_timeout=30: Aurora Serverless v2 auto-pause resume takes ~15s (longer after 24h+ pause)
// https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless-v2-auto-pause.html
const option = '?connection_limit=1&connect_timeout=30';
return {
DATABASE_HOST: conn.host,
DATABASE_NAME: databaseName,
DATABASE_USER: conn.username,
DATABASE_PASSWORD: conn.password,
DATABASE_ENGINE: conn.engine,
DATABASE_PORT: conn.port,
DATABASE_OPTION: option,
DATABASE_URL: `${conn.engine}://${conn.username}:${conn.password}@${conn.host}:${conn.port}/${databaseName}${option}`,
};
}
}
================================================
FILE: cdk/lib/constructs/event-bus/handler.mjs
================================================
import { util } from '@aws-appsync/utils';
/**
* Allow subscription only for the channels that
* 1. begin with /public
* 2. begin with /user/<userId>
* https://docs.aws.amazon.com/appsync/latest/eventapi/channel-namespace-handlers.html
*/
export function onSubscribe(ctx) {
if (ctx.info.channel.path.startsWith(`/event-bus/public`)) {
return;
}
if (ctx.info.channel.path.startsWith(`/event-bus/user/${ctx.identity.username}`)) {
return;
}
console.log(`user ${ctx.identity.username} tried connecting to wrong channel: ${ctx.channel}`);
util.unauthorized();
}
================================================
FILE: cdk/lib/constructs/event-bus/index.ts
================================================
import { Construct } from 'constructs';
import * as appsync from 'aws-cdk-lib/aws-appsync';
import { CfnOutput, CfnResource, Names, Stack } from 'aws-cdk-lib';
import { IUserPool } from 'aws-cdk-lib/aws-cognito';
import { join } from 'path';
export interface EventBusProps {}
export class EventBus extends Construct {
public readonly httpEndpoint: string;
public readonly api: appsync.EventApi;
private userPoolCount = 0;
constructor(scope: Construct, id: string, props: EventBusProps) {
super(scope, id);
const api = new appsync.EventApi(this, 'Api', {
apiName: Names.uniqueResourceName(this, { maxLength: 30 }),
authorizationConfig: {
authProviders: [
{
authorizationType: appsync.AppSyncAuthorizationType.IAM,
},
],
connectionAuthModeTypes: [appsync.AppSyncAuthorizationType.IAM],
defaultPublishAuthModeTypes: [appsync.AppSyncAuthorizationType.IAM],
defaultSubscribeAuthModeTypes: [appsync.AppSyncAuthorizationType.IAM],
},
});
new appsync.ChannelNamespace(this, 'Namespace', {
api,
channelNamespaceName: 'event-bus',
code: appsync.Code.fromAsset(join(__dirname, 'handler.mjs')),
});
this.httpEndpoint = `https://${api.httpDns}`;
this.api = api;
new CfnOutput(this, 'HttpEndpoint', { value: this.httpEndpoint });
}
public addUserPoolProvider(userPool: IUserPool) {
if (this.userPoolCount == 0) {
(this.api.node.defaultChild as CfnResource).addPropertyOverride('EventConfig.ConnectionAuthModes.1', {
AuthType: 'AMAZON_COGNITO_USER_POOLS',
});
(this.api.node.defaultChild as CfnResource).addPropertyOverride('EventConfig.DefaultSubscribeAuthModes.1', {
AuthType: 'AMAZON_COGNITO_USER_POOLS',
});
}
this.userPoolCount += 1;
(this.api.node.defaultChild as CfnResource).addPropertyOverride(`EventConfig.AuthProviders.${this.userPoolCount}`, {
AuthType: 'AMAZON_COGNITO_USER_POOLS',
CognitoConfig: {
AwsRegion: Stack.of(this).region,
UserPoolId: userPool.userPoolId,
},
});
}
}
================================================
FILE: cdk/lib/constructs/webapp.ts
================================================
import { IgnoreMode, Duration, CfnOutput, Stack, RemovalPolicy } from 'aws-cdk-lib';
import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs';
import { Platform } from 'aws-cdk-lib/aws-ecr-assets';
import { DockerImageFunction, DockerImageCode, Architecture } from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
import { readFileSync } from 'fs';
import { CloudFrontLambdaFunctionUrlService } from './cf-lambda-furl-service/service';
import { IHostedZone } from 'aws-cdk-lib/aws-route53';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { Database } from './database';
import { EdgeFunction } from './cf-lambda-furl-service/edge-function';
import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager';
import { Auth } from './auth/';
import { ContainerImageBuild } from 'deploy-time-build';
import { join } from 'path';
import { EventBus } from './event-bus/';
import { AsyncJob } from './async-job';
import { Trigger } from 'aws-cdk-lib/triggers';
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from 'aws-cdk-lib/custom-resources';
export interface WebappProps {
database: Database;
signPayloadHandler: EdgeFunction;
accessLogBucket: Bucket;
auth: Auth;
eventBus: EventBus;
asyncJob: AsyncJob;
/**
* Route 53 hosted zone for custom domain.
*
* @default No custom domain. The webapp will use CloudFront's default domain (e.g., d1234567890.cloudfront.net).
*/
hostedZone?: IHostedZone;
/**
* ACM certificate for custom domain (must be in us-east-1 for CloudFront).
*
* @default No custom domain.
*/
certificate?: ICertificate;
/**
* Subdomain name for the webapp. If not specified, the root domain will be used.
*
* @default Use root domain
*/
subDomain?: string;
}
export class Webapp extends Construct {
public readonly baseUrl: string;
constructor(scope: Construct, id: string, props: WebappProps) {
super(scope, id);
const { database, hostedZone, auth, subDomain, eventBus, asyncJob } = props;
// Use ContainerImageBuild to inject deploy-time values in the build environment
const image = new ContainerImageBuild(this, 'Build', {
directory: join('..', 'webapp'),
platform: Platform.LINUX_ARM64,
ignoreMode: IgnoreMode.DOCKER,
exclude: readFileSync(join('..', 'webapp', '.dockerignore'))
.toString()
.split('\n'),
tagPrefix: 'webapp-starter-',
buildArgs: {
ALLOWED_ORIGIN_HOST: hostedZone ? `*.${hostedZone.zoneName}` : '*.cloudfront.net',
SKIP_TS_BUILD: 'true',
NEXT_PUBLIC_EVENT_HTTP_ENDPOINT: eventBus.httpEndpoint,
NEXT_PUBLIC_AWS_REGION: Stack.of(this).region,
},
});
const handler = new DockerImageFunction(this, 'Handler', {
code: image.toLambdaDockerImageCode(),
timeout: Duration.minutes(3),
environment: {
...database.getLambdaEnvironment('main'),
COGNITO_DOMAIN: auth.domainName,
USER_POOL_ID: auth.userPool.userPoolId,
USER_POOL_CLIENT_ID: auth.client.userPoolClientId,
ASYNC_JOB_HANDLER_ARN: asyncJob.handler.functionArn,
},
vpc: database.cluster.vpc,
memorySize: 1024,
architecture: Architecture.ARM_64,
logGroup: new LogGroup(this, 'HandlerLogs', {
retention: RetentionDays.ONE_WEEK,
removalPolicy: RemovalPolicy.DESTROY,
}),
});
handler.connections.allowToDefaultPort(database);
asyncJob.handler.grantInvoke(handler);
const service = new CloudFrontLambdaFunctionUrlService(this, 'Resource', {
subDomain,
handler,
serviceName: 'Webapp',
hostedZone,
certificate: props.certificate,
accessLogBucket: props.accessLogBucket,
signPayloadHandler: props.signPayloadHandler,
});
this.baseUrl = service.url;
if (hostedZone) {
auth.addAllowedCallbackUrls(
`http://localhost:3010/api/auth/sign-in-callback`,
`http://localhost:3010/api/auth/sign-out-callback`,
);
auth.addAllowedCallbackUrls(
`${this.baseUrl}/api/auth/sign-in-callback`,
`${this.baseUrl}/api/auth/sign-out-callback`,
);
handler.addEnvironment('AMPLIFY_APP_ORIGIN', service.url);
} else {
auth.updateAllowedCallbackUrls(
[`${this.baseUrl}/api/auth/sign-in-callback`, `http://localhost:3010/api/auth/sign-in-callback`],
[`${this.baseUrl}/api/auth/sign-out-callback`, `http://localhost:3010/api/auth/sign-out-callback`],
);
const originSourceParameter = new StringParameter(this, 'OriginSourceParameter', {
stringValue: 'dummy',
});
originSourceParameter.grantRead(handler);
handler.addEnvironment('AMPLIFY_APP_ORIGIN_SOURCE_PARAMETER', originSourceParameter.parameterName);
// We need to pass AMPLIFY_APP_ORIGIN environment variable for callback URL,
// but we cannot know CloudFront domain before deploying Lambda function.
// To avoid the circular dependency, we fetch the domain name on runtime.
new AwsCustomResource(this, 'UpdateAmplifyOriginSourceParameter', {
onUpdate: {
service: 'ssm',
action: 'putParameter',
parameters: {
Name: originSourceParameter.parameterName,
Value: service.url,
Overwrite: true,
},
physicalResourceId: PhysicalResourceId.of(originSourceParameter.parameterName),
},
policy: AwsCustomResourcePolicy.fromSdkCalls({
resources: [originSourceParameter.parameterArn],
}),
});
}
const migrationRunner = new DockerImageFunction(this, 'MigrationRunner', {
code: DockerImageCode.fromImageAsset(join('..', 'webapp'), {
platform: Platform.LINUX_ARM64,
cmd: ['migration-runner.handler'],
file: 'job.Dockerfile',
}),
architecture: Architecture.ARM_64,
timeout: Duration.minutes(5),
environment: {
...database.getLambdaEnvironment('main'),
},
vpc: database.cluster.vpc,
memorySize: 256,
logGroup: new LogGroup(this, 'MigrationRunnerLogs', {
retention: RetentionDays.ONE_WEEK,
removalPolicy: RemovalPolicy.DESTROY,
}),
});
migrationRunner.connections.allowToDefaultPort(database);
// Run database migration during CDK deployment
// The Trigger construct automatically invokes the migration runner with default payload (command: 'deploy')
// To manually run migrations with different commands (e.g., 'force'), use the AWS CLI command shown in the CDK output below
const trigger = new Trigger(this, 'MigrationTrigger', {
handler: migrationRunner,
});
// make sure migration is executed after the database cluster is available.
trigger.node.addDependency(database.cluster);
// Output migration-related information for manual invocation
// Available commands: "deploy" (default), "force" (with --accept-data-loss)
// Example: aws lambda invoke --function-name <FUNCTION_NAME> --payload '{"command":"force"}' --cli-binary-format raw-in-base64-out /dev/stdout
new CfnOutput(Stack.of(this), 'MigrationFunctionName', { value: migrationRunner.functionName });
new CfnOutput(Stack.of(this), 'MigrationCommand', {
value: `aws lambda invoke --function-name ${migrationRunner.functionName} --payload '{"command":"deploy"}' --cli-binary-format raw-in-base64-out /dev/stdout`,
});
}
}
================================================
FILE: cdk/lib/main-stack.ts
================================================
import { CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';
import { BlockPublicAccess, Bucket, BucketEncryption, ObjectOwnership } from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';
import { AsyncJob } from './constructs/async-job';
import { Auth } from './constructs/auth/';
import { Database } from './constructs/database';
import { InstanceClass, InstanceSize, InstanceType, NatProvider, UserData, Vpc } from 'aws-cdk-lib/aws-ec2';
import { HostedZone } from 'aws-cdk-lib/aws-route53';
import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager';
import { Webapp } from './constructs/webapp';
import { EdgeFunction } from './constructs/cf-lambda-furl-service/edge-function';
import { EventBus } from './constructs/event-bus/';
interface MainStackProps extends StackProps {
readonly signPayloadHandler: EdgeFunction;
/**
* Custom domain name for the webapp and Cognito.
*
* @default No custom domain. CloudFront and Cognito will use their default domains.
*/
readonly domainName?: string;
/**
* ACM certificate for custom domain (must be in us-east-1).
*
* @default No custom domain.
*/
readonly sharedCertificate?: ICertificate;
/**
* Use a NAT instance instead of NAT Gateways for cost optimization.
*
* @default true
*/
readonly useNatInstance?: boolean;
}
export class MainStack extends Stack {
constructor(scope: Construct, id: string, props: MainStackProps) {
super(scope, id, { description: 'Serverless fullstack webapp stack (uksb-1tupboc47)', ...props });
const { useNatInstance = true } = props;
const hostedZone = props.domainName
? HostedZone.fromLookup(this, 'HostedZone', {
domainName: props.domainName,
})
: undefined;
const accessLogBucket = new Bucket(this, 'AccessLogBucket', {
encryption: BucketEncryption.S3_MANAGED,
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
enforceSSL: true,
removalPolicy: RemovalPolicy.DESTROY,
objectOwnership: ObjectOwnership.OBJECT_WRITER,
autoDeleteObjects: true,
});
// Custom user data for NAT instance to support Amazon Linux 2023.
// CDK's default user data uses `route` command which requires net-tools package,
// but AL2023 doesn't have net-tools pre-installed. We use `ip route` instead.
// Retry yum install to handle RPM lock conflicts during boot.
const natUserData = UserData.forLinux();
natUserData.addCommands(
// Retry yum install up to 5 times with 10 second intervals
'for i in {1..5}; do yum install iptables-services -y && break || sleep 10; done',
'systemctl enable iptables',
'systemctl start iptables',
'echo "net.ipv4.ip_forward=1" > /etc/sysctl.d/custom-ip-forwarding.conf',
'sysctl -p /etc/sysctl.d/custom-ip-forwarding.conf',
"IFACE=$(ip route show default | awk '{print $5}')",
'/sbin/iptables -t nat -A POSTROUTING -o $IFACE -j MASQUERADE',
'/sbin/iptables -F FORWARD',
'service iptables save',
);
const vpc = new Vpc(this, `Vpc`, {
...(useNatInstance
? {
natGatewayProvider: NatProvider.instanceV2({
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.NANO),
associatePublicIpAddress: true,
userData: natUserData,
}),
natGateways: 1,
}
: {}),
});
const database = new Database(this, 'Database', { vpc });
const auth = new Auth(this, 'Auth', {
hostedZone,
sharedCertificate: props.sharedCertificate,
});
const eventBus = new EventBus(this, 'EventBus', {});
eventBus.addUserPoolProvider(auth.userPool);
const asyncJob = new AsyncJob(this, 'AsyncJob', { database: database, eventBus });
const webapp = new Webapp(this, 'Webapp', {
database,
hostedZone,
certificate: props.sharedCertificate,
signPayloadHandler: props.signPayloadHandler,
accessLogBucket,
auth,
eventBus,
asyncJob,
subDomain: 'web',
});
new CfnOutput(this, 'FrontendDomainName', {
value: webapp.baseUrl,
});
}
}
================================================
FILE: cdk/lib/us-east-1-stack.ts
================================================
import * as cdk from 'aws-cdk-lib';
import { Certificate, CertificateValidation, ICertificate } from 'aws-cdk-lib/aws-certificatemanager';
import { ARecord, HostedZone, RecordTarget } from 'aws-cdk-lib/aws-route53';
import { Construct } from 'constructs';
import { EdgeFunction } from './constructs/cf-lambda-furl-service/edge-function';
import { join } from 'path';
interface UsEast1StackProps extends cdk.StackProps {
/**
* Custom domain name for the webapp and Cognito.
*
* @default No custom domain. CloudFront and Cognito will use their default domains.
*/
domainName?: string;
}
export class UsEast1Stack extends cdk.Stack {
/**
* the ACM certificate for CloudFront (it must be deployed in us-east-1).
* undefined if domainName is not set.
*/
public readonly certificate: ICertificate | undefined = undefined;
/**
* the signer L@E function (it must be deployed in us-east-1).
*/
public readonly signPayloadHandler: EdgeFunction;
constructor(scope: Construct, id: string, props: UsEast1StackProps) {
super(scope, id, props);
if (props.domainName) {
const hostedZone = HostedZone.fromLookup(this, 'HostedZone', {
domainName: props.domainName,
});
// cognito requires A record for Hosted UI custom domain
// https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html#cognito-user-pools-add-custom-domain-adding
// > Its parent domain must have a valid DNS A record. You can assign any value to this record.
new ARecord(this, 'Record', {
zone: hostedZone,
target: RecordTarget.fromIpAddresses('8.8.8.8'),
});
const cert = new Certificate(this, 'CertificateV2', {
domainName: `*.${hostedZone.zoneName}`,
validation: CertificateValidation.fromDns(hostedZone),
subjectAlternativeNames: [hostedZone.zoneName],
});
this.certificate = cert;
}
const signPayloadHandler = new EdgeFunction(this, 'SignPayloadHandler', {
entryPath: join(__dirname, 'constructs', 'cf-lambda-furl-service', 'lambda', 'sign-payload.ts'),
});
this.signPayloadHandler = signPayloadHandler;
}
}
================================================
FILE: cdk/package.json
================================================
{
"name": "@aws-samples/serverless-fullstack-webapp-starter-kit",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"test": "jest",
"format": "prettier --write './**/*.{ts,tsx,mjs,mts}'",
"format:check": "prettier --check './**/*.{ts,tsx,mjs,mts}'",
"cdk": "cdk"
},
"devDependencies": {
"@types/jest": "^27.5.0",
"@types/node": "^22.14.1",
"@types/prettier": "2.6.0",
"aws-cdk": "^2.1007.0",
"esbuild": "^0.25.4",
"jest": "^30.1.3",
"prettier": "^3.5.3",
"ts-jest": "^29.4.4",
"ts-node": "^10.7.0",
"typescript": "^5.9.2"
},
"dependencies": {
"@aws-appsync/utils": "^1.12.0",
"@aws-sdk/client-cognito-identity-provider": "^3.987.0",
"@types/aws-lambda": "^8.10.149",
"aws-cdk-lib": "^2.189.1",
"cdk-nag": "^2.14.29",
"constructs": "^10.0.0",
"deploy-time-build": "^0.3.32",
"source-map-support": "^0.5.21"
}
}
================================================
FILE: cdk/test/__snapshots__/serverless-fullstack-webapp-starter-kit-without-domain.test.ts.snap
================================================
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Snapshot test 1`] = `
{
"Parameters": {
"BootstrapVersion": {
"Default": "/cdk-bootstrap/hnb659fds/version",
"Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]",
"Type": "AWS::SSM::Parameter::Value<String>",
},
},
"Resources": {
"CustomCrossRegionExportWriterCustomResourceProviderHandlerD8786E8A": {
"DependsOn": [
"CustomCrossRegionExportWriterCustomResourceProviderRoleC951B1E1",
],
"Properties": {
"Code": {
"S3Bucket": "cdk-hnb659fds-assets-123456789012-us-east-1",
"S3Key": "REDACTED",
},
"Handler": "__entrypoint__.handler",
"MemorySize": 128,
"Role": {
"Fn::GetAtt": [
"CustomCrossRegionExportWriterCustomResourceProviderRoleC951B1E1",
"Arn",
],
},
"Runtime": "nodejs22.x",
"Timeout": 900,
},
"Type": "AWS::Lambda::Function",
},
"CustomCrossRegionExportWriterCustomResourceProviderRoleC951B1E1": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
"ManagedPolicyArns": [
{
"Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
},
],
"Policies": [
{
"PolicyDocument": {
"Statement": [
{
"Action": [
"ssm:DeleteParameters",
"ssm:ListTagsForResource",
"ssm:GetParameters",
"ssm:PutParameter",
],
"Effect": "Allow",
"Resource": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":ssm:us-west-2:123456789012:parameter/cdk/exports/*",
],
],
},
],
},
],
"Version": "2012-10-17",
},
"PolicyName": "Inline",
},
],
},
"Type": "AWS::IAM::Role",
},
"ExportsWriteruswest209BD44F0A7CF058B": {
"DeletionPolicy": "Delete",
"Properties": {
"ServiceToken": {
"Fn::GetAtt": [
"CustomCrossRegionExportWriterCustomResourceProviderHandlerD8786E8A",
"Arn",
],
},
"WriterProps": {
"exports": {
"/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefSignPayloadHandlerFunctionVersionF9FE430A3006B9FA": {
"Ref": "SignPayloadHandlerFunctionVersionF9FE430A",
},
},
"region": "us-west-2",
},
},
"Type": "Custom::CrossRegionExportWriter",
"UpdateReplacePolicy": "Delete",
},
"SignPayloadHandlerCFEAA14C": {
"DependsOn": [
"SignPayloadHandlerServiceRole29261232",
],
"Properties": {
"Code": {
"S3Bucket": "cdk-hnb659fds-assets-123456789012-us-east-1",
"S3Key": "REDACTED",
},
"Handler": "index.handler",
"Role": {
"Fn::GetAtt": [
"SignPayloadHandlerServiceRole29261232",
"Arn",
],
},
"Runtime": "nodejs22.x",
},
"Type": "AWS::Lambda::Function",
},
"SignPayloadHandlerCurrentVersionREDACTED": {
"Properties": {
"FunctionName": {
"Ref": "SignPayloadHandlerCFEAA14C",
},
},
"Type": "AWS::Lambda::Version",
},
"SignPayloadHandlerFunctionVersionF9FE430A": {
"Properties": {
"Type": "String",
"Value": {
"Ref": "SignPayloadHandlerCurrentVersionREDACTED",
},
},
"Type": "AWS::SSM::Parameter",
},
"SignPayloadHandlerServiceRole29261232": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com",
},
},
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "edgelambda.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
],
],
},
],
},
"Type": "AWS::IAM::Role",
},
},
"Rules": {
"CheckBootstrapVersion": {
"Assertions": [
{
"Assert": {
"Fn::Not": [
{
"Fn::Contains": [
[
"1",
"2",
"3",
"4",
"5",
],
{
"Ref": "BootstrapVersion",
},
],
},
],
},
"AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.",
},
],
},
},
}
`;
exports[`Snapshot test 2`] = `
{
"Description": "Serverless fullstack webapp stack (uksb-1tupboc47)",
"Outputs": {
"AsyncJobHandlerArnCA46B385": {
"Value": {
"Fn::GetAtt": [
"AsyncJobHandler438266BD",
"Arn",
],
},
},
"AuthUserPoolClientId8216BF9A": {
"Value": {
"Ref": "AuthUserPoolClientC635291F",
},
},
"AuthUserPoolDomainName8D4A2606": {
"Value": {
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"AuthDomainPrefixE1742B23",
"generated",
],
},
".auth.us-west-2.amazoncognito.com",
],
],
},
},
"AuthUserPoolIdC0605E59": {
"Value": {
"Ref": "AuthUserPool8115E87F",
},
},
"DatabaseBastionHostBastionHostId1600F37C": {
"Description": "Instance ID of the bastion host. Use this to connect via SSM Session Manager",
"Value": {
"Ref": "DatabaseBastionHost4C4FAD9C",
},
},
"DatabaseDatabaseSecretsCommandF4A622EB": {
"Value": {
"Fn::Join": [
"",
[
"aws secretsmanager get-secret-value --secret-id ",
{
"Fn::Join": [
"-",
[
{
"Fn::Select": [
0,
{
"Fn::Split": [
"-",
{
"Fn::Select": [
6,
{
"Fn::Split": [
":",
{
"Ref": "DatabaseClusterSecretD1FB634F",
},
],
},
],
},
],
},
],
},
{
"Fn::Select": [
1,
{
"Fn::Split": [
"-",
{
"Fn::Select": [
6,
{
"Fn::Split": [
":",
{
"Ref": "DatabaseClusterSecretD1FB634F",
},
],
},
],
},
],
},
],
},
],
],
},
" --region us-west-2",
],
],
},
},
"DatabasePortForwardCommandC3718B89": {
"Value": {
"Fn::Join": [
"",
[
"aws ssm start-session --region us-west-2 --target ",
{
"Ref": "DatabaseBastionHost4C4FAD9C",
},
" --document-name AWS-StartPortForwardingSessionToRemoteHost --parameters '{"portNumber":["",
{
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Port",
],
},
""], "localPortNumber":["5433"], "host": ["",
{
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Address",
],
},
""]}'",
],
],
},
},
"EventBusHttpEndpoint1C68A807": {
"Value": {
"Fn::Join": [
"",
[
"https://",
{
"Fn::GetAtt": [
"EventBusApi6E8C7C94",
"Dns.Http",
],
},
],
],
},
},
"FrontendDomainName": {
"Value": {
"Fn::Join": [
"",
[
"https://",
{
"Fn::GetAtt": [
"Webapp107041BD",
"DomainName",
],
},
],
],
},
},
"MigrationCommand": {
"Value": {
"Fn::Join": [
"",
[
"aws lambda invoke --function-name ",
{
"Ref": "WebappMigrationRunnerAC67C012",
},
" --payload '{"command":"deploy"}' --cli-binary-format raw-in-base64-out /dev/stdout",
],
],
},
},
"MigrationFunctionName": {
"Value": {
"Ref": "WebappMigrationRunnerAC67C012",
},
},
},
"Parameters": {
"BootstrapVersion": {
"Default": "/cdk-bootstrap/hnb659fds/version",
"Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]",
"Type": "AWS::SSM::Parameter::Value<String>",
},
"SsmParameterValueawsserviceamiamazonlinuxlatestal2023amikernel61arm64C96584B6F00A464EAD1953AFF4B05118Parameter": {
"Default": "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64",
"Type": "AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>",
},
},
"Resources": {
"AWS679f53fac002430cb0da5b7982bd22872D164C4C": {
"DependsOn": [
"AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2",
],
"Properties": {
"Code": {
"S3Bucket": "cdk-hnb659fds-assets-123456789012-us-west-2",
"S3Key": "REDACTED",
},
"Handler": "index.handler",
"Role": {
"Fn::GetAtt": [
"AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2",
"Arn",
],
},
"Runtime": "nodejs22.x",
"Timeout": 120,
},
"Type": "AWS::Lambda::Function",
},
"AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
],
],
},
],
},
"Type": "AWS::IAM::Role",
},
"AWSCDKTriggerCustomResourceProviderCustomResourceProviderHandler97BECD91": {
"DependsOn": [
"AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A",
],
"Properties": {
"Code": {
"S3Bucket": "cdk-hnb659fds-assets-123456789012-us-west-2",
"S3Key": "REDACTED",
},
"Handler": "__entrypoint__.handler",
"MemorySize": 128,
"Role": {
"Fn::GetAtt": [
"AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A",
"Arn",
],
},
"Runtime": "nodejs22.x",
"Timeout": 900,
},
"Type": "AWS::Lambda::Function",
},
"AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
"ManagedPolicyArns": [
{
"Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
},
],
"Policies": [
{
"PolicyDocument": {
"Statement": [
{
"Action": [
"lambda:InvokeFunction",
],
"Effect": "Allow",
"Resource": [
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"WebappMigrationRunnerAC67C012",
"Arn",
],
},
":*",
],
],
},
],
},
],
"Version": "2012-10-17",
},
"PolicyName": "Inline",
},
],
},
"Type": "AWS::IAM::Role",
},
"AccessLogBucketAutoDeleteObjectsCustomResource01AB31E8": {
"DeletionPolicy": "Delete",
"DependsOn": [
"AccessLogBucketPolicyF52D2D01",
],
"Properties": {
"BucketName": {
"Ref": "AccessLogBucketDA470295",
},
"ServiceToken": {
"Fn::GetAtt": [
"CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F",
"Arn",
],
},
},
"Type": "Custom::S3AutoDeleteObjects",
"UpdateReplacePolicy": "Delete",
},
"AccessLogBucketDA470295": {
"DeletionPolicy": "Delete",
"Properties": {
"BucketEncryption": {
"ServerSideEncryptionConfiguration": [
{
"ServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256",
},
},
],
},
"OwnershipControls": {
"Rules": [
{
"ObjectOwnership": "ObjectWriter",
},
],
},
"PublicAccessBlockConfiguration": {
"BlockPublicAcls": true,
"BlockPublicPolicy": true,
"IgnorePublicAcls": true,
"RestrictPublicBuckets": true,
},
"Tags": [
{
"Key": "aws-cdk:auto-delete-objects",
"Value": "true",
},
],
},
"Type": "AWS::S3::Bucket",
"UpdateReplacePolicy": "Delete",
},
"AccessLogBucketPolicyF52D2D01": {
"Properties": {
"Bucket": {
"Ref": "AccessLogBucketDA470295",
},
"PolicyDocument": {
"Statement": [
{
"Action": "s3:*",
"Condition": {
"Bool": {
"aws:SecureTransport": "false",
},
},
"Effect": "Deny",
"Principal": {
"AWS": "*",
},
"Resource": [
{
"Fn::GetAtt": [
"AccessLogBucketDA470295",
"Arn",
],
},
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"AccessLogBucketDA470295",
"Arn",
],
},
"/*",
],
],
},
],
},
{
"Action": [
"s3:PutBucketPolicy",
"s3:GetBucket*",
"s3:List*",
"s3:DeleteObject*",
],
"Effect": "Allow",
"Principal": {
"AWS": {
"Fn::GetAtt": [
"CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092",
"Arn",
],
},
},
"Resource": [
{
"Fn::GetAtt": [
"AccessLogBucketDA470295",
"Arn",
],
},
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"AccessLogBucketDA470295",
"Arn",
],
},
"/*",
],
],
},
],
},
],
"Version": "2012-10-17",
},
},
"Type": "AWS::S3::BucketPolicy",
},
"AsyncJobHandler438266BD": {
"DependsOn": [
"AsyncJobHandlerServiceRoleDefaultPolicy0B2DEDB5",
"AsyncJobHandlerServiceRoleFE9F530F",
"VpcPrivateSubnet1DefaultRouteBE02A9ED",
"VpcPrivateSubnet1RouteTableAssociation70C59FA6",
"VpcPrivateSubnet2DefaultRoute060D2087",
"VpcPrivateSubnet2RouteTableAssociationA89CAD56",
"VpcPrivateSubnet3DefaultRoute94B74F0D",
"VpcPrivateSubnet3RouteTableAssociation16BDDC43",
],
"Properties": {
"Architectures": [
"arm64",
],
"Code": {
"ImageUri": {
"Fn::Sub": "REDACTED",
},
},
"Environment": {
"Variables": {
"DATABASE_ENGINE": {
"Fn::Join": [
"",
[
"{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretAttachmentDC8466C0",
},
":SecretString:engine::}}",
],
],
},
"DATABASE_HOST": {
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Address",
],
},
"DATABASE_NAME": "main",
"DATABASE_OPTION": "?connection_limit=1&connect_timeout=30",
"DATABASE_PASSWORD": {
"Fn::Join": [
"",
[
"{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretAttachmentDC8466C0",
},
":SecretString:password::}}",
],
],
},
"DATABASE_PORT": {
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Port",
],
},
"DATABASE_URL": {
"Fn::Join": [
"",
[
"{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretAttachmentDC8466C0",
},
":SecretString:engine::}}://{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretAttachmentDC8466C0",
},
":SecretString:username::}}:{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretAttachmentDC8466C0",
},
":SecretString:password::}}@",
{
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Address",
],
},
":",
{
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Port",
],
},
"/main?connection_limit=1&connect_timeout=30",
],
],
},
"DATABASE_USER": {
"Fn::Join": [
"",
[
"{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretAttachmentDC8466C0",
},
":SecretString:username::}}",
],
],
},
"EVENT_HTTP_ENDPOINT": {
"Fn::Join": [
"",
[
"https://",
{
"Fn::GetAtt": [
"EventBusApi6E8C7C94",
"Dns.Http",
],
},
],
],
},
},
},
"ImageConfig": {
"Command": [
"async-job-runner.handler",
],
},
"LoggingConfig": {
"LogGroup": {
"Ref": "AsyncJobHandlerLogs20DFEE3E",
},
},
"MemorySize": 256,
"PackageType": "Image",
"ReservedConcurrentExecutions": 1,
"Role": {
"Fn::GetAtt": [
"AsyncJobHandlerServiceRoleFE9F530F",
"Arn",
],
},
"Timeout": 600,
"VpcConfig": {
"SecurityGroupIds": [
{
"Fn::GetAtt": [
"AsyncJobHandlerSecurityGroupF59812E6",
"GroupId",
],
},
],
"SubnetIds": [
{
"Ref": "VpcPrivateSubnet1Subnet536B997A",
},
{
"Ref": "VpcPrivateSubnet2Subnet3788AAA1",
},
{
"Ref": "VpcPrivateSubnet3SubnetF258B56E",
},
],
},
},
"Type": "AWS::Lambda::Function",
},
"AsyncJobHandlerLogs20DFEE3E": {
"DeletionPolicy": "Delete",
"Properties": {
"RetentionInDays": 7,
},
"Type": "AWS::Logs::LogGroup",
"UpdateReplacePolicy": "Delete",
},
"AsyncJobHandlerSecurityGroupF59812E6": {
"DependsOn": [
"VpcPrivateSubnet1DefaultRouteBE02A9ED",
"VpcPrivateSubnet1RouteTableAssociation70C59FA6",
"VpcPrivateSubnet2DefaultRoute060D2087",
"VpcPrivateSubnet2RouteTableAssociationA89CAD56",
"VpcPrivateSubnet3DefaultRoute94B74F0D",
"VpcPrivateSubnet3RouteTableAssociation16BDDC43",
],
"Properties": {
"GroupDescription": "Automatic security group for Lambda Function ServerlessWebappStarterKitStackAsyncJobHandlerF20B94EA",
"SecurityGroupEgress": [
{
"CidrIp": "0.0.0.0/0",
"Description": "Allow all outbound traffic by default",
"IpProtocol": "-1",
},
],
"VpcId": {
"Ref": "Vpc8378EB38",
},
},
"Type": "AWS::EC2::SecurityGroup",
},
"AsyncJobHandlerServiceRoleDefaultPolicy0B2DEDB5": {
"DependsOn": [
"VpcPrivateSubnet1DefaultRouteBE02A9ED",
"VpcPrivateSubnet1RouteTableAssociation70C59FA6",
"VpcPrivateSubnet2DefaultRoute060D2087",
"VpcPrivateSubnet2RouteTableAssociationA89CAD56",
"VpcPrivateSubnet3DefaultRoute94B74F0D",
"VpcPrivateSubnet3RouteTableAssociation16BDDC43",
],
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "appsync:EventPublish",
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":appsync:us-west-2:123456789012:apis/",
{
"Fn::GetAtt": [
"EventBusApi6E8C7C94",
"ApiId",
],
},
"/channelNamespace/*",
],
],
},
},
{
"Action": [
"translate:TranslateText",
"comprehend:DetectDominantLanguage",
],
"Effect": "Allow",
"Resource": "*",
},
],
"Version": "2012-10-17",
},
"PolicyName": "AsyncJobHandlerServiceRoleDefaultPolicy0B2DEDB5",
"Roles": [
{
"Ref": "AsyncJobHandlerServiceRoleFE9F530F",
},
],
},
"Type": "AWS::IAM::Policy",
},
"AsyncJobHandlerServiceRoleFE9F530F": {
"DependsOn": [
"VpcPrivateSubnet1DefaultRouteBE02A9ED",
"VpcPrivateSubnet1RouteTableAssociation70C59FA6",
"VpcPrivateSubnet2DefaultRoute060D2087",
"VpcPrivateSubnet2RouteTableAssociationA89CAD56",
"VpcPrivateSubnet3DefaultRoute94B74F0D",
"VpcPrivateSubnet3RouteTableAssociation16BDDC43",
],
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
],
],
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole",
],
],
},
],
},
"Type": "AWS::IAM::Role",
},
"AsyncJobSampleJob3C1EBA2C": {
"Properties": {
"FlexibleTimeWindow": {
"Mode": "OFF",
},
"ScheduleExpression": "cron(0 0 1 * ? *)",
"ScheduleExpressionTimezone": "Etc/UTC",
"State": "ENABLED",
"Target": {
"Arn": {
"Fn::GetAtt": [
"AsyncJobHandler438266BD",
"Arn",
],
},
"Input": "{"jobType":"SampleJob"}",
"RetryPolicy": {
"MaximumEventAgeInSeconds": 86400,
"MaximumRetryAttempts": 5,
},
"RoleArn": {
"Fn::GetAtt": [
"SchedulerRoleForTarget44ece2CFC6840F",
"Arn",
],
},
},
},
"Type": "AWS::Scheduler::Schedule",
},
"AuthBranding34BB87FD": {
"Properties": {
"ClientId": {
"Ref": "AuthUserPoolClientC635291F",
},
"UseCognitoProvidedValues": true,
"UserPoolId": {
"Ref": "AuthUserPool8115E87F",
},
},
"Type": "AWS::Cognito::ManagedLoginBranding",
},
"AuthDomainPrefixE1742B23": {
"DeletionPolicy": "Delete",
"Properties": {
"ServiceTimeout": "10",
"ServiceToken": {
"Fn::GetAtt": [
"RandomStringGenerator11e9c903f11a4989833c985dddef5eb28C5103D0",
"Arn",
],
},
"length": 10,
"prefix": "webapp-",
},
"Type": "Custom::RandomString",
"UpdateReplacePolicy": "Delete",
},
"AuthUpdateCallbackUrlsA55622E9": {
"DeletionPolicy": "Delete",
"DependsOn": [
"AuthUpdateCallbackUrlsCustomResourcePolicy14EEB23D",
],
"Properties": {
"Create": {
"Fn::Join": [
"",
[
"{"service":"@aws-sdk/client-cognito-identity-provider","action":"updateUserPoolClient","parameters":{"ClientId":"",
{
"Ref": "AuthUserPoolClientC635291F",
},
"","UserPoolId":"",
{
"Ref": "AuthUserPool8115E87F",
},
"","AllowedOAuthFlows":["code"],"AllowedOAuthFlowsUserPoolClient":true,"AllowedOAuthScopes":["profile","phone","email","openid","aws.cognito.signin.user.admin"],"ExplicitAuthFlows":["ALLOW_USER_PASSWORD_AUTH","ALLOW_USER_SRP_AUTH","ALLOW_REFRESH_TOKEN_AUTH"],"CallbackURLs":["https://",
{
"Fn::GetAtt": [
"Webapp107041BD",
"DomainName",
],
},
"/api/auth/sign-in-callback","http://localhost:3010/api/auth/sign-in-callback"],"LogoutURLs":["https://",
{
"Fn::GetAtt": [
"Webapp107041BD",
"DomainName",
],
},
"/api/auth/sign-out-callback","http://localhost:3010/api/auth/sign-out-callback"],"SupportedIdentityProviders":["COGNITO"],"TokenValidityUnits":{"IdToken":"minutes"},"IdTokenValidity":1440},"physicalResourceId":{"id":"",
{
"Ref": "AuthUserPool8115E87F",
},
""}}",
],
],
},
"InstallLatestAwsSdk": true,
"ServiceToken": {
"Fn::GetAtt": [
"AWS679f53fac002430cb0da5b7982bd22872D164C4C",
"Arn",
],
},
"Update": {
"Fn::Join": [
"",
[
"{"service":"@aws-sdk/client-cognito-identity-provider","action":"updateUserPoolClient","parameters":{"ClientId":"",
{
"Ref": "AuthUserPoolClientC635291F",
},
"","UserPoolId":"",
{
"Ref": "AuthUserPool8115E87F",
},
"","AllowedOAuthFlows":["code"],"AllowedOAuthFlowsUserPoolClient":true,"AllowedOAuthScopes":["profile","phone","email","openid","aws.cognito.signin.user.admin"],"ExplicitAuthFlows":["ALLOW_USER_PASSWORD_AUTH","ALLOW_USER_SRP_AUTH","ALLOW_REFRESH_TOKEN_AUTH"],"CallbackURLs":["https://",
{
"Fn::GetAtt": [
"Webapp107041BD",
"DomainName",
],
},
"/api/auth/sign-in-callback","http://localhost:3010/api/auth/sign-in-callback"],"LogoutURLs":["https://",
{
"Fn::GetAtt": [
"Webapp107041BD",
"DomainName",
],
},
"/api/auth/sign-out-callback","http://localhost:3010/api/auth/sign-out-callback"],"SupportedIdentityProviders":["COGNITO"],"TokenValidityUnits":{"IdToken":"minutes"},"IdTokenValidity":1440},"physicalResourceId":{"id":"",
{
"Ref": "AuthUserPool8115E87F",
},
""}}",
],
],
},
},
"Type": "Custom::AWS",
"UpdateReplacePolicy": "Delete",
},
"AuthUpdateCallbackUrlsCustomResourcePolicy14EEB23D": {
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "cognito-idp:UpdateUserPoolClient",
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"AuthUserPool8115E87F",
"Arn",
],
},
},
],
"Version": "2012-10-17",
},
"PolicyName": "AuthUpdateCallbackUrlsCustomResourcePolicy14EEB23D",
"Roles": [
{
"Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2",
},
],
},
"Type": "AWS::IAM::Policy",
},
"AuthUserPool8115E87F": {
"DeletionPolicy": "Delete",
"Properties": {
"AccountRecoverySetting": {
"RecoveryMechanisms": [
{
"Name": "verified_phone_number",
"Priority": 1,
},
{
"Name": "verified_email",
"Priority": 2,
},
],
},
"AdminCreateUserConfig": {
"AllowAdminCreateUserOnly": true,
},
"AutoVerifiedAttributes": [
"email",
],
"EmailVerificationMessage": "The verification code to your new account is {####}",
"EmailVerificationSubject": "Verify your new account",
"Policies": {
"PasswordPolicy": {
"MinimumLength": 8,
"RequireNumbers": true,
"RequireSymbols": true,
"RequireUppercase": true,
},
},
"SmsVerificationMessage": "The verification code to your new account is {####}",
"UsernameAttributes": [
"email",
],
"VerificationMessageTemplate": {
"DefaultEmailOption": "CONFIRM_WITH_CODE",
"EmailMessage": "The verification code to your new account is {####}",
"EmailSubject": "Verify your new account",
"SmsMessage": "The verification code to your new account is {####}",
},
},
"Type": "AWS::Cognito::UserPool",
"UpdateReplacePolicy": "Delete",
},
"AuthUserPoolClientC635291F": {
"Properties": {
"AllowedOAuthFlows": [
"code",
],
"AllowedOAuthFlowsUserPoolClient": true,
"AllowedOAuthScopes": [
"profile",
"phone",
"email",
"openid",
"aws.cognito.signin.user.admin",
],
"CallbackURLs": [
"http://localhost/dummy",
],
"ExplicitAuthFlows": [
"ALLOW_USER_PASSWORD_AUTH",
"ALLOW_USER_SRP_AUTH",
"ALLOW_REFRESH_TOKEN_AUTH",
],
"IdTokenValidity": 1440,
"LogoutURLs": [
"http://localhost/dummy",
],
"SupportedIdentityProviders": [
"COGNITO",
],
"TokenValidityUnits": {
"IdToken": "minutes",
},
"UserPoolId": {
"Ref": "AuthUserPool8115E87F",
},
},
"Type": "AWS::Cognito::UserPoolClient",
},
"AuthUserPoolCognitoDomainAD9D79E1": {
"Properties": {
"Domain": {
"Fn::GetAtt": [
"AuthDomainPrefixE1742B23",
"generated",
],
},
"ManagedLoginVersion": 2,
"UserPoolId": {
"Ref": "AuthUserPool8115E87F",
},
},
"Type": "AWS::Cognito::UserPoolDomain",
},
"ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549": {
"Properties": {
"Artifacts": {
"Type": "NO_ARTIFACTS",
},
"Cache": {
"Type": "NO_CACHE",
},
"EncryptionKey": "alias/aws/s3",
"Environment": {
"ComputeType": "BUILD_GENERAL1_SMALL",
"Image": "aws/codebuild/amazonlinux2-aarch64-standard:3.0",
"ImagePullCredentialsType": "CODEBUILD",
"PrivilegedMode": true,
"Type": "ARM_CONTAINER",
},
"ServiceRole": {
"Fn::GetAtt": [
"ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleC5F7BBFE",
"Arn",
],
},
"Source": {
"BuildSpec": "{
"version": "0.2",
"phases": {
"build": {
"commands": [
"current_dir=$(pwd)",
"echo \\"$input\\"",
"mkdir workdir",
"cd workdir",
"aws s3 cp \\"$sourceS3Url\\" temp.zip",
"unzip temp.zip",
"ls -la",
"aws ecr get-login-password | docker login --username AWS --password-stdin $repositoryAuthUri",
"aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws",
"docker buildx create --use",
"docker buildx ls",
"eval \\"$buildCommand\\""
]
},
"post_build": {
"commands": [
"echo Build completed on \`date\`",
"\\nSTATUS='SUCCESS'\\nif [ $CODEBUILD_BUILD_SUCCEEDING -ne 1 ] # Test if the build is failing\\nthen\\nSTATUS='FAILED'\\nREASON=\\"ContainerImageBuild failed. See CloudWatch Log stream for the detailed reason: \\nhttps://$AWS_REGION.console.aws.amazon.com/cloudwatch/home?region=$AWS_REGION#logsV2:log-groups/log-group/\\\\$252Faws\\\\$252Fcodebuild\\\\$252F$projectName/log-events/$CODEBUILD_LOG_PATH\\"\\nfi\\ncat <<EOF > payload.json\\n{\\n \\"StackId\\": \\"$stackId\\",\\n \\"RequestId\\": \\"$requestId\\",\\n \\"LogicalResourceId\\":\\"$logicalResourceId\\",\\n \\"PhysicalResourceId\\": \\"$imageTag\\",\\n \\"Status\\": \\"$STATUS\\",\\n \\"Reason\\": \\"$REASON\\",\\n \\"Data\\": {\\n \\"ImageTag\\": \\"$imageTag\\"\\n }\\n}\\nEOF\\ncurl -v -i -X PUT -H 'Content-Type:' -d \\"@payload.json\\" \\"$responseURL\\"\\n "
]
}
}
}",
"Type": "NO_SOURCE",
},
},
"Type": "AWS::CodeBuild::Project",
},
"ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleC5F7BBFE": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "codebuild.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":iam::aws:policy/AmazonElasticContainerRegistryPublicReadOnly",
],
],
},
],
},
"Type": "AWS::IAM::Role",
},
"ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleDefaultPolicy2316728F": {
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
],
"Effect": "Allow",
"Resource": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":logs:us-west-2:123456789012:log-group:/aws/codebuild/",
{
"Ref": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549",
},
],
],
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":logs:us-west-2:123456789012:log-group:/aws/codebuild/",
{
"Ref": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549",
},
":*",
],
],
},
],
},
{
"Action": [
"codebuild:CreateReportGroup",
"codebuild:CreateReport",
"codebuild:UpdateReport",
"codebuild:BatchPutTestCases",
"codebuild:BatchPutCodeCoverages",
],
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":codebuild:us-west-2:123456789012:report-group/",
{
"Ref": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549",
},
"-*",
],
],
},
},
{
"Action": [
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:CompleteLayerUpload",
"ecr:UploadLayerPart",
"ecr:InitiateLayerUpload",
"ecr:PutImage",
],
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"WebappBuildRepository4C93D48D",
"Arn",
],
},
},
{
"Action": "ecr:GetAuthorizationToken",
"Effect": "Allow",
"Resource": "*",
},
{
"Action": "ecr:DescribeImages",
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"WebappBuildRepository4C93D48D",
"Arn",
],
},
},
{
"Action": [
"s3:GetObject*",
"s3:GetBucket*",
"s3:List*",
],
"Effect": "Allow",
"Resource": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":s3:::cdk-hnb659fds-assets-123456789012-us-west-2",
],
],
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":s3:::cdk-hnb659fds-assets-123456789012-us-west-2/*",
],
],
},
],
},
],
"Version": "2012-10-17",
},
"PolicyName": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleDefaultPolicy2316728F",
"Roles": [
{
"Ref": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleC5F7BBFE",
},
],
},
"Type": "AWS::IAM::Policy",
},
"CustomCrossRegionExportReaderCustomResourceProviderHandler46647B68": {
"DependsOn": [
"CustomCrossRegionExportReaderCustomResourceProviderRole10531BBD",
],
"Properties": {
"Code": {
"S3Bucket": "cdk-hnb659fds-assets-123456789012-us-west-2",
"S3Key": "REDACTED",
},
"Handler": "__entrypoint__.handler",
"MemorySize": 128,
"Role": {
"Fn::GetAtt": [
"CustomCrossRegionExportReaderCustomResourceProviderRole10531BBD",
"Arn",
],
},
"Runtime": "nodejs22.x",
"Timeout": 900,
},
"Type": "AWS::Lambda::Function",
},
"CustomCrossRegionExportReaderCustomResourceProviderRole10531BBD": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
"ManagedPolicyArns": [
{
"Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
},
],
"Policies": [
{
"PolicyDocument": {
"Statement": [
{
"Action": [
"ssm:AddTagsToResource",
"ssm:RemoveTagsFromResource",
"ssm:GetParameters",
],
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":ssm:us-west-2:123456789012:parameter/cdk/exports/ServerlessWebappStarterKitStack/*",
],
],
},
},
],
"Version": "2012-10-17",
},
"PolicyName": "Inline",
},
],
},
"Type": "AWS::IAM::Role",
},
"CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": {
"DependsOn": [
"CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092",
],
"Properties": {
"Code": {
"S3Bucket": "cdk-hnb659fds-assets-123456789012-us-west-2",
"S3Key": "REDACTED",
},
"Description": {
"Fn::Join": [
"",
[
"Lambda function for auto-deleting objects in ",
{
"Ref": "AccessLogBucketDA470295",
},
" S3 bucket.",
],
],
},
"Handler": "index.handler",
"MemorySize": 128,
"Role": {
"Fn::GetAtt": [
"CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092",
"Arn",
],
},
"Runtime": "nodejs22.x",
"Timeout": 900,
},
"Type": "AWS::Lambda::Function",
},
"CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
"ManagedPolicyArns": [
{
"Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
},
],
},
"Type": "AWS::IAM::Role",
},
"DatabaseBastionHost4C4FAD9C": {
"DependsOn": [
"DatabaseBastionHostInstanceRoleDefaultPolicy15D8D0EA",
"DatabaseBastionHostInstanceRole87A429B0",
],
"Properties": {
"AvailabilityZone": "dummy1a",
"BlockDeviceMappings": [
{
"DeviceName": "/dev/sdf",
"Ebs": {
"Encrypted": true,
"VolumeSize": 8,
},
},
],
"IamInstanceProfile": {
"Ref": "DatabaseBastionHostInstanceProfile0F4F3411",
},
"ImageId": {
"Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestal2023amikernel61arm64C96584B6F00A464EAD1953AFF4B05118Parameter",
},
"InstanceType": "t4g.nano",
"SecurityGroupIds": [
{
"Fn::GetAtt": [
"DatabaseBastionHostInstanceSecurityGroup39D7809A",
"GroupId",
],
},
],
"SubnetId": {
"Ref": "VpcPrivateSubnet1Subnet536B997A",
},
"Tags": [
{
"Key": "Name",
"Value": "BastionHost",
},
],
"UserData": {
"Fn::Base64": "#!/bin/bash",
},
},
"Type": "AWS::EC2::Instance",
},
"DatabaseBastionHostInstanceProfile0F4F3411": {
"Properties": {
"Roles": [
{
"Ref": "DatabaseBastionHostInstanceRole87A429B0",
},
],
},
"Type": "AWS::IAM::InstanceProfile",
},
"DatabaseBastionHostInstanceRole87A429B0": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
"Tags": [
{
"Key": "Name",
"Value": "BastionHost",
},
],
},
"Type": "AWS::IAM::Role",
},
"DatabaseBastionHostInstanceRoleDefaultPolicy15D8D0EA": {
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"ssmmessages:*",
"ssm:UpdateInstanceInformation",
"ec2messages:*",
],
"Effect": "Allow",
"Resource": "*",
},
],
"Version": "2012-10-17",
},
"PolicyName": "DatabaseBastionHostInstanceRoleDefaultPolicy15D8D0EA",
"Roles": [
{
"Ref": "DatabaseBastionHostInstanceRole87A429B0",
},
],
},
"Type": "AWS::IAM::Policy",
},
"DatabaseBastionHostInstanceSecurityGroup39D7809A": {
"Properties": {
"GroupDescription": "ServerlessWebappStarterKitStack/Database/BastionHost/Resource/InstanceSecurityGroup",
"SecurityGroupEgress": [
{
"CidrIp": "0.0.0.0/0",
"Description": "Allow all outbound traffic by default",
"IpProtocol": "-1",
},
],
"Tags": [
{
"Key": "Name",
"Value": "BastionHost",
},
],
"VpcId": {
"Ref": "Vpc8378EB38",
},
},
"Type": "AWS::EC2::SecurityGroup",
},
"DatabaseCluster5B53A178": {
"DeletionPolicy": "Snapshot",
"Properties": {
"CopyTagsToSnapshot": true,
"DBClusterParameterGroupName": {
"Ref": "DatabaseParameterGroup2A921026",
},
"DBSubnetGroupName": {
"Ref": "DatabaseClusterSubnets5540150D",
},
"EnableCloudwatchLogsExports": [
"postgresql",
],
"EnableHttpEndpoint": true,
"Engine": "aurora-postgresql",
"EngineVersion": "16.6",
"MasterUserPassword": {
"Fn::Join": [
"",
[
"{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretD1FB634F",
},
":SecretString:password::}}",
],
],
},
"MasterUsername": {
"Fn::Join": [
"",
[
"{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretD1FB634F",
},
":SecretString:username::}}",
],
],
},
"Port": 5432,
"ServerlessV2ScalingConfiguration": {
"MaxCapacity": 2,
"MinCapacity": 0,
},
"StorageEncrypted": true,
"VpcSecurityGroupIds": [
{
"Fn::GetAtt": [
"DatabaseClusterSecurityGroupFEF1426A",
"GroupId",
],
},
],
},
"Type": "AWS::RDS::DBCluster",
"UpdateReplacePolicy": "Snapshot",
},
"DatabaseClusterLogRetentionpostgresql025D39CE": {
"Properties": {
"LogGroupName": {
"Fn::Join": [
"",
[
"/aws/rds/cluster/",
{
"Ref": "DatabaseCluster5B53A178",
},
"/postgresql",
],
],
},
"RetentionInDays": 7,
"ServiceToken": {
"Fn::GetAtt": [
"LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A",
"Arn",
],
},
},
"Type": "Custom::LogRetention",
},
"DatabaseClusterSecretAttachmentDC8466C0": {
"Properties": {
"SecretId": {
"Ref": "DatabaseClusterSecretD1FB634F",
},
"TargetId": {
"Ref": "DatabaseCluster5B53A178",
},
"TargetType": "AWS::RDS::DBCluster",
},
"Type": "AWS::SecretsManager::SecretTargetAttachment",
},
"DatabaseClusterSecretD1FB634F": {
"DeletionPolicy": "Delete",
"Properties": {
"Description": {
"Fn::Join": [
"",
[
"Generated by the CDK for stack: ",
{
"Ref": "AWS::StackName",
},
],
],
},
"GenerateSecretString": {
"ExcludeCharacters": " %+~\`#$&*()|[]{}:;<>?!'/@"\\,=^",
"GenerateStringKey": "password",
"PasswordLength": 30,
"SecretStringTemplate": "{"username":"postgres"}",
},
},
"Type": "AWS::SecretsManager::Secret",
"UpdateReplacePolicy": "Delete",
},
"DatabaseClusterSecurityGroupFEF1426A": {
"Properties": {
"GroupDescription": "RDS security group",
"SecurityGroupEgress": [
{
"CidrIp": "0.0.0.0/0",
"Description": "Allow all outbound traffic by default",
"IpProtocol": "-1",
},
],
"VpcId": {
"Ref": "Vpc8378EB38",
},
},
"Type": "AWS::EC2::SecurityGroup",
},
"DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackAsyncJobHandlerSecurityGroup5220DFB3IndirectPort9323962E": {
"Properties": {
"Description": "from ServerlessWebappStarterKitStackAsyncJobHandlerSecurityGroup5220DFB3:{IndirectPort}",
"FromPort": {
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Port",
],
},
"GroupId": {
"Fn::GetAtt": [
"DatabaseClusterSecurityGroupFEF1426A",
"GroupId",
],
},
"IpProtocol": "tcp",
"SourceSecurityGroupId": {
"Fn::GetAtt": [
"AsyncJobHandlerSecurityGroupF59812E6",
"GroupId",
],
},
"ToPort": {
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Port",
],
},
},
"Type": "AWS::EC2::SecurityGroupIngress",
},
"DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackDatabaseBastionHostInstanceSecurityGroup4F0DD25BIndirectPort8AFD9922": {
"Properties": {
"Description": "from ServerlessWebappStarterKitStackDatabaseBastionHostInstanceSecurityGroup4F0DD25B:{IndirectPort}",
"FromPort": {
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Port",
],
},
"GroupId": {
"Fn::GetAtt": [
"DatabaseClusterSecurityGroupFEF1426A",
"GroupId",
],
},
"IpProtocol": "tcp",
"SourceSecurityGroupId": {
"Fn::GetAtt": [
"DatabaseBastionHostInstanceSecurityGroup39D7809A",
"GroupId",
],
},
"ToPort": {
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Port",
],
},
},
"Type": "AWS::EC2::SecurityGroupIngress",
},
"DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackWebappHandlerSecurityGroupA009CF4AIndirectPort7311063E": {
"Properties": {
"Description": "from ServerlessWebappStarterKitStackWebappHandlerSecurityGroupA009CF4A:{IndirectPort}",
"FromPort": {
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Port",
],
},
"GroupId": {
"Fn::GetAtt": [
"DatabaseClusterSecurityGroupFEF1426A",
"GroupId",
],
},
"IpProtocol": "tcp",
"SourceSecurityGroupId": {
"Fn::GetAtt": [
"WebappHandlerSecurityGroup5451B519",
"GroupId",
],
},
"ToPort": {
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Port",
],
},
},
"Type": "AWS::EC2::SecurityGroupIngress",
},
"DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackWebappMigrationRunnerSecurityGroupC0959349IndirectPortDCF4A356": {
"Properties": {
"Description": "from ServerlessWebappStarterKitStackWebappMigrationRunnerSecurityGroupC0959349:{IndirectPort}",
"FromPort": {
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Port",
],
},
"GroupId": {
"Fn::GetAtt": [
"DatabaseClusterSecurityGroupFEF1426A",
"GroupId",
],
},
"IpProtocol": "tcp",
"SourceSecurityGroupId": {
"Fn::GetAtt": [
"WebappMigrationRunnerSecurityGroup7F0DF264",
"GroupId",
],
},
"ToPort": {
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Port",
],
},
},
"Type": "AWS::EC2::SecurityGroupIngress",
},
"DatabaseClusterSubnets5540150D": {
"Properties": {
"DBSubnetGroupDescription": "Subnets for Cluster database",
"SubnetIds": [
{
"Ref": "VpcPrivateSubnet1Subnet536B997A",
},
{
"Ref": "VpcPrivateSubnet2Subnet3788AAA1",
},
{
"Ref": "VpcPrivateSubnet3SubnetF258B56E",
},
],
},
"Type": "AWS::RDS::DBSubnetGroup",
},
"DatabaseClusterWriterD43085C6": {
"DeletionPolicy": "Delete",
"DependsOn": [
"VpcPrivateSubnet1DefaultRouteBE02A9ED",
"VpcPrivateSubnet1RouteTableAssociation70C59FA6",
"VpcPrivateSubnet2DefaultRoute060D2087",
"VpcPrivateSubnet2RouteTableAssociationA89CAD56",
"VpcPrivateSubnet3DefaultRoute94B74F0D",
"VpcPrivateSubnet3RouteTableAssociation16BDDC43",
],
"Properties": {
"AutoMinorVersionUpgrade": true,
"DBClusterIdentifier": {
"Ref": "DatabaseCluster5B53A178",
},
"DBInstanceClass": "db.serverless",
"EnablePerformanceInsights": true,
"Engine": "aurora-postgresql",
"PerformanceInsightsRetentionPeriod": 7,
"PromotionTier": 0,
"PubliclyAccessible": false,
},
"Type": "AWS::RDS::DBInstance",
"UpdateReplacePolicy": "Delete",
},
"DatabaseParameterGroup2A921026": {
"Properties": {
"Description": "Cluster parameter group for aurora-postgresql16",
"Family": "aurora-postgresql16",
"Parameters": {
"idle_session_timeout": "60000",
"log_connections": "1",
"log_disconnections": "1",
},
},
"Type": "AWS::RDS::DBClusterParameterGroup",
},
"DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f306AEFF37": {
"DependsOn": [
"DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleDefaultPolicyFECC51DC",
"DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleB008BAA4",
],
"Properties": {
"Code": {
"S3Bucket": "cdk-hnb659fds-assets-123456789012-us-west-2",
"S3Key": "REDACTED",
},
"Handler": "index.handler",
"Role": {
"Fn::GetAtt": [
"DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleB008BAA4",
"Arn",
],
},
"Runtime": "nodejs20.x",
"Timeout": 300,
},
"Type": "AWS::Lambda::Function",
},
"DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleB008BAA4": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
],
],
},
],
},
"Type": "AWS::IAM::Role",
},
"DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleDefaultPolicyFECC51DC": {
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "codebuild:StartBuild",
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549",
"Arn",
],
},
},
],
"Version": "2012-10-17",
},
"PolicyName": "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleDefaultPolicyFECC51DC",
"Roles": [
{
"Ref": "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleB008BAA4",
},
],
},
"Type": "AWS::IAM::Policy",
},
"EventBusApi6E8C7C94": {
"Properties": {
"EventConfig": {
"AuthProviders": [
{
"AuthType": "AWS_IAM",
},
{
"AuthType": "AMAZON_COGNITO_USER_POOLS",
"CognitoConfig": {
"AwsRegion": "us-west-2",
"UserPoolId": {
"Ref": "AuthUserPool8115E87F",
},
},
},
],
"ConnectionAuthModes": [
{
"AuthType": "AWS_IAM",
},
{
"AuthType": "AMAZON_COGNITO_USER_POOLS",
},
],
"DefaultPublishAuthModes": [
{
"AuthType": "AWS_IAM",
},
],
"DefaultSubscribeAuthModes": [
{
"AuthType": "AWS_IAM",
},
{
"AuthType": "AMAZON_COGNITO_USER_POOLS",
},
],
},
"Name": "ServerlessWackEventBus8815362F",
},
"Type": "AWS::AppSync::Api",
},
"EventBusNamespaceA69F015E": {
"Properties": {
"ApiId": {
"Fn::GetAtt": [
"EventBusApi6E8C7C94",
"ApiId",
],
},
"CodeS3Location": "s3://cdk-hnb659fds-assets-123456789012-us-west-2/REDACTED",
"HandlerConfigs": {},
"Name": "event-bus",
},
"Type": "AWS::AppSync::ChannelNamespace",
},
"ExportsReader8B249524": {
"DeletionPolicy": "Delete",
"Properties": {
"ReaderProps": {
"imports": {
"/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefSignPayloadHandlerFunctionVersionF9FE430A3006B9FA": "{{resolve:ssm:/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefSignPayloadHandlerFunctionVersionF9FE430A3006B9FA}}",
},
"prefix": "ServerlessWebappStarterKitStack",
"region": "us-west-2",
},
"ServiceToken": {
"Fn::GetAtt": [
"CustomCrossRegionExportReaderCustomResourceProviderHandler46647B68",
"Arn",
],
},
},
"Type": "Custom::CrossRegionExportReader",
"UpdateReplacePolicy": "Delete",
},
"LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A": {
"DependsOn": [
"LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB",
"LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB",
],
"Properties": {
"Code": {
"S3Bucket": "cdk-hnb659fds-assets-123456789012-us-west-2",
"S3Key": "REDACTED",
},
"Handler": "index.handler",
"Role": {
"Fn::GetAtt": [
"LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB",
"Arn",
],
},
"Runtime": "nodejs22.x",
"Timeout": 900,
},
"Type": "AWS::Lambda::Function",
},
"LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
],
],
},
],
},
"Type": "AWS::IAM::Role",
},
"LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB": {
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"logs:PutRetentionPolicy",
"logs:DeleteRetentionPolicy",
],
"Effect": "Allow",
"Resource": "*",
},
],
"Version": "2012-10-17",
},
"PolicyName": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB",
"Roles": [
{
"Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB",
},
],
},
"Type": "AWS::IAM::Policy",
},
"LookupVersionArnc8730278af02f875114ca902814c77b68f19b0087110E04D0A": {
"DeletionPolicy": "Delete",
"DependsOn": [
"LookupVersionArnc8730278af02f875114ca902814c77b68f19b00871CustomResourcePolicy7CD24C5F",
],
"Properties": {
"Create": {
"Fn::Join": [
"",
[
"{"service":"SSM","action":"getParameter","parameters":{"Name":"",
{
"Fn::GetAtt": [
"ExportsReader8B249524",
"/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefSignPayloadHandlerFunctionVersionF9FE430A3006B9FA",
],
},
""},"physicalResourceId":{"id":"1577836800000"},"region":"us-east-1"}",
],
],
},
"InstallLatestAwsSdk": true,
"ServiceToken": {
"Fn::GetAtt": [
"AWS679f53fac002430cb0da5b7982bd22872D164C4C",
"Arn",
],
},
"Update": {
"Fn::Join": [
"",
[
"{"service":"SSM","action":"getParameter","parameters":{"Name":"",
{
"Fn::GetAtt": [
"ExportsReader8B249524",
"/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefSignPayloadHandlerFunctionVersionF9FE430A3006B9FA",
],
},
""},"physicalResourceId":{"id":"1577836800000"},"region":"us-east-1"}",
],
],
},
},
"Type": "Custom::AWS",
"UpdateReplacePolicy": "Delete",
},
"LookupVersionArnc8730278af02f875114ca902814c77b68f19b00871CustomResourcePolicy7CD24C5F": {
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "ssm:GetParameter",
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":ssm:us-east-1:123456789012:parameter/",
{
"Fn::GetAtt": [
"ExportsReader8B249524",
"/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefSignPayloadHandlerFunctionVersionF9FE430A3006B9FA",
],
},
],
],
},
},
],
"Version": "2012-10-17",
},
"PolicyName": "LookupVersionArnc8730278af02f875114ca902814c77b68f19b00871CustomResourcePolicy7CD24C5F",
"Roles": [
{
"Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2",
},
],
},
"Type": "AWS::IAM::Policy",
},
"RandomStringGenerator11e9c903f11a4989833c985dddef5eb28C5103D0": {
"DependsOn": [
"RandomStringGenerator11e9c903f11a4989833c985dddef5eb2ServiceRoleAB6B57A9",
],
"Properties": {
"Code": {
"ZipFile": "const response = require('cfn-response');
const crypto = require('crypto');
exports.handler = async function (event, context) {
try {
console.log(event);
if (event.RequestType == 'Delete') {
return await response.send(event, context, response.SUCCESS);
}
const prefix = event.ResourceProperties.prefix ?? '';
const length = event.ResourceProperties.length ?? '8';
const generate = () => {
const random = crypto.randomBytes(parseInt(length)).toString('hex');
return \`\${prefix}\${random.slice(0, length)}\`;
};
if (event.RequestType == 'Create') {
const generated = generate();
return await response.send(event, context, response.SUCCESS, { generated }, generated);
}
if (event.RequestType == 'Update') {
const current = event.PhysicalResourceId;
if (current.startsWith(prefix)) {
return await response.send(event, context, response.SUCCESS, { generated: current }, current);
}
const generated = generate();
return await response.send(event, context, response.SUCCESS, { generated }, generated);
}
} catch (e) {
console.log(e);
await response.send(event, context, response.FAILED);
}
};
",
},
"Handler": "index.handler",
"Role": {
"Fn::GetAtt": [
"RandomStringGenerator11e9c903f11a4989833c985dddef5eb2ServiceRoleAB6B57A9",
"Arn",
],
},
"Runtime": "nodejs22.x",
"Timeout": 5,
},
"Type": "AWS::Lambda::Function",
},
"RandomStringGenerator11e9c903f11a4989833c985dddef5eb2ServiceRoleAB6B57A9": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
],
],
},
],
},
"Type": "AWS::IAM::Role",
},
"SchedulerRoleForTarget44ece2CFC6840F": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "123456789012",
"aws:SourceArn": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":scheduler:us-west-2:123456789012:schedule-group/default",
],
],
},
},
},
"Effect": "Allow",
"Principal": {
"Service": "scheduler.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
},
"Type": "AWS::IAM::Role",
},
"SchedulerRoleForTarget44ece2DefaultPolicyFDF3E159": {
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "lambda:InvokeFunction",
"Effect": "Allow",
"Resource": [
{
"Fn::GetAtt": [
"AsyncJobHandler438266BD",
"Arn",
],
},
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"AsyncJobHandler438266BD",
"Arn",
],
},
":*",
],
],
},
],
},
],
"Version": "2012-10-17",
},
"PolicyName": "SchedulerRoleForTarget44ece2DefaultPolicyFDF3E159",
"Roles": [
{
"Ref": "SchedulerRoleForTarget44ece2CFC6840F",
},
],
},
"Type": "AWS::IAM::Policy",
},
"Vpc8378EB38": {
"Properties": {
"CidrBlock": "10.0.0.0/16",
"EnableDnsHostnames": true,
"EnableDnsSupport": true,
"InstanceTenancy": "default",
"Tags": [
{
"Key": "Name",
"Value": "ServerlessWebappStarterKitStack/Vpc",
},
],
},
"Type": "AWS::EC2::VPC",
},
"VpcIGWD7BA715C": {
"Properties": {
"Tags": [
{
"Key": "Name",
"Value": "ServerlessWebappStarterKitStack/Vpc",
},
],
},
"Type": "AWS::EC2::InternetGateway",
},
"VpcNatSecurityGroup8DA26EDC": {
"Properties": {
"GroupDescription": "Security Group for NAT instances",
"SecurityGroupEgress": [
{
"CidrIp": "0.0.0.0/0",
"Description": "Allow all outbound traffic by default",
"IpProtocol": "-1",
},
],
"SecurityGroupIngress": [
{
"CidrIp": "0.0.0.0/0",
"Description": "from 0.0.0.0/0:ALL TRAFFIC",
"IpProtocol": "-1",
},
],
"Tags": [
{
"Key": "Name",
"Value": "ServerlessWebappStarterKitStack/Vpc",
},
],
"VpcId": {
"Ref": "Vpc8378EB38",
},
},
"Type": "AWS::EC2::SecurityGroup",
},
"VpcPrivateSubnet1DefaultRouteBE02A9ED": {
"Properties": {
"DestinationCidrBlock": "0.0.0.0/0",
"InstanceId": {
"Ref": "VpcPublicSubnet1NatInstance57B636B8",
},
"RouteTableId": {
"Ref": "VpcPrivateSubnet1RouteTableB2C5B500",
},
},
"Type": "AWS::EC2::Route",
},
"VpcPrivateSubnet1RouteTableAssociation70C59FA6": {
"Properties": {
"RouteTableId": {
"Ref": "VpcPrivateSubnet1RouteTableB2C5B500",
},
"SubnetId": {
"Ref": "VpcPrivateSubnet1Subnet536B997A",
},
},
"Type": "AWS::EC2::SubnetRouteTableAssociation",
},
"VpcPrivateSubnet1RouteTableB2C5B500": {
"Properties": {
"Tags": [
{
"Key": "Name",
"Value": "ServerlessWebappStarterKitStack/Vpc/PrivateSubnet1",
},
],
"VpcId": {
"Ref": "Vpc8378EB38",
},
},
"Type": "AWS::EC2::RouteTable",
},
"VpcPrivateSubnet1Subnet536B997A": {
"Properties": {
"AvailabilityZone": "dummy1a",
"CidrBlock": "10.0.96.0/19",
"MapPublicIpOnLaunch": false,
"Tags": [
{
"Key": "aws-cdk:subnet-name",
"Value": "Private",
},
{
"Key": "aws-cdk:subnet-type",
"Value": "Private",
},
{
"Key": "Name",
"Value": "ServerlessWebappStarterKitStack/Vpc/PrivateSubnet1",
},
],
"VpcId": {
"Ref": "Vpc8378EB38",
},
},
"Type": "AWS::EC2::Subnet",
},
"VpcPrivateSubnet2DefaultRoute060D2087": {
"Properties": {
"DestinationCidrBlock": "0.0.0.0/0",
"InstanceId": {
"Ref": "VpcPublicSubnet1NatInstance57B636B8",
},
"RouteTableId": {
"Ref": "VpcPrivateSubnet2RouteTableA678073B",
},
},
"Type": "AWS::EC2::Route",
},
"VpcPrivateSubnet2RouteTableA678073B": {
"Properties": {
"Tags": [
{
"Key": "Name",
"Value": "ServerlessWebappStarterKitStack/Vpc/PrivateSubnet2",
},
],
"VpcId": {
"Ref": "Vpc8378EB38",
},
},
"Type": "AWS::EC2::RouteTable",
},
"VpcPrivateSubnet2RouteTableAssociationA89CAD56": {
"Properties": {
"RouteTableId": {
"Ref": "VpcPrivateSubnet2RouteTableA678073B",
},
"SubnetId": {
"Ref": "VpcPrivateSubnet2Subnet3788AAA1",
},
},
"Type": "AWS::EC2::SubnetRouteTableAssociation",
},
"VpcPrivateSubnet2Subnet3788AAA1": {
"Properties": {
"AvailabilityZone": "dummy1b",
"CidrBlock": "10.0.128.0/19",
"MapPublicIpOnLaunch": false,
"Tags": [
{
"Key": "aws-cdk:subnet-name",
"Value": "Private",
},
{
"Key": "aws-cdk:subnet-type",
"Value": "Private",
},
{
"Key": "Name",
"Value": "ServerlessWebappStarterKitStack/Vpc/PrivateSubnet2",
},
],
"VpcId": {
"Ref": "Vpc8378EB38",
},
},
"Type": "AWS::EC2::Subnet",
},
"VpcPrivateSubnet3DefaultRoute94B74F0D": {
"Properties": {
"DestinationCidrBlock": "0.0.0.0/0",
"InstanceId": {
"Ref": "VpcPublicSubnet1NatInstance57B636B8",
},
"RouteTableId": {
"Ref": "VpcPrivateSubnet3RouteTableD98824C7",
},
},
"Type": "AWS::EC2::Route",
},
"VpcPrivateSubnet3RouteTableAssociation16BDDC43": {
"Properties": {
"RouteTableId": {
"Ref": "VpcPrivateSubnet3RouteTableD98824C7",
},
"SubnetId": {
"Ref": "VpcPrivateSubnet3SubnetF258B56E",
},
},
"Type": "AWS::EC2::SubnetRouteTableAssociation",
},
"VpcPrivateSubnet3RouteTableD98824C7": {
"Properties": {
"Tags": [
{
"Key": "Name",
"Value": "ServerlessWebappStarterKitStack/Vpc/PrivateSubnet3",
},
],
"VpcId": {
"Ref": "Vpc8378EB38",
},
},
"Type": "AWS::EC2::RouteTable",
},
"VpcPrivateSubnet3SubnetF258B56E": {
"Properties": {
"AvailabilityZone": "dummy1c",
"CidrBlock": "10.0.160.0/19",
"MapPublicIpOnLaunch": false,
"Tags": [
{
"Key": "aws-cdk:subnet-name",
"Value": "Private",
},
{
"Key": "aws-cdk:subnet-type",
"Value": "Private",
},
{
"Key": "Name",
"Value": "ServerlessWebappStarterKitStack/Vpc/PrivateSubnet3",
},
],
"VpcId": {
"Ref": "Vpc8378EB38",
},
},
"Type": "AWS::EC2::Subnet",
},
"VpcPublicSubnet1DefaultRoute3DA9E72A": {
"DependsOn": [
"VpcVPCGWBF912B6E",
],
"Properties": {
"DestinationCidrBlock": "0.0.0.0/0",
"GatewayId": {
"Ref": "VpcIGWD7BA715C",
},
"RouteTableId": {
"Ref": "VpcPublicSubnet1RouteTable6C95E38E",
},
},
"Type": "AWS::EC2::Route",
},
"VpcPublicSubnet1NatInstance57B636B8": {
"DependsOn": [
"VpcPublicSubnet1DefaultRoute3DA9E72A",
"VpcPublicSubnet1NatInstanceInstanceRole9D835E32",
"VpcPublicSubnet1RouteTableAssociation97140677",
],
"Properties": {
"AvailabilityZone": "dummy1a",
"IamInstanceProfile": {
"Ref": "VpcPublicSubnet1NatInstanceInstanceProfileEE10C485",
},
"ImageId": {
"Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestal2023amikernel61arm64C96584B6F00A464EAD1953AFF4B05118Parameter",
},
"InstanceType": "t4g.nano",
"NetworkInterfaces": [
{
"AssociatePublicIpAddress": true,
"DeviceIndex": "0",
"GroupSet": [
{
"Fn::GetAtt": [
"VpcNatSecurityGroup8DA26EDC",
"GroupId",
],
},
],
"SubnetId": {
"Ref": "VpcPublicSubnet1Subnet5C2D37C4",
},
},
],
"SourceDestCheck": false,
"Tags": [
{
"Key": "Name",
"Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet1/NatInstance",
},
],
"UserData": {
"Fn::Base64": "#!/bin/bash
for i in {1..5}; do yum install iptables-services -y && break || sleep 10; done
systemctl enable iptables
systemctl start iptables
echo "net.ipv4.ip_forward=1" > /etc/sysctl.d/custom-ip-forwarding.conf
sysctl -p /etc/sysctl.d/custom-ip-forwarding.conf
IFACE=$(ip route show default | awk '{print $5}')
/sbin/iptables -t nat -A POSTROUTING -o $IFACE -j MASQUERADE
/sbin/iptables -F FORWARD
service iptables save",
},
},
"Type": "AWS::EC2::Instance",
},
"VpcPublicSubnet1NatInstanceInstanceProfileEE10C485": {
"Properties": {
"Roles": [
{
"Ref": "VpcPublicSubnet1NatInstanceInstanceRole9D835E32",
},
],
},
"Type": "AWS::IAM::InstanceProfile",
},
"VpcPublicSubnet1NatInstanceInstanceRole9D835E32": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
"Tags": [
{
"Key": "Name",
"Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet1/NatInstance",
},
],
},
"Type": "AWS::IAM::Role",
},
"VpcPublicSubnet1RouteTable6C95E38E": {
"Properties": {
"Tags": [
{
"Key": "Name",
"Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet1",
},
],
"VpcId": {
"Ref": "Vpc8378EB38",
},
},
"Type": "AWS::EC2::RouteTable",
},
"VpcPublicSubnet1RouteTableAssociation97140677": {
"Properties": {
"RouteTableId": {
"Ref": "VpcPublicSubnet1RouteTable6C95E38E",
},
"SubnetId": {
"Ref": "VpcPublicSubnet1Subnet5C2D37C4",
},
},
"Type": "AWS::EC2::SubnetRouteTableAssociation",
},
"VpcPublicSubnet1Subnet5C2D37C4": {
"Properties": {
"AvailabilityZone": "dummy1a",
"CidrBlock": "10.0.0.0/19",
"MapPublicIpOnLaunch": true,
"Tags": [
{
"Key": "aws-cdk:subnet-name",
"Value": "Public",
},
{
"Key": "aws-cdk:subnet-type",
"Value": "Public",
},
{
"Key": "Name",
"Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet1",
},
],
"VpcId": {
"Ref": "Vpc8378EB38",
},
},
"Type": "AWS::EC2::Subnet",
},
"VpcPublicSubnet2DefaultRoute97F91067": {
"DependsOn": [
"VpcVPCGWBF912B6E",
],
"Properties": {
"DestinationCidrBlock": "0.0.0.0/0",
"GatewayId": {
"Ref": "VpcIGWD7BA715C",
},
"RouteTableId": {
"Ref": "VpcPublicSubnet2RouteTable94F7E489",
},
},
"Type": "AWS::EC2::Route",
},
"VpcPublicSubnet2RouteTable94F7E489": {
"Properties": {
"Tags": [
{
"Key": "Name",
"Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet2",
},
],
"VpcId": {
"Ref": "Vpc8378EB38",
},
},
"Type": "AWS::EC2::RouteTable",
},
"VpcPublicSubnet2RouteTableAssociationDD5762D8": {
"Properties": {
"RouteTableId": {
"Ref": "VpcPublicSubnet2RouteTable94F7E489",
},
"SubnetId": {
"Ref": "VpcPublicSubnet2Subnet691E08A3",
},
},
"Type": "AWS::EC2::SubnetRouteTableAssociation",
},
"VpcPublicSubnet2Subnet691E08A3": {
"Properties": {
"AvailabilityZone": "dummy1b",
"CidrBlock": "10.0.32.0/19",
"MapPublicIpOnLaunch": true,
"Tags": [
{
"Key": "aws-cdk:subnet-name",
"Value": "Public",
},
{
"Key": "aws-cdk:subnet-type",
"Value": "Public",
},
{
"Key": "Name",
"Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet2",
},
],
"VpcId": {
"Ref": "Vpc8378EB38",
},
},
"Type": "AWS::EC2::Subnet",
},
"VpcPublicSubnet3DefaultRoute4697774F": {
"DependsOn": [
"VpcVPCGWBF912B6E",
],
"Properties": {
"DestinationCidrBlock": "0.0.0.0/0",
"GatewayId": {
"Ref": "VpcIGWD7BA715C",
},
"RouteTableId": {
"Ref": "VpcPublicSubnet3RouteTable93458DBB",
},
},
"Type": "AWS::EC2::Route",
},
"VpcPublicSubnet3RouteTable93458DBB": {
"Properties": {
"Tags": [
{
"Key": "Name",
"Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet3",
},
],
"VpcId": {
"Ref": "Vpc8378EB38",
},
},
"Type": "AWS::EC2::RouteTable",
},
"VpcPublicSubnet3RouteTableAssociation1F1EDF02": {
"Properties": {
"RouteTableId": {
"Ref": "VpcPublicSubnet3RouteTable93458DBB",
},
"SubnetId": {
"Ref": "VpcPublicSubnet3SubnetBE12F0B6",
},
},
"Type": "AWS::EC2::SubnetRouteTableAssociation",
},
"VpcPublicSubnet3SubnetBE12F0B6": {
"Properties": {
"AvailabilityZone": "dummy1c",
"CidrBlock": "10.0.64.0/19",
"MapPublicIpOnLaunch": true,
"Tags": [
{
"Key": "aws-cdk:subnet-name",
"Value": "Public",
},
{
"Key": "aws-cdk:subnet-type",
"Value": "Public",
},
{
"Key": "Name",
"Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet3",
},
],
"VpcId": {
"Ref": "Vpc8378EB38",
},
},
"Type": "AWS::EC2::Subnet",
},
"VpcVPCGWBF912B6E": {
"Properties": {
"InternetGatewayId": {
"Ref": "VpcIGWD7BA715C",
},
"VpcId": {
"Ref": "Vpc8378EB38",
},
},
"Type": "AWS::EC2::VPCGatewayAttachment",
},
"Webapp107041BD": {
"Properties": {
"DistributionConfig": {
"Comment": "CloudFront for Webapp",
"DefaultCacheBehavior": {
"AllowedMethods": [
"GET",
"HEAD",
"OPTIONS",
"PUT",
"PATCH",
"POST",
"DELETE",
],
"CachePolicyId": {
"Ref": "WebappSharedCachePolicy14FEE4A0",
},
"Compress": true,
"FunctionAssociations": [
{
"EventType": "viewer-request",
"FunctionARN": {
"Fn::GetAtt": [
"WebappCacheKeyFunction6C227CE2",
"FunctionARN",
],
},
},
],
"LambdaFunctionAssociations": [
{
"EventType": "origin-request",
"IncludeBody": true,
"LambdaFunctionARN": {
"Fn::GetAtt": [
"LookupVersionArnc8730278af02f875114ca902814c77b68f19b0087110E04D0A",
"Parameter.Value",
],
},
},
],
"OriginRequestPolicyId": "b689b0a8-53d0-40ab-baf2-68738e2966ac",
"TargetOriginId": "ServerlessWebappStarterKitStackWebappOrigin1D7B867FF",
"ViewerProtocolPolicy": "allow-all",
},
"Enabled": true,
"HttpVersion": "http2",
"IPV6Enabled": true,
"Logging": {
"Bucket": {
"Fn::GetAtt": [
"AccessLogBucketDA470295",
"RegionalDomainName",
],
},
"Prefix": "Webapp/",
},
"Origins": [
{
"ConnectionTimeout": 6,
"CustomOriginConfig": {
"OriginProtocolPolicy": "https-only",
"OriginReadTimeout": 60,
"OriginSSLProtocols": [
"TLSv1.2",
],
},
"DomainName": {
"Fn::Select": [
2,
{
"Fn::Split": [
"/",
{
"Fn::GetAtt": [
"WebappHandlerFunctionUrl7AEF8DEE",
"FunctionUrl",
],
},
],
},
],
},
"Id": "ServerlessWebappStarterKitStackWebappOrigin1D7B867FF",
"OriginAccessControlId": {
"Fn::GetAtt": [
"WebappOrigin1FunctionUrlOriginAccessControlEA98B600",
"Id",
],
},
},
],
},
},
"Type": "AWS::CloudFront::Distribution",
},
"WebappBuild348806E3": {
"DeletionPolicy": "Delete",
"DependsOn": [
"ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549",
"ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleDefaultPolicy2316728F",
"ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleC5F7BBFE",
],
"Properties": {
"ServiceToken": {
"Fn::GetAtt": [
"DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f306AEFF37",
"Arn",
],
},
"buildCommand": {
"Fn::Join": [
"",
[
"docker buildx build --build-arg ALLOWED_ORIGIN_HOST=*.cloudfront.net --build-arg SKIP_TS_BUILD=true --build-arg NEXT_PUBLIC_EVENT_HTTP_ENDPOINT=https://",
{
"Fn::GetAtt": [
"EventBusApi6E8C7C94",
"Dns.Http",
],
},
" --build-arg NEXT_PUBLIC_AWS_REGION=us-west-2 --platform linux/arm64 --output type=image,name=",
{
"Fn::Select": [
4,
{
"Fn::Split": [
":",
{
"Fn::GetAtt": [
"WebappBuildRepository4C93D48D",
"Arn",
],
},
],
},
],
},
".dkr.ecr.",
{
"Fn::Select": [
3,
{
"Fn::Split": [
":",
{
"Fn::GetAtt": [
"WebappBuildRepository4C93D48D",
"Arn",
],
},
],
},
],
},
".",
{
"Ref": "AWS::URLSuffix",
},
"/",
{
"Ref": "WebappBuildRepository4C93D48D",
},
":<IMAGE_TAG>,push=true --provenance=false .",
],
],
},
"codeBuildProjectName": {
"Ref": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549",
},
"repositoryUri": {
"Fn::Join": [
"",
[
{
"Fn::Select": [
4,
{
"Fn::Split": [
":",
{
"Fn::GetAtt": [
"WebappBuildRepository4C93D48D",
"Arn",
],
},
],
},
],
},
".dkr.ecr.",
{
"Fn::Select": [
3,
{
"Fn::Split": [
":",
{
"Fn::GetAtt": [
"WebappBuildRepository4C93D48D",
"Arn",
],
},
],
},
],
},
".",
{
"Ref": "AWS::URLSuffix",
},
"/",
{
"Ref": "WebappBuildRepository4C93D48D",
},
],
],
},
"sourceS3Url": "s3://cdk-hnb659fds-assets-123456789012-us-west-2/REDACTED",
"tagPrefix": "REDACTED",
"type": "ContainerImageBuild",
},
"Type": "Custom::CDKContainerImageBuild",
"UpdateReplacePolicy": "Delete",
},
"WebappBuildRepository4C93D48D": {
"DeletionPolicy": "Delete",
"Properties": {
"EmptyOnDelete": true,
"RepositoryPolicyText": {
"Statement": [
{
"Action": [
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
],
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
},
"Type": "AWS::ECR::Repository",
"UpdateReplacePolicy": "Delete",
},
"WebappCacheKeyFunction6C227CE2": {
"Properties": {
"AutoPublish": true,
"FunctionCode": "// CloudFront Functions JS 2.0
// Combines Next.js RSC-related headers into a single hashed cache key header
// to prevent cache poisoning between HTML and RSC flight responses.
//
// Next.js App Router sets Vary: rsc, next-router-state-tree, next-router-prefetch,
// next-router-segment-prefetch (and next-url for interception routes).
// CloudFront ignores Vary and requires explicit cache key configuration,
// but its Cache Policy has a 10-header limit. This function hashes all
// RSC headers into one header to stay within the limit.
async function handler(event) {
var h = event.request.headers;
var parts = [
'rsc',
'next-router-prefetch',
'next-router-state-tree',
'next-router-segment-prefetch',
'next-url',
];
var key = '';
for (var i = 0; i < parts.length; i++) {
if (h[parts[i]]) {
key += parts[i] + '=' + h[parts[i]].value + ';';
}
}
if (key) {
// FNV-1a hash (32-bit). Cryptographic strength is unnecessary;
// we only need distinct cache keys for distinct header combinations.
// See: https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
var FNV_OFFSET_BASIS = 2166136261;
var FNV_PRIME = 16777619;
var hash = FNV_OFFSET_BASIS;
for (var j = 0; j < key.length; j++) {
hash ^= key.charCodeAt(j);
hash = (hash * FNV_PRIME) | 0;
}
event.request.headers['x-nextjs-cache-key'] = { value: String(hash >>> 0) };
}
return event.request;
}
",
"FunctionConfig": {
"Comment": "us-west-2ServerlessWebappCacheKeyFunction86D1ABE9",
"Runtime": "cloudfront-js-2.0",
},
"Name": "us-west-2ServerlessWebappCacheKeyFunction86D1ABE9",
},
"Type": "AWS::CloudFront::Function",
},
"WebappCloudFrontInvalidation588CF152": {
"DeletionPolicy": "Delete",
"DependsOn": [
"WebappCloudFrontInvalidationCustomResourcePolicy18C215D6",
],
"Properties": {
"Create": {
"Fn::Join": [
"",
[
"{"service":"cloudfront","action":"createInvalidation","parameters":{"DistributionId":"",
{
"Ref": "Webapp107041BD",
},
"","InvalidationBatch":{"CallerReference":"",
{
"Fn::GetAtt": [
"WebappHandlerCurrentVersionREDACTED",
"Version",
],
},
"","Paths":{"Quantity":1,"Items":["/*"]}}},"physicalResourceId":{"id":"invalidation"}}",
],
],
},
"InstallLatestAwsSdk": true,
"ServiceToken": {
"Fn::GetAtt": [
"AWS679f53fac002430cb0da5b7982bd22872D164C4C",
"Arn",
],
},
"Update": {
"Fn::Join": [
"",
[
"{"service":"cloudfront","action":"createInvalidation","parameters":{"DistributionId":"",
{
"Ref": "Webapp107041BD",
},
"","InvalidationBatch":{"CallerReference":"",
{
"Fn::GetAtt": [
"WebappHandlerCurrentVersionREDACTED",
"Version",
],
},
"","Paths":{"Quantity":1,"Items":["/*"]}}},"physicalResourceId":{"id":"invalidation"}}",
],
],
},
},
"Type": "Custom::AWS",
"UpdateReplacePolicy": "Delete",
},
"WebappCloudFrontInvalidationCustomResourcePolicy18C215D6": {
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "cloudfront:CreateInvalidation",
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":cloudfront::123456789012:distribution/",
{
"Ref": "Webapp107041BD",
},
],
],
},
},
],
"Version": "2012-10-17",
},
"PolicyName": "WebappCloudFrontInvalidationCustomResourcePolicy18C215D6",
"Roles": [
{
"Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2",
},
],
},
"Type": "AWS::IAM::Policy",
},
"WebappHandler8DD158A3": {
"DependsOn": [
"VpcPrivateSubnet1DefaultRouteBE02A9ED",
"VpcPrivateSubnet1RouteTableAssociation70C59FA6",
"VpcPrivateSubnet2DefaultRoute060D2087",
"VpcPrivateSubnet2RouteTableAssociationA89CAD56",
"VpcPrivateSubnet3DefaultRoute94B74F0D",
"VpcPrivateSubnet3RouteTableAssociation16BDDC43",
"WebappHandlerServiceRoleDefaultPolicy7D06F4EA",
"WebappHandlerServiceRole4F4D1ACD",
],
"Properties": {
"Architectures": [
"arm64",
],
"Code": {
"ImageUri": {
"Fn::Join": [
"",
[
{
"Fn::Select": [
4,
{
"Fn::Split": [
":",
{
"Fn::GetAtt": [
"WebappBuildRepository4C93D48D",
"Arn",
],
},
],
},
],
},
".dkr.ecr.",
{
"Fn::Select": [
3,
{
"Fn::Split": [
":",
{
"Fn::GetAtt": [
"WebappBuildRepository4C93D48D",
"Arn",
],
},
],
},
],
},
".",
{
"Ref": "AWS::URLSuffix",
},
"/",
{
"Ref": "WebappBuildRepository4C93D48D",
},
":",
{
"Fn::GetAtt": [
"WebappBuild348806E3",
"ImageTag",
],
},
],
],
},
},
"Environment": {
"Variables": {
"AMPLIFY_APP_ORIGIN_SOURCE_PARAMETER": {
"Ref": "WebappOriginSourceParameterD87E143B",
},
"ASYNC_JOB_HANDLER_ARN": {
"Fn::GetAtt": [
"AsyncJobHandler438266BD",
"Arn",
],
},
"COGNITO_DOMAIN": {
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"AuthDomainPrefixE1742B23",
"generated",
],
},
".auth.us-west-2.amazoncognito.com",
],
],
},
"DATABASE_ENGINE": {
"Fn::Join": [
"",
[
"{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretAttachmentDC8466C0",
},
":SecretString:engine::}}",
],
],
},
"DATABASE_HOST": {
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Address",
],
},
"DATABASE_NAME": "main",
"DATABASE_OPTION": "?connection_limit=1&connect_timeout=30",
"DATABASE_PASSWORD": {
"Fn::Join": [
"",
[
"{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretAttachmentDC8466C0",
},
":SecretString:password::}}",
],
],
},
"DATABASE_PORT": {
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Port",
],
},
"DATABASE_URL": {
"Fn::Join": [
"",
[
"{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretAttachmentDC8466C0",
},
":SecretString:engine::}}://{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretAttachmentDC8466C0",
},
":SecretString:username::}}:{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretAttachmentDC8466C0",
},
":SecretString:password::}}@",
{
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Address",
],
},
":",
{
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Port",
],
},
"/main?connection_limit=1&connect_timeout=30",
],
],
},
"DATABASE_USER": {
"Fn::Join": [
"",
[
"{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretAttachmentDC8466C0",
},
":SecretString:username::}}",
],
],
},
"USER_POOL_CLIENT_ID": {
"Ref": "AuthUserPoolClientC635291F",
},
"USER_POOL_ID": {
"Ref": "AuthUserPool8115E87F",
},
},
},
"LoggingConfig": {
"LogGroup": {
"Ref": "WebappHandlerLogs87A6D2D7",
},
},
"MemorySize": 1024,
"PackageType": "Image",
"Role": {
"Fn::GetAtt": [
"WebappHandlerServiceRole4F4D1ACD",
"Arn",
],
},
"Timeout": 180,
"VpcConfig": {
"SecurityGroupIds": [
{
"Fn::GetAtt": [
"WebappHandlerSecurityGroup5451B519",
"GroupId",
],
},
],
"SubnetIds": [
{
"Ref": "VpcPrivateSubnet1Subnet536B997A",
},
{
"Ref": "VpcPrivateSubnet2Subnet3788AAA1",
},
{
"Ref": "VpcPrivateSubnet3SubnetF258B56E",
},
],
},
},
"Type": "AWS::Lambda::Function",
},
"WebappHandlerCurrentVersionREDACTED": {
"DependsOn": [
"VpcPrivateSubnet1DefaultRouteBE02A9ED",
"VpcPrivateSubnet1RouteTableAssociation70C59FA6",
"VpcPrivateSubnet2DefaultRoute060D2087",
"VpcPrivateSubnet2RouteTableAssociationA89CAD56",
"VpcPrivateSubnet3DefaultRoute94B74F0D",
"VpcPrivateSubnet3RouteTableAssociation16BDDC43",
],
"Properties": {
"FunctionName": {
"Ref": "WebappHandler8DD158A3",
},
},
"Type": "AWS::Lambda::Version",
},
"WebappHandlerFunctionUrl7AEF8DEE": {
"DependsOn": [
"VpcPrivateSubnet1DefaultRouteBE02A9ED",
"VpcPrivateSubnet1RouteTableAssociation70C59FA6",
"VpcPrivateSubnet2DefaultRoute060D2087",
"VpcPrivateSubnet2RouteTableAssociationA89CAD56",
"VpcPrivateSubnet3DefaultRoute94B74F0D",
"VpcPrivateSubnet3RouteTableAssociation16BDDC43",
],
"Properties": {
"AuthType": "AWS_IAM",
"InvokeMode": "RESPONSE_STREAM",
"TargetFunctionArn": {
"Fn::GetAtt": [
"WebappHandler8DD158A3",
"Arn",
],
},
},
"Type": "AWS::Lambda::Url",
},
"WebappHandlerLogs87A6D2D7": {
"DeletionPolicy": "Delete",
"Properties": {
"RetentionInDays": 7,
},
"Type": "AWS::Logs::LogGroup",
"UpdateReplacePolicy": "Delete",
},
"WebappHandlerSecurityGroup5451B519": {
"DependsOn": [
"VpcPrivateSubnet1DefaultRouteBE02A9ED",
"VpcPrivateSubnet1RouteTableAssociation70C59FA6",
"VpcPrivateSubnet2DefaultRoute060D2087",
"VpcPrivateSubnet2RouteTableAssociationA89CAD56",
"VpcPrivateSubnet3DefaultRoute94B74F0D",
"VpcPrivateSubnet3RouteTableAssociation16BDDC43",
],
"Properties": {
"GroupDescription": "Automatic security group for Lambda Function ServerlessWebappStarterKitStackWebappHandlerF1A4ACC9",
"SecurityGroupEgress": [
{
"CidrIp": "0.0.0.0/0",
"Description": "Allow all outbound traffic by default",
"IpProtocol": "-1",
},
],
"VpcId": {
"Ref": "Vpc8378EB38",
},
},
"Type": "AWS::EC2::SecurityGroup",
},
"WebappHandlerServiceRole4F4D1ACD": {
"DependsOn": [
"VpcPrivateSubnet1DefaultRouteBE02A9ED",
"VpcPrivateSubnet1RouteTableAssociation70C59FA6",
"VpcPrivateSubnet2DefaultRoute060D2087",
"VpcPrivateSubnet2RouteTableAssociationA89CAD56",
"VpcPrivateSubnet3DefaultRoute94B74F0D",
"VpcPrivateSubnet3RouteTableAssociation16BDDC43",
],
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
],
],
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole",
],
],
},
],
},
"Type": "AWS::IAM::Role",
},
"WebappHandlerServiceRoleDefaultPolicy7D06F4EA": {
"DependsOn": [
"VpcPrivateSubnet1DefaultRouteBE02A9ED",
"VpcPrivateSubnet1RouteTableAssociation70C59FA6",
"VpcPrivateSubnet2DefaultRoute060D2087",
"VpcPrivateSubnet2RouteTableAssociationA89CAD56",
"VpcPrivateSubnet3DefaultRoute94B74F0D",
"VpcPrivateSubnet3RouteTableAssociation16BDDC43",
],
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "lambda:InvokeFunction",
"Effect": "Allow",
"Resource": [
{
"Fn::GetAtt": [
"AsyncJobHandler438266BD",
"Arn",
],
},
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"AsyncJobHandler438266BD",
"Arn",
],
},
":*",
],
],
},
],
},
{
"Action": [
"ssm:DescribeParameters",
"ssm:GetParameters",
"ssm:GetParameter",
"ssm:GetParameterHistory",
],
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":ssm:us-west-2:123456789012:parameter/",
{
"Ref": "WebappOriginSourceParameterD87E143B",
},
],
],
},
},
],
"Version": "2012-10-17",
},
"PolicyName": "WebappHandlerServiceRoleDefaultPolicy7D06F4EA",
"Roles": [
{
"Ref": "WebappHandlerServiceRole4F4D1ACD",
},
],
},
"Type": "AWS::IAM::Policy",
},
"WebappInvokeFunctionPermission8F3F2610": {
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Fn::GetAtt": [
"WebappHandler8DD158A3",
"Arn",
],
},
"Principal": "cloudfront.amazonaws.com",
"SourceArn": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":cloudfront::",
{
"Ref": "AWS::AccountId",
},
":distribution/",
{
"Ref": "Webapp107041BD",
},
],
],
},
},
"Type": "AWS::Lambda::Permission",
},
"WebappMigrationRunnerAC67C012": {
"DependsOn": [
"VpcPrivateSubnet1DefaultRouteBE02A9ED",
"VpcPrivateSubnet1RouteTableAssociation70C59FA6",
"VpcPrivateSubnet2DefaultRoute060D2087",
"VpcPrivateSubnet2RouteTableAssociationA89CAD56",
"VpcPrivateSubnet3DefaultRoute94B74F0D",
"VpcPrivateSubnet3RouteTableAssociation16BDDC43",
"WebappMigrationRunnerServiceRoleE27E1F7A",
],
"Properties": {
"Architectures": [
"arm64",
],
"Code": {
"ImageUri": {
"Fn::Sub": "REDACTED",
},
},
"Environment": {
"Variables": {
"DATABASE_ENGINE": {
"Fn::Join": [
"",
[
"{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretAttachmentDC8466C0",
},
":SecretString:engine::}}",
],
],
},
"DATABASE_HOST": {
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Address",
],
},
"DATABASE_NAME": "main",
"DATABASE_OPTION": "?connection_limit=1&connect_timeout=30",
"DATABASE_PASSWORD": {
"Fn::Join": [
"",
[
"{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretAttachmentDC8466C0",
},
":SecretString:password::}}",
],
],
},
"DATABASE_PORT": {
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Port",
],
},
"DATABASE_URL": {
"Fn::Join": [
"",
[
"{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretAttachmentDC8466C0",
},
":SecretString:engine::}}://{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretAttachmentDC8466C0",
},
":SecretString:username::}}:{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretAttachmentDC8466C0",
},
":SecretString:password::}}@",
{
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Address",
],
},
":",
{
"Fn::GetAtt": [
"DatabaseCluster5B53A178",
"Endpoint.Port",
],
},
"/main?connection_limit=1&connect_timeout=30",
],
],
},
"DATABASE_USER": {
"Fn::Join": [
"",
[
"{{resolve:secretsmanager:",
{
"Ref": "DatabaseClusterSecretAttachmentDC8466C0",
},
":SecretString:username::}}",
],
],
},
},
},
"ImageConfig": {
"Command": [
"migration-runner.handler",
],
},
"LoggingConfig": {
"LogGroup": {
"Ref": "WebappMigrationRunnerLogsD9A84B90",
},
},
gitextract_lfz83ak7/
├── .github/
│ └── workflows/
│ ├── build.yml
│ ├── commitlint.yml
│ ├── needs-triage.yml
│ ├── release-please.yml
│ ├── stale.yml
│ └── update_snapshot.yml
├── .prettierrc
├── .release-please-manifest.json
├── .serverless-full-stack-webapp-starter-kit/
│ └── design/
│ └── DESIGN_PRINCIPLES.md
├── AGENTS.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── cdk/
│ ├── .gitignore
│ ├── README.md
│ ├── bin/
│ │ └── cdk.ts
│ ├── cdk.json
│ ├── jest.config.js
│ ├── lib/
│ │ ├── constructs/
│ │ │ ├── async-job.ts
│ │ │ ├── auth/
│ │ │ │ ├── .gitignore
│ │ │ │ ├── index.ts
│ │ │ │ └── prefix-generator.js
│ │ │ ├── cf-lambda-furl-service/
│ │ │ │ ├── edge-function.ts
│ │ │ │ ├── lambda/
│ │ │ │ │ └── sign-payload.ts
│ │ │ │ └── service.ts
│ │ │ ├── database.ts
│ │ │ ├── event-bus/
│ │ │ │ ├── handler.mjs
│ │ │ │ └── index.ts
│ │ │ └── webapp.ts
│ │ ├── main-stack.ts
│ │ └── us-east-1-stack.ts
│ ├── package.json
│ ├── test/
│ │ ├── __snapshots__/
│ │ │ ├── serverless-fullstack-webapp-starter-kit-without-domain.test.ts.snap
│ │ │ └── serverless-fullstack-webapp-starter-kit.test.ts.snap
│ │ ├── serverless-fullstack-webapp-starter-kit-without-domain.test.ts
│ │ ├── serverless-fullstack-webapp-starter-kit.test.ts
│ │ └── snapshot-plugin.ts
│ └── tsconfig.json
├── compose.yaml
├── release-please-config.json
└── webapp/
├── .dockerignore
├── .env.local.example
├── .gitignore
├── Dockerfile
├── README.md
├── components.json
├── eslint.config.mjs
├── job.Dockerfile
├── next.config.ts
├── package.json
├── postcss.config.mjs
├── prisma/
│ └── schema.prisma
├── run.sh
├── src/
│ ├── app/
│ │ ├── (root)/
│ │ │ ├── actions.ts
│ │ │ ├── components/
│ │ │ │ ├── CreateTodoForm.tsx
│ │ │ │ └── TodoItem.tsx
│ │ │ ├── page.tsx
│ │ │ └── schemas.ts
│ │ ├── api/
│ │ │ ├── auth/
│ │ │ │ └── [slug]/
│ │ │ │ └── route.ts
│ │ │ └── cognito-token/
│ │ │ └── route.ts
│ │ ├── auth-callback/
│ │ │ └── page.tsx
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ └── sign-in/
│ │ └── page.tsx
│ ├── components/
│ │ ├── Header.tsx
│ │ └── ui/
│ │ └── sonner.tsx
│ ├── hooks/
│ │ └── use-event-bus.ts
│ ├── jobs/
│ │ ├── async-job/
│ │ │ └── translate.ts
│ │ ├── async-job-runner.ts
│ │ └── migration-runner.ts
│ ├── lib/
│ │ ├── amplifyServerUtils.ts
│ │ ├── auth.ts
│ │ ├── events.ts
│ │ ├── jobs.ts
│ │ ├── prisma.ts
│ │ ├── safe-action.ts
│ │ └── utils.ts
│ └── proxy.ts
└── tsconfig.json
SYMBOL INDEX (62 symbols across 30 files)
FILE: cdk/bin/cdk.ts
type EnvironmentProps (line 9) | interface EnvironmentProps {
FILE: cdk/lib/constructs/async-job.ts
type AsyncJobProps (line 13) | interface AsyncJobProps {
class AsyncJob (line 18) | class AsyncJob extends Construct {
method constructor (line 21) | constructor(scope: Construct, id: string, props: AsyncJobProps) {
method addSchedule (line 67) | public addSchedule(jobType: string, schedule: ScheduleExpression, payl...
FILE: cdk/lib/constructs/auth/index.ts
type AuthProps (line 12) | interface AuthProps {
class Auth (line 27) | class Auth extends Construct {
method constructor (line 34) | constructor(scope: Construct, id: string, props: AuthProps) {
method addAllowedCallbackUrls (line 135) | public addAllowedCallbackUrls(callbackUrl: string, logoutUrl: string) {
method updateAllowedCallbackUrls (line 145) | public updateAllowedCallbackUrls(callbackUrls: string[], logoutUrls: s...
FILE: cdk/lib/constructs/cf-lambda-furl-service/edge-function.ts
type EdgeFunctionProps (line 12) | interface EdgeFunctionProps {
class EdgeFunction (line 16) | class EdgeFunction extends Construct {
method constructor (line 19) | constructor(scope: Construct, id: string, props: EdgeFunctionProps) {
method versionArn (line 42) | public versionArn(scope: Construct) {
FILE: cdk/lib/constructs/cf-lambda-furl-service/service.ts
type CloudFrontLambdaFunctionUrlServiceProps (line 29) | interface CloudFrontLambdaFunctionUrlServiceProps {
class CloudFrontLambdaFunctionUrlService (line 65) | class CloudFrontLambdaFunctionUrlService extends Construct {
method constructor (line 70) | constructor(scope: Construct, id: string, props: CloudFrontLambdaFunct...
FILE: cdk/lib/constructs/database.ts
type DatabaseProps (line 8) | interface DatabaseProps {
class Database (line 12) | class Database extends Construct implements ec2.IConnectable {
method constructor (line 17) | constructor(scope: Construct, id: string, props: DatabaseProps) {
method getConnectionInfo (line 85) | public getConnectionInfo() {
method getLambdaEnvironment (line 97) | public getLambdaEnvironment(databaseName: string) {
FILE: cdk/lib/constructs/event-bus/handler.mjs
function onSubscribe (line 9) | function onSubscribe(ctx) {
FILE: cdk/lib/constructs/event-bus/index.ts
type EventBusProps (line 7) | interface EventBusProps {}
class EventBus (line 9) | class EventBus extends Construct {
method constructor (line 15) | constructor(scope: Construct, id: string, props: EventBusProps) {
method addUserPoolProvider (line 44) | public addUserPoolProvider(userPool: IUserPool) {
FILE: cdk/lib/constructs/webapp.ts
type WebappProps (line 22) | interface WebappProps {
class Webapp (line 50) | class Webapp extends Construct {
method constructor (line 53) | constructor(scope: Construct, id: string, props: WebappProps) {
FILE: cdk/lib/main-stack.ts
type MainStackProps (line 14) | interface MainStackProps extends StackProps {
class MainStack (line 38) | class MainStack extends Stack {
method constructor (line 39) | constructor(scope: Construct, id: string, props: MainStackProps) {
FILE: cdk/lib/us-east-1-stack.ts
type UsEast1StackProps (line 8) | interface UsEast1StackProps extends cdk.StackProps {
class UsEast1Stack (line 17) | class UsEast1Stack extends cdk.Stack {
method constructor (line 28) | constructor(scope: Construct, id: string, props: UsEast1StackProps) {
FILE: webapp/src/app/(root)/components/CreateTodoForm.tsx
function CreateTodoForm (line 12) | function CreateTodoForm(props: { userId: string }) {
FILE: webapp/src/app/(root)/components/TodoItem.tsx
type TodoItemProps (line 12) | interface TodoItemProps {
function TodoItemComponent (line 16) | function TodoItemComponent({ todo }: TodoItemProps) {
FILE: webapp/src/app/(root)/page.tsx
function Home (line 8) | async function Home() {
FILE: webapp/src/app/api/auth/[slug]/route.ts
constant GET (line 3) | const GET = createAuthRouteHandlers({
FILE: webapp/src/app/api/cognito-token/route.ts
function GET (line 4) | async function GET() {
FILE: webapp/src/app/auth-callback/page.tsx
function AuthCallbackPage (line 7) | async function AuthCallbackPage() {
FILE: webapp/src/app/layout.tsx
function RootLayout (line 4) | function RootLayout({ children }: { children: React.ReactNode }) {
FILE: webapp/src/app/sign-in/page.tsx
function SignInPage (line 1) | function SignInPage() {
FILE: webapp/src/components/Header.tsx
function Header (line 3) | function Header() {
FILE: webapp/src/hooks/use-event-bus.ts
type UseEventBusProps (line 31) | type UseEventBusProps = {
FILE: webapp/src/jobs/async-job-runner.ts
type JobPayloadProps (line 12) | type JobPayloadProps = z.infer<typeof jobPayloadPropsSchema>;
FILE: webapp/src/jobs/migration-runner.ts
function runPrismaDbPush (line 36) | async function runPrismaDbPush(options: string[], maxRetries = 5, baseDe...
FILE: webapp/src/lib/auth.ts
function tryGetAuthSession (line 35) | async function tryGetAuthSession() {
class UserNotFoundError (line 56) | class UserNotFoundError {
method constructor (line 57) | constructor(public readonly userId: string) {}
FILE: webapp/src/lib/events.ts
function sendEvent (line 9) | async function sendEvent(channelName: string, payload: unknown) {
FILE: webapp/src/lib/jobs.ts
function runJob (line 8) | async function runJob(props: JobPayloadProps) {
FILE: webapp/src/lib/prisma.ts
function isRetryableError (line 13) | function isRetryableError(error: unknown): boolean {
function withRetry (line 35) | async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3, baseDe...
method $allOperations (line 61) | async $allOperations({ args, query }) {
FILE: webapp/src/lib/safe-action.ts
class MyCustomError (line 4) | class MyCustomError extends Error {
method constructor (line 5) | constructor(message: string) {
method handleServerError (line 12) | handleServerError(e) {
FILE: webapp/src/lib/utils.ts
function cn (line 4) | function cn(...inputs: ClassValue[]) {
FILE: webapp/src/proxy.ts
function proxy (line 6) | async function proxy(request: NextRequest) {
Condensed preview — 81 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (414K chars).
[
{
"path": ".github/workflows/build.yml",
"chars": 1104,
"preview": "permissions:\n contents: read\nname: Build\non:\n push:\n branches:\n - main\n workflow_dispatch:\n pull_request:\njo"
},
{
"path": ".github/workflows/commitlint.yml",
"chars": 291,
"preview": "name: PR Title\n\non:\n pull_request_target:\n types: [opened, edited, synchronize]\n\npermissions:\n pull-requests: read\n"
},
{
"path": ".github/workflows/needs-triage.yml",
"chars": 523,
"preview": "name: Add needs-triage label\non:\n issues:\n types: [opened]\n\npermissions:\n issues: write\n\njobs:\n add-label:\n if:"
},
{
"path": ".github/workflows/release-please.yml",
"chars": 229,
"preview": "on:\n push:\n branches:\n - main\n\npermissions:\n contents: write\n pull-requests: write\n\nname: release-please\n\njob"
},
{
"path": ".github/workflows/stale.yml",
"chars": 865,
"preview": "name: Close stale issues and PRs\non:\n schedule:\n - cron: '0 0 * * *'\n\npermissions:\n issues: write\n pull-requests: "
},
{
"path": ".github/workflows/update_snapshot.yml",
"chars": 950,
"preview": "name: Update snapshot\n\non:\n workflow_dispatch:\n push:\n branches:\n - 'dependabot/**'\n issue_comment:\n types"
},
{
"path": ".prettierrc",
"chars": 115,
"preview": "{\n \"trailingComma\": \"all\",\n \"tabWidth\": 2,\n \"semi\": true,\n \"singleQuote\": true,\n \"printWidth\": 120\n}"
},
{
"path": ".release-please-manifest.json",
"chars": 19,
"preview": "{\n \".\": \"2.1.0\"\n}\n"
},
{
"path": ".serverless-full-stack-webapp-starter-kit/design/DESIGN_PRINCIPLES.md",
"chars": 3590,
"preview": "# Design Principles\n\nThis document is for kit maintainers. If you copied this kit to build your own app, you can safely "
},
{
"path": "AGENTS.md",
"chars": 4165,
"preview": "# AGENTS.md\n\n## Commands\n\n```bash\n# webapp\ncd webapp && npm ci\ncd webapp && npm run dev # starts on port 3010\nc"
},
{
"path": "CHANGELOG.md",
"chars": 4346,
"preview": "# Changelog\n\n## [2.1.0](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/compare/v2.0.0...v2.1.0)"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 309,
"preview": "## Code of Conduct\nThis project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-condu"
},
{
"path": "CONTRIBUTING.md",
"chars": 3497,
"preview": "# Contributing Guidelines\n\nThank you for your interest in contributing to our project. Whether it's a bug report, new fe"
},
{
"path": "LICENSE",
"chars": 927,
"preview": "Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n\nPermission is hereby granted, free of charge, to any"
},
{
"path": "README.md",
"chars": 7104,
"preview": "# Serverless Full Stack WebApp Starter Kit\n[\nThis is the IaC project written in AWS Cloud Development Kit (CDK).\n\n## Useful comman"
},
{
"path": "cdk/bin/cdk.ts",
"chars": 1783,
"preview": "#!/usr/bin/env node\nimport 'source-map-support/register';\nimport * as cdk from 'aws-cdk-lib';\nimport { MainStack } from "
},
{
"path": "cdk/cdk.json",
"chars": 2046,
"preview": "{\n \"app\": \"npx ts-node --prefer-ts-exts bin/cdk.ts\",\n \"watch\": {\n \"include\": [\n \"**\"\n ],\n \"exclude\": [\n "
},
{
"path": "cdk/jest.config.js",
"chars": 219,
"preview": "module.exports = {\n testEnvironment: 'node',\n roots: ['<rootDir>/test'],\n testMatch: ['**/*.test.ts'],\n transform: {"
},
{
"path": "cdk/lib/constructs/async-job.ts",
"chars": 2631,
"preview": "import { Construct } from 'constructs';\nimport { CfnOutput, Duration, RemovalPolicy, TimeZone } from 'aws-cdk-lib';\nimpo"
},
{
"path": "cdk/lib/constructs/auth/.gitignore",
"chars": 21,
"preview": "!prefix-generator.js\n"
},
{
"path": "cdk/lib/constructs/auth/index.ts",
"chars": 6575,
"preview": "import { UpdateUserPoolClientCommandInput } from '@aws-sdk/client-cognito-identity-provider';\nimport { CfnOutput, CfnRes"
},
{
"path": "cdk/lib/constructs/auth/prefix-generator.js",
"chars": 1212,
"preview": "const response = require('cfn-response');\nconst crypto = require('crypto');\n\nexports.handler = async function (event, co"
},
{
"path": "cdk/lib/constructs/cf-lambda-furl-service/edge-function.ts",
"chars": 2597,
"preview": "import { Stack } from 'aws-cdk-lib';\nimport { PolicyStatement, ServicePrincipal, Role } from 'aws-cdk-lib/aws-iam';\nimpo"
},
{
"path": "cdk/lib/constructs/cf-lambda-furl-service/lambda/sign-payload.ts",
"chars": 833,
"preview": "import type { CloudFrontRequestHandler } from 'aws-lambda';\nimport { createHash } from 'crypto';\n\nconst hashPayload = (p"
},
{
"path": "cdk/lib/constructs/cf-lambda-furl-service/service.ts",
"chars": 6802,
"preview": "import { Construct } from 'constructs';\nimport { Aws, Duration } from 'aws-cdk-lib';\nimport { FunctionUrlAuthType, Funct"
},
{
"path": "cdk/lib/constructs/database.ts",
"chars": 4647,
"preview": "import { CfnOutput, Stack, Token } from 'aws-cdk-lib';\nimport * as ec2 from 'aws-cdk-lib/aws-ec2';\nimport * as logs from"
},
{
"path": "cdk/lib/constructs/event-bus/handler.mjs",
"chars": 584,
"preview": "import { util } from '@aws-appsync/utils';\n\n/**\n * Allow subscription only for the channels that\n * 1. begin with /publi"
},
{
"path": "cdk/lib/constructs/event-bus/index.ts",
"chars": 2133,
"preview": "import { Construct } from 'constructs';\nimport * as appsync from 'aws-cdk-lib/aws-appsync';\nimport { CfnOutput, CfnResou"
},
{
"path": "cdk/lib/constructs/webapp.ts",
"chars": 7519,
"preview": "import { IgnoreMode, Duration, CfnOutput, Stack, RemovalPolicy } from 'aws-cdk-lib';\nimport { LogGroup, RetentionDays } "
},
{
"path": "cdk/lib/main-stack.ts",
"chars": 4176,
"preview": "import { CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';\nimport { BlockPublicAccess, Bucket, BucketEnc"
},
{
"path": "cdk/lib/us-east-1-stack.ts",
"chars": 2194,
"preview": "import * as cdk from 'aws-cdk-lib';\nimport { Certificate, CertificateValidation, ICertificate } from 'aws-cdk-lib/aws-ce"
},
{
"path": "cdk/package.json",
"chars": 959,
"preview": "{\n \"name\": \"@aws-samples/serverless-fullstack-webapp-starter-kit\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"scripts\""
},
{
"path": "cdk/test/__snapshots__/serverless-fullstack-webapp-starter-kit-without-domain.test.ts.snap",
"chars": 130856,
"preview": "// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing\n\nexports[`Snapshot test 1`] = `\n{\n \"Parameters\": {\n \"Bo"
},
{
"path": "cdk/test/__snapshots__/serverless-fullstack-webapp-starter-kit.test.ts.snap",
"chars": 122415,
"preview": "// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing\n\nexports[`Snapshot test 1`] = `\n{\n \"Parameters\": {\n \"Bo"
},
{
"path": "cdk/test/serverless-fullstack-webapp-starter-kit-without-domain.test.ts",
"chars": 1010,
"preview": "import * as cdk from 'aws-cdk-lib';\nimport { Template } from 'aws-cdk-lib/assertions';\nimport { MainStack } from '../lib"
},
{
"path": "cdk/test/serverless-fullstack-webapp-starter-kit.test.ts",
"chars": 1154,
"preview": "import * as cdk from 'aws-cdk-lib';\nimport { Template } from 'aws-cdk-lib/assertions';\nimport { MainStack } from '../lib"
},
{
"path": "cdk/test/snapshot-plugin.ts",
"chars": 410,
"preview": "module.exports = {\n test: (val: any) => typeof val === 'string',\n serialize: (val: string) => {\n return `\"${val //\n"
},
{
"path": "cdk/tsconfig.json",
"chars": 670,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2018\",\n \"module\": \"commonjs\",\n \"lib\": [\n \"es2018\"\n ],\n \"decla"
},
{
"path": "compose.yaml",
"chars": 307,
"preview": "services:\n postgres:\n image: \"public.ecr.aws/docker/library/postgres:16\"\n ports:\n - \"5432:5432\"\n volumes:"
},
{
"path": "release-please-config.json",
"chars": 106,
"preview": "{\n \"packages\": {\n \".\": {\n \"release-type\": \"simple\",\n \"initial-version\": \"2.0.0\"\n }\n }\n}\n"
},
{
"path": "webapp/.dockerignore",
"chars": 35,
"preview": "node_modules\ndist\n.next\n.env.local\n"
},
{
"path": "webapp/.env.local.example",
"chars": 223,
"preview": "COGNITO_DOMAIN=auth.example.com\nAMPLIFY_APP_ORIGIN=http://localhost:3010\nUSER_POOL_CLIENT_ID=dummy\nUSER_POOL_ID=us-west-"
},
{
"path": "webapp/.gitignore",
"chars": 538,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "webapp/Dockerfile",
"chars": 993,
"preview": "FROM public.ecr.aws/lambda/nodejs:22 AS builder\nWORKDIR /build\nCOPY package*.json ./\nRUN --mount=type=cache,target=/root"
},
{
"path": "webapp/README.md",
"chars": 953,
"preview": "## Run locally\n\n```bash\n# Run this command in the repository root\ndocker compose up -d\n\n# Run these commands in the weba"
},
{
"path": "webapp/components.json",
"chars": 430,
"preview": "{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"new-york\",\n \"rsc\": true,\n \"tsx\": true,\n \"tailwind\": {"
},
{
"path": "webapp/eslint.config.mjs",
"chars": 496,
"preview": "import { dirname } from 'path';\nimport { fileURLToPath } from 'url';\nimport { FlatCompat } from '@eslint/eslintrc';\nimpo"
},
{
"path": "webapp/job.Dockerfile",
"chars": 543,
"preview": "FROM public.ecr.aws/lambda/nodejs:22 AS builder\nWORKDIR /build\nCOPY package*.json ./\nRUN --mount=type=cache,target=/root"
},
{
"path": "webapp/next.config.ts",
"chars": 525,
"preview": "import type { NextConfig } from 'next';\n\nconst allowedOrigins = ['localhost:3000'];\nif (process.env.ALLOWED_ORIGIN_HOST)"
},
{
"path": "webapp/package.json",
"chars": 1441,
"preview": "{\n \"name\": \"webapp\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev --turbopack -p 3010\","
},
{
"path": "webapp/postcss.config.mjs",
"chars": 81,
"preview": "const config = {\n plugins: ['@tailwindcss/postcss'],\n};\n\nexport default config;\n"
},
{
"path": "webapp/prisma/schema.prisma",
"chars": 797,
"preview": "generator client {\n provider = \"prisma-client-js\"\n output = \"../node_modules/.prisma/client\"\n}\n\ngenerator zod {\n pr"
},
{
"path": "webapp/run.sh",
"chars": 82,
"preview": "#!/bin/bash -x\n\n[ ! -d '/tmp/cache' ] && mkdir -p /tmp/cache\n\nexec node server.js\n"
},
{
"path": "webapp/src/app/(root)/actions.ts",
"chars": 2102,
"preview": "'use server';\n\nimport { authActionClient } from '@/lib/safe-action';\nimport {\n createTodoSchema,\n deleteTodoSchema,\n "
},
{
"path": "webapp/src/app/(root)/components/CreateTodoForm.tsx",
"chars": 4021,
"preview": "'use client';\n\nimport { useState } from 'react';\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport { useHook"
},
{
"path": "webapp/src/app/(root)/components/TodoItem.tsx",
"chars": 7339,
"preview": "'use client';\n\nimport { TodoItem, TodoItemStatus } from '@prisma/client';\nimport { useState } from 'react';\nimport { use"
},
{
"path": "webapp/src/app/(root)/page.tsx",
"chars": 1917,
"preview": "import { prisma } from '@/lib/prisma';\nimport { getAuthSession } from '@/lib/auth';\nimport TodoItemComponent from './com"
},
{
"path": "webapp/src/app/(root)/schemas.ts",
"chars": 761,
"preview": "import { z } from 'zod';\nimport TodoItemStatusSchema from '@/lib/generated/prisma/zod/inputTypeSchemas/TodoItemStatusSch"
},
{
"path": "webapp/src/app/api/auth/[slug]/route.ts",
"chars": 205,
"preview": "import { createAuthRouteHandlers } from '@/lib/amplifyServerUtils';\n\nexport const GET = createAuthRouteHandlers({\n redi"
},
{
"path": "webapp/src/app/api/cognito-token/route.ts",
"chars": 528,
"preview": "import { NextResponse } from 'next/server';\nimport { tryGetAuthSession } from '@/lib/auth';\n\nexport async function GET()"
},
{
"path": "webapp/src/app/auth-callback/page.tsx",
"chars": 441,
"preview": "import { redirect } from 'next/navigation';\nimport { getAuthSession } from '@/lib/auth';\nimport { prisma } from '@/lib/p"
},
{
"path": "webapp/src/app/globals.css",
"chars": 4168,
"preview": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n --color-backg"
},
{
"path": "webapp/src/app/layout.tsx",
"chars": 485,
"preview": "import './globals.css';\nimport { Toaster } from 'sonner';\n\nexport default function RootLayout({ children }: { children: "
},
{
"path": "webapp/src/app/sign-in/page.tsx",
"chars": 1801,
"preview": "export default function SignInPage() {\n return (\n <div className=\"min-h-screen bg-gray-50 flex flex-col justify-cent"
},
{
"path": "webapp/src/components/Header.tsx",
"chars": 1167,
"preview": "import Link from 'next/link';\n\nexport default function Header() {\n return (\n <header className=\"bg-indigo-600 text-w"
},
{
"path": "webapp/src/components/ui/sonner.tsx",
"chars": 571,
"preview": "'use client';\n\nimport { useTheme } from 'next-themes';\nimport { Toaster as Sonner, ToasterProps } from 'sonner';\n\nconst "
},
{
"path": "webapp/src/hooks/use-event-bus.ts",
"chars": 1416,
"preview": "import { decodeJWT } from 'aws-amplify/auth';\nimport { Amplify } from 'aws-amplify';\nimport { events } from 'aws-amplify"
},
{
"path": "webapp/src/jobs/async-job/translate.ts",
"chars": 1638,
"preview": "import { sendEvent } from '@/lib/events';\nimport { prisma } from '@/lib/prisma';\nimport { z } from 'zod';\nimport { Trans"
},
{
"path": "webapp/src/jobs/async-job-runner.ts",
"chars": 773,
"preview": "import { translateJobHandler, translateJobSchema } from '@/jobs/async-job/translate';\nimport { Handler } from 'aws-lambd"
},
{
"path": "webapp/src/jobs/migration-runner.ts",
"chars": 3224,
"preview": "import { Handler } from 'aws-lambda';\nimport { execFile } from 'child_process';\nimport path from 'path';\n\nexport const h"
},
{
"path": "webapp/src/lib/amplifyServerUtils.ts",
"chars": 1242,
"preview": "import { createServerRunner } from '@aws-amplify/adapter-nextjs';\nimport { GetParameterCommand, SSMClient } from '@aws-s"
},
{
"path": "webapp/src/lib/auth.ts",
"chars": 1769,
"preview": "import { cache } from 'react';\nimport { cookies } from 'next/headers';\nimport { fetchAuthSession } from 'aws-amplify/aut"
},
{
"path": "webapp/src/lib/events.ts",
"chars": 1314,
"preview": "import { SignatureV4 } from '@smithy/signature-v4';\nimport { defaultProvider } from '@aws-sdk/credential-provider-node';"
},
{
"path": "webapp/src/lib/jobs.ts",
"chars": 438,
"preview": "import { JobPayloadProps } from '@/jobs/async-job-runner';\nimport { InvokeCommand, LambdaClient } from '@aws-sdk/client-"
},
{
"path": "webapp/src/lib/prisma.ts",
"chars": 2412,
"preview": "import { Prisma, PrismaClient } from '@prisma/client';\n\n// https://www.prisma.io/docs/guides/nextjs\n\nconst globalForPris"
},
{
"path": "webapp/src/lib/safe-action.ts",
"chars": 922,
"preview": "import { getSessionWithUser } from '@/lib/auth';\nimport { createSafeActionClient, DEFAULT_SERVER_ERROR_MESSAGE } from 'n"
},
{
"path": "webapp/src/lib/utils.ts",
"chars": 169,
"preview": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: C"
},
{
"path": "webapp/src/proxy.ts",
"chars": 1188,
"preview": "import { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\nimport { fetchAuthSession } "
},
{
"path": "webapp/tsconfig.json",
"chars": 702,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2017\",\n \"lib\": [\n \"dom\",\n \"dom.iterable\",\n \"esnext\"\n ],\n "
}
]
About this extraction
This page contains the full source code of the aws-samples/serverless-full-stack-webapp-starter-kit GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 81 files (373.6 KB), approximately 93.9k tokens, and a symbol index with 62 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.