@jaypie/constructs
Prerequisites: npm install @jaypie/constructs aws-cdk-lib constructs
Overview
Section titled “Overview”@jaypie/constructs provides AWS CDK constructs preconfigured with Jaypie conventions, Datadog integration, and environment variable patterns.
Installation
Section titled “Installation”npm install @jaypie/constructs aws-cdk-lib constructsQuick Reference
Section titled “Quick Reference”Constructs
Section titled “Constructs”| Construct | Purpose |
|---|---|
JaypieLambda |
Lambda function with Datadog |
JaypieQueuedLambda |
SQS-triggered Lambda |
JaypieBucketQueuedLambda |
S3-triggered Lambda via SQS |
JaypieExpressLambda |
Express app on Lambda |
JaypieApiGateway |
REST API with custom domain |
JaypieWebDeploymentBucket |
Static site with CloudFront |
JaypieDynamoDb |
DynamoDB with GSI patterns |
JaypieNextJs |
Next.js deployment via cdk-nextjs-standalone |
JaypieEnvSecret |
Secret reference for injection |
Stack Classes
Section titled “Stack Classes”| Class | Purpose |
|---|---|
JaypieStack |
Base stack with tags and naming |
JaypieAppStack |
Application resources |
JaypieInfrastructureStack |
Shared infrastructure |
JaypieLambda
Section titled “JaypieLambda”Standard Lambda with Datadog integration.
import { JaypieLambda } from "@jaypie/constructs";
new JaypieLambda(this, "Api", { code: "../api/dist", handler: "handler.handler", secrets: [mongoSecret], timeout: cdk.Duration.seconds(30), memorySize: 256, environment: { CUSTOM_VAR: "value", },});Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
code |
string |
- | Path to code directory |
handler |
string |
- | Handler function (file.export) |
secrets |
JaypieEnvSecret[] |
- | Secrets to inject |
timeout |
Duration |
30s | Function timeout |
memorySize |
number |
128 | Memory in MB |
environment |
object |
- | Additional env vars |
vpc |
IVpc |
- | VPC for function |
JaypieQueuedLambda
Section titled “JaypieQueuedLambda”Lambda triggered by SQS queue.
import { JaypieQueuedLambda } from "@jaypie/constructs";
new JaypieQueuedLambda(this, "Worker", { code: "../worker/dist", handler: "index.handler", queueName: "tasks", batchSize: 10, maxBatchingWindow: cdk.Duration.seconds(5),});// Sets CDK_ENV_QUEUE_URL automaticallyOptions
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
queueName |
string |
- | SQS queue name |
batchSize |
number |
10 | Max messages per batch |
maxBatchingWindow |
Duration |
0 | Batch window duration |
JaypieBucketQueuedLambda
Section titled “JaypieBucketQueuedLambda”Lambda triggered by S3 events via SQS.
import { JaypieBucketQueuedLambda } from "@jaypie/constructs";
new JaypieBucketQueuedLambda(this, "FileProcessor", { code: "../processor/dist", handler: "index.handler", bucketName: "uploads", events: ["s3:ObjectCreated:*"], prefix: "incoming/",});// Sets CDK_ENV_BUCKET automaticallyJaypieExpressLambda
Section titled “JaypieExpressLambda”Express.js application on Lambda.
import { JaypieExpressLambda } from "@jaypie/constructs";
new JaypieExpressLambda(this, "Api", { code: "../api/dist", handler: "handler.handler", secrets: [mongoSecret, apiKeySecret],});JaypieApiGateway
Section titled “JaypieApiGateway”REST API Gateway with custom domain.
import { JaypieApiGateway } from "@jaypie/constructs";
new JaypieApiGateway(this, "Gateway", { handler: lambdaFunction, host: "api.example.com", zone: "example.com",});Options
Section titled “Options”| Option | Type | Description |
|---|---|---|
handler |
IFunction |
Lambda function |
host |
string |
Custom domain hostname |
zone |
string |
Route53 hosted zone |
JaypieDistribution
Section titled “JaypieDistribution”CloudFront distribution over a Lambda Function URL or any origin, with ACM
certificate, Route53 records, security headers, and access logging by default.
WAFv2 is opt-in via waf: true.
import { JaypieDistribution, JaypieExpressLambda } from "@jaypie/constructs";
const api = new JaypieExpressLambda(this, "Api", { code: "../api/dist", handler: "handler.handler",});
new JaypieDistribution(this, "Distribution", { handler: api, host: "api.example.com", zone: "example.com",});Multiple Hosts
Section titled “Multiple Hosts”host accepts an array. The first entry is primary: it supplies
PROJECT_BASE_URL and the certificate’s domainName, and the rest become
subject alternative names. Every entry gets an A and AAAA record, which serves a
zero-downtime hostname cutover.
new JaypieDistribution(this, "Distribution", { handler: api, host: ["api0.example.com", "api.example.com"], zone: "example.com", deleteExistingRecord: ["api.example.com"],});deleteExistingRecord force-deletes a conflicting Route53 record before the
alias is created. It accepts true (every host), a hostname, or an array of
hostnames.
Options
Section titled “Options”| Option | Type | Description |
|---|---|---|
handler |
IOrigin | IFunctionUrl | IFunction |
Origin; an IFunction gets a Function URL |
host |
string | HostConfig | array of either |
Domain name or names; first entry is primary |
zone |
string | IHostedZone |
Route53 hosted zone |
deleteExistingRecord |
boolean | string | string[] |
Force-delete conflicting records |
streaming |
boolean |
Lambda response streaming |
waf |
boolean | JaypieWafConfig |
WAFv2 WebACL (default disabled) |
securityHeaders |
boolean | overrides |
Security response headers (default enabled) |
logRetention |
Duration | number |
Retention for the log buckets this construct creates (default 365 days) |
originAccessControl |
boolean |
Make CloudFront the only caller of the Function URL (default disabled) |
Origin Access Control
Section titled “Origin Access Control”With an IFunction handler, the Function URL is AuthType: NONE by default, so the Lambda is invokable around CloudFront and the WAF. originAccessControl: true creates an AWS_IAM Function URL, uses FunctionUrlOrigin.withOriginAccessControl, and adds the lambda:InvokeFunction permission Lambda requires alongside lambda:InvokeFunctionUrl:
new JaypieDistribution(this, "Distribution", { handler, originAccessControl: true,});Clients must then send the body SHA-256 in x-amz-content-sha256 on POST and PUT. For a body carrying low-entropy secrets, salt it with a nonce so the hash is not reversible; that header is redacted from WAF logs by default.
Log Buckets
Section titled “Log Buckets”The CloudFront access log bucket and WAF log bucket this construct creates block all public access, enforce SSL, are versioned, use SSE-S3, expire objects after logRetention, and carry RemovalPolicy.RETAIN with no auto-delete. They survive stack deletion. waf.logRetention overrides the retention for the WAF bucket alone.
WAF logs always redact authorization, cookie, x-amz-content-sha256, and x-api-key. waf.redactedHeaders merges with that list rather than replacing it, and waf.redactedFields passes query string, URI path, and JSON body matchers through.
JaypieWebDeploymentBucket
Section titled “JaypieWebDeploymentBucket”Static site on S3 fronted by CloudFront, ACM, and Route53. Ships with default security headers and CloudFront access logging, and opt-in WAFv2 — same override mechanisms as JaypieDistribution (securityHeaders, responseHeadersPolicy, waf, logBucket, destination). When CDK_ENV_REPO is set, also provisions a scoped GitHub OIDC deploy role with cloudfront:CreateInvalidation on the distribution.
import { JaypieWebDeploymentBucket } from "@jaypie/constructs";
new JaypieWebDeploymentBucket(this, "Web", { host: "app.example.com", zone: "example.com",});
// host accepts a HostConfig object resolved via envHostname()new JaypieWebDeploymentBucket(this, "Web", { host: { subdomain: "app", domain: "example.com" }, zone: "example.com",});Bucket Naming
Section titled “Bucket Naming”The bucket name defaults to constructEnvName(component), and component defaults to "web" — independent of the construct id and of host. Two instances in one account and region collide on <env>-<key>-web-<nonce>, so give the second one a component (or an explicit name):
new JaypieWebDeploymentBucket(this, "Web", { host: webHost, zone });new JaypieWebDeploymentBucket(this, "App", { component: "app", host: appHost, zone });Changing component or name on a deployed stack renames the bucket, which replaces it.
Additional Behaviors
Section titled “Additional Behaviors”The default behavior serves the S3 static website origin, with CACHING_OPTIMIZED in production and CACHING_DISABLED elsewhere. No /* behavior is created, so paths registered afterward are evaluated ahead of the default and a static site can share a distribution with a Lambda surface:
const web = new JaypieWebDeploymentBucket(this, "Web", { host, zone });web.distribution.addBehavior("/app/*", new origins.FunctionUrlOrigin(api.functionUrl));Pass defaultBehavior to override the default behavior. It merges over the construct’s values rather than replacing them, so an override names only what it changes and the S3 website origin stays wired up:
new JaypieWebDeploymentBucket(this, "Web", { host, zone, defaultBehavior: { functionAssociations: [ { eventType: cloudfront.FunctionEventType.VIEWER_REQUEST, function: myFunction }, ], },});Origin Access Control
Section titled “Origin Access Control”The bucket is a public S3 website endpoint by default, which CloudFront cannot front with origin access control. originAccessControl: true serves it privately through the S3 REST endpoint instead:
new JaypieWebDeploymentBucket(this, "Web", { host, originAccessControl: true, spa: true, zone,});The bucket gets BLOCK_ALL and enforceSSL with no ACL, no public read, and no website configuration, and the distribution sets defaultRootObject: "index.html". Pair it with spa: true: the REST endpoint has no error document and OAC grants only s3:GetObject, so a miss returns 403 rather than the app shell.
Single-Page App
Section titled “Single-Page App”A single-page app has one index.html, so a deep link like /jobs has no S3 key behind it. The website error document renders the shell correctly but the response carries a 404 status — invisible to a human, wrong for crawlers, uptime checks, and any client branching on res.ok.
spa: true attaches a viewer-request CloudFront Function that rewrites any URI whose last segment has no file extension to /index.html, so S3 serves the real index key and returns 200:
new JaypieWebDeploymentBucket(this, "App", { component: "app", host: appHost, spa: true, zone,});The function is scoped to the default behavior, so paths registered with addBehavior keep their genuine 404s. This is why a distribution-wide errorResponses mapping is the wrong tool when a Lambda surface shares the distribution: it would rewrite those 404s into 200s serving the app shell.
Combining spa: true with a caller-supplied viewer-request association throws ConfigurationError at synth, since CloudFront permits one function per event type. Other event types compose, with the rewrite appended last.
Deploying Without a Hosted Zone
Section titled “Deploying Without a Hosted Zone”The distribution is unconditional. With no host or no zone, the construct skips only the certificate and the Route53 alias record; the distribution, response headers policy, access log bucket, SPA function, WebACL, DistributionId output, and the deploy role’s invalidation grant are all still created. The site serves on the CloudFront default domain:
const web = new JaypieWebDeploymentBucket(this, "Web", { spa: true });web.distributionDomainName; // d111111abcdef8.cloudfront.netdistribution and distributionDomainName are always defined, so a sandbox deployed before its hosted zone exists is reachable, and a downstream build can resolve the URL from the stack. Adding host and zone later adds the certificate and alias record without replacing the distribution.
Stable Outputs for cdk-outputs.json
Section titled “Stable Outputs for cdk-outputs.json”Call exportOutputs() to emit stack-level CfnOutputs with hash-free logical IDs (DestinationBucketName, DestinationBucketDeployRoleArn, DistributionId, CertificateArn):
const web = new JaypieWebDeploymentBucket(this, "Web", { host, zone });web.exportOutputs();
// Multi-instance stacks: use prefix to avoid collisionsappWeb.exportOutputs({ prefix: "App" }); // AppDestinationBucketName, ...docsWeb.exportOutputs({ prefix: "Docs" }); // DocsDestinationBucketName, ...Outputs whose underlying resource doesn’t exist (e.g., no deploy role, no certificate) are skipped.
GitHub OIDC Deploy Role Trust
Section titled “GitHub OIDC Deploy Role Trust”The deploy role trusts two GitHub OIDC sub patterns. GitHub issues newer repositories an id-embedded subject (repo:acme@162184378/widget@1339091097:environment:sandbox) while older repositories still present the plain repo:<org>/<repo>:* form. A repo-level subject template does not remove the ids, so a trust policy matching only the plain form fails with Not authorized to perform sts:AssumeRoleWithWebIdentity. The derived default emits both, wildcarding the ids it does not know.
// Default from CDK_ENV_REPO=acme/widget:// repo:acme/widget:* and repo:acme@*/widget@*:*new JaypieWebDeploymentBucket(this, "Web", { host, zone });
// Pin the organization id (prop, CDK_ENV_REPO_ORGANIZATION_ID, or// PROJECT_REPO_ORGANIZATION_ID)new JaypieWebDeploymentBucket(this, "Web", { host, zone, organizationId: "162184378",});
// Full override: a string, or an array trusting any one pattern.// Also creates the deploy role when CDK_ENV_REPO is unset.new JaypieWebDeploymentBucket(this, "Web", { host, zone, repoRestriction: ["repo:acme/widget:*", "repo:acme@162184378/widget@*:*"],});JaypieGitHubDeployRole takes the same organizationId and repoRestriction props, scoped to the whole organization rather than one repository. The githubOidcSubjects() helper builds the pair directly.
JaypieDynamoDb
Section titled “JaypieDynamoDb”DynamoDB table with Jaypie single-table design patterns.
import { JaypieDynamoDb } from "@jaypie/constructs";import { fabricIndex } from "@jaypie/fabric";
// Basic table (no GSIs by default)new JaypieDynamoDb(this, "myApp");
// With indexes using fabricIndex()new JaypieDynamoDb(this, "myApp", { indexes: [ fabricIndex(), // indexModel: pk=["model"], sk=["scope","updatedAt"] fabricIndex("alias"), // indexModelAlias: pk=["model","alias"], sparse fabricIndex("xid"), // indexModelXid: pk=["model","xid"], sparse ],});Creates table with:
- Primary key:
id(PK) — no sort key - No GSIs by default — use
indexesprop withfabricIndex()to add them
JaypieNextJs
Section titled “JaypieNextJs”Next.js deployment using cdk-nextjs-standalone.
import { JaypieNextJs } from "@jaypie/constructs";
// With custom domainnew JaypieNextJs(this, "App", { domainName: "app.example.com", hostedZone: "example.com", nextjsPath: "../nextjs",});
// CloudFront URL only (no custom domain)new JaypieNextJs(this, "App", { domainProps: false, nextjsPath: "../nextjs",});
// With response streamingnew JaypieNextJs(this, "App", { nextjsPath: "../nextjs", streaming: true,});Options
Section titled “Options”| Option | Type | Description |
|---|---|---|
nextjsPath |
string |
Path to Next.js application |
domainName |
string |
Custom domain name |
hostedZone |
string |
Route53 hosted zone |
domainProps |
false |
Set to false for CloudFront-only deployment |
streaming |
boolean |
Enable Lambda response streaming |
secrets |
JaypieEnvSecret[] |
Secrets to inject |
tables |
ITable[] |
DynamoDB tables to grant access |
Streaming Requirement
Section titled “Streaming Requirement”When using streaming: true, you must also create open-next.config.ts in your Next.js app:
import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
const config = { default: { override: { wrapper: "aws-lambda-streaming", }, },} satisfies OpenNextConfig;
export default config;Without this, the Lambda returns a JSON envelope instead of streamed HTML because Lambda’s RESPONSE_STREAM invoke mode requires the OpenNext streaming wrapper.
JaypieSecret
Section titled “JaypieSecret”Secret reference for Lambda injection. Reads process.env[envKey], an explicit value, or generateSecretString. Supports removalPolicy as boolean (true = RETAIN, false = DESTROY) or CDK RemovalPolicy.
import { JaypieSecret, JaypieLambda } from "@jaypie/constructs";import { isProductionEnv } from "@jaypie/kit";
const mongoSecret = new JaypieSecret(this, "MongoSecret", { envKey: "MONGODB_URI", removalPolicy: isProductionEnv(),});
new JaypieLambda(this, "Api", { secrets: [mongoSecret],});// Lambda has SECRET_MONGODB_URI env varA SCREAMING_SNAKE_CASE construct id is shorthand for the envKey, so new JaypieSecret(this, "MONGODB_URI") is equivalent to the call above.
Empty Secret Guard
Section titled “Empty Secret Guard”Synth throws ConfigurationError whenever a declared secret source produces no secret string, so a blank credential never defers to runtime. A source is declared by envKey or by passing a value key, and an empty string counts as empty.
new JaypieSecret(this, "ApiKey", { value: process.env.MISSING }); // throwsnew JaypieSecret(this, "ApiKey", { envKey: "MISSING" }); // throwsnew JaypieSecret(this, "Placeholder"); // generated valueExternal Secrets
Section titled “External Secrets”external: true creates an empty secret, so the value never enters the template, the CDK assets bucket, cdk.out, or cdk diff. CI sets the value after deploy with aws secretsmanager put-secret-value, using the ARN output described by the envKey. envKey still names the runtime variable.
new JaypieSecret(this, "ANTHROPIC_API_KEY", { external: true });JaypieSsoSyncApplication accepts googleCredentialsSecret and scimEndpointAccessTokenSecret to deploy SSOSync without credentials in the template.
JaypieEnvSecret
Section titled “JaypieEnvSecret”Extends JaypieSecret with an environment-driven provider/consumer pattern for sharing secrets across stacks. Accepted anywhere a JaypieSecret is. Deprecated; removed in 2.0. The empty secret guard applies, except in consumer environments, where the secret is imported rather than created.
import { JaypieEnvSecret } from "@jaypie/constructs";
// Sandbox stack provides the secretnew JaypieEnvSecret(this, "MONGODB_URI", { provider: true });
// Personal build consumes itnew JaypieEnvSecret(this, "MONGODB_URI");Stack Classes
Section titled “Stack Classes”JaypieStack
Section titled “JaypieStack”Base stack for every Jaypie CDK stack. Extending it automatically:
- Names the stack from environment variables:
cdk-{PROJECT_SPONSOR}-{PROJECT_KEY}-{PROJECT_ENV}-{PROJECT_NONCE}[-{key}] - Resolves account & region from
CDK_DEFAULT_ACCOUNT/CDK_DEFAULT_REGION(overridable viaenv) - Applies standard tags (
env,project,sponsor,nonce,commit,buildHex,buildDate,buildTime,version,service,creation,role,stack) and propagates them to every taggable child resource
When CDK_DEFAULT_ACCOUNT is set and PROJECT_NONCE is unset or not lowercase hex of six or more characters, synth warns @jaypie/constructs:projectNonceFormat. Generate the nonce once per environment with openssl rand -hex 4.
import { JaypieStack, JaypieStackProps } from "@jaypie/constructs";import type { Construct } from "constructs";
export class DataStack extends JaypieStack { constructor(scope: Construct, id: string, props: JaypieStackProps = {}) { super(scope, id, { key: "data", ...props }); // ...your resources... }}JaypieStackProps extends CDK’s StackProps with one optional key field (suffix for the generated stack name). Most projects use JaypieAppStack or JaypieInfrastructureStack; extend JaypieStack directly only for additional categories.
JaypieAppStack
Section titled “JaypieAppStack”For application resources (Lambda, API Gateway). Pre-fills key: "app":
import { JaypieAppStack, JaypieLambda } from "@jaypie/constructs";import type { Construct } from "constructs";
export class ApiStack extends JaypieAppStack { constructor(scope: Construct, id: string) { super(scope, id);
new JaypieLambda(this, "Api", { code: "../api/dist", handler: "handler.handler", }); }}JaypieInfrastructureStack
Section titled “JaypieInfrastructureStack”For shared infrastructure. Pre-fills key: "infra" and tags the stack with stackSha from CDK_ENV_INFRASTRUCTURE_STACK_SHA when set:
import { JaypieInfrastructureStack, JaypieEnvSecret } from "@jaypie/constructs";import type { Construct } from "constructs";
export class InfraStack extends JaypieInfrastructureStack { public readonly mongoSecret: JaypieEnvSecret;
constructor(scope: Construct, id: string) { super(scope, id);
this.mongoSecret = new JaypieEnvSecret(this, "MongoSecret", { secretName: "prod/mongodb-uri", envName: "MONGODB_URI", }); }}Environment Variables
Section titled “Environment Variables”Constructs automatically set:
| Variable | Source |
|---|---|
PROJECT_ENV |
Stack environment |
PROJECT_KEY |
Stack name |
CDK_ENV_QUEUE_URL |
Queue constructs |
CDK_ENV_BUCKET |
Bucket constructs |
CDK_ENV_DATADOG_API_KEY_ARN |
Datadog integration |
SECRET_* |
Secret constructs |
All resources are tagged:
| Tag | Value |
|---|---|
project |
Project name |
environment |
Environment name |
stack |
Stack ID |
CDK Entry Point
Section titled “CDK Entry Point”#!/usr/bin/env nodeimport * as cdk from "aws-cdk-lib";import { ApiStack } from "../lib/api-stack.js";import { InfraStack } from "../lib/infra-stack.js";
const app = new cdk.App();
new InfraStack(app, "MyProject-Infra");new ApiStack(app, "MyProject-Api");Related
Section titled “Related”- CDK Infrastructure - Setup guide
- CI/CD - Deployment workflows
- Environment Variables - Environment patterns