Deploy a serverless React + Postgres blueprint on GCP with Pulumi

Switch variant

Choose a different cloud.

Ship a React SPA plus a serverless API that queries Cloud SQL for PostgreSQL behind Google Cloud CDN in front of a global external HTTPS load balancer on GCP. 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 GCP landing zone first.

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

Download blueprint

Get this GCP 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 GCP: 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 Google Cloud CDN in front of a global external HTTPS load balancer
  • A Google Cloud Run functions (2nd gen) function with one route, GET /api/random, that runs SELECT floor(random()*100)::int AS n against Cloud SQL for PostgreSQL
  • a URL map with two backends - a backend bucket for /* and a serverless NEG pointing at the function for /api/* so the browser stays on a single origin and never sees CORS
  • Database credentials generated during pulumi up and stored in Google Secret Manager - mounted into the Cloud Run function as a secret volume at /secrets/db-password
  • Private networking: the database runs on a Serverless VPC Access connector that lets the Cloud Run function reach the Cloud SQL private IP 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 GCP

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

  • Database - provisions Cloud SQL for PostgreSQL at the db-f1-micro with private IP on the landing-zone VPC tier, attaches it to the landing-zone private network, generates a random.RandomPassword, and stores the DB URL in Google Secret Manager.
  • Edge - creates the bucket for the SPA, the Google Cloud Run functions (2nd gen) that runs the API handler, and Google Cloud CDN in front of a global external HTTPS load balancer with a URL map with two backends - a backend bucket for /* and a serverless NEG pointing at the function for /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: Cloud Run functions scale to zero when idle. Cloud SQL does not scale compute to zero; the blueprint picks the cheapest db-f1-micro size and the cost + cleanup section shows how to stop the instance manually.

The GCP variant uses Google Cloud Run functions (2nd generation, which run on Cloud Run under the hood) for scale-to-zero HTTP compute, Cloud SQL for PostgreSQL on the db-f1-micro tier with private IP on the landing-zone VPC, Secret Manager for the DB password (mounted as a secret volume inside the function), and a global external HTTPS load balancer fronted by Cloud CDN for same-origin routing.

Prerequisites

  • Pulumi account and CLI
  • Node.js 20 or newer and npm
  • a Google Cloud project where the Pulumi landing-zone stack is already deployed and you have permission to create Cloud SQL instances, Cloud Run functions, Cloud Storage buckets, global HTTPS load balancers, Secret Manager secrets, 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 GCP 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/gcp stack through pulumi.StackReference:

  • networkName - the VPC the Cloud SQL instance and the Serverless VPC Access connector attach to
  • privateSubnetName - the subnet where the Serverless VPC Access connector runs
  • secretsStore - the Secret Manager location the blueprint writes the DB password into

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 GCP 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 gcp:project my-project-id
pulumi config set gcp:region us-central1
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 gcp.sql.DatabaseInstance with settings.ipConfiguration.privateNetwork at the db-f1-micro with private IP on the landing-zone VPC 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 Google Secret Manager (gcp.secretmanager.Secret + gcp.secretmanager.SecretVersion).
  3. The Google Cloud Run functions (2nd gen) function is configured so the connection string is mounted into the Cloud Run function as a secret volume at /secrets/db-password.

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 Google Secret Manager in plaintext, and rotates automatically if you change the config.

Database networking: a Serverless VPC Access connector that lets the Cloud Run function reach the Cloud SQL private IP. 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 Google Cloud CDN in front of a global external HTTPS load balancer URL that serves the SPA
  • apiUrl - the same hostname plus /api, useful for integration tests
  • dbSecretId - the handle to the database secret stored in Google Secret Manager

Cloud-specific outputs on this variant:

  • dbInstanceName - the Cloud SQL instance name for portal navigation and gcloud sql connect
  • functionName - the Cloud Run functions name so you can tail logs with gcloud functions logs read
  • loadBalancerIpAddress - the global external IP, useful for DNS A records

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 go to Cloud Logging under the Cloud Run functions resource. Cloud SQL metrics are on the instance page. Invalidate the CDN cache with gcloud compute url-maps invalidate-cdn-cache <url-map> --path "/*" after each npm run build in website/.

Cost

Cloud Run functions charges per-request + CPU time, so idle cost is zero. Cloud SQL runs continuously on the db-f1-micro tier; stop it with gcloud sql instances patch <name> --activation-policy=NEVER when you are not using it. The global HTTPS load balancer has a small hourly minimum plus per-GB charges.

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") ?? "POSTGRES_16";
const functionMemory = config.get("functionMemory") ?? "512Mi";
const websiteDistPath = config.get("websiteDistPath") ?? "./website/dist";
const apiHandlerPath = config.get("apiHandlerPath") ?? "./api/dist";

const gcpConfig = new pulumi.Config("gcp");
const project = gcpConfig.require("project");
const region = gcpConfig.require("region");

const landingZone = new pulumi.StackReference(landingZoneStackName);
const networkName = landingZone.requireOutput("networkName") as pulumi.Output<string>;
const privateSubnetName = landingZone.requireOutput("privateSubnetName") as pulumi.Output<string>;

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

const database = new Database("db", {
    project,
    region,
    networkName,
    dbVersion: dbEngineVersion,
    namePrefix: projectName,
    labels: commonLabels,
});

const edge = new Edge("edge", {
    project,
    region,
    networkName,
    privateSubnetName,
    databaseSecretName: database.secretName,
    databaseSecretVersionId: database.secretVersionId,
    websiteDistPath,
    apiHandlerPath,
    functionMemory,
    namePrefix: projectName,
    labels: commonLabels,
});

export const siteUrl = edge.siteUrl;
export const apiUrl = edge.apiUrl;
export const dbSecretId = database.secretId;
export const dbInstanceName = database.instanceName;
export const functionName = edge.functionName;
export const loadBalancerIpAddress = edge.loadBalancerIp;
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 "POSTGRES_16"
function_memory = config.get("functionMemory") or "512Mi"
website_dist_path = config.get("websiteDistPath") or "./website/dist"
api_handler_path = config.get("apiHandlerPath") or "./api/dist"

gcp_config = pulumi.Config("gcp")
project = gcp_config.require("project")
region = gcp_config.require("region")

landing_zone = pulumi.StackReference(landing_zone_stack_name)
network_name = landing_zone.require_output("networkName")
private_subnet_name = landing_zone.require_output("privateSubnetName")

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

database = Database(
    "db",
    DatabaseArgs(
        project=project,
        region=region,
        network_name=network_name,
        db_version=db_engine_version,
        name_prefix=project_name,
        labels=common_labels,
    ),
)

edge = Edge(
    "edge",
    EdgeArgs(
        project=project,
        region=region,
        network_name=network_name,
        private_subnet_name=private_subnet_name,
        database_secret_name=database.secret_name,
        database_secret_version_id=database.secret_version_id,
        website_dist_path=website_dist_path,
        api_handler_path=api_handler_path,
        function_memory=function_memory,
        name_prefix=project_name,
        labels=common_labels,
    ),
)

pulumi.export("siteUrl", edge.site_url)
pulumi.export("apiUrl", edge.api_url)
pulumi.export("dbSecretId", database.secret_id)
pulumi.export("dbInstanceName", database.instance_name)
pulumi.export("functionName", edge.function_name)
pulumi.export("loadBalancerIpAddress", edge.load_balancer_ip)
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-gcp/database"
	"serverless-react-postgres-gcp/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 = "POSTGRES_16"
	}
	functionMemory := cfg.Get("functionMemory")
	if functionMemory == "" {
		functionMemory = "512Mi"
	}
	websiteDistPath := cfg.Get("websiteDistPath")
	if websiteDistPath == "" {
		websiteDistPath = "./website/dist"
	}
	apiHandlerPath := cfg.Get("apiHandlerPath")
	if apiHandlerPath == "" {
		apiHandlerPath = "./api/dist"
	}

	gcpConfig := config.New(ctx, "gcp")
	project := gcpConfig.Require("project")
	region := gcpConfig.Require("region")

	landingZone, err := pulumi.NewStackReference(ctx, landingZoneStackName, nil)
	if err != nil {
		return err
	}
	networkName := landingZone.GetStringOutput(pulumi.String("networkName"))
	privateSubnetName := landingZone.GetStringOutput(pulumi.String("privateSubnetName"))

	projectName := fmt.Sprintf("%s-serverless-react-postgres", ctx.Stack())
	commonLabels := pulumi.StringMap{
		"environment":     pulumi.String(ctx.Stack()),
		"solution-family": pulumi.String("serverless-react-postgres"),
		"cloud":           pulumi.String("gcp"),
		"language":        pulumi.String("go"),
	}

	db, err := database.New(ctx, "db", &database.Args{
		Project:     pulumi.String(project),
		Region:      pulumi.String(region),
		NetworkName: networkName,
		DbVersion:   pulumi.String(dbEngineVersion),
		NamePrefix:  pulumi.String(projectName),
		Labels:      commonLabels,
	})
	if err != nil {
		return err
	}

	e, err := edge.New(ctx, "edge", &edge.Args{
		Project:                 pulumi.String(project),
		Region:                  pulumi.String(region),
		NetworkName:             networkName,
		PrivateSubnetName:       privateSubnetName,
		DatabaseSecretName:      db.SecretName,
		DatabaseSecretVersionId: db.SecretVersionId,
		WebsiteDistPath:         websiteDistPath,
		ApiHandlerPath:          apiHandlerPath,
		FunctionMemory:          pulumi.String(functionMemory),
		NamePrefix:              pulumi.String(projectName),
		Labels:                  commonLabels,
	})
	if err != nil {
		return err
	}

	ctx.Export("siteUrl", e.SiteUrl)
	ctx.Export("apiUrl", e.ApiUrl)
	ctx.Export("dbSecretId", db.SecretId)
	ctx.Export("dbInstanceName", db.InstanceName)
	ctx.Export("functionName", e.FunctionName)
	ctx.Export("loadBalancerIpAddress", e.LoadBalancerIp)
	ctx.Export("escEnvironment", pulumi.Sprintf("%s-serverless-react-postgres", ctx.Stack()))
	return nil
}

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 Cloud SQL instance on the landing-zone private network, generates a strong database password, and stores it in Google Secret Manager for the function to read.

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

export interface DatabaseArgs {
    project: pulumi.Input<string>;
    region: pulumi.Input<string>;
    networkName: pulumi.Input<string>;
    dbVersion: pulumi.Input<string>;
    namePrefix: pulumi.Input<string>;
    labels: Record<string, string>;
}

export class Database extends pulumi.ComponentResource {
    public readonly instanceName: pulumi.Output<string>;
    public readonly connectionName: pulumi.Output<string>;
    public readonly secretId: pulumi.Output<string>;
    public readonly secretName: pulumi.Output<string>;
    public readonly secretVersionId: pulumi.Output<string>;
    public readonly databaseName: pulumi.Output<string>;
    public readonly user: pulumi.Output<string>;

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

        const databaseName = "appdb";
        const dbUser = "appuser";

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

        // The landing zone exposes only a VPC name, so this blueprint allocates its
        // own VPC peering range and creates the service-networking connection within
        // the stack. If your landing zone already peers the VPC with
        // servicenetworking.googleapis.com, you can delete these two resources and
        // point Cloud SQL's privateNetwork at the shared network directly.
        const networkSelfLink = pulumi.interpolate`projects/${args.project}/global/networks/${args.networkName}`;

        const peeringRange = new gcp.compute.GlobalAddress(`${name}-peer-range`, {
            project: args.project,
            name: pulumi.interpolate`${args.namePrefix}-sql-peer`,
            purpose: "VPC_PEERING",
            addressType: "INTERNAL",
            prefixLength: 16,
            network: networkSelfLink,
            labels: args.labels,
        }, parent);

        const networkingConnection = new gcp.servicenetworking.Connection(`${name}-sn-conn`, {
            network: networkSelfLink,
            service: "servicenetworking.googleapis.com",
            reservedPeeringRanges: [peeringRange.name],
        }, parent);

        const instance = new gcp.sql.DatabaseInstance(`${name}-pg`, {
            project: args.project,
            region: args.region,
            databaseVersion: args.dbVersion,
            deletionProtection: false,
            settings: {
                tier: "db-f1-micro",
                ipConfiguration: {
                    ipv4Enabled: false,
                    privateNetwork: networkSelfLink,
                },
                backupConfiguration: {
                    enabled: true,
                },
                userLabels: args.labels,
            },
        }, { ...parent, dependsOn: [networkingConnection] });

        new gcp.sql.Database(`${name}-db`, {
            project: args.project,
            name: databaseName,
            instance: instance.name,
        }, parent);

        const user = new gcp.sql.User(`${name}-user`, {
            project: args.project,
            name: dbUser,
            instance: instance.name,
            password: password.result,
        }, parent);

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

        const secret = new gcp.secretmanager.Secret(`${name}-secret`, {
            project: args.project,
            secretId: pulumi.interpolate`${args.namePrefix}-database-url`,
            replication: {
                auto: {},
            },
            labels: args.labels,
        }, parent);

        const secretVersion = new gcp.secretmanager.SecretVersion(`${name}-secret-version`, {
            secret: secret.id,
            secretData: connectionUrl,
        }, { ...parent, dependsOn: [user] });

        this.instanceName = instance.name;
        this.connectionName = instance.connectionName;
        this.secretId = secret.id;
        this.secretName = secret.secretId;
        this.secretVersionId = secretVersion.id;
        this.databaseName = pulumi.output(databaseName);
        this.user = pulumi.output(dbUser);

        this.registerOutputs({
            instanceName: this.instanceName,
            connectionName: this.connectionName,
            secretId: this.secretId,
            secretName: this.secretName,
            secretVersionId: this.secretVersionId,
            databaseName: this.databaseName,
            user: this.user,
        });
    }
}

components/edge.ts

Provisions the Google Cloud Run functions (2nd gen) function that runs the API, uploads the SPA to object storage, and wires Google Cloud CDN in front of a global external HTTPS load balancer so /* serves the SPA and /api/* reaches the function.

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

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

export interface EdgeArgs {
    project: pulumi.Input<string>;
    region: pulumi.Input<string>;
    networkName: pulumi.Input<string>;
    privateSubnetName: pulumi.Input<string>;
    databaseSecretName: pulumi.Input<string>;
    databaseSecretVersionId: pulumi.Input<string>;
    websiteDistPath: string;
    apiHandlerPath: string;
    functionMemory: pulumi.Input<string>;
    namePrefix: pulumi.Input<string>;
    labels: 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;
}

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

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

        const networkSelfLink = pulumi.interpolate`projects/${args.project}/global/networks/${args.networkName}`;

        // Serverless VPC Access connector so the Cloud Run function can reach
        // Cloud SQL on the landing-zone VPC's private IP.
        const connector = new gcp.vpcaccess.Connector(`${name}-vpc-conn`, {
            project: args.project,
            region: args.region,
            network: args.networkName,
            ipCidrRange: "10.124.0.0/28",
            minInstances: 2,
            maxInstances: 3,
            machineType: "e2-micro",
        }, parent);

        // Dedicated service account for the function: secret access + Cloud SQL client.
        const functionSa = new gcp.serviceaccount.Account(`${name}-fn-sa`, {
            project: args.project,
            accountId: pulumi.interpolate`${args.namePrefix}-fn`.apply((s) =>
                s.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 30),
            ),
            displayName: "serverless-react-postgres function",
        }, parent);

        new gcp.secretmanager.SecretIamMember(`${name}-fn-secret-access`, {
            project: args.project,
            secretId: args.databaseSecretName,
            role: "roles/secretmanager.secretAccessor",
            member: pulumi.interpolate`serviceAccount:${functionSa.email}`,
        }, parent);

        new gcp.projects.IAMMember(`${name}-fn-sql-client`, {
            project: args.project,
            role: "roles/cloudsql.client",
            member: pulumi.interpolate`serviceAccount:${functionSa.email}`,
        }, parent);

        // Bucket that holds the zipped function source.
        const functionSourceBucket = new gcp.storage.Bucket(`${name}-fn-src`, {
            project: args.project,
            location: args.region,
            uniformBucketLevelAccess: true,
            forceDestroy: true,
            labels: args.labels,
        }, parent);

        const apiArchive = new pulumi.asset.FileArchive(args.apiHandlerPath);
        const functionSourceObject = new gcp.storage.BucketObject(`${name}-fn-src-zip`, {
            bucket: functionSourceBucket.name,
            name: "api.zip",
            source: apiArchive,
        }, parent);

        // Cloud Run function (v2). Runs on Cloud Run under the hood so IAM is managed
        // through the Cloud Run invoker role.
        const fn = new gcp.cloudfunctionsv2.Function(`${name}-fn`, {
            project: args.project,
            location: args.region,
            name: pulumi.interpolate`${args.namePrefix}-api`.apply((s) =>
                s.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 62),
            ),
            buildConfig: {
                runtime: "nodejs20",
                entryPoint: "api",
                source: {
                    storageSource: {
                        bucket: functionSourceBucket.name,
                        object: functionSourceObject.name,
                    },
                },
            },
            serviceConfig: {
                availableMemory: args.functionMemory,
                timeoutSeconds: 30,
                serviceAccountEmail: functionSa.email,
                vpcConnector: connector.id,
                vpcConnectorEgressSettings: "PRIVATE_RANGES_ONLY",
                ingressSettings: "ALLOW_ALL",
                secretVolumes: [{
                    mountPath: "/secrets",
                    projectId: args.project,
                    secret: args.databaseSecretName,
                    versions: [{ version: "latest", path: "database-url" }],
                }],
            },
            labels: args.labels,
        }, parent);

        const cloudRunService = fn.serviceConfig.apply((sc) => sc?.service ?? "");
        const functionUri = fn.serviceConfig.apply((sc) => sc?.uri ?? "");

        // Make the function publicly invokable via the Cloud Run v2 IAM binding.
        new gcp.cloudrunv2.ServiceIamMember(`${name}-fn-invoker`, {
            project: args.project,
            location: args.region,
            name: cloudRunService,
            role: "roles/run.invoker",
            member: "allUsers",
        }, parent);

        // Static site bucket - uniform access + allUsers object viewer so the
        // BackendBucket origin can fetch anonymously.
        const siteBucket = new gcp.storage.Bucket(`${name}-site`, {
            project: args.project,
            location: args.region,
            uniformBucketLevelAccess: true,
            forceDestroy: true,
            website: {
                mainPageSuffix: "index.html",
                notFoundPage: "index.html",
            },
            labels: args.labels,
        }, parent);

        new gcp.storage.BucketIAMMember(`${name}-site-public`, {
            bucket: siteBucket.name,
            role: "roles/storage.objectViewer",
            member: "allUsers",
        }, 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 gcp.storage.BucketObject(`${name}-site-${urlSafeKey}`, {
                bucket: siteBucket.name,
                name: key,
                source: new pulumi.asset.FileAsset(file),
                contentType: contentTypeFor(file),
            }, parent);
        }

        // Global external HTTP load balancer. Demo blueprint: HTTP-only, no custom
        // domain. Users who want HTTPS can attach a ManagedSslCertificate + target
        // HTTPS proxy once they have a real domain pointing at the LB IP.
        const backendBucket = new gcp.compute.BackendBucket(`${name}-site-backend`, {
            project: args.project,
            bucketName: siteBucket.name,
            enableCdn: true,
        }, parent);

        const apiNeg = new gcp.compute.RegionNetworkEndpointGroup(`${name}-api-neg`, {
            project: args.project,
            region: args.region,
            networkEndpointType: "SERVERLESS",
            cloudRun: {
                service: cloudRunService,
            },
        }, parent);

        const apiBackend = new gcp.compute.BackendService(`${name}-api-backend`, {
            project: args.project,
            protocol: "HTTPS",
            loadBalancingScheme: "EXTERNAL_MANAGED",
            enableCdn: false,
            backends: [{
                group: apiNeg.id,
            }],
        }, parent);

        const urlMap = new gcp.compute.URLMap(`${name}-urlmap`, {
            project: args.project,
            defaultService: backendBucket.id,
            hostRules: [{
                hosts: ["*"],
                pathMatcher: "main",
            }],
            pathMatchers: [{
                name: "main",
                defaultService: backendBucket.id,
                pathRules: [{
                    paths: ["/api", "/api/*"],
                    service: apiBackend.id,
                }],
            }],
        }, parent);

        const httpProxy = new gcp.compute.TargetHttpProxy(`${name}-http-proxy`, {
            project: args.project,
            urlMap: urlMap.id,
        }, parent);

        const lbAddress = new gcp.compute.GlobalAddress(`${name}-lb-ip`, {
            project: args.project,
            addressType: "EXTERNAL",
        }, parent);

        new gcp.compute.GlobalForwardingRule(`${name}-fwd`, {
            project: args.project,
            target: httpProxy.id,
            portRange: "80",
            ipAddress: lbAddress.address,
            loadBalancingScheme: "EXTERNAL_MANAGED",
        }, parent);

        this.siteUrl = pulumi.interpolate`http://${lbAddress.address}/`;
        this.apiUrl = pulumi.interpolate`http://${lbAddress.address}/api`;
        this.functionName = fn.name;
        this.serviceAccountEmail = functionSa.email;
        this.loadBalancerIp = lbAddress.address;

        // Silence unused-variable lint on the URI without changing resource graph.
        void functionUri;

        this.registerOutputs({
            siteUrl: this.siteUrl,
            apiUrl: this.apiUrl,
            functionName: this.functionName,
            serviceAccountEmail: this.serviceAccountEmail,
            loadBalancerIp: this.loadBalancerIp,
        });
    }
}

components/database.py

Provisions the Cloud SQL instance on the landing-zone private network, generates a strong database password, and stores it in Google Secret Manager for the function to read.

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

import pulumi
import pulumi_gcp as gcp
import pulumi_random as random


@dataclass
class DatabaseArgs:
    project: pulumi.Input[str]
    region: pulumi.Input[str]
    network_name: pulumi.Input[str]
    db_version: pulumi.Input[str]
    name_prefix: pulumi.Input[str]
    labels: 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:gcp:Database", name, None, opts)
        parent = pulumi.ResourceOptions(parent=self)
        labels = dict(args.labels)

        database_name = "appdb"
        db_user = "appuser"

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

        # The landing zone exposes only a VPC name, so this blueprint allocates its
        # own VPC peering range and creates the service-networking connection within
        # the stack. If your landing zone already peers the VPC with
        # servicenetworking.googleapis.com, you can delete these two resources and
        # point Cloud SQL's private_network at the shared network directly.
        network_self_link = pulumi.Output.all(args.project, args.network_name).apply(
            lambda vs: f"projects/{vs[0]}/global/networks/{vs[1]}"
        )

        peering_range = gcp.compute.GlobalAddress(
            f"{name}-peer-range",
            project=args.project,
            name=pulumi.Output.concat(args.name_prefix, "-sql-peer"),
            purpose="VPC_PEERING",
            address_type="INTERNAL",
            prefix_length=16,
            network=network_self_link,
            labels=labels,
            opts=parent,
        )

        networking_connection = gcp.servicenetworking.Connection(
            f"{name}-sn-conn",
            network=network_self_link,
            service="servicenetworking.googleapis.com",
            reserved_peering_ranges=[peering_range.name],
            opts=parent,
        )

        instance = gcp.sql.DatabaseInstance(
            f"{name}-pg",
            project=args.project,
            region=args.region,
            database_version=args.db_version,
            deletion_protection=False,
            settings=gcp.sql.DatabaseInstanceSettingsArgs(
                tier="db-f1-micro",
                ip_configuration=gcp.sql.DatabaseInstanceSettingsIpConfigurationArgs(
                    ipv4_enabled=False,
                    private_network=network_self_link,
                ),
                backup_configuration=gcp.sql.DatabaseInstanceSettingsBackupConfigurationArgs(
                    enabled=True,
                ),
                user_labels=labels,
            ),
            opts=pulumi.ResourceOptions(parent=self, depends_on=[networking_connection]),
        )

        gcp.sql.Database(
            f"{name}-db",
            project=args.project,
            name=database_name,
            instance=instance.name,
            opts=parent,
        )

        user = gcp.sql.User(
            f"{name}-user",
            project=args.project,
            name=db_user,
            instance=instance.name,
            password=password.result,
            opts=parent,
        )

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

        connection_url = pulumi.Output.all(password.result, instance.private_ip_address).apply(_build_url)

        secret = gcp.secretmanager.Secret(
            f"{name}-secret",
            project=args.project,
            secret_id=pulumi.Output.concat(args.name_prefix, "-database-url"),
            replication=gcp.secretmanager.SecretReplicationArgs(
                auto=gcp.secretmanager.SecretReplicationAutoArgs(),
            ),
            labels=labels,
            opts=parent,
        )

        secret_version = gcp.secretmanager.SecretVersion(
            f"{name}-secret-version",
            secret=secret.id,
            secret_data=connection_url,
            opts=pulumi.ResourceOptions(parent=self, depends_on=[user]),
        )

        self.instance_name = instance.name
        self.connection_name = instance.connection_name
        self.secret_id = secret.id
        self.secret_name = secret.secret_id
        self.secret_version_id = secret_version.id
        self.database_name = pulumi.Output.from_input(database_name)
        self.user = pulumi.Output.from_input(db_user)

        self.register_outputs({
            "instance_name": self.instance_name,
            "connection_name": self.connection_name,
            "secret_id": self.secret_id,
            "secret_name": self.secret_name,
            "secret_version_id": self.secret_version_id,
            "database_name": self.database_name,
            "user": self.user,
        })

components/edge.py

Provisions the Google Cloud Run functions (2nd gen) function that runs the API, uploads the SPA to object storage, and wires Google Cloud CDN in front of a global external HTTPS load balancer so /* serves the SPA and /api/* reaches the function.

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

import pulumi
import pulumi_gcp as gcp


@dataclass
class EdgeArgs:
    project: pulumi.Input[str]
    region: pulumi.Input[str]
    network_name: pulumi.Input[str]
    private_subnet_name: pulumi.Input[str]
    database_secret_name: pulumi.Input[str]
    database_secret_version_id: pulumi.Input[str]
    website_dist_path: str
    api_handler_path: str
    function_memory: pulumi.Input[str]
    name_prefix: pulumi.Input[str]
    labels: Mapping[str, str] = field(default_factory=dict)


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",
}


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


def _walk(root: str):
    for dirpath, _, filenames in os.walk(root):
        for fn in filenames:
            yield os.path.join(dirpath, fn)


def _sanitize_id(value: str, max_len: int) -> str:
    cleaned = re.sub(r"[^a-z0-9-]", "-", value.lower())
    return cleaned[:max_len]


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

        # Serverless VPC Access connector so the Cloud Run function can reach
        # Cloud SQL on the landing-zone VPC's private IP.
        connector = gcp.vpcaccess.Connector(
            f"{name}-vpc-conn",
            project=args.project,
            region=args.region,
            network=args.network_name,
            ip_cidr_range="10.124.0.0/28",
            min_instances=2,
            max_instances=3,
            machine_type="e2-micro",
            opts=parent,
        )

        # Dedicated service account for the function: secret access + Cloud SQL client.
        function_sa = gcp.serviceaccount.Account(
            f"{name}-fn-sa",
            project=args.project,
            account_id=pulumi.Output.concat(args.name_prefix, "-fn").apply(
                lambda s: _sanitize_id(s, 30)
            ),
            display_name="serverless-react-postgres function",
            opts=parent,
        )

        gcp.secretmanager.SecretIamMember(
            f"{name}-fn-secret-access",
            project=args.project,
            secret_id=args.database_secret_name,
            role="roles/secretmanager.secretAccessor",
            member=pulumi.Output.concat("serviceAccount:", function_sa.email),
            opts=parent,
        )

        gcp.projects.IAMMember(
            f"{name}-fn-sql-client",
            project=args.project,
            role="roles/cloudsql.client",
            member=pulumi.Output.concat("serviceAccount:", function_sa.email),
            opts=parent,
        )

        # Bucket that holds the zipped function source.
        function_source_bucket = gcp.storage.Bucket(
            f"{name}-fn-src",
            project=args.project,
            location=args.region,
            uniform_bucket_level_access=True,
            force_destroy=True,
            labels=labels,
            opts=parent,
        )

        api_archive = pulumi.FileArchive(args.api_handler_path)
        function_source_object = gcp.storage.BucketObject(
            f"{name}-fn-src-zip",
            bucket=function_source_bucket.name,
            name="api.zip",
            source=api_archive,
            opts=parent,
        )

        fn_name = pulumi.Output.concat(args.name_prefix, "-api").apply(
            lambda s: _sanitize_id(s, 62)
        )

        fn = gcp.cloudfunctionsv2.Function(
            f"{name}-fn",
            project=args.project,
            location=args.region,
            name=fn_name,
            build_config=gcp.cloudfunctionsv2.FunctionBuildConfigArgs(
                runtime="nodejs20",
                entry_point="api",
                source=gcp.cloudfunctionsv2.FunctionBuildConfigSourceArgs(
                    storage_source=gcp.cloudfunctionsv2.FunctionBuildConfigSourceStorageSourceArgs(
                        bucket=function_source_bucket.name,
                        object=function_source_object.name,
                    ),
                ),
            ),
            service_config=gcp.cloudfunctionsv2.FunctionServiceConfigArgs(
                available_memory=args.function_memory,
                timeout_seconds=30,
                service_account_email=function_sa.email,
                vpc_connector=connector.id,
                vpc_connector_egress_settings="PRIVATE_RANGES_ONLY",
                ingress_settings="ALLOW_ALL",
                secret_volumes=[
                    gcp.cloudfunctionsv2.FunctionServiceConfigSecretVolumeArgs(
                        mount_path="/secrets",
                        project_id=args.project,
                        secret=args.database_secret_name,
                        versions=[
                            gcp.cloudfunctionsv2.FunctionServiceConfigSecretVolumeVersionArgs(
                                version="latest",
                                path="database-url",
                            ),
                        ],
                    ),
                ],
            ),
            labels=labels,
            opts=parent,
        )

        cloud_run_service = fn.service_config.apply(lambda sc: sc.service if sc else "")

        # Make the function publicly invokable via the Cloud Run v2 IAM binding.
        gcp.cloudrunv2.ServiceIamMember(
            f"{name}-fn-invoker",
            project=args.project,
            location=args.region,
            name=cloud_run_service,
            role="roles/run.invoker",
            member="allUsers",
            opts=parent,
        )

        # Static site bucket - uniform access + allUsers object viewer so the
        # BackendBucket origin can fetch anonymously.
        site_bucket = gcp.storage.Bucket(
            f"{name}-site",
            project=args.project,
            location=args.region,
            uniform_bucket_level_access=True,
            force_destroy=True,
            website=gcp.storage.BucketWebsiteArgs(
                main_page_suffix="index.html",
                not_found_page="index.html",
            ),
            labels=labels,
            opts=parent,
        )

        gcp.storage.BucketIAMMember(
            f"{name}-site-public",
            bucket=site_bucket.name,
            role="roles/storage.objectViewer",
            member="allUsers",
            opts=parent,
        )

        for file_path in _walk(args.website_dist_path):
            key = os.path.relpath(file_path, args.website_dist_path).replace(os.sep, "/")
            url_safe_key = re.sub(r"[^A-Za-z0-9._-]", "_", key)
            gcp.storage.BucketObject(
                f"{name}-site-{url_safe_key}",
                bucket=site_bucket.name,
                name=key,
                source=pulumi.FileAsset(file_path),
                content_type=_content_type(file_path),
                opts=parent,
            )

        # Global external HTTP load balancer. Demo blueprint: HTTP-only, no custom
        # domain. Users who want HTTPS can attach a ManagedSslCertificate + target
        # HTTPS proxy once they have a real domain pointing at the LB IP.
        backend_bucket = gcp.compute.BackendBucket(
            f"{name}-site-backend",
            project=args.project,
            bucket_name=site_bucket.name,
            enable_cdn=True,
            opts=parent,
        )

        api_neg = gcp.compute.RegionNetworkEndpointGroup(
            f"{name}-api-neg",
            project=args.project,
            region=args.region,
            network_endpoint_type="SERVERLESS",
            cloud_run=gcp.compute.RegionNetworkEndpointGroupCloudRunArgs(
                service=cloud_run_service,
            ),
            opts=parent,
        )

        api_backend = gcp.compute.BackendService(
            f"{name}-api-backend",
            project=args.project,
            protocol="HTTPS",
            load_balancing_scheme="EXTERNAL_MANAGED",
            enable_cdn=False,
            backends=[
                gcp.compute.BackendServiceBackendArgs(group=api_neg.id),
            ],
            opts=parent,
        )

        url_map = gcp.compute.URLMap(
            f"{name}-urlmap",
            project=args.project,
            default_service=backend_bucket.id,
            host_rules=[
                gcp.compute.URLMapHostRuleArgs(hosts=["*"], path_matcher="main"),
            ],
            path_matchers=[
                gcp.compute.URLMapPathMatcherArgs(
                    name="main",
                    default_service=backend_bucket.id,
                    path_rules=[
                        gcp.compute.URLMapPathMatcherPathRuleArgs(
                            paths=["/api", "/api/*"],
                            service=api_backend.id,
                        ),
                    ],
                ),
            ],
            opts=parent,
        )

        http_proxy = gcp.compute.TargetHttpProxy(
            f"{name}-http-proxy",
            project=args.project,
            url_map=url_map.id,
            opts=parent,
        )

        lb_address = gcp.compute.GlobalAddress(
            f"{name}-lb-ip",
            project=args.project,
            address_type="EXTERNAL",
            opts=parent,
        )

        gcp.compute.GlobalForwardingRule(
            f"{name}-fwd",
            project=args.project,
            target=http_proxy.id,
            port_range="80",
            ip_address=lb_address.address,
            load_balancing_scheme="EXTERNAL_MANAGED",
            opts=parent,
        )

        self.site_url = lb_address.address.apply(lambda a: f"http://{a}/")
        self.api_url = lb_address.address.apply(lambda a: f"http://{a}/api")
        self.function_name = fn.name
        self.service_account_email = function_sa.email
        self.load_balancer_ip = lb_address.address

        self.register_outputs({
            "site_url": self.site_url,
            "api_url": self.api_url,
            "function_name": self.function_name,
            "service_account_email": self.service_account_email,
            "load_balancer_ip": self.load_balancer_ip,
        })

database/database.go

Provisions the Cloud SQL instance on the landing-zone private network, generates a strong database password, and stores it in Google Secret Manager for the function to read.

package database

import (
	"fmt"
	"net/url"

	"github.com/pulumi/pulumi-gcp/sdk/v8/go/gcp/compute"
	"github.com/pulumi/pulumi-gcp/sdk/v8/go/gcp/secretmanager"
	"github.com/pulumi/pulumi-gcp/sdk/v8/go/gcp/servicenetworking"
	"github.com/pulumi/pulumi-gcp/sdk/v8/go/gcp/sql"
	"github.com/pulumi/pulumi-random/sdk/v4/go/random"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

type Args struct {
	Project     pulumi.StringInput
	Region      pulumi.StringInput
	NetworkName pulumi.StringInput
	DbVersion   pulumi.StringInput
	NamePrefix  pulumi.StringInput
	Labels      pulumi.StringMapInput
}

type Database struct {
	pulumi.ResourceState

	InstanceName    pulumi.StringOutput
	ConnectionName  pulumi.StringOutput
	SecretId        pulumi.StringOutput
	SecretName      pulumi.StringOutput
	SecretVersionId pulumi.StringOutput
	DatabaseName    pulumi.StringOutput
	User            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:gcp:Database", name, component, opts...); err != nil {
		return nil, err
	}
	parent := pulumi.Parent(component)

	const databaseName = "appdb"
	const dbUser = "appuser"

	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
	}

	// The landing zone exposes only a VPC name, so this blueprint allocates its
	// own VPC peering range and creates the service-networking connection within
	// the stack. If your landing zone already peers the VPC with
	// servicenetworking.googleapis.com, you can delete these two resources and
	// point Cloud SQL's PrivateNetwork at the shared network directly.
	networkSelfLink := pulumi.All(args.Project, args.NetworkName).ApplyT(func(vs []interface{}) string {
		return fmt.Sprintf("projects/%s/global/networks/%s", vs[0].(string), vs[1].(string))
	}).(pulumi.StringOutput)

	peeringRangeName := args.NamePrefix.ToStringOutput().ApplyT(func(p string) string {
		return p + "-sql-peer"
	}).(pulumi.StringOutput)

	peeringRange, err := compute.NewGlobalAddress(ctx, fmt.Sprintf("%s-peer-range", name), &compute.GlobalAddressArgs{
		Project:      args.Project,
		Name:         peeringRangeName,
		Purpose:      pulumi.String("VPC_PEERING"),
		AddressType:  pulumi.String("INTERNAL"),
		PrefixLength: pulumi.Int(16),
		Network:      networkSelfLink,
		Labels:       args.Labels,
	}, parent)
	if err != nil {
		return nil, err
	}

	networkingConnection, err := servicenetworking.NewConnection(ctx, fmt.Sprintf("%s-sn-conn", name), &servicenetworking.ConnectionArgs{
		Network: networkSelfLink,
		Service: pulumi.String("servicenetworking.googleapis.com"),
		ReservedPeeringRanges: pulumi.StringArray{
			peeringRange.Name,
		},
	}, parent)
	if err != nil {
		return nil, err
	}

	instance, err := sql.NewDatabaseInstance(ctx, fmt.Sprintf("%s-pg", name), &sql.DatabaseInstanceArgs{
		Project:            args.Project,
		Region:             args.Region,
		DatabaseVersion:    args.DbVersion,
		DeletionProtection: pulumi.Bool(false),
		Settings: &sql.DatabaseInstanceSettingsArgs{
			Tier: pulumi.String("db-f1-micro"),
			IpConfiguration: &sql.DatabaseInstanceSettingsIpConfigurationArgs{
				Ipv4Enabled:    pulumi.Bool(false),
				PrivateNetwork: networkSelfLink,
			},
			BackupConfiguration: &sql.DatabaseInstanceSettingsBackupConfigurationArgs{
				Enabled: pulumi.Bool(true),
			},
			UserLabels: args.Labels,
		},
	}, pulumi.Parent(component), pulumi.DependsOn([]pulumi.Resource{networkingConnection}))
	if err != nil {
		return nil, err
	}

	if _, err := sql.NewDatabase(ctx, fmt.Sprintf("%s-db", name), &sql.DatabaseArgs{
		Project:  args.Project,
		Name:     pulumi.String(databaseName),
		Instance: instance.Name,
	}, parent); err != nil {
		return nil, err
	}

	user, err := sql.NewUser(ctx, fmt.Sprintf("%s-user", name), &sql.UserArgs{
		Project:  args.Project,
		Name:     pulumi.String(dbUser),
		Instance: instance.Name,
		Password: password.Result,
	}, parent)
	if err != nil {
		return nil, err
	}

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

	secretId := args.NamePrefix.ToStringOutput().ApplyT(func(p string) string {
		return p + "-database-url"
	}).(pulumi.StringOutput)

	secret, err := secretmanager.NewSecret(ctx, fmt.Sprintf("%s-secret", name), &secretmanager.SecretArgs{
		Project:  args.Project,
		SecretId: secretId,
		Replication: &secretmanager.SecretReplicationArgs{
			Auto: &secretmanager.SecretReplicationAutoArgs{},
		},
		Labels: args.Labels,
	}, parent)
	if err != nil {
		return nil, err
	}

	secretVersion, err := secretmanager.NewSecretVersion(ctx, fmt.Sprintf("%s-secret-version", name), &secretmanager.SecretVersionArgs{
		Secret:     secret.ID(),
		SecretData: connectionUrl,
	}, pulumi.Parent(component), pulumi.DependsOn([]pulumi.Resource{user}))
	if err != nil {
		return nil, err
	}

	component.InstanceName = instance.Name
	component.ConnectionName = instance.ConnectionName
	component.SecretId = secret.ID().ToStringOutput()
	component.SecretName = secret.SecretId
	component.SecretVersionId = secretVersion.ID().ToStringOutput()
	component.DatabaseName = pulumi.String(databaseName).ToStringOutput()
	component.User = pulumi.String(dbUser).ToStringOutput()

	if err := ctx.RegisterResourceOutputs(component, pulumi.Map{
		"instanceName":    component.InstanceName,
		"connectionName":  component.ConnectionName,
		"secretId":        component.SecretId,
		"secretName":      component.SecretName,
		"secretVersionId": component.SecretVersionId,
		"databaseName":    component.DatabaseName,
		"user":            component.User,
	}); err != nil {
		return nil, err
	}

	return component, nil
}

edge/edge.go

Provisions the Google Cloud Run functions (2nd gen) function that runs the API, uploads the SPA to object storage, and wires Google Cloud CDN in front of a global external HTTPS load balancer so /* serves the SPA and /api/* reaches the function.

package edge

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

	"github.com/pulumi/pulumi-gcp/sdk/v8/go/gcp/cloudfunctionsv2"
	"github.com/pulumi/pulumi-gcp/sdk/v8/go/gcp/cloudrunv2"
	"github.com/pulumi/pulumi-gcp/sdk/v8/go/gcp/compute"
	"github.com/pulumi/pulumi-gcp/sdk/v8/go/gcp/projects"
	"github.com/pulumi/pulumi-gcp/sdk/v8/go/gcp/secretmanager"
	"github.com/pulumi/pulumi-gcp/sdk/v8/go/gcp/serviceaccount"
	"github.com/pulumi/pulumi-gcp/sdk/v8/go/gcp/storage"
	"github.com/pulumi/pulumi-gcp/sdk/v8/go/gcp/vpcaccess"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

type Args struct {
	Project                 pulumi.StringInput
	Region                  pulumi.StringInput
	NetworkName             pulumi.StringInput
	PrivateSubnetName       pulumi.StringInput
	DatabaseSecretName      pulumi.StringInput
	DatabaseSecretVersionId pulumi.StringInput
	WebsiteDistPath         string
	ApiHandlerPath          string
	FunctionMemory          pulumi.StringInput
	NamePrefix              pulumi.StringInput
	Labels                  pulumi.StringMapInput
}

type Edge struct {
	pulumi.ResourceState

	SiteUrl             pulumi.StringOutput
	ApiUrl              pulumi.StringOutput
	FunctionName        pulumi.StringOutput
	ServiceAccountEmail pulumi.StringOutput
	LoadBalancerIp      pulumi.StringOutput
}

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._-]`)
var idSanitizeRE = regexp.MustCompile(`[^a-z0-9-]`)

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

func sanitizeID(value string, maxLen int) string {
	cleaned := idSanitizeRE.ReplaceAllString(strings.ToLower(value), "-")
	if len(cleaned) > maxLen {
		cleaned = cleaned[:maxLen]
	}
	return cleaned
}

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

	// Serverless VPC Access connector so the Cloud Run function can reach
	// Cloud SQL on the landing-zone VPC's private IP.
	connector, err := vpcaccess.NewConnector(ctx, fmt.Sprintf("%s-vpc-conn", name), &vpcaccess.ConnectorArgs{
		Project:      args.Project,
		Region:       args.Region,
		Network:      args.NetworkName,
		IpCidrRange:  pulumi.String("10.124.0.0/28"),
		MinInstances: pulumi.Int(2),
		MaxInstances: pulumi.Int(3),
		MachineType:  pulumi.String("e2-micro"),
	}, parent)
	if err != nil {
		return nil, err
	}

	// Dedicated service account for the function: secret access + Cloud SQL client.
	saAccountId := args.NamePrefix.ToStringOutput().ApplyT(func(p string) string {
		return sanitizeID(p+"-fn", 30)
	}).(pulumi.StringOutput)

	functionSa, err := serviceaccount.NewAccount(ctx, fmt.Sprintf("%s-fn-sa", name), &serviceaccount.AccountArgs{
		Project:     args.Project,
		AccountId:   saAccountId,
		DisplayName: pulumi.String("serverless-react-postgres function"),
	}, parent)
	if err != nil {
		return nil, err
	}

	saMember := functionSa.Email.ApplyT(func(e string) string { return "serviceAccount:" + e }).(pulumi.StringOutput)

	if _, err := secretmanager.NewSecretIamMember(ctx, fmt.Sprintf("%s-fn-secret-access", name), &secretmanager.SecretIamMemberArgs{
		Project:  args.Project,
		SecretId: args.DatabaseSecretName,
		Role:     pulumi.String("roles/secretmanager.secretAccessor"),
		Member:   saMember,
	}, parent); err != nil {
		return nil, err
	}

	if _, err := projects.NewIAMMember(ctx, fmt.Sprintf("%s-fn-sql-client", name), &projects.IAMMemberArgs{
		Project: args.Project,
		Role:    pulumi.String("roles/cloudsql.client"),
		Member:  saMember,
	}, parent); err != nil {
		return nil, err
	}

	// Bucket that holds the zipped function source.
	functionSourceBucket, err := storage.NewBucket(ctx, fmt.Sprintf("%s-fn-src", name), &storage.BucketArgs{
		Project:                  args.Project,
		Location:                 args.Region,
		UniformBucketLevelAccess: pulumi.Bool(true),
		ForceDestroy:             pulumi.Bool(true),
		Labels:                   args.Labels,
	}, parent)
	if err != nil {
		return nil, err
	}

	functionSourceObject, err := storage.NewBucketObject(ctx, fmt.Sprintf("%s-fn-src-zip", name), &storage.BucketObjectArgs{
		Bucket: functionSourceBucket.Name,
		Name:   pulumi.String("api.zip"),
		Source: pulumi.NewFileArchive(args.ApiHandlerPath),
	}, parent)
	if err != nil {
		return nil, err
	}

	fnName := args.NamePrefix.ToStringOutput().ApplyT(func(p string) string {
		return sanitizeID(p+"-api", 62)
	}).(pulumi.StringOutput)

	fn, err := cloudfunctionsv2.NewFunction(ctx, fmt.Sprintf("%s-fn", name), &cloudfunctionsv2.FunctionArgs{
		Project:  args.Project,
		Location: args.Region,
		Name:     fnName,
		BuildConfig: &cloudfunctionsv2.FunctionBuildConfigArgs{
			Runtime:    pulumi.String("nodejs20"),
			EntryPoint: pulumi.String("api"),
			Source: &cloudfunctionsv2.FunctionBuildConfigSourceArgs{
				StorageSource: &cloudfunctionsv2.FunctionBuildConfigSourceStorageSourceArgs{
					Bucket: functionSourceBucket.Name,
					Object: functionSourceObject.Name,
				},
			},
		},
		ServiceConfig: &cloudfunctionsv2.FunctionServiceConfigArgs{
			AvailableMemory:            args.FunctionMemory,
			TimeoutSeconds:             pulumi.Int(30),
			ServiceAccountEmail:        functionSa.Email,
			VpcConnector:               connector.ID(),
			VpcConnectorEgressSettings: pulumi.String("PRIVATE_RANGES_ONLY"),
			IngressSettings:            pulumi.String("ALLOW_ALL"),
			SecretVolumes: cloudfunctionsv2.FunctionServiceConfigSecretVolumeArray{
				&cloudfunctionsv2.FunctionServiceConfigSecretVolumeArgs{
					MountPath: pulumi.String("/secrets"),
					ProjectId: args.Project,
					Secret:    args.DatabaseSecretName,
					Versions: cloudfunctionsv2.FunctionServiceConfigSecretVolumeVersionArray{
						&cloudfunctionsv2.FunctionServiceConfigSecretVolumeVersionArgs{
							Version: pulumi.String("latest"),
							Path:    pulumi.String("database-url"),
						},
					},
				},
			},
		},
		Labels: args.Labels,
	}, parent)
	if err != nil {
		return nil, err
	}

	cloudRunService := fn.ServiceConfig.ApplyT(func(sc *cloudfunctionsv2.FunctionServiceConfig) string {
		if sc == nil || sc.Service == nil {
			return ""
		}
		return *sc.Service
	}).(pulumi.StringOutput)

	// Make the function publicly invokable via the Cloud Run v2 IAM binding.
	if _, err := cloudrunv2.NewServiceIamMember(ctx, fmt.Sprintf("%s-fn-invoker", name), &cloudrunv2.ServiceIamMemberArgs{
		Project:  args.Project,
		Location: args.Region,
		Name:     cloudRunService,
		Role:     pulumi.String("roles/run.invoker"),
		Member:   pulumi.String("allUsers"),
	}, parent); err != nil {
		return nil, err
	}

	// Static site bucket - uniform access + allUsers object viewer so the
	// BackendBucket origin can fetch anonymously.
	siteBucket, err := storage.NewBucket(ctx, fmt.Sprintf("%s-site", name), &storage.BucketArgs{
		Project:                  args.Project,
		Location:                 args.Region,
		UniformBucketLevelAccess: pulumi.Bool(true),
		ForceDestroy:             pulumi.Bool(true),
		Website: &storage.BucketWebsiteArgs{
			MainPageSuffix: pulumi.String("index.html"),
			NotFoundPage:   pulumi.String("index.html"),
		},
		Labels: args.Labels,
	}, parent)
	if err != nil {
		return nil, err
	}

	if _, err := storage.NewBucketIAMMember(ctx, fmt.Sprintf("%s-site-public", name), &storage.BucketIAMMemberArgs{
		Bucket: siteBucket.Name,
		Role:   pulumi.String("roles/storage.objectViewer"),
		Member: pulumi.String("allUsers"),
	}, 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 := storage.NewBucketObject(ctx, fmt.Sprintf("%s-site-%s", name, safe), &storage.BucketObjectArgs{
			Bucket:      siteBucket.Name,
			Name:        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
	}

	// Global external HTTP load balancer. Demo blueprint: HTTP-only, no custom
	// domain. Users who want HTTPS can attach a ManagedSslCertificate + target
	// HTTPS proxy once they have a real domain pointing at the LB IP.
	backendBucket, err := compute.NewBackendBucket(ctx, fmt.Sprintf("%s-site-backend", name), &compute.BackendBucketArgs{
		Project:    args.Project,
		BucketName: siteBucket.Name,
		EnableCdn:  pulumi.Bool(true),
	}, parent)
	if err != nil {
		return nil, err
	}

	apiNeg, err := compute.NewRegionNetworkEndpointGroup(ctx, fmt.Sprintf("%s-api-neg", name), &compute.RegionNetworkEndpointGroupArgs{
		Project:             args.Project,
		Region:              args.Region,
		NetworkEndpointType: pulumi.String("SERVERLESS"),
		CloudRun: &compute.RegionNetworkEndpointGroupCloudRunArgs{
			Service: cloudRunService,
		},
	}, parent)
	if err != nil {
		return nil, err
	}

	apiBackend, err := compute.NewBackendService(ctx, fmt.Sprintf("%s-api-backend", name), &compute.BackendServiceArgs{
		Project:             args.Project,
		Protocol:            pulumi.String("HTTPS"),
		LoadBalancingScheme: pulumi.String("EXTERNAL_MANAGED"),
		EnableCdn:           pulumi.Bool(false),
		Backends: compute.BackendServiceBackendArray{
			&compute.BackendServiceBackendArgs{
				Group: apiNeg.ID(),
			},
		},
	}, parent)
	if err != nil {
		return nil, err
	}

	urlMap, err := compute.NewURLMap(ctx, fmt.Sprintf("%s-urlmap", name), &compute.URLMapArgs{
		Project:        args.Project,
		DefaultService: backendBucket.ID(),
		HostRules: compute.URLMapHostRuleArray{
			&compute.URLMapHostRuleArgs{
				Hosts:       pulumi.StringArray{pulumi.String("*")},
				PathMatcher: pulumi.String("main"),
			},
		},
		PathMatchers: compute.URLMapPathMatcherArray{
			&compute.URLMapPathMatcherArgs{
				Name:           pulumi.String("main"),
				DefaultService: backendBucket.ID(),
				PathRules: compute.URLMapPathMatcherPathRuleArray{
					&compute.URLMapPathMatcherPathRuleArgs{
						Paths: pulumi.StringArray{
							pulumi.String("/api"),
							pulumi.String("/api/*"),
						},
						Service: apiBackend.ID(),
					},
				},
			},
		},
	}, parent)
	if err != nil {
		return nil, err
	}

	httpProxy, err := compute.NewTargetHttpProxy(ctx, fmt.Sprintf("%s-http-proxy", name), &compute.TargetHttpProxyArgs{
		Project: args.Project,
		UrlMap:  urlMap.ID(),
	}, parent)
	if err != nil {
		return nil, err
	}

	lbAddress, err := compute.NewGlobalAddress(ctx, fmt.Sprintf("%s-lb-ip", name), &compute.GlobalAddressArgs{
		Project:     args.Project,
		AddressType: pulumi.String("EXTERNAL"),
	}, parent)
	if err != nil {
		return nil, err
	}

	if _, err := compute.NewGlobalForwardingRule(ctx, fmt.Sprintf("%s-fwd", name), &compute.GlobalForwardingRuleArgs{
		Project:             args.Project,
		Target:              httpProxy.ID(),
		PortRange:           pulumi.String("80"),
		IpAddress:           lbAddress.Address,
		LoadBalancingScheme: pulumi.String("EXTERNAL_MANAGED"),
	}, parent); err != nil {
		return nil, err
	}

	component.SiteUrl = lbAddress.Address.ApplyT(func(a string) string { return "http://" + a + "/" }).(pulumi.StringOutput)
	component.ApiUrl = lbAddress.Address.ApplyT(func(a string) string { return "http://" + a + "/api" }).(pulumi.StringOutput)
	component.FunctionName = fn.Name
	component.ServiceAccountEmail = functionSa.Email
	component.LoadBalancerIp = lbAddress.Address

	if err := ctx.RegisterResourceOutputs(component, pulumi.Map{
		"siteUrl":             component.SiteUrl,
		"apiUrl":              component.ApiUrl,
		"functionName":        component.FunctionName,
		"serviceAccountEmail": component.ServiceAccountEmail,
		"loadBalancerIp":      component.LoadBalancerIp,
	}); 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.