Deploy a serverless React + Postgres blueprint on AWS with Pulumi

Switch variant

Choose a different cloud.

Ship a React SPA plus a serverless API that queries Aurora Serverless v2 for PostgreSQL behind Amazon CloudFront on AWS. Consumes the Pulumi landing-zone stack for network and secret wiring, and exports the public site URL downstream projects can reuse.

Before you deploy: deploy the AWS landing zone first.

This blueprint consumes shared network, identity, and secret-store outputs from the AWS landing-zone stack in the same cloud account. If you haven't deployed one yet, follow Build a AWS landing zone and come back with the stack name.

Download blueprint

Get this AWS blueprint project as a zip. Switch Pulumi language here to keep the download aligned with the install commands and blueprint program on the page.

Download the TypeScript blueprint with the matching Pulumi program, dependency files, and README.

Download TypeScript blueprint

Download the Python blueprint with the matching Pulumi program, dependency files, and README.

Download Python blueprint

Download the Go blueprint with the matching Pulumi program, dependency files, and README.

Download Go blueprint

This blueprint ships a full-stack serverless application on AWS: a React single-page app served from a CDN, a single serverless function that runs a Postgres query, and a managed PostgreSQL database. One pulumi up provisions everything and returns a URL you can open in a browser.

The blueprint covers:

  • A static React + Vite bundle served from Amazon CloudFront
  • A AWS Lambda function with one route, GET /api/random, that runs SELECT floor(random()*100)::int AS n against Aurora Serverless v2 for PostgreSQL
  • the S3 bucket origin serves /* and the Lambda Function URL origin serves /api/* so the browser stays on a single origin and never sees CORS
  • Database credentials generated during pulumi up and stored in AWS Secrets Manager - injected into the Lambda through a SECRET_ARN env var, read on first invocation with GetSecretValue
  • Private networking: the database runs on a Lambda VPC config targeting the landing-zone private subnets attached to the landing-zone private subnets, never exposed to the public internet

The blueprint consumes the Pulumi landing-zone stack through a StackReference so every downstream project reuses the same network and secret conventions.

Architecture on AWS

The Pulumi program is split into two reusable components plus an entrypoint that wires them together:

  • Database - provisions Aurora Serverless v2 for PostgreSQL at the serverlessv2ScalingConfiguration.minCapacity: 0 tier, attaches it to the landing-zone private network, generates a random.RandomPassword, and stores the DB URL in AWS Secrets Manager.
  • Edge - creates the bucket for the SPA, the AWS Lambda that runs the API handler, and Amazon CloudFront with the S3 bucket origin serves /* and the Lambda Function URL origin serves /api/*. The entrypoint wires these together and exports the public site URL.

The function runs Node.js 20 and ships as a bundled handler.js (esbuild) that imports pg and queries the database. The SPA is a plain Vite + React project with one page and one fetch("/api/random") call; because the API is same-origin, there is no CORS setup and no bearer token in the browser.

Scaling: Lambda scales to zero when no requests are in flight. Aurora Serverless v2 scales ACUs to zero after inactivity and restarts on first query.

The AWS variant uses AWS Lambda behind a Function URL, Aurora Serverless v2 for PostgreSQL with minCapacity: 0 so the database scales compute to zero during idle periods, AWS Secrets Manager for the DB password, and CloudFront as the same-origin CDN. The Lambda runs inside the landing-zone VPC so it can reach the private Aurora endpoint without exposing the database to the public internet.

Prerequisites

  • Pulumi account and CLI
  • Node.js 20 or newer and npm
  • an AWS account where the Pulumi landing-zone stack is already deployed and you have permission to create RDS, Lambda, CloudFront, S3, Secrets Manager, and related resources
  • Node.js 20 or newer and npm for building the React SPA and the API bundle (both ship as Node packages regardless of the Pulumi language you pick)
  • A deployed Pulumi landing-zone stack in the same AWS account; see the blueprint prerequisite banner at the top of this page for the link

Landing-zone inputs

The blueprint reads these outputs from your landing-zone/aws stack through pulumi.StackReference:

  • networkId - the VPC id where Aurora and the Lambda VPC config run
  • privateSubnetIds - the two private subnets the DB cluster and Lambda ENIs use
  • secretsStore - the prefix under which Secrets Manager entries for this stack are created

If the reference is missing or the output keys are not present, pulumi up fails fast at preview time. Point the stack reference at the right name with:

pulumi config set landingZoneStack <your-org>/landing-zone/<stack>

You do not have to re-deploy the landing-zone stack to iterate on this blueprint. Once the outputs exist, every change here is additive.

Download the blueprint

Use the Download blueprint button at the top of this page to grab the AWS zip for the Pulumi language you selected in the chooser. Each zip contains:

  • index.ts as the Pulumi entrypoint
  • components/database.ts and components/edge.ts as the reusable modules
  • website/ (React + Vite) and api/ (Node handler) as the application code
  • package.json and tsconfig.json for the root Pulumi project

Unzip, change into the directory, and continue with the quickstart below.

Quickstart

Build the SPA and the API bundle, initialize the Pulumi stack, and deploy. The Pulumi program uploads the built artifacts - it does not run the build itself, so you can iterate on the app and redeploy without any Pulumi-side changes.

# 1. Build the React SPA
cd website
npm install
npm run build
cd ..

# 2. Build the API bundle
cd api
npm install
npm run build
cd ..

# 3. Install root Pulumi dependencies
npm install

# 4. Initialize the stack and point it at your landing zone
pulumi stack init dev
pulumi config set aws:region us-west-2
pulumi config set landingZoneStack <your-org>/landing-zone/dev

# 5. Deploy
pulumi up

pulumi up finishes in 5-10 minutes on a cold account, mostly waiting for the database to become available. When it completes, Pulumi prints a siteUrl output; open it in a browser and you should see the SPA showing the random number the API returned from the database.

App walkthrough

The application ships as two Node packages inside the downloadable zip, both independent of the Pulumi language you chose:

  • website/ - a Vite + React project. One page, App.tsx, fetches /api/random and shows the number. There is no client-side router, no state library, and no auth; it is the smallest possible proof that the frontend reaches the backend.
  • api/ - a Node.js 20 TypeScript package. One router in src/handler.ts, one route (GET /api/random), and one pg pool in src/db.ts. esbuild bundles the whole thing to dist/handler.js so the Pulumi program uploads a single file.

The handler

// api/src/handler.ts
import { pool } from "./db";

export async function handle(path: string) {
  if (path === "/api/random") {
    const result = await pool.query<{ n: number }>(
      "SELECT floor(random()*100)::int AS n",
    );
    return { status: 200, body: JSON.stringify({ n: result.rows[0].n }) };
  }
  return { status: 404, body: JSON.stringify({ error: "not found" }) };
}

Adding more API routes is an edit + npm run build + pulumi up cycle. No function-specific glue; the handler is pure TypeScript.

The SPA

// website/src/App.tsx
import { useEffect, useState } from "react";

export default function App() {
  const [n, setN] = useState<number | null>(null);
  useEffect(() => {
    fetch("/api/random")
      .then((r) => r.json())
      .then((data) => setN(data.n));
  }, []);
  return <main>Backend says your lucky number is: {n ?? "…"}</main>;
}

Database and secret wiring

The Database component provisions aws.rds.Cluster + aws.rds.ClusterInstance at the serverlessv2ScalingConfiguration.minCapacity: 0 tier and a fresh database named after the Pulumi stack. During pulumi up:

  1. Pulumi generates a random.RandomPassword (32 characters, no shell-unsafe symbols).
  2. The password, plus the DB host, port, and database name, is assembled into a Postgres connection string and written to AWS Secrets Manager (aws.secretsmanager.Secret + aws.secretsmanager.SecretVersion).
  3. The AWS Lambda function is configured so the connection string is injected into the Lambda through a SECRET_ARN env var, read on first invocation with GetSecretValue.

The function reads the secret once on cold start and reuses the pg.Pool across invocations. The password never appears in Pulumi stack outputs, never leaves AWS Secrets Manager in plaintext, and rotates automatically if you change the config.

Database networking: a Lambda VPC config targeting the landing-zone private subnets. The DB endpoint has no public IP, so the function is the only path into it.

Deploy

Run pulumi up once the SPA and API builds are in place. The preview shows three components’ worth of resources: the Database cluster plus secret, the Edge bucket + function + CDN, and the wiring that routes requests between them. Approve the preview and Pulumi deploys the whole stack.

When the deploy finishes, Pulumi prints:

  • siteUrl - the public CDN URL; open it to verify the SPA calls the API and renders the random number
  • apiUrl - the same hostname plus /api, useful when iterating on the SPA locally with npm run dev
  • stack-scoped identifiers (DB name, function name, CDN id) that make log tailing and cache invalidation straightforward

Re-running pulumi up after an app change is fast: Pulumi only updates the function code and re-uploads the SPA, leaving the database and CDN alone.

Stack outputs

Every variant exports the same core outputs so downstream Pulumi projects can consume them with StackReference or Pulumi ESC:

  • siteUrl - the public Amazon CloudFront URL that serves the SPA
  • apiUrl - the same hostname plus /api, useful for integration tests
  • dbSecretId - the handle to the database secret stored in AWS Secrets Manager

Cloud-specific outputs on this variant:

  • dbClusterArn - the Aurora cluster ARN for CloudWatch alarms or IAM scoping in other stacks
  • lambdaFunctionName - the Lambda function name so you can tail logs with sam logs or aws logs tail
  • cloudfrontDistributionId - the CloudFront distribution id for cache invalidations after SPA rebuilds

Run pulumi stack output to see the full list with values. Secret-typed outputs require --show-secrets.

Operations and cost

Logs and cache

Function logs land in the CloudWatch log group /aws/lambda/<stack>-api. Aurora Serverless v2 reports scaling events in the RDS console under “Logs & events”. The CloudFront distribution is stack-scoped so you can invalidate the SPA cache with aws cloudfront create-invalidation --distribution-id <id> --paths "/*" after each npm run build in website/.

Cost

Idle cost is dominated by the Aurora Serverless v2 minimum (0 ACUs when paused, plus storage and backups), the CloudFront distribution minimum charges, and the S3 bucket storage. Lambda and the Function URL cost nothing when no requests arrive.

Cleanup

pulumi destroy tears the whole stack down. The object-storage bucket is emptied during destroy so the delete succeeds; if you attached extra objects outside Pulumi, remove them first.

The landing-zone stack is a separate project; destroying this blueprint does not touch it.

Blueprint Pulumi program

The entrypoint reads the landing-zone outputs, creates the database component, then the edge component, and exports the site URL you can open in a browser once pulumi up completes.

import * as pulumi from "@pulumi/pulumi";

import { Database } from "./components/database";
import { Edge } from "./components/edge";

const config = new pulumi.Config();
const landingZoneStackName = config.require("landingZoneStack");
const dbEngineVersion = config.get("dbEngineVersion") ?? "16.4";
const functionMemoryMB = config.getNumber("functionMemoryMB") ?? 512;
const websiteDistPath = config.get("websiteDistPath") ?? "./website/dist";
const apiHandlerPath = config.get("apiHandlerPath") ?? "./api/dist";

const landingZone = new pulumi.StackReference(landingZoneStackName);
const vpcId = landingZone.requireOutput("networkId") as pulumi.Output<string>;
const privateSubnetIds = landingZone.requireOutput("privateSubnetIds") as pulumi.Output<string[]>;
const secretsStore = landingZone.requireOutput("secretsStore") as pulumi.Output<string>;

const projectName = `${pulumi.getStack()}-serverless-react-postgres`;
const commonTags: Record<string, string> = {
    environment: pulumi.getStack(),
    "solution-family": "serverless-react-postgres",
    cloud: "aws",
    language: "typescript",
};

const database = new Database("db", {
    vpcId,
    privateSubnetIds,
    secretsStore,
    engineVersion: dbEngineVersion,
    namePrefix: projectName,
    tags: commonTags,
});

const edge = new Edge("edge", {
    databaseSecretArn: database.secretArn,
    databaseSecurityGroupId: database.securityGroupId,
    vpcId,
    privateSubnetIds,
    websiteDistPath,
    apiHandlerPath,
    functionMemoryMB,
    namePrefix: projectName,
    tags: commonTags,
});

export const siteUrl = edge.siteUrl;
export const apiUrl = edge.apiUrl;
export const dbSecretId = database.secretArn;
export const cloudfrontDistributionId = edge.distributionId;
export const lambdaFunctionName = edge.functionName;
export const dbClusterArn = database.clusterArn;
export const bucketName = edge.bucketName;
export const escEnvironment = `${pulumi.getStack()}-serverless-react-postgres`;
import pulumi

from components.database import Database, DatabaseArgs
from components.edge import Edge, EdgeArgs

config = pulumi.Config()
landing_zone_stack_name = config.require("landingZoneStack")
db_engine_version = config.get("dbEngineVersion") or "16.4"
function_memory_mb = config.get_int("functionMemoryMB") or 512
website_dist_path = config.get("websiteDistPath") or "./website/dist"
api_handler_path = config.get("apiHandlerPath") or "./api/dist"

landing_zone = pulumi.StackReference(landing_zone_stack_name)
vpc_id = landing_zone.require_output("networkId")
private_subnet_ids = landing_zone.require_output("privateSubnetIds")
secrets_store = landing_zone.require_output("secretsStore")

project_name = f"{pulumi.get_stack()}-serverless-react-postgres"
common_tags = {
    "environment": pulumi.get_stack(),
    "solution-family": "serverless-react-postgres",
    "cloud": "aws",
    "language": "python",
}

database = Database(
    "db",
    DatabaseArgs(
        vpc_id=vpc_id,
        private_subnet_ids=private_subnet_ids,
        secrets_store=secrets_store,
        engine_version=db_engine_version,
        name_prefix=project_name,
        tags=common_tags,
    ),
)

edge = Edge(
    "edge",
    EdgeArgs(
        database_secret_arn=database.secret_arn,
        database_security_group_id=database.security_group_id,
        vpc_id=vpc_id,
        private_subnet_ids=private_subnet_ids,
        website_dist_path=website_dist_path,
        api_handler_path=api_handler_path,
        function_memory_mb=function_memory_mb,
        name_prefix=project_name,
        tags=common_tags,
    ),
)

pulumi.export("siteUrl", edge.site_url)
pulumi.export("apiUrl", edge.api_url)
pulumi.export("dbSecretId", database.secret_arn)
pulumi.export("cloudfrontDistributionId", edge.distribution_id)
pulumi.export("lambdaFunctionName", edge.function_name)
pulumi.export("dbClusterArn", database.cluster_arn)
pulumi.export("bucketName", edge.bucket_name)
pulumi.export("escEnvironment", f"{pulumi.get_stack()}-serverless-react-postgres")
package main

import (
	"fmt"

	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"

	"serverless-react-postgres-aws/database"
	"serverless-react-postgres-aws/edge"
)

func main() {
	pulumi.Run(Program)
}

func Program(ctx *pulumi.Context) error {
	cfg := config.New(ctx, "")
	landingZoneStackName := cfg.Require("landingZoneStack")

	dbEngineVersion := cfg.Get("dbEngineVersion")
	if dbEngineVersion == "" {
		dbEngineVersion = "16.4"
	}
	functionMemoryMB := cfg.GetInt("functionMemoryMB")
	if functionMemoryMB == 0 {
		functionMemoryMB = 512
	}
	websiteDistPath := cfg.Get("websiteDistPath")
	if websiteDistPath == "" {
		websiteDistPath = "./website/dist"
	}
	apiHandlerPath := cfg.Get("apiHandlerPath")
	if apiHandlerPath == "" {
		apiHandlerPath = "./api/dist"
	}

	landingZone, err := pulumi.NewStackReference(ctx, landingZoneStackName, nil)
	if err != nil {
		return err
	}

	vpcId := landingZone.GetStringOutput(pulumi.String("networkId"))
	privateSubnetIds := landingZone.GetOutput(pulumi.String("privateSubnetIds")).ApplyT(func(v interface{}) []string {
		return castStringSlice(v)
	}).(pulumi.StringArrayOutput)
	secretsStore := landingZone.GetStringOutput(pulumi.String("secretsStore"))

	projectName := fmt.Sprintf("%s-serverless-react-postgres", ctx.Stack())

	tags := pulumi.StringMap{
		"environment":     pulumi.String(ctx.Stack()),
		"solution-family": pulumi.String("serverless-react-postgres"),
		"cloud":           pulumi.String("aws"),
		"language":        pulumi.String("go"),
	}

	db, err := database.New(ctx, "db", &database.Args{
		VpcId:            vpcId,
		PrivateSubnetIds: privateSubnetIds,
		SecretsStore:     secretsStore,
		EngineVersion:    pulumi.String(dbEngineVersion),
		NamePrefix:       pulumi.String(projectName),
		Tags:             tags,
	})
	if err != nil {
		return err
	}

	ed, err := edge.New(ctx, "edge", &edge.Args{
		DatabaseSecretArn:       db.SecretArn,
		DatabaseSecurityGroupId: db.SecurityGroupId,
		VpcId:                   vpcId,
		PrivateSubnetIds:        privateSubnetIds,
		WebsiteDistPath:         websiteDistPath,
		ApiHandlerPath:          apiHandlerPath,
		FunctionMemoryMB:        pulumi.Int(functionMemoryMB),
		NamePrefix:              pulumi.String(projectName),
		Tags:                    tags,
	})
	if err != nil {
		return err
	}

	ctx.Export("siteUrl", ed.SiteUrl)
	ctx.Export("apiUrl", ed.ApiUrl)
	ctx.Export("dbSecretId", db.SecretArn)
	ctx.Export("cloudfrontDistributionId", ed.DistributionId)
	ctx.Export("lambdaFunctionName", ed.FunctionName)
	ctx.Export("dbClusterArn", db.ClusterArn)
	ctx.Export("bucketName", ed.BucketName)
	ctx.Export("escEnvironment", pulumi.Sprintf("%s-serverless-react-postgres", ctx.Stack()))
	return nil
}

func castStringSlice(v interface{}) []string {
	if v == nil {
		return nil
	}
	raw, ok := v.([]interface{})
	if !ok {
		return nil
	}
	result := make([]string, 0, len(raw))
	for _, item := range raw {
		if s, ok := item.(string); ok {
			result = append(result, s)
		}
	}
	return result
}

Reusable components

The database wiring and the CDN + function + bucket wiring each live in a reusable module. Copy them into other Pulumi projects or adapt per team.

components/database.ts

Provisions the Aurora Serverless v2 instance on the landing-zone private network, generates a strong database password, and stores it in AWS Secrets Manager for the function to read.

import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";
import * as random from "@pulumi/random";

export interface DatabaseArgs {
    vpcId: pulumi.Input<string>;
    privateSubnetIds: pulumi.Input<pulumi.Input<string>[]>;
    secretsStore: pulumi.Input<string>;
    engineVersion: pulumi.Input<string>;
    namePrefix: pulumi.Input<string>;
    tags: Record<string, string>;
}

export class Database extends pulumi.ComponentResource {
    public readonly clusterArn: pulumi.Output<string>;
    public readonly secretArn: pulumi.Output<string>;
    public readonly securityGroupId: pulumi.Output<string>;
    public readonly databaseName: pulumi.Output<string>;

    constructor(name: string, args: DatabaseArgs, opts?: pulumi.ComponentResourceOptions) {
        super("serverless-react-postgres:aws:Database", name, {}, opts);
        const parent = { parent: this };

        const databaseName = "appdb";
        const masterUsername = "pgadmin";

        const password = new random.RandomPassword(`${name}-password`, {
            length: 32,
            special: false,
        }, parent);

        const securityGroup = new aws.ec2.SecurityGroup(`${name}-sg`, {
            vpcId: args.vpcId,
            description: "Aurora Serverless v2 PostgreSQL access",
            tags: args.tags,
        }, parent);

        const subnetGroup = new aws.rds.SubnetGroup(`${name}-subnets`, {
            subnetIds: args.privateSubnetIds,
            description: "Private subnets for Aurora Serverless v2 PostgreSQL",
            tags: args.tags,
        }, parent);

        const cluster = new aws.rds.Cluster(`${name}-cluster`, {
            engine: aws.rds.EngineType.AuroraPostgresql,
            engineMode: "provisioned",
            engineVersion: args.engineVersion,
            databaseName: databaseName,
            masterUsername: masterUsername,
            masterPassword: password.result,
            dbSubnetGroupName: subnetGroup.name,
            vpcSecurityGroupIds: [securityGroup.id],
            storageEncrypted: true,
            skipFinalSnapshot: true,
            serverlessv2ScalingConfiguration: {
                minCapacity: 0,
                maxCapacity: 2,
            },
            tags: args.tags,
        }, parent);

        const instance = new aws.rds.ClusterInstance(`${name}-instance`, {
            clusterIdentifier: cluster.id,
            instanceClass: "db.serverless",
            engine: aws.rds.EngineType.AuroraPostgresql,
            engineVersion: cluster.engineVersion,
            dbSubnetGroupName: subnetGroup.name,
            publiclyAccessible: false,
            tags: args.tags,
        }, parent);

        const connectionUrl = pulumi.all([
            password.result,
            cluster.endpoint,
        ]).apply(([pw, host]) =>
            `postgresql://${masterUsername}:${encodeURIComponent(pw)}@${host}:5432/${databaseName}?sslmode=require`,
        );

        const secretName = pulumi.interpolate`${args.secretsStore}/${args.namePrefix}/database-url`;

        const secret = new aws.secretsmanager.Secret(`${name}-secret`, {
            name: secretName,
            description: "Aurora Serverless v2 PostgreSQL connection URL",
            recoveryWindowInDays: 0,
            tags: args.tags,
        }, { ...parent, dependsOn: [instance] });

        const secretVersion = new aws.secretsmanager.SecretVersion(`${name}-secret-version`, {
            secretId: secret.id,
            secretString: connectionUrl.apply((url) => JSON.stringify({ DATABASE_URL: url })),
        }, parent);

        this.clusterArn = cluster.arn;
        this.secretArn = secret.arn;
        this.securityGroupId = securityGroup.id;
        this.databaseName = pulumi.output(databaseName);

        this.registerOutputs({
            clusterArn: this.clusterArn,
            secretArn: this.secretArn,
            securityGroupId: this.securityGroupId,
            databaseName: this.databaseName,
        });
    }
}

components/edge.ts

Provisions the AWS Lambda function that runs the API, uploads the SPA to object storage, and wires Amazon CloudFront so /* serves the SPA and /api/* reaches the function.

import * as fs from "fs";
import * as path from "path";

import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";

export interface EdgeArgs {
    databaseSecretArn: pulumi.Input<string>;
    databaseSecurityGroupId: pulumi.Input<string>;
    vpcId: pulumi.Input<string>;
    privateSubnetIds: pulumi.Input<pulumi.Input<string>[]>;
    websiteDistPath: string;
    apiHandlerPath: string;
    functionMemoryMB: pulumi.Input<number>;
    namePrefix: pulumi.Input<string>;
    tags: Record<string, string>;
}

const MIME_TYPES: Record<string, string> = {
    ".html": "text/html; charset=utf-8",
    ".js": "application/javascript; charset=utf-8",
    ".mjs": "application/javascript; charset=utf-8",
    ".css": "text/css; charset=utf-8",
    ".json": "application/json; charset=utf-8",
    ".map": "application/json; charset=utf-8",
    ".svg": "image/svg+xml",
    ".png": "image/png",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".gif": "image/gif",
    ".ico": "image/x-icon",
    ".webp": "image/webp",
    ".txt": "text/plain; charset=utf-8",
    ".woff": "font/woff",
    ".woff2": "font/woff2",
};

function contentTypeFor(filePath: string): string {
    const ext = path.extname(filePath).toLowerCase();
    return MIME_TYPES[ext] ?? "application/octet-stream";
}

function walk(dir: string): string[] {
    const results: string[] = [];
    for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
        const full = path.join(dir, entry.name);
        if (entry.isDirectory()) {
            results.push(...walk(full));
        } else if (entry.isFile()) {
            results.push(full);
        }
    }
    return results;
}

const MANAGED_CACHING_OPTIMIZED_ID = "658327ea-f89d-4fab-a63d-7e88639e58f6";
const MANAGED_CACHING_DISABLED_ID = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad";
const MANAGED_ALL_VIEWER_EXCEPT_HOST_HEADER_ID = "b689b0a8-53d0-40ab-baf2-68738e2966ac";

export class Edge extends pulumi.ComponentResource {
    public readonly siteUrl: pulumi.Output<string>;
    public readonly apiUrl: pulumi.Output<string>;
    public readonly distributionId: pulumi.Output<string>;
    public readonly functionName: pulumi.Output<string>;
    public readonly bucketName: pulumi.Output<string>;

    constructor(name: string, args: EdgeArgs, opts?: pulumi.ComponentResourceOptions) {
        super("serverless-react-postgres:aws:Edge", name, {}, opts);
        const parent = { parent: this };

        const lambdaSecurityGroup = new aws.ec2.SecurityGroup(`${name}-fn-sg`, {
            vpcId: args.vpcId,
            description: "Egress for Lambda to reach Aurora PostgreSQL",
            tags: args.tags,
        }, parent);

        new aws.vpc.SecurityGroupEgressRule(`${name}-fn-egress-db`, {
            securityGroupId: lambdaSecurityGroup.id,
            ipProtocol: "tcp",
            fromPort: 5432,
            toPort: 5432,
            referencedSecurityGroupId: args.databaseSecurityGroupId,
            description: "PostgreSQL to DB",
        }, parent);

        new aws.vpc.SecurityGroupIngressRule(`${name}-db-ingress-fn`, {
            securityGroupId: args.databaseSecurityGroupId,
            ipProtocol: "tcp",
            fromPort: 5432,
            toPort: 5432,
            referencedSecurityGroupId: lambdaSecurityGroup.id,
            description: "PostgreSQL from Lambda",
        }, parent);

        const lambdaRole = new aws.iam.Role(`${name}-fn-role`, {
            assumeRolePolicy: JSON.stringify({
                Version: "2012-10-17",
                Statement: [{
                    Effect: "Allow",
                    Principal: { Service: "lambda.amazonaws.com" },
                    Action: "sts:AssumeRole",
                }],
            }),
            tags: args.tags,
        }, parent);

        new aws.iam.RolePolicyAttachment(`${name}-fn-vpc-access`, {
            role: lambdaRole.name,
            policyArn: "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole",
        }, parent);

        new aws.iam.RolePolicy(`${name}-fn-secret`, {
            role: lambdaRole.id,
            policy: pulumi.output(args.databaseSecretArn).apply((arn) => JSON.stringify({
                Version: "2012-10-17",
                Statement: [{
                    Effect: "Allow",
                    Action: ["secretsmanager:GetSecretValue"],
                    Resource: arn,
                }],
            })),
        }, parent);

        const lambdaFunction = new aws.lambda.Function(`${name}-fn`, {
            role: lambdaRole.arn,
            runtime: aws.lambda.Runtime.NodeJS20dX,
            handler: "handler.handler",
            code: new pulumi.asset.FileArchive(args.apiHandlerPath),
            memorySize: args.functionMemoryMB,
            timeout: 30,
            environment: {
                variables: {
                    SECRET_ARN: pulumi.output(args.databaseSecretArn),
                },
            },
            vpcConfig: {
                subnetIds: args.privateSubnetIds,
                securityGroupIds: [lambdaSecurityGroup.id],
            },
            tags: args.tags,
        }, parent);

        const functionUrl = new aws.lambda.FunctionUrl(`${name}-fn-url`, {
            functionName: lambdaFunction.name,
            authorizationType: "NONE",
        }, parent);

        const functionUrlHost = functionUrl.functionUrl.apply((url) => {
            const stripped = url.replace(/^https:\/\//, "");
            return stripped.replace(/\/$/, "");
        });

        const bucket = new aws.s3.BucketV2(`${name}-site`, {
            forceDestroy: true,
            tags: args.tags,
        }, parent);

        new aws.s3.BucketOwnershipControls(`${name}-site-ownership`, {
            bucket: bucket.id,
            rule: { objectOwnership: "BucketOwnerEnforced" },
        }, parent);

        new aws.s3.BucketPublicAccessBlock(`${name}-site-pab`, {
            bucket: bucket.id,
            blockPublicAcls: true,
            blockPublicPolicy: true,
            ignorePublicAcls: true,
            restrictPublicBuckets: true,
        }, parent);

        const websiteFiles = walk(args.websiteDistPath);
        for (const file of websiteFiles) {
            const key = path.relative(args.websiteDistPath, file).split(path.sep).join("/");
            const urlSafeKey = key.replace(/[^A-Za-z0-9._-]/g, "_");
            new aws.s3.BucketObjectv2(`${name}-site-${urlSafeKey}`, {
                bucket: bucket.id,
                key,
                source: new pulumi.asset.FileAsset(file),
                contentType: contentTypeFor(file),
            }, parent);
        }

        const originAccessControl = new aws.cloudfront.OriginAccessControl(`${name}-oac`, {
            originAccessControlOriginType: "s3",
            signingBehavior: "always",
            signingProtocol: "sigv4",
        }, parent);

        const s3OriginId = "s3-site";
        const apiOriginId = "lambda-api";

        const distribution = new aws.cloudfront.Distribution(`${name}-cdn`, {
            enabled: true,
            isIpv6Enabled: true,
            defaultRootObject: "index.html",
            priceClass: "PriceClass_100",
            origins: [
                {
                    originId: s3OriginId,
                    domainName: bucket.bucketRegionalDomainName,
                    originAccessControlId: originAccessControl.id,
                    s3OriginConfig: {
                        originAccessIdentity: "",
                    },
                },
                {
                    originId: apiOriginId,
                    domainName: functionUrlHost,
                    customOriginConfig: {
                        httpPort: 80,
                        httpsPort: 443,
                        originProtocolPolicy: "https-only",
                        originSslProtocols: ["TLSv1.2"],
                    },
                },
            ],
            defaultCacheBehavior: {
                targetOriginId: s3OriginId,
                viewerProtocolPolicy: "redirect-to-https",
                allowedMethods: ["GET", "HEAD"],
                cachedMethods: ["GET", "HEAD"],
                compress: true,
                cachePolicyId: MANAGED_CACHING_OPTIMIZED_ID,
            },
            orderedCacheBehaviors: [
                {
                    pathPattern: "/api/*",
                    targetOriginId: apiOriginId,
                    viewerProtocolPolicy: "redirect-to-https",
                    allowedMethods: ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"],
                    cachedMethods: ["GET", "HEAD"],
                    compress: true,
                    cachePolicyId: MANAGED_CACHING_DISABLED_ID,
                    originRequestPolicyId: MANAGED_ALL_VIEWER_EXCEPT_HOST_HEADER_ID,
                },
            ],
            customErrorResponses: [
                { errorCode: 403, responseCode: 200, responsePagePath: "/index.html" },
                { errorCode: 404, responseCode: 200, responsePagePath: "/index.html" },
            ],
            restrictions: {
                geoRestriction: { restrictionType: "none" },
            },
            viewerCertificate: {
                cloudfrontDefaultCertificate: true,
            },
            tags: args.tags,
        }, parent);

        const bucketPolicyDocument = aws.iam.getPolicyDocumentOutput({
            statements: [
                {
                    sid: "AllowCloudFrontServicePrincipal",
                    effect: "Allow",
                    principals: [{
                        type: "Service",
                        identifiers: ["cloudfront.amazonaws.com"],
                    }],
                    actions: ["s3:GetObject"],
                    resources: [pulumi.interpolate`${bucket.arn}/*`],
                    conditions: [{
                        test: "StringEquals",
                        variable: "AWS:SourceArn",
                        values: [distribution.arn],
                    }],
                },
            ] as aws.types.input.iam.GetPolicyDocumentStatementArgs[],
        });

        new aws.s3.BucketPolicy(`${name}-site-policy`, {
            bucket: bucket.id,
            policy: bucketPolicyDocument.json,
        }, parent);

        this.siteUrl = distribution.domainName.apply((d) => `https://${d}`);
        this.apiUrl = distribution.domainName.apply((d) => `https://${d}/api`);
        this.distributionId = distribution.id;
        this.functionName = lambdaFunction.name;
        this.bucketName = bucket.bucket;

        this.registerOutputs({
            siteUrl: this.siteUrl,
            apiUrl: this.apiUrl,
            distributionId: this.distributionId,
            functionName: this.functionName,
            bucketName: this.bucketName,
        });
    }
}

components/database.py

Provisions the Aurora Serverless v2 instance on the landing-zone private network, generates a strong database password, and stores it in AWS Secrets Manager for the function to read.

import json
from dataclasses import dataclass, field
from typing import Mapping, Sequence
from urllib.parse import quote

import pulumi
import pulumi_aws as aws
import pulumi_random as random


@dataclass
class DatabaseArgs:
    vpc_id: pulumi.Input[str]
    private_subnet_ids: pulumi.Input[Sequence[pulumi.Input[str]]]
    secrets_store: pulumi.Input[str]
    engine_version: pulumi.Input[str]
    name_prefix: pulumi.Input[str]
    tags: Mapping[str, str] = field(default_factory=dict)


class Database(pulumi.ComponentResource):
    def __init__(self, name: str, args: DatabaseArgs, opts: pulumi.ResourceOptions | None = None):
        super().__init__("serverless-react-postgres:aws:Database", name, None, opts)
        parent = pulumi.ResourceOptions(parent=self)

        database_name = "appdb"
        master_username = "pgadmin"

        password = random.RandomPassword(
            f"{name}-password",
            length=32,
            special=False,
            opts=parent,
        )

        security_group = aws.ec2.SecurityGroup(
            f"{name}-sg",
            vpc_id=args.vpc_id,
            description="Aurora Serverless v2 PostgreSQL access",
            tags=dict(args.tags),
            opts=parent,
        )

        subnet_group = aws.rds.SubnetGroup(
            f"{name}-subnets",
            subnet_ids=args.private_subnet_ids,
            description="Private subnets for Aurora Serverless v2 PostgreSQL",
            tags=dict(args.tags),
            opts=parent,
        )

        cluster = aws.rds.Cluster(
            f"{name}-cluster",
            engine="aurora-postgresql",
            engine_mode="provisioned",
            engine_version=args.engine_version,
            database_name=database_name,
            master_username=master_username,
            master_password=password.result,
            db_subnet_group_name=subnet_group.name,
            vpc_security_group_ids=[security_group.id],
            storage_encrypted=True,
            skip_final_snapshot=True,
            serverlessv2_scaling_configuration=aws.rds.ClusterServerlessv2ScalingConfigurationArgs(
                min_capacity=0,
                max_capacity=2,
            ),
            tags=dict(args.tags),
            opts=parent,
        )

        instance = aws.rds.ClusterInstance(
            f"{name}-instance",
            cluster_identifier=cluster.id,
            instance_class="db.serverless",
            engine="aurora-postgresql",
            engine_version=cluster.engine_version,
            db_subnet_group_name=subnet_group.name,
            publicly_accessible=False,
            tags=dict(args.tags),
            opts=parent,
        )

        def _build_url(values):
            pw, host = values
            return f"postgresql://{master_username}:{quote(pw, safe='')}@{host}:5432/{database_name}?sslmode=require"

        connection_url = pulumi.Output.all(password.result, cluster.endpoint).apply(_build_url)

        secret_name = pulumi.Output.all(args.secrets_store, args.name_prefix).apply(
            lambda vs: f"{vs[0]}/{vs[1]}/database-url"
        )

        secret = aws.secretsmanager.Secret(
            f"{name}-secret",
            name=secret_name,
            description="Aurora Serverless v2 PostgreSQL connection URL",
            recovery_window_in_days=0,
            tags=dict(args.tags),
            opts=pulumi.ResourceOptions(parent=self, depends_on=[instance]),
        )

        aws.secretsmanager.SecretVersion(
            f"{name}-secret-version",
            secret_id=secret.id,
            secret_string=connection_url.apply(lambda url: json.dumps({"DATABASE_URL": url})),
            opts=parent,
        )

        self.cluster_arn = cluster.arn
        self.secret_arn = secret.arn
        self.security_group_id = security_group.id
        self.database_name = pulumi.Output.from_input(database_name)

        self.register_outputs({
            "cluster_arn": self.cluster_arn,
            "secret_arn": self.secret_arn,
            "security_group_id": self.security_group_id,
            "database_name": self.database_name,
        })

components/edge.py

Provisions the AWS Lambda function that runs the API, uploads the SPA to object storage, and wires Amazon CloudFront so /* serves the SPA and /api/* reaches the function.

import json
import os
import re
from dataclasses import dataclass, field
from typing import Mapping, Sequence

import pulumi
import pulumi_aws as aws


_MIME_TYPES = {
    ".html": "text/html; charset=utf-8",
    ".js": "application/javascript; charset=utf-8",
    ".mjs": "application/javascript; charset=utf-8",
    ".css": "text/css; charset=utf-8",
    ".json": "application/json; charset=utf-8",
    ".map": "application/json; charset=utf-8",
    ".svg": "image/svg+xml",
    ".png": "image/png",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".gif": "image/gif",
    ".ico": "image/x-icon",
    ".webp": "image/webp",
    ".txt": "text/plain; charset=utf-8",
    ".woff": "font/woff",
    ".woff2": "font/woff2",
}

_MANAGED_CACHING_OPTIMIZED_ID = "658327ea-f89d-4fab-a63d-7e88639e58f6"
_MANAGED_CACHING_DISABLED_ID = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad"
_MANAGED_ALL_VIEWER_EXCEPT_HOST_HEADER_ID = "b689b0a8-53d0-40ab-baf2-68738e2966ac"

_SAFE_KEY_RE = re.compile(r"[^A-Za-z0-9._-]")


def _content_type_for(path: str) -> str:
    _, ext = os.path.splitext(path)
    return _MIME_TYPES.get(ext.lower(), "application/octet-stream")


@dataclass
class EdgeArgs:
    database_secret_arn: pulumi.Input[str]
    database_security_group_id: pulumi.Input[str]
    vpc_id: pulumi.Input[str]
    private_subnet_ids: pulumi.Input[Sequence[pulumi.Input[str]]]
    website_dist_path: str
    api_handler_path: str
    function_memory_mb: pulumi.Input[int]
    name_prefix: pulumi.Input[str]
    tags: Mapping[str, str] = field(default_factory=dict)


class Edge(pulumi.ComponentResource):
    def __init__(self, name: str, args: EdgeArgs, opts: pulumi.ResourceOptions | None = None):
        super().__init__("serverless-react-postgres:aws:Edge", name, None, opts)
        parent = pulumi.ResourceOptions(parent=self)

        lambda_security_group = aws.ec2.SecurityGroup(
            f"{name}-fn-sg",
            vpc_id=args.vpc_id,
            description="Egress for Lambda to reach Aurora PostgreSQL",
            tags=dict(args.tags),
            opts=parent,
        )

        aws.vpc.SecurityGroupEgressRule(
            f"{name}-fn-egress-db",
            security_group_id=lambda_security_group.id,
            ip_protocol="tcp",
            from_port=5432,
            to_port=5432,
            referenced_security_group_id=args.database_security_group_id,
            description="PostgreSQL to DB",
            opts=parent,
        )

        aws.vpc.SecurityGroupIngressRule(
            f"{name}-db-ingress-fn",
            security_group_id=args.database_security_group_id,
            ip_protocol="tcp",
            from_port=5432,
            to_port=5432,
            referenced_security_group_id=lambda_security_group.id,
            description="PostgreSQL from Lambda",
            opts=parent,
        )

        lambda_role = aws.iam.Role(
            f"{name}-fn-role",
            assume_role_policy=json.dumps({
                "Version": "2012-10-17",
                "Statement": [{
                    "Effect": "Allow",
                    "Principal": {"Service": "lambda.amazonaws.com"},
                    "Action": "sts:AssumeRole",
                }],
            }),
            tags=dict(args.tags),
            opts=parent,
        )

        aws.iam.RolePolicyAttachment(
            f"{name}-fn-vpc-access",
            role=lambda_role.name,
            policy_arn="arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole",
            opts=parent,
        )

        aws.iam.RolePolicy(
            f"{name}-fn-secret",
            role=lambda_role.id,
            policy=pulumi.Output.from_input(args.database_secret_arn).apply(lambda arn: json.dumps({
                "Version": "2012-10-17",
                "Statement": [{
                    "Effect": "Allow",
                    "Action": ["secretsmanager:GetSecretValue"],
                    "Resource": arn,
                }],
            })),
            opts=parent,
        )

        lambda_function = aws.lambda_.Function(
            f"{name}-fn",
            role=lambda_role.arn,
            runtime="nodejs20.x",
            handler="handler.handler",
            code=pulumi.FileArchive(args.api_handler_path),
            memory_size=args.function_memory_mb,
            timeout=30,
            environment=aws.lambda_.FunctionEnvironmentArgs(
                variables={
                    "SECRET_ARN": pulumi.Output.from_input(args.database_secret_arn),
                },
            ),
            vpc_config=aws.lambda_.FunctionVpcConfigArgs(
                subnet_ids=args.private_subnet_ids,
                security_group_ids=[lambda_security_group.id],
            ),
            tags=dict(args.tags),
            opts=parent,
        )

        function_url = aws.lambda_.FunctionUrl(
            f"{name}-fn-url",
            function_name=lambda_function.name,
            authorization_type="NONE",
            opts=parent,
        )

        def _strip_scheme(url: str) -> str:
            if url.startswith("https://"):
                url = url[len("https://"):]
            return url.rstrip("/")

        function_url_host = function_url.function_url.apply(_strip_scheme)

        bucket = aws.s3.BucketV2(
            f"{name}-site",
            force_destroy=True,
            tags=dict(args.tags),
            opts=parent,
        )

        aws.s3.BucketOwnershipControls(
            f"{name}-site-ownership",
            bucket=bucket.id,
            rule=aws.s3.BucketOwnershipControlsRuleArgs(object_ownership="BucketOwnerEnforced"),
            opts=parent,
        )

        aws.s3.BucketPublicAccessBlock(
            f"{name}-site-pab",
            bucket=bucket.id,
            block_public_acls=True,
            block_public_policy=True,
            ignore_public_acls=True,
            restrict_public_buckets=True,
            opts=parent,
        )

        for root, _dirs, files in os.walk(args.website_dist_path):
            for file_name in files:
                full_path = os.path.join(root, file_name)
                rel_path = os.path.relpath(full_path, args.website_dist_path).replace(os.sep, "/")
                safe_suffix = _SAFE_KEY_RE.sub("_", rel_path)
                aws.s3.BucketObjectv2(
                    f"{name}-site-{safe_suffix}",
                    bucket=bucket.id,
                    key=rel_path,
                    source=pulumi.FileAsset(full_path),
                    content_type=_content_type_for(full_path),
                    opts=parent,
                )

        origin_access_control = aws.cloudfront.OriginAccessControl(
            f"{name}-oac",
            origin_access_control_origin_type="s3",
            signing_behavior="always",
            signing_protocol="sigv4",
            opts=parent,
        )

        s3_origin_id = "s3-site"
        api_origin_id = "lambda-api"

        distribution = aws.cloudfront.Distribution(
            f"{name}-cdn",
            enabled=True,
            is_ipv6_enabled=True,
            default_root_object="index.html",
            price_class="PriceClass_100",
            origins=[
                aws.cloudfront.DistributionOriginArgs(
                    origin_id=s3_origin_id,
                    domain_name=bucket.bucket_regional_domain_name,
                    origin_access_control_id=origin_access_control.id,
                    s3_origin_config=aws.cloudfront.DistributionOriginS3OriginConfigArgs(
                        origin_access_identity="",
                    ),
                ),
                aws.cloudfront.DistributionOriginArgs(
                    origin_id=api_origin_id,
                    domain_name=function_url_host,
                    custom_origin_config=aws.cloudfront.DistributionOriginCustomOriginConfigArgs(
                        http_port=80,
                        https_port=443,
                        origin_protocol_policy="https-only",
                        origin_ssl_protocols=["TLSv1.2"],
                    ),
                ),
            ],
            default_cache_behavior=aws.cloudfront.DistributionDefaultCacheBehaviorArgs(
                target_origin_id=s3_origin_id,
                viewer_protocol_policy="redirect-to-https",
                allowed_methods=["GET", "HEAD"],
                cached_methods=["GET", "HEAD"],
                compress=True,
                cache_policy_id=_MANAGED_CACHING_OPTIMIZED_ID,
            ),
            ordered_cache_behaviors=[
                aws.cloudfront.DistributionOrderedCacheBehaviorArgs(
                    path_pattern="/api/*",
                    target_origin_id=api_origin_id,
                    viewer_protocol_policy="redirect-to-https",
                    allowed_methods=["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"],
                    cached_methods=["GET", "HEAD"],
                    compress=True,
                    cache_policy_id=_MANAGED_CACHING_DISABLED_ID,
                    origin_request_policy_id=_MANAGED_ALL_VIEWER_EXCEPT_HOST_HEADER_ID,
                ),
            ],
            custom_error_responses=[
                aws.cloudfront.DistributionCustomErrorResponseArgs(
                    error_code=403, response_code=200, response_page_path="/index.html",
                ),
                aws.cloudfront.DistributionCustomErrorResponseArgs(
                    error_code=404, response_code=200, response_page_path="/index.html",
                ),
            ],
            restrictions=aws.cloudfront.DistributionRestrictionsArgs(
                geo_restriction=aws.cloudfront.DistributionRestrictionsGeoRestrictionArgs(
                    restriction_type="none",
                ),
            ),
            viewer_certificate=aws.cloudfront.DistributionViewerCertificateArgs(
                cloudfront_default_certificate=True,
            ),
            tags=dict(args.tags),
            opts=parent,
        )

        bucket_policy_document = aws.iam.get_policy_document_output(
            statements=[
                aws.iam.GetPolicyDocumentStatementArgs(
                    sid="AllowCloudFrontServicePrincipal",
                    effect="Allow",
                    principals=[aws.iam.GetPolicyDocumentStatementPrincipalArgs(
                        type="Service",
                        identifiers=["cloudfront.amazonaws.com"],
                    )],
                    actions=["s3:GetObject"],
                    resources=[bucket.arn.apply(lambda arn: f"{arn}/*")],
                    conditions=[aws.iam.GetPolicyDocumentStatementConditionArgs(
                        test="StringEquals",
                        variable="AWS:SourceArn",
                        values=[distribution.arn],
                    )],
                ),
            ],
        )

        aws.s3.BucketPolicy(
            f"{name}-site-policy",
            bucket=bucket.id,
            policy=bucket_policy_document.json,
            opts=parent,
        )

        self.site_url = distribution.domain_name.apply(lambda d: f"https://{d}")
        self.api_url = distribution.domain_name.apply(lambda d: f"https://{d}/api")
        self.distribution_id = distribution.id
        self.function_name = lambda_function.name
        self.bucket_name = bucket.bucket

        self.register_outputs({
            "site_url": self.site_url,
            "api_url": self.api_url,
            "distribution_id": self.distribution_id,
            "function_name": self.function_name,
            "bucket_name": self.bucket_name,
        })

database/database.go

Provisions the Aurora Serverless v2 instance on the landing-zone private network, generates a strong database password, and stores it in AWS Secrets Manager for the function to read.

package database

import (
	"encoding/json"
	"fmt"
	"net/url"

	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ec2"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/rds"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/secretsmanager"
	"github.com/pulumi/pulumi-random/sdk/v4/go/random"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

type Args struct {
	VpcId            pulumi.StringInput
	PrivateSubnetIds pulumi.StringArrayInput
	SecretsStore     pulumi.StringInput
	EngineVersion    pulumi.StringInput
	NamePrefix       pulumi.StringInput
	Tags             pulumi.StringMapInput
}

type Database struct {
	pulumi.ResourceState

	ClusterArn      pulumi.StringOutput
	SecretArn       pulumi.StringOutput
	SecurityGroupId pulumi.StringOutput
	DatabaseName    pulumi.StringOutput
}

func New(ctx *pulumi.Context, name string, args *Args, opts ...pulumi.ResourceOption) (*Database, error) {
	component := &Database{}
	if err := ctx.RegisterComponentResource("serverless-react-postgres:aws:Database", name, component, opts...); err != nil {
		return nil, err
	}
	parent := pulumi.Parent(component)

	const databaseName = "appdb"
	const masterUsername = "pgadmin"

	password, err := random.NewRandomPassword(ctx, fmt.Sprintf("%s-password", name), &random.RandomPasswordArgs{
		Length:  pulumi.Int(32),
		Special: pulumi.Bool(false),
	}, parent)
	if err != nil {
		return nil, err
	}

	securityGroup, err := ec2.NewSecurityGroup(ctx, fmt.Sprintf("%s-sg", name), &ec2.SecurityGroupArgs{
		VpcId:       args.VpcId,
		Description: pulumi.String("Aurora Serverless v2 PostgreSQL access"),
		Tags:        args.Tags,
	}, parent)
	if err != nil {
		return nil, err
	}

	subnetGroup, err := rds.NewSubnetGroup(ctx, fmt.Sprintf("%s-subnets", name), &rds.SubnetGroupArgs{
		SubnetIds:   args.PrivateSubnetIds,
		Description: pulumi.String("Private subnets for Aurora Serverless v2 PostgreSQL"),
		Tags:        args.Tags,
	}, parent)
	if err != nil {
		return nil, err
	}

	cluster, err := rds.NewCluster(ctx, fmt.Sprintf("%s-cluster", name), &rds.ClusterArgs{
		Engine:              pulumi.String("aurora-postgresql"),
		EngineMode:          pulumi.String("provisioned"),
		EngineVersion:       args.EngineVersion,
		DatabaseName:        pulumi.String(databaseName),
		MasterUsername:      pulumi.String(masterUsername),
		MasterPassword:      password.Result,
		DbSubnetGroupName:   subnetGroup.Name,
		VpcSecurityGroupIds: pulumi.StringArray{securityGroup.ID().ToStringOutput()},
		StorageEncrypted:    pulumi.Bool(true),
		SkipFinalSnapshot:   pulumi.Bool(true),
		Serverlessv2ScalingConfiguration: &rds.ClusterServerlessv2ScalingConfigurationArgs{
			MinCapacity: pulumi.Float64(0),
			MaxCapacity: pulumi.Float64(2),
		},
		Tags: args.Tags,
	}, parent)
	if err != nil {
		return nil, err
	}

	instance, err := rds.NewClusterInstance(ctx, fmt.Sprintf("%s-instance", name), &rds.ClusterInstanceArgs{
		ClusterIdentifier:  cluster.ID(),
		InstanceClass:      pulumi.String("db.serverless"),
		Engine:             rds.EngineType("aurora-postgresql"),
		EngineVersion:      cluster.EngineVersion,
		DbSubnetGroupName:  subnetGroup.Name,
		PubliclyAccessible: pulumi.Bool(false),
		Tags:               args.Tags,
	}, parent)
	if err != nil {
		return nil, err
	}

	connectionUrl := pulumi.All(password.Result, cluster.Endpoint).ApplyT(func(vs []interface{}) string {
		pw := vs[0].(string)
		host := vs[1].(string)
		return fmt.Sprintf("postgresql://%s:%s@%s:5432/%s?sslmode=require",
			masterUsername, url.QueryEscape(pw), host, databaseName)
	}).(pulumi.StringOutput)

	secretName := pulumi.Sprintf("%s/%s/database-url", args.SecretsStore, args.NamePrefix)

	secret, err := secretsmanager.NewSecret(ctx, fmt.Sprintf("%s-secret", name), &secretsmanager.SecretArgs{
		Name:                 secretName,
		Description:          pulumi.String("Aurora Serverless v2 PostgreSQL connection URL"),
		RecoveryWindowInDays: pulumi.Int(0),
		Tags:                 args.Tags,
	}, pulumi.Parent(component), pulumi.DependsOn([]pulumi.Resource{instance}))
	if err != nil {
		return nil, err
	}

	secretString := connectionUrl.ApplyT(func(u string) (string, error) {
		data, err := json.Marshal(map[string]string{"DATABASE_URL": u})
		if err != nil {
			return "", err
		}
		return string(data), nil
	}).(pulumi.StringOutput)

	if _, err := secretsmanager.NewSecretVersion(ctx, fmt.Sprintf("%s-secret-version", name), &secretsmanager.SecretVersionArgs{
		SecretId:     secret.ID(),
		SecretString: secretString,
	}, parent); err != nil {
		return nil, err
	}

	component.ClusterArn = cluster.Arn
	component.SecretArn = secret.Arn
	component.SecurityGroupId = securityGroup.ID().ToStringOutput()
	component.DatabaseName = pulumi.String(databaseName).ToStringOutput()

	if err := ctx.RegisterResourceOutputs(component, pulumi.Map{
		"clusterArn":      component.ClusterArn,
		"secretArn":       component.SecretArn,
		"securityGroupId": component.SecurityGroupId,
		"databaseName":    component.DatabaseName,
	}); err != nil {
		return nil, err
	}

	return component, nil
}

edge/edge.go

Provisions the AWS Lambda function that runs the API, uploads the SPA to object storage, and wires Amazon CloudFront so /* serves the SPA and /api/* reaches the function.

package edge

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/cloudfront"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ec2"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/iam"
	awslambda "github.com/pulumi/pulumi-aws/sdk/v7/go/aws/lambda"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/s3"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/vpc"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

type Args struct {
	DatabaseSecretArn       pulumi.StringInput
	DatabaseSecurityGroupId pulumi.StringInput
	VpcId                   pulumi.StringInput
	PrivateSubnetIds        pulumi.StringArrayInput
	WebsiteDistPath         string
	ApiHandlerPath          string
	FunctionMemoryMB        pulumi.IntInput
	NamePrefix              pulumi.StringInput
	Tags                    pulumi.StringMapInput
}

type Edge struct {
	pulumi.ResourceState

	SiteUrl        pulumi.StringOutput
	ApiUrl         pulumi.StringOutput
	DistributionId pulumi.StringOutput
	FunctionName   pulumi.StringOutput
	BucketName     pulumi.StringOutput
}

const (
	managedCachingOptimizedID          = "658327ea-f89d-4fab-a63d-7e88639e58f6"
	managedCachingDisabledID           = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad"
	managedAllViewerExceptHostHeaderID = "b689b0a8-53d0-40ab-baf2-68738e2966ac"
	s3OriginID                         = "s3-site"
	apiOriginID                        = "lambda-api"
)

var mimeTypes = map[string]string{
	".html":  "text/html; charset=utf-8",
	".js":    "application/javascript; charset=utf-8",
	".mjs":   "application/javascript; charset=utf-8",
	".css":   "text/css; charset=utf-8",
	".json":  "application/json; charset=utf-8",
	".map":   "application/json; charset=utf-8",
	".svg":   "image/svg+xml",
	".png":   "image/png",
	".jpg":   "image/jpeg",
	".jpeg": "image/jpeg",
	".gif":   "image/gif",
	".ico":   "image/x-icon",
	".webp":  "image/webp",
	".txt":   "text/plain; charset=utf-8",
	".woff":  "font/woff",
	".woff2": "font/woff2",
}

var safeKeyRE = regexp.MustCompile(`[^A-Za-z0-9._-]`)

func contentTypeFor(path string) string {
	if ct, ok := mimeTypes[strings.ToLower(filepath.Ext(path))]; ok {
		return ct
	}
	return "application/octet-stream"
}

func New(ctx *pulumi.Context, name string, args *Args, opts ...pulumi.ResourceOption) (*Edge, error) {
	component := &Edge{}
	if err := ctx.RegisterComponentResource("serverless-react-postgres:aws:Edge", name, component, opts...); err != nil {
		return nil, err
	}
	parent := pulumi.Parent(component)

	lambdaSecurityGroup, err := ec2.NewSecurityGroup(ctx, fmt.Sprintf("%s-fn-sg", name), &ec2.SecurityGroupArgs{
		VpcId:       args.VpcId,
		Description: pulumi.String("Egress for Lambda to reach Aurora PostgreSQL"),
		Tags:        args.Tags,
	}, parent)
	if err != nil {
		return nil, err
	}

	if _, err := vpc.NewSecurityGroupEgressRule(ctx, fmt.Sprintf("%s-fn-egress-db", name), &vpc.SecurityGroupEgressRuleArgs{
		SecurityGroupId:           lambdaSecurityGroup.ID(),
		IpProtocol:                pulumi.String("tcp"),
		FromPort:                  pulumi.Int(5432),
		ToPort:                    pulumi.Int(5432),
		ReferencedSecurityGroupId: args.DatabaseSecurityGroupId,
		Description:               pulumi.String("PostgreSQL to DB"),
	}, parent); err != nil {
		return nil, err
	}

	if _, err := vpc.NewSecurityGroupIngressRule(ctx, fmt.Sprintf("%s-db-ingress-fn", name), &vpc.SecurityGroupIngressRuleArgs{
		SecurityGroupId:           args.DatabaseSecurityGroupId,
		IpProtocol:                pulumi.String("tcp"),
		FromPort:                  pulumi.Int(5432),
		ToPort:                    pulumi.Int(5432),
		ReferencedSecurityGroupId: lambdaSecurityGroup.ID().ToStringOutput(),
		Description:               pulumi.String("PostgreSQL from Lambda"),
	}, parent); err != nil {
		return nil, err
	}

	lambdaRole, err := iam.NewRole(ctx, fmt.Sprintf("%s-fn-role", name), &iam.RoleArgs{
		AssumeRolePolicy: pulumi.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}`),
		Tags:             args.Tags,
	}, parent)
	if err != nil {
		return nil, err
	}

	if _, err := iam.NewRolePolicyAttachment(ctx, fmt.Sprintf("%s-fn-vpc-access", name), &iam.RolePolicyAttachmentArgs{
		Role:      lambdaRole.Name,
		PolicyArn: pulumi.String("arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"),
	}, parent); err != nil {
		return nil, err
	}

	secretPolicy := args.DatabaseSecretArn.ToStringOutput().ApplyT(func(arn string) (string, error) {
		doc := map[string]any{
			"Version": "2012-10-17",
			"Statement": []map[string]any{{
				"Effect":   "Allow",
				"Action":   []string{"secretsmanager:GetSecretValue"},
				"Resource": arn,
			}},
		}
		data, err := json.Marshal(doc)
		if err != nil {
			return "", err
		}
		return string(data), nil
	}).(pulumi.StringOutput)

	if _, err := iam.NewRolePolicy(ctx, fmt.Sprintf("%s-fn-secret", name), &iam.RolePolicyArgs{
		Role:   lambdaRole.ID(),
		Policy: secretPolicy,
	}, parent); err != nil {
		return nil, err
	}

	lambdaFunction, err := awslambda.NewFunction(ctx, fmt.Sprintf("%s-fn", name), &awslambda.FunctionArgs{
		Role:       lambdaRole.Arn,
		Runtime:    pulumi.String("nodejs20.x"),
		Handler:    pulumi.String("handler.handler"),
		Code:       pulumi.NewFileArchive(args.ApiHandlerPath),
		MemorySize: args.FunctionMemoryMB,
		Timeout:    pulumi.Int(30),
		Environment: &awslambda.FunctionEnvironmentArgs{
			Variables: pulumi.StringMap{
				"SECRET_ARN": args.DatabaseSecretArn,
			},
		},
		VpcConfig: &awslambda.FunctionVpcConfigArgs{
			SubnetIds:        args.PrivateSubnetIds,
			SecurityGroupIds: pulumi.StringArray{lambdaSecurityGroup.ID().ToStringOutput()},
		},
		Tags: args.Tags,
	}, parent)
	if err != nil {
		return nil, err
	}

	functionUrl, err := awslambda.NewFunctionUrl(ctx, fmt.Sprintf("%s-fn-url", name), &awslambda.FunctionUrlArgs{
		FunctionName:      lambdaFunction.Name,
		AuthorizationType: pulumi.String("NONE"),
	}, parent)
	if err != nil {
		return nil, err
	}

	functionUrlHost := functionUrl.FunctionUrl.ApplyT(func(u string) string {
		u = strings.TrimPrefix(u, "https://")
		u = strings.TrimSuffix(u, "/")
		return u
	}).(pulumi.StringOutput)

	bucket, err := s3.NewBucketV2(ctx, fmt.Sprintf("%s-site", name), &s3.BucketV2Args{
		ForceDestroy: pulumi.Bool(true),
		Tags:         args.Tags,
	}, parent)
	if err != nil {
		return nil, err
	}

	if _, err := s3.NewBucketOwnershipControls(ctx, fmt.Sprintf("%s-site-ownership", name), &s3.BucketOwnershipControlsArgs{
		Bucket: bucket.ID(),
		Rule: &s3.BucketOwnershipControlsRuleArgs{
			ObjectOwnership: pulumi.String("BucketOwnerEnforced"),
		},
	}, parent); err != nil {
		return nil, err
	}

	if _, err := s3.NewBucketPublicAccessBlock(ctx, fmt.Sprintf("%s-site-pab", name), &s3.BucketPublicAccessBlockArgs{
		Bucket:                bucket.ID(),
		BlockPublicAcls:       pulumi.Bool(true),
		BlockPublicPolicy:     pulumi.Bool(true),
		IgnorePublicAcls:      pulumi.Bool(true),
		RestrictPublicBuckets: pulumi.Bool(true),
	}, parent); err != nil {
		return nil, err
	}

	walkErr := filepath.Walk(args.WebsiteDistPath, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if info.IsDir() {
			return nil
		}
		rel, err := filepath.Rel(args.WebsiteDistPath, path)
		if err != nil {
			return err
		}
		key := filepath.ToSlash(rel)
		safe := safeKeyRE.ReplaceAllString(key, "_")
		if _, err := s3.NewBucketObjectv2(ctx, fmt.Sprintf("%s-site-%s", name, safe), &s3.BucketObjectv2Args{
			Bucket:      bucket.ID(),
			Key:         pulumi.String(key),
			Source:      pulumi.NewFileAsset(path),
			ContentType: pulumi.String(contentTypeFor(path)),
		}, parent); err != nil {
			return err
		}
		return nil
	})
	if walkErr != nil {
		return nil, walkErr
	}

	originAccessControl, err := cloudfront.NewOriginAccessControl(ctx, fmt.Sprintf("%s-oac", name), &cloudfront.OriginAccessControlArgs{
		OriginAccessControlOriginType: pulumi.String("s3"),
		SigningBehavior:               pulumi.String("always"),
		SigningProtocol:               pulumi.String("sigv4"),
	}, parent)
	if err != nil {
		return nil, err
	}

	distribution, err := cloudfront.NewDistribution(ctx, fmt.Sprintf("%s-cdn", name), &cloudfront.DistributionArgs{
		Enabled:           pulumi.Bool(true),
		IsIpv6Enabled:     pulumi.Bool(true),
		DefaultRootObject: pulumi.String("index.html"),
		PriceClass:        pulumi.String("PriceClass_100"),
		Origins: cloudfront.DistributionOriginArray{
			&cloudfront.DistributionOriginArgs{
				OriginId:              pulumi.String(s3OriginID),
				DomainName:            bucket.BucketRegionalDomainName,
				OriginAccessControlId: originAccessControl.ID(),
				S3OriginConfig: &cloudfront.DistributionOriginS3OriginConfigArgs{
					OriginAccessIdentity: pulumi.String(""),
				},
			},
			&cloudfront.DistributionOriginArgs{
				OriginId:   pulumi.String(apiOriginID),
				DomainName: functionUrlHost,
				CustomOriginConfig: &cloudfront.DistributionOriginCustomOriginConfigArgs{
					HttpPort:             pulumi.Int(80),
					HttpsPort:            pulumi.Int(443),
					OriginProtocolPolicy: pulumi.String("https-only"),
					OriginSslProtocols:   pulumi.StringArray{pulumi.String("TLSv1.2")},
				},
			},
		},
		DefaultCacheBehavior: &cloudfront.DistributionDefaultCacheBehaviorArgs{
			TargetOriginId:       pulumi.String(s3OriginID),
			ViewerProtocolPolicy: pulumi.String("redirect-to-https"),
			AllowedMethods:       pulumi.StringArray{pulumi.String("GET"), pulumi.String("HEAD")},
			CachedMethods:        pulumi.StringArray{pulumi.String("GET"), pulumi.String("HEAD")},
			Compress:             pulumi.Bool(true),
			CachePolicyId:        pulumi.String(managedCachingOptimizedID),
		},
		OrderedCacheBehaviors: cloudfront.DistributionOrderedCacheBehaviorArray{
			&cloudfront.DistributionOrderedCacheBehaviorArgs{
				PathPattern:          pulumi.String("/api/*"),
				TargetOriginId:       pulumi.String(apiOriginID),
				ViewerProtocolPolicy: pulumi.String("redirect-to-https"),
				AllowedMethods: pulumi.StringArray{
					pulumi.String("GET"), pulumi.String("HEAD"), pulumi.String("OPTIONS"),
					pulumi.String("PUT"), pulumi.String("POST"), pulumi.String("PATCH"), pulumi.String("DELETE"),
				},
				CachedMethods:         pulumi.StringArray{pulumi.String("GET"), pulumi.String("HEAD")},
				Compress:              pulumi.Bool(true),
				CachePolicyId:         pulumi.String(managedCachingDisabledID),
				OriginRequestPolicyId: pulumi.String(managedAllViewerExceptHostHeaderID),
			},
		},
		CustomErrorResponses: cloudfront.DistributionCustomErrorResponseArray{
			&cloudfront.DistributionCustomErrorResponseArgs{
				ErrorCode:        pulumi.Int(403),
				ResponseCode:     pulumi.Int(200),
				ResponsePagePath: pulumi.String("/index.html"),
			},
			&cloudfront.DistributionCustomErrorResponseArgs{
				ErrorCode:        pulumi.Int(404),
				ResponseCode:     pulumi.Int(200),
				ResponsePagePath: pulumi.String("/index.html"),
			},
		},
		Restrictions: &cloudfront.DistributionRestrictionsArgs{
			GeoRestriction: &cloudfront.DistributionRestrictionsGeoRestrictionArgs{
				RestrictionType: pulumi.String("none"),
			},
		},
		ViewerCertificate: &cloudfront.DistributionViewerCertificateArgs{
			CloudfrontDefaultCertificate: pulumi.Bool(true),
		},
		Tags: args.Tags,
	}, parent)
	if err != nil {
		return nil, err
	}

	bucketPolicyDocument := iam.GetPolicyDocumentOutput(ctx, iam.GetPolicyDocumentOutputArgs{
		Statements: iam.GetPolicyDocumentStatementArray{
			&iam.GetPolicyDocumentStatementArgs{
				Sid:    pulumi.String("AllowCloudFrontServicePrincipal"),
				Effect: pulumi.String("Allow"),
				Principals: iam.GetPolicyDocumentStatementPrincipalArray{
					&iam.GetPolicyDocumentStatementPrincipalArgs{
						Type:        pulumi.String("Service"),
						Identifiers: pulumi.StringArray{pulumi.String("cloudfront.amazonaws.com")},
					},
				},
				Actions: pulumi.StringArray{pulumi.String("s3:GetObject")},
				Resources: pulumi.StringArray{
					bucket.Arn.ApplyT(func(arn string) string { return arn + "/*" }).(pulumi.StringOutput),
				},
				Conditions: iam.GetPolicyDocumentStatementConditionArray{
					&iam.GetPolicyDocumentStatementConditionArgs{
						Test:     pulumi.String("StringEquals"),
						Variable: pulumi.String("AWS:SourceArn"),
						Values:   pulumi.StringArray{distribution.Arn},
					},
				},
			},
		},
	})

	if _, err := s3.NewBucketPolicy(ctx, fmt.Sprintf("%s-site-policy", name), &s3.BucketPolicyArgs{
		Bucket: bucket.ID(),
		Policy: bucketPolicyDocument.Json(),
	}, parent); err != nil {
		return nil, err
	}

	component.SiteUrl = distribution.DomainName.ApplyT(func(d string) string { return "https://" + d }).(pulumi.StringOutput)
	component.ApiUrl = distribution.DomainName.ApplyT(func(d string) string { return "https://" + d + "/api" }).(pulumi.StringOutput)
	component.DistributionId = distribution.ID().ToStringOutput()
	component.FunctionName = lambdaFunction.Name
	component.BucketName = bucket.Bucket

	if err := ctx.RegisterResourceOutputs(component, pulumi.Map{
		"siteUrl":        component.SiteUrl,
		"apiUrl":         component.ApiUrl,
		"distributionId": component.DistributionId,
		"functionName":   component.FunctionName,
		"bucketName":     component.BucketName,
	}); err != nil {
		return nil, err
	}

	return component, nil
}

Frequently asked questions

Do I need the Pulumi landing-zone stack first?
Yes. This blueprint reads the landing-zone networkId, privateSubnetIds, and secretsStore outputs through a StackReference so the database lives on the shared private network and the function secret lands under your cloud’s central secret store. Deploy the landing-zone family in the same cloud account first, then pulumi config set landingZoneStack <your-org>/landing-zone/dev.
Why a SPA plus API instead of server-side rendering?
Pure FaaS (Lambda, Azure Functions, Cloud Run functions) is a natural fit for JSON-returning handlers but awkward for SSR because streaming responses and adapter shims vary per cloud. Splitting the app into a static bundle plus one JSON endpoint keeps the backend trivial and lets the CDN cache the SPA.
Does this scale to zero?
The function scales to zero on all three clouds. The database tier varies - Aurora Serverless v2 on AWS can scale to 0 ACUs, so idle cost is storage plus backups only. Azure Database for PostgreSQL Flexible Server and Cloud SQL for PostgreSQL keep a minimum compute tier running; the blueprint picks the cheapest Burstable / db-f1-micro size and the cost + cleanup section shows how to stop the server manually.
How is the database password stored?
Pulumi generates a random.RandomPassword during pulumi up, writes it into {{secret_service}} under the landing-zone’s secrets-store scope, and injects the secret value into the function at cold start. The password never appears in stack outputs or in state files; pulumi config keeps the secret handle, not the value.
How do I add more API routes?
Edit api/src/handler.ts. The blueprint ships a one-route router for GET /api/random; add more case arms for new paths, run npm run build in api/, and rerun pulumi up. The Pulumi program repackages the bundle and redeploys the function.
How is the SPA bundled?
The website/ folder is a Vite + React project. Run npm install and npm run build in website/ before pulumi up; the Pulumi program uploads the built website/dist/ directory to the object-storage bucket and invalidates the CDN. If you want to test the SPA against the deployed API first, run npm run dev and point Vite at the Pulumi-exported apiUrl.
Can I use this without the landing-zone stack?
Yes, but you have to replace the StackReference block in the entrypoint with the network id, private subnets, and a secret-store scope you already control. The blueprint assumes the landing-zone values exist so the DB can land in a private subnet and the function can reach it through the cloud’s VPC integration.
What does this cost?
Idle cost on AWS is approximately storage plus the Aurora Serverless v2 minimum if ACUs are paused, plus the CloudFront distribution. Azure and GCP keep a small DB compute tier running continuously, so expect a low-double-digit monthly baseline even at zero traffic. pulumi destroy tears the whole stack down when you are done.