A friend had been building a web app with Claude Code — Express server, vanilla HTML/CSS/JS, in-memory data storage. The app worked and kept growing. But it was running on her laptop with no deployment, no database, and no auth. She needed to get it to AWS and had limited experience with cloud infrastructure.
I couldn't see her code, so I built a Claude Code plugin that could walk her through the deployment. The plugin analyzes an Express app, converts routes to Lambda functions, sets up S3 + CloudFront hosting, and optionally adds DynamoDB and Cognito. Each step explains what's happening and why.
The original app had no authentication and no persistent storage. The plugin noticed both and explained why they matter — what happens when your server restarts and your in-memory data disappears, why you want users to authenticate before creating content. She didn't know she needed those things. The plugin surfaced them as part of the conversation and let her decide.
Prompts all the way down
The entire plugin is markdown files. No code.
plugin/
├── agents/
│ └── aws-deployment-guide.md
├── settings.json
├── .mcp.json
└── skills/
├── analyze/SKILL.md
├── scaffold/SKILL.md
├── create-api/SKILL.md
├── add-database/SKILL.md
├── add-auth/SKILL.md
├── setup-frontend/SKILL.md
├── deploy/SKILL.md
├── test/SKILL.md
├── teardown/SKILL.md
└── guide/SKILL.md
One agent definition sets the personality. Ten skills handle each phase of the migration. Two bundled MCP servers — AWS Documentation for looking up real docs during the conversation, and Playwright for browser-based testing after deployment.
settings.json is one line:
{
"agent": "aws-deployment-guide"
}
No code was needed. The plugin system gives you agents, skills, and MCP servers — that covered everything required to encode a multi-step deployment workflow.
The agent definition
The agent (aws-deployment-guide.md) opens with:
You are a patient, knowledgeable mentor guiding a novice developer through deploying their Node.js/Express application to AWS using serverless architecture. You explain every concept, ask before acting, and ensure the user understands each step.
Every skill follows the same pattern: Explore → Plan → Discuss → Execute → Verify. Claude reads the code, proposes a plan, discusses it with the user, makes changes, and confirms the result.
The agent defines analogies for AWS concepts:
- Lambda = "A function that runs only when called, like a vending machine"
- S3 = "Cloud file storage, like a hard drive in the cloud"
- CloudFront = "A CDN that copies your files to servers worldwide for fast loading"
- DynamoDB = "A cloud database that scales automatically"
- Cognito = "A managed auth service — handles user accounts so you don't have to build login from scratch"
- CDK = "Infrastructure as code — your cloud setup is version-controlled just like your app code"
The agent also specifies how to handle decisions. Don't list options — make a recommendation and explain why. A beginner facing three AWS regions with no context will freeze. "us-east-1 is the standard default, you can't easily change this later but for a learning project it doesn't matter" gets them moving.
Skills as a sequence
The ten skills form a dependency chain. Each produces artifacts that the next one consumes.
flowchart LR
A[analyze] --> B[scaffold]
B --> C[create-api]
C --> D{needs\ndata?}
D -->|yes| E[add-database]
D -->|no| F{needs\nauth?}
E --> F
F -->|yes| G[add-auth]
F -->|no| H[setup-frontend]
G --> H
H --> I[deploy]
I --> J[test]
J --> K[teardown]
The analyze skill reads the Express app — entry point, routes, static files, data patterns, auth — and writes a migration plan to .migration/plan.md. Every subsequent skill reads that plan.
create-api maps each Express route to a Lambda handler:
Express Route → Lambda Handler → API Gateway Route
GET /api/notes → get-notes.ts → GET /notes
POST /api/notes → create-note.ts → POST /notes
DELETE /api/notes/:id → delete-note.ts → DELETE /notes/{id}
The skill shows the user a before and after for their simplest route:
// Before (Express):
app.get('/api/notes', (req, res) => {
res.json(notes);
});
// After (Lambda):
export const handler = async (event: APIGatewayProxyEvent) => {
const result = await docClient.send(new ScanCommand({
TableName: process.env.TABLE_NAME,
}));
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result.Items),
};
};
add-database and add-auth are optional. They activate only if the app's data patterns or route protection needs suggest them. An app with in-memory storage technically works without a database — the plugin explains that the data disappears on restart and offers DynamoDB.
Baked-in defaults
The plugin is opinionated. These are non-negotiable across all skills:
- Least-privilege IAM — each Lambda gets only the permissions it needs. Read handlers get
dynamodb:GetItemanddynamodb:Query. Write handlers getdynamodb:PutItem. Never*. - No public S3 buckets — Origin Access Control with CloudFront. Block all public access on S3.
- REST API over HTTP API — REST API supports Cognito authorization directly. HTTP API would require custom auth code.
- Pay-per-request DynamoDB — no capacity planning. First 25 GB free.
- HTTPS only — CloudFront redirects HTTP to HTTPS.
- RemovalPolicy.DESTROY — for development, everything cleans up when you delete the stack.
The security section of the agent prompt lists eight rules: input validation in every Lambda handler, explicit CORS origins, no secrets in code, encrypted DynamoDB, strong Cognito password policy. These show up in the generated infrastructure because every skill is told to follow them.
Before and after
I built sample app to test the plugin — a notes app that mimics what my friend had. The starting point:
const express = require('express');
const app = express();
app.use(express.json());
app.use(express.static('public'));
let notes = [];
let nextId = 1;
app.get('/api/notes', (req, res) => {
res.json(notes);
});
app.post('/api/notes', (req, res) => {
const note = {
id: nextId++,
title: req.body.title,
body: req.body.body,
createdAt: new Date().toISOString()
};
notes.push(note);
res.json(note);
});
app.delete('/api/notes/:id', (req, res) => {
const id = parseInt(req.params.id);
notes = notes.filter(note => note.id !== id);
res.json({ success: true });
});
app.listen(3000);
37 lines. In-memory array. Vanilla HTML/CSS/JS in a public/ directory.
After running the plugin:
basic-site/
├── server.js # Original (unchanged)
├── public/ # Original frontend (unchanged)
├── infrastructure/
│ ├── bin/app.ts # CDK entry — 3 stacks
│ ├── lib/
│ │ ├── auth-stack.ts # Cognito User Pool
│ │ ├── api-stack.ts # Lambda + API Gateway + DynamoDB
│ │ └── frontend-stack.ts # S3 + CloudFront
│ └── lambda/
│ ├── handlers/
│ │ ├── get-notes.ts
│ │ ├── create-note.ts
│ │ └── delete-note.ts
│ └── shared/
│ └── response.ts
└── frontend/
├── src/
│ ├── App.tsx
│ ├── components/
│ │ ├── AuthPage.tsx
│ │ ├── NoteForm.tsx
│ │ └── NoteCard.tsx
│ ├── context/AuthContext.tsx
│ ├── hooks/useNotes.ts
│ └── lib/
│ ├── api.ts
│ └── auth.ts
└── vite.config.ts
The plugin created three, well-structured and planned out CDK stacks. Instead of an Express routerm, we now have three Lambda handlers with least-privilege IAM. The local storage has been upgraded to DynamoDB with pay-per-request billing. Because we are not just running locally, the plugin added Cognito with email verification and strong password policy. React + Vite frontend with typed API client and auth context. CloudFront with OAC, HTTPS redirect, and security headers.
The original server.js and public/ directory are untouched. The plugin builds alongside the existing app.
Replacing READMEs
Going forward, I expect this to be my standard process for documenting my projects. Instead of writing complex READMEs or deploy scripts that offer options and data capture, I'll just write Claude Code skills that will guide Claude to getting that information from the user and assist them with the deployment process.
The plugin source is available on GitHub.