Deploy an Add-on to Your Own Infrastructure
By default, an add-on uses API Gateway and Lambda to host the AWS architecture for the add-on. You can instead deploy the infrastructure to the topology of your choice. Choose this approach when you need to deploy on non-AWS infrastructure, integrate with existing container orchestration, or maintain a unified deployment pipeline across services.
This document uses AWS Elastic Kubernetes Service (EKS) as an example, but you are not restricted to this option.
Prerequisites
Tools required:
- Node.js 24+, npm
- AWS CLI v2 (configured with appropriate Identity and Access Management (IAM) permissions)
- Cloud Development Kit (CDK) CLI (
npx cdkor globally installed) - Docker or Finch (for container image builds)
- Access to a Cloudflare account (to configure Domain Name System (DNS) + proxy)
Knowledge assumed:
- TypeScript / Node.js development
- Basic AWS CDK familiarity (deploy, bootstrap)
- Cloudflare dashboard (DNS records, Secure Sockets Layer (SSL) settings)
If you are using EKS as in this example, you also need the following:
- Additional tools:—
kubectl - Additional IAM permissions:—EKS, Elastic Container Registry (ECR), Virtual Private Cloud (VPC), Elastic Load Balancing (ELB) permissions on your AWS IAM role
- Additional knowledge assumed:—Basic Kubernetes concepts (pods, deployments, services, ingress)
If you are not using EKS, refer to your platform's documentation for equivalent requirements.
Glossary
| Term | Meaning |
|---|---|
verifyRequest |
Auth function that validates incoming requests. In production, this function verifies Alexa's certificate-based signature. In test mode, the function accepts HTTP Basic Auth credentials instead. |
alexa-ai test |
CLI command that runs integration tests against your add-on. Sets ALEXA_ADDON_UNDER_TEST=true and passes Basic Auth credentials so your service can authenticate test traffic without Alexa's production certificates. |
alexa-ai deploy |
CLI command that packages and deploys your add-on manifest (descriptions, endpoint URL, media assets) to Alexa+. Does not deploy infrastructure — your Infrastructure as Code (IaC) tooling handles that. |
ServiceHandler |
Smithy's built-in HTTP router. Given an HTTP request, it matches the method + path to the correct operation handler. Replaces API Gateway's OpenAPI-based routing. |
config_builder.ts |
Generates the add-on configuration that tells Alexa which endpoint URL to call. |
local/server.ts |
Development-only server used by alexa-ai test. Runs without auth so the test framework can invoke operations directly. Not deployed to production. |
server.ts |
Production server. Validates auth (verifyRequest), then routes to operations via ServiceHandler. |
Architecture and component model
Every add-on deployment has four logical components in the request path.
| Component | Responsibility |
|---|---|
| Alexa | Sends signed HTTPS requests to the add-on endpoint and validates responses. |
| Edge Proxy (optional) | Terminates TLS, provides WAF/DDoS protection, reverse-proxies traffic to Gateway. Not a cache. |
| Gateway | Accepts inbound traffic, enforces IP/network restrictions, routes to correct instance of compute. |
| Compute | Runs the application code, validates authentication, and implements business logic. |
The specifics of these components differ depending on whether you use the default architecture or self-hosting.
Default architecture (API Gateway + Lambda)
When using the default architecture, Alexa sends the HTTPS requests to the AWS API Gateway, which then invokes a Lambda function. Each operation the add-on can perform has handler for the Lambda entry point and operation code for the business logic.
| Component | Implementation | Notes |
|---|---|---|
| Alexa | Alexa | Sends signed HTTPS requests to the add-on endpoint |
| Edge Proxy | — | Not present in default architecture |
| Gateway | AWS API Gateway | Routes by HTTP method + path (OpenAPI spec), TLS termination |
| Compute | AWS Lambda (one function per operation) | Each handler does authentication (verifyRequest), deserialization, and operation execution |
The code provided when you create a new add-on maps to the components as shown below.
[Infrastructure] action/infrastructure/cdk/
├── bin/app.ts → CDK app entry point
└── lib/add-on-infra-stack.ts → Provisions API Gateway + Lambda
[Business Logic] action/home-services/src/
├── apigateway.ts → Auth layer (verifyRequest + event conversion)
├── handlers/*_handler.ts → One Lambda entry point per operation
├── operations/*.ts → Business logic (shared across architectures)
├── local/server.ts → Local dev server for `alexa-ai test`
└── scripts/bundle/index.js → esbuild bundler
Self-hosted architecture
For the self-hosted architecture, Alexa sends the HTTPS requests to your configured Edge Proxy (such as CloudFlare), which routes to your gateway (network load balancer). Operations are then handled via smithy ServiceHandler.
The Edge Proxy, Gateway, and Compute layers are platform-specific. For the EKS example, they might look like the following.
| Component | Implementation | Notes |
|---|---|---|
| Alexa | Alexa | Same — sends signed HTTPS requests to Cloudflare URL |
| Edge Proxy | Cloudflare | TLS termination, DDoS protection, pass-through proxy (SSL mode: Full) |
| Gateway | Network Load Balancer (NLB) + nginx ingress | NLB restricted to Cloudflare IPs, nginx routes all paths to the service |
| Compute | Node.js server pod (EKS) | Single process handles all operations via Smithy ServiceHandler |
Differences between default and self-hosted architectures
What stays the same across both architectures:
- Alexa sends the same signed HTTPS request
verifyRequest()does the same cert-based authoperations/*.tsbusiness logic is identicalalexa-ai testbasic auth mechanism is unchanged
What changes:
- You add an Edge Proxy (Cloudflare) — handles TLS so origin doesn't need a valid cert
- Gateway changes from managed (API Gateway with OpenAPI routing) to self-managed (NLB + nginx)
- Compute changes from serverless (one Lambda per operation) to a long-running process (one server, all operations)
- Security boundary is unchanged — the auth mechanism (certificate verification) is identical code running in a different hosting layer. No new attack surface is introduced by the migration itself.
Make the code changes
Step 1: Create action/home-services/src/server.ts
This is the new Compute entry point — a single Node.js HTTP server that handles request authentication, operation routing, and business logic. This server replaces the Lambda handlers used in the default architecture.
import http from "node:http";
import { getHomeServicesServiceServiceHandler } from "@alexa-ai/category-home-services-spi";
import { convertRequest, writeResponse } from "@aws-smithy/server-node";
import { verifyRequest, BasicAuthCredentialProvider } from "@alexa-ai/category-request-authorizer";
import { HandlerContext } from "./types";
import { ListProfessionalsOperation } from "./operations/list_professionals";
import { MessageProfessionalOperation } from "./operations/message_professional";
import { GetUserInfoOperation } from "./operations/get_user_info";
import { GetServicesOperation } from "./operations/get_services";
import { GetServiceSpecificationOperation } from "./operations/get_service_specification";
import { CreateBookingOperation } from "./operations/create_booking";
import { CancelBookingOperation } from "./operations/cancel_booking";
import { ModifyBookingOperation } from "./operations/modify_booking";
import { GetBookingOperation } from "./operations/get_booking";
// Smithy ServiceHandler — handles operation routing (replaces API Gateway's OpenAPI routing)
const serviceHandler = getHomeServicesServiceServiceHandler({
ListProfessionals: ListProfessionalsOperation,
MessageProfessional: MessageProfessionalOperation,
GetUserInfo: GetUserInfoOperation,
GetServices: GetServicesOperation,
GetServiceSpecification: GetServiceSpecificationOperation,
CreateBooking: CreateBookingOperation,
CancelBooking: CancelBookingOperation,
ModifyBooking: ModifyBookingOperation,
GetBooking: GetBookingOperation,
});
// Basic auth support for `alexa-ai test` (same env vars as Lambda CDK stack).
// In production (ALEXA_ADDON_UNDER_TEST unset), verifyRequest uses cert-based signing.
const basicAuthCredentialProvider: BasicAuthCredentialProvider | undefined =
process.env["ALEXA_ADDON_UNDER_TEST"]?.toLowerCase() === "true"
? {
getCredentials: async () => ({
username: process.env["BASIC_AUTH_USERNAME"] ?? "",
password: process.env["BASIC_AUTH_PASSWORD"] ?? "",
}),
}
: undefined;
const PORT = Number(process.env["PORT"]) || 3000;
const server = http.createServer(async (req, res) => {
// Health check — used by your Gateway for readiness/liveness probes
if (req.url === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "healthy" }));
return;
}
// Collect request body (needed for signature verification)
const chunks: Buffer[] = [];
for await (const chunk of req) chunks.push(chunk as Buffer);
const body = Buffer.concat(chunks).toString();
// Request auth — verifies Alexa's signed request (same as Lambda path)
const errorResponse = await verifyRequest(
{ headers: req.headers as Record<string, string>, body },
{ basicAuthCredentialProvider }
);
if (errorResponse) {
console.warn("Auth rejected:", errorResponse.statusCode);
res.writeHead(errorResponse.statusCode, { "Content-Type": "application/json" });
res.end(errorResponse.body);
return;
}
// Re-create readable stream with body (convertRequest needs to read it again)
const bufferedReq = Object.assign(
new (require("stream").Readable)({
read() { this.push(body); this.push(null); }
}),
{ headers: req.headers, method: req.method, url: req.url }
) as unknown as typeof req;
// Operation routing + business logic execution
const context: HandlerContext = { user: "anonymous-user" };
try {
const httpRequest = await convertRequest(bufferedReq);
const httpResponse = await serviceHandler.handle(httpRequest, context);
writeResponse(httpResponse, res);
} catch (err: any) {
console.error("Handler error:", err);
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: err.message }));
}
});
server.listen(PORT, () => {
console.log(`Add-on service listening on port ${PORT}`);
});
Step 2: Create action/home-services/src/types.ts
This is a shared type used by server.ts and all operation files.
// Context passed to each operation handler by the ServiceHandler
export interface HandlerContext {
user: string;
}
Update all operations/*.ts imports to use the new type. You can use the following command in your terminal to change them all at one time.
find action/home-services/src/operations -name "*.ts" -exec sed -i '' 's|from "../apigateway"|from "../types"|g' {} +
- import { HandlerContext } from "../apigateway";
+ import { HandlerContext } from "../types";
Step 3: Remove Lambda artifacts and update dependencies
The file deletions and most dependency changes apply to all platforms. The following sections call out EKS-specific dependency changes separately.
Delete the old Lambda files.
rm -rf action/home-services/src/handlers/
rm -f action/home-services/src/apigateway.ts
rm -f action/home-services/tests/api_gateway.test.ts
Update the root package.json to remove the Lambda dependencies. For EKS, you add the new EKS dependency.
Remove (all platforms):
@alexa-ai/category-cdk-constructs@aws-smithy/server-apigateway@types/aws-lambda
Add (all platforms):
- Move
@aws-smithy/server-nodefrom devDependencies to dependencies (it is needed at runtime in the self-hosted server)"^1.0.0-alpha.5" @types/node"^24.10.1"(devDependencies)
EKS-specific — add these if using EKS:
@aws-cdk/lambda-layer-kubectl-v32"^2.1.0"(dependencies)aws-cdk-lib→"^2.260.0"— EKS L2 constructs require ≥2.260aws-cdk→"^2.260.0"— must matchaws-cdk-lib
After making these updates, run npm install.
Verify: At this point, npm run build should complete without errors.
Step 4: Update action/home-services/scripts/bundle/index.js
Bundle only server.ts — the single production entry point (no Lambda handlers):
const esbuild = require("esbuild");
const fs = require("fs");
const path = require("path");
const external_modules = ["re2-wasm"];
const outDir = path.join("action", "build", "dist");
esbuild.build({
entryPoints: ["action/home-services/src/server.ts"],
platform: "node",
target: "node24",
bundle: true,
minify: true,
legalComments: "none",
outdir: outDir,
external: external_modules
}).catch(() => process.exit(1))
.then(() => {
const packageJson = { dependencies: {} };
external_modules.forEach(mod => {
packageJson.dependencies[mod] = require(`${mod}/package.json`).version;
});
fs.existsSync(outDir) || fs.mkdirSync(outDir);
fs.writeFileSync(path.join(outDir, "package.json"), JSON.stringify(packageJson));
fs.copyFileSync("package-lock.json", path.join(outDir, "package-lock.json"));
});
Verify: npm run clean && npm run bundle should produce only action/build/dist/server.js. (The clean script should already exist in your template's package.json.)
Step 5: Create Dockerfile (project root)
This is a container image for the Compute component.
FROM node:24-alpine
WORKDIR /app
COPY action/build/dist/ ./
RUN npm ci --omit=dev
EXPOSE 3000
CMD ["node", "server.js"]
Verify: docker build --platform linux/amd64 -t addon-service . should complete successfully.
Step 6: Provision gateway + compute infrastructure
Delete the old Lambda CDK stack:
rm -f action/infrastructure/cdk/lib/add-on-infra-stack.ts
rm -f action/infrastructure/cdk/bin/app.ts
The CDK stack uses the root package.json, tsconfig.json, and cdk.json — no separate package needed. Dependencies were already added in Step 3: Remove Lambda artifacts and update dependencies.
Root cdk.json (verify it points to the correct path):
{
"app": "npx ts-node action/infrastructure/cdk/bin/app.ts"
}
New action/infrastructure/cdk/bin/app.ts:
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { EksAddOnInfraStack } from '../lib/eks-addon-infra-stack';
const app = new cdk.App();
new EksAddOnInfraStack(app, 'HomeServices-AddOn-EKS', {
env: {
account: process.env['CDK_DEFAULT_ACCOUNT'],
region: process.env['CDK_DEFAULT_REGION'] ?? 'us-east-1',
},
});
New action/infrastructure/cdk/lib/eks-addon-infra-stack.ts:
Provisions the Gateway and Compute infrastructure: VPC, EKS cluster, NLB (restricted to Cloudflare IPs), nginx ingress controller, ECR repo, and Kubernetes deployment/service/ingress manifests.
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as eks from 'aws-cdk-lib/aws-eks';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
import { KubectlV32Layer } from '@aws-cdk/lambda-layer-kubectl-v32';
// Cloudflare IPv4 ranges (from https://www.cloudflare.com/ips-v4/)
const CLOUDFLARE_IPV4_RANGES = [
'173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22',
'141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20',
'197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13',
'104.24.0.0/14', '172.64.0.0/13', '131.0.72.0/22',
];
export class EksAddOnInfraStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// VPC
const vpc = new ec2.Vpc(this, 'Vpc', { maxAzs: 2, natGateways: 1 });
// EKS Cluster
// Allow any IAM role in the account to access kubectl
const mastersRole = new iam.Role(this, 'ClusterAdmin', {
assumedBy: new iam.AccountRootPrincipal(),
});
const cluster = new eks.Cluster(this, 'Cluster', {
clusterName: `${id}-cluster`,
version: eks.KubernetesVersion.V1_32,
kubectlLayer: new KubectlV32Layer(this, 'KubectlLayer'),
vpc,
mastersRole,
defaultCapacity: 2,
defaultCapacityInstance: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
endpointAccess: eks.EndpointAccess.PUBLIC_AND_PRIVATE,
});
// Attach any required security/monitoring policies for your AWS account here
// cluster.defaultNodegroup!.role.addManagedPolicy(...);
// AWS Load Balancer Controller
const lbControllerSa = cluster.addServiceAccount('AwsLbController', {
name: 'aws-load-balancer-controller',
namespace: 'kube-system',
});
lbControllerSa.role.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName('ElasticLoadBalancingFullAccess')
);
// WARNING: The IAM permissions below are intentionally broad for simplicity.
// For production use, scope them down using the official recommended policy:
// https://docs.aws.amazon.com/eks/latest/userguide/lbc-manifest.html
lbControllerSa.role.addToPrincipalPolicy(new iam.PolicyStatement({
actions: [
'ec2:Describe*', 'ec2:AuthorizeSecurityGroupIngress', 'ec2:RevokeSecurityGroupIngress',
'ec2:CreateSecurityGroup', 'ec2:DeleteSecurityGroup', 'ec2:CreateTags', 'ec2:DeleteTags',
'elasticloadbalancing:*', 'acm:ListCertificates', 'acm:DescribeCertificate',
'iam:ListServerCertificates', 'iam:GetServerCertificate',
'tag:GetResources', 'tag:TagResources',
],
resources: ['*'],
}));
const lbControllerChart = cluster.addHelmChart('LbController', {
chart: 'aws-load-balancer-controller',
release: 'aws-lb-controller',
repository: 'https://aws.github.io/eks-charts',
namespace: 'kube-system',
values: {
clusterName: cluster.clusterName,
serviceAccount: { create: false, name: 'aws-load-balancer-controller' },
region: this.region,
vpcId: vpc.vpcId,
},
});
// nginx ingress with NLB (Cloudflare IPs only)
const nginxIngress = cluster.addHelmChart('NginxIngress', {
chart: 'ingress-nginx',
release: 'ingress-nginx',
repository: 'https://kubernetes.github.io/ingress-nginx',
namespace: 'ingress-nginx',
createNamespace: true,
values: {
controller: {
service: {
type: 'LoadBalancer',
annotations: {
'service.beta.kubernetes.io/aws-load-balancer-type': 'external',
'service.beta.kubernetes.io/aws-load-balancer-nlb-target-type': 'ip',
'service.beta.kubernetes.io/aws-load-balancer-scheme': 'internet-facing',
'service.beta.kubernetes.io/aws-load-balancer-source-ranges': CLOUDFLARE_IPV4_RANGES.join(','),
'service.beta.kubernetes.io/aws-load-balancer-target-group-attributes': 'preserve_client_ip.enabled=true',
},
},
},
},
});
nginxIngress.node.addDependency(lbControllerChart);
// ECR Repository
const repo = new ecr.Repository(this, 'AddonRepo', {
repositoryName: `${id.toLowerCase()}`,
removalPolicy: cdk.RemovalPolicy.DESTROY,
emptyOnDelete: true,
});
// Kubernetes Deployment + Service + Ingress
cluster.addManifest('AddonWorkload', {
apiVersion: 'apps/v1',
kind: 'Deployment',
metadata: { name: 'addon-service', namespace: 'default' },
spec: {
replicas: 2,
selector: { matchLabels: { app: 'addon-service' } },
template: {
metadata: { labels: { app: 'addon-service' } },
spec: {
containers: [{
name: 'addon',
image: `${repo.repositoryUri}:latest`,
ports: [{ containerPort: 3000 }],
env: [{ name: 'PORT', value: '3000' }],
readinessProbe: { httpGet: { path: '/health', port: 3000 }, initialDelaySeconds: 5, periodSeconds: 10 },
livenessProbe: { httpGet: { path: '/health', port: 3000 }, initialDelaySeconds: 10, periodSeconds: 30 },
}],
},
},
},
}, {
apiVersion: 'v1',
kind: 'Service',
metadata: { name: 'addon-service', namespace: 'default' },
spec: { selector: { app: 'addon-service' }, ports: [{ port: 80, targetPort: 3000 }] },
}, {
apiVersion: 'networking.k8s.io/v1',
kind: 'Ingress',
metadata: { name: 'addon-ingress', namespace: 'default' },
spec: {
ingressClassName: 'nginx',
rules: [{ http: { paths: [{ path: '/', pathType: 'Prefix', backend: { service: { name: 'addon-service', port: { number: 80 } } } }] } }],
},
});
// Outputs
new cdk.CfnOutput(this, 'ClusterName', { value: cluster.clusterName });
new cdk.CfnOutput(this, 'EcrRepoUri', { value: repo.repositoryUri });
new cdk.CfnOutput(this, 'KubeconfigCommand', {
value: `aws eks update-kubeconfig --name ${cluster.clusterName} --region ${this.region}`,
});
}
}
Verify: npx cdk synth should produce a CloudFormation template without errors.
Step 7: Update config_builder.ts
Point the add-on endpoint to your Edge Proxy (Cloudflare) URL. This is the public entry point Alexa calls when sending requests to your add-on:
.endpoints(ep => ep
.default("https://your-subdomain.yourdomain.com")
)
Timing: You can set this now with the intended URL. Traffic doesn't flow until Cloudflare DNS is live during deployment, so there is no risk in setting it early.
Deploy your add-on
In the following steps, you deploy your add-on so that you can verify that traffic flows through the full path from Alexa -> Edge Proxy -> Gateway -> Compute.
Step 1: Build and bundle
Run the following command to build and bundle the add-on.
npm run build && npm run bundle
Step 2: Deploy infrastructure
Provision your load balancer and container runtime using your platform's tooling. Your goal at the end of this step is a load balancer DNS hostname to use as the Cloudflare CNAME target when you configure your Edge Proxy later.
For the EKS example, you do this with the following commands:
npx cdk bootstrap # one-time per account/region
npx cdk deploy
Note the outputs printed after deploy:
EcrRepoUri— needed for pushing container imagesClusterName— needed forkubectlaccessKubeconfigCommand— run this to configurekubectl
Ensure your AWS credentials point to YOUR infrastructure account (not the CodeArtifact account). If you previously ran aws codeartifact login, verify with aws sts get-caller-identity before proceeding.
Step 3: Push the container image to registry
You can use any container registry accessible from your compute platform, such as Elastic Container Registry (ECR). The EKS example uses ECR.
# Authenticate Docker to ECR
aws ecr get-login-password --region <region> | docker login --username AWS --password-stdin <account-id>.dkr.ecr.<region>.amazonaws.com
# Build and push
docker build --platform linux/amd64 -t addon-service .
docker tag addon-service:latest <EcrRepoUri>:latest
docker push <EcrRepoUri>:latest
Step 4: Deploy pods and start compute
This example shows using kubectl for EKS. Replace with your platform's equivalent.
# Configure kubectl (use KubeconfigCommand output from Step 2: Deploy infrastructure)
aws eks update-kubeconfig --name <cluster-name> --region <region>
# First deploy: pods start automatically when CDK creates the Deployment manifest
# Subsequent deploys: restart to pick up new image
kubectl rollout restart deployment/addon-service
# Verify pods are running
kubectl get pods
Step 5: Configure your Edge Proxy
To complete this step, you need your load balancer DNS hostname from the earlier Step 2: Deploy infrastructure.
In the Cloudflare dashboard:
- Get your load balancer DNS hostname:
- EKS:
kubectl get svc -n ingress-nginx(copy theEXTERNAL-IPvalue) - Other platforms: retrieve from your platform's console or CLI.
- EKS:
- Add a CNAME record:
your-subdomain→<load balancer DNS hostname>. - Set Proxy status: Proxied (orange cloud).
- Set SSL/TLS mode: Full (not Strict — the origin does not need a valid public certificate).
To verify the Edge Proxy, run the following curl command. This should return {"status":"healthy"} once DNS propagates.
curl -s https://your-subdomain.yourdomain.com/health
If this returns 403, see Known Issues.
Step 6: Deploy add-on manifest to Alexa
Run the following command to deploy your add-on manifest to Alexa.
alexa-ai deploy
This registers your Cloudflare URL with Alexa. After this, Alexa routes traffic to your service.
Verify your add-on end-to-end in the Alexa simulator
Open the Alexa web simulator and invoke your add-on to confirm traffic flows through the full path (Alexa → Edge Proxy → Gateway → Compute).
For details about using the web simulator, see Test with the Web Simulator.
Make sure you start a new simulator session to avoid issues with caching. Older sessions might still route to a stale endpoint.
Invoke operations like "get services" or "list professionals" and confirm you get valid responses from your service.
For a detailed comparison, see Summary of what changes.
Summary of what changes vs. what stays the same
The following table shows the differences between the default Lambda architecture and the self-hosted solution.
| Aspect | Old (Lambda) | New (Self-Hosted) | EKS Example | Platform-Dependent? | Changed? |
|---|---|---|---|---|---|
operations/*.ts |
Business logic | Same | Same | No | No |
types.ts |
N/A | HandlerContext type | Same | No | New |
server.ts |
N/A | HTTP server + Smithy routing | Same | No | New |
local/server.ts |
Local dev server (alexa-ai test) |
Same | Same | No | No |
Dockerfile |
N/A | Container build | Same | No | New |
bundle/index.js |
Lambda handlers only | Bundles server.ts only |
Same | No | Rewritten |
infrastructure/cdk/ |
API GW + Lambda | Your platform's IaC | EKS + NLB + nginx | Yes | Replaced |
config_builder.ts |
API GW URL | Edge Proxy URL | Cloudflare URL | No | Modified (URL only) |
apigateway.ts |
Lambda auth layer | Removed | Removed | No | Deleted |
handlers/*.ts |
Lambda entry points | Removed | Removed | No | Deleted |
| Auth mechanism | Cert-based (verifyRequest) |
Same | Same | No | No |
alexa-ai test |
Basic auth via env vars | Same | Same | No | No |
Directory structure
A self-hosted add-on EKS-hosted example has the following project structure.
project-root/
├── action/
│ ├── home-services/
│ │ ├── src/
│ │ │ ├── server.ts ← Production EKS entrypoint
│ │ │ ├── types.ts ← HandlerContext interface
│ │ │ ├── config_builder.ts ← Endpoint URL (Cloudflare)
│ │ │ ├── operations/*.ts ← Business logic (unchanged)
│ │ │ └── local/server.ts ← Local dev server for `alexa-ai test`
│ │ ├── scripts/bundle/index.js ← Bundles server.ts only
│ │ └── test-suites/test-config.json
│ └── infrastructure/cdk/ ← EKS stack (uses root package.json/tsconfig)
│ ├── bin/app.ts
│ └── lib/eks-addon-infra-stack.ts
├── addon-package/ ← UNCHANGED
├── Dockerfile ← Container build
├── cdk.json ← Points to action/infrastructure/cdk/bin/app.ts
├── k8s/deployment.yaml ← K8s manifests (optional, CDK handles this)
├── package.json ← All deps (EKS CDK + smithy + SPI)
└── tsconfig.json ← Compiles all action/**/*
Known issues
Cloudflare Web Application Firewall (WAF) blocks curl/local requests
If Cloudflare WAF/bot protection is active, direct curl requests from your laptop are blocked (403). Alexa's server-to-server traffic passes through fine. This is expected — test via the Alexa Simulator, not curl.
Running server.ts locally (CJS/ESM conflict on Node.js 24)
server.ts cannot be run directly with ts-node or tsx due to a CJS/ESM conflict in node-forge (used by @alexa-ai/category-request-authorizer). Use the bundled output for local testing:
npm run bundle
ALEXA_ADDON_UNDER_TEST=true BASIC_AUTH_USERNAME=test BASIC_AUTH_PASSWORD=test node action/build/dist/server.js
The local/server.ts (which skips verifyRequest) still works with tsx for alexa-ai test purposes.
Related topics
Last updated: Jul 06, 2026

