feat(infra): call dev API directly, drop the CloudFront /api proxy

Adopt the direct cross-origin API approach (from feat/cd-cdk-fixes) now that the
dev backend allows CORS:

- VITE_API_URL=https://api.dev.seahaven.com/api (absolute, baked into the build)
- remove the CloudFront /api/* proxy behavior + HTTP origin; CloudFront serves
  static content only
- keep the custom domain (dev.seahaven.com), ACM cert, and Route 53 apex alias

VITE_API_URL is per-environment (baked at vite build), so staging/prod builds
must override it and the backend must allow their origins.
This commit is contained in:
Alexandre Brandizzi 2026-07-06 15:03:54 -03:00
parent d1cca884eb
commit 0b8d3fa290
6 changed files with 45 additions and 85 deletions

View file

@ -1,4 +1,4 @@
# Production API base (same-origin). CloudFront proxies /api/* to the backend,
# so the HTTPS SPA reaches the HTTP-only API without mixed-content blocking.
# The backend host is set on the CloudFront /api behavior in infra/cdk.
VITE_API_URL=/api
# Production API base — the SPA calls the backend directly over HTTPS.
# NOTE: baked into the build at `vite build`, so this is the DEV value. Staging
# and prod builds must override VITE_API_URL per environment (api.staging..., etc.).
VITE_API_URL=https://api.dev.seahaven.com/api

View file

@ -67,9 +67,10 @@ keys):
- Push to `dev` → `.github/workflows/deploy.yml` calls the org reusable
`cd-cdk.yaml`, which runs `cdk deploy` (infra) then `scripts/deploy-web.sh`
(builds the SPA, syncs `dist/` to S3, invalidates CloudFront).
- The SPA calls its API same-origin at `/api`; CloudFront proxies `/api/*` to
the backend, so the HTTPS app can use the HTTP-only API with no mixed-content
blocking.
- Served on the custom domain `dev.seahaven.com`; the SPA calls the backend
directly over HTTPS at `VITE_API_URL` (`https://api.dev.seahaven.com/api`,
cross-origin — the backend allows CORS). `VITE_API_URL` is baked into the
build, so it is per-environment.
- First-time provisioning (OIDC provider, CDK bootstrap, first local deploy, the
`AWS_DEPLOY_ROLE_ARN` secret) is a one-time admin task — see
[`infra/cdk/README.md`](infra/cdk/README.md).

View file

@ -5,10 +5,12 @@ deployed through the org's **reusable** GitHub Actions workflow.
- **Hosting:** private S3 bucket (origin) + CloudFront, served on the custom
domain **`dev.seahaven.com`** (ACM `*.seahaven.com`, Route 53 apex alias).
- **API:** CloudFront proxies `/api/*` over HTTPS to `api.dev.seahaven.com`, so
the SPA calls it same-origin (`VITE_API_URL=/api`) — no CORS, no mixed content.
- Env-specific values (domain, cert, zone, API host) live in `cdk.json` context
so the CI `cdk deploy` picks them up with no flags.
- **API:** the SPA calls the backend **directly** over HTTPS at
`https://api.dev.seahaven.com/api` (`VITE_API_URL`, cross-origin; the backend
allows CORS). CloudFront serves static content only — no `/api` proxy.
- Domain/cert/zone values live in `cdk.json` context so the CI `cdk deploy`
picks them up with no flags. `VITE_API_URL` is baked into the build, so it's
per-environment (see the note under "Adding staging / prod").
- **Auth:** GitHub Actions → AWS via **OIDC** (no long-lived keys)
- **CD workflow:** `.github/workflows/deploy.yml` is a thin caller of the org's
`Sea-Haven-Industries/.github` → `cd-cdk.yaml`. That workflow runs `cdk deploy`
@ -32,7 +34,7 @@ scripts/deploy-web.sh build SPA -> s3 sync -> CloudFront invalidation
| Resource | Purpose |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| S3 bucket `seahaven-shoc-frontend-dev` | private origin (BLOCK_ALL, SSE, OAC-only reads) |
| CloudFront distribution | HTTPS, gzip/br; default behavior → S3, `/api/*` → backend (HTTP origin) |
| CloudFront distribution | HTTPS, gzip/br; serves the static SPA from S3 (the app calls the API directly, cross-origin) |
| CloudFront Function (viewer request) | SPA routing: rewrites extensionless paths to `/index.html` (scoped to the S3 behavior, so it never touches `/api`) |
| IAM role `githubdeploy-shoc-frontend-new-dev` | assumed by GitHub Actions via OIDC, scoped to `repo:Sea-Haven-Industries/shoc-frontend-new:ref:refs/heads/dev` |
@ -73,21 +75,20 @@ npm ci
npx cdk bootstrap aws://<ACCOUNT_ID>/us-east-1
```
### 4. Domain, cert, and API origin (already wired for dev)
### 4. Domain, cert, and API URL (already wired for dev)
For dev these are set in `cdk.json` context (account `396287094661`):
Domain/cert/zone are set in `cdk.json` context (account `396287094661`):
| Context key | Value |
| --------------------------------------- | ------------------------------------------------------------ |
| `domainNames` | `dev.seahaven.com` |
| `certificateArn` | `…:certificate/2b78e74f-…` (ACM `*.seahaven.com`, us-east-1) |
| `hostedZoneId` / `hostedZoneName` | `Z07671212N75U4YLPWZR8` / `dev.seahaven.com` |
| `apiOriginDomain` / `apiOriginProtocol` | `api.dev.seahaven.com` / `https` |
| Context key | Value |
| --------------------------------- | ------------------------------------------------------------ |
| `domainNames` | `dev.seahaven.com` |
| `certificateArn` | `…:certificate/2b78e74f-…` (ACM `*.seahaven.com`, us-east-1) |
| `hostedZoneId` / `hostedZoneName` | `Z07671212N75U4YLPWZR8` / `dev.seahaven.com` |
The SPA calls `/api` same-origin; CloudFront proxies `/api/*` over **HTTPS** to
`api.dev.seahaven.com`. The stack creates the apex A/AAAA alias in the hosted
zone (which is in this account, delegated from the parent `seahaven.com` zone).
For staging/prod, override these context keys per environment.
The stack creates the apex A/AAAA alias in the hosted zone (in this account,
delegated from the parent `seahaven.com` zone). The **API URL is not infra** —
it's `VITE_API_URL` in `.env.production` (`https://api.dev.seahaven.com/api`),
baked into the build. Per-environment; override for staging/prod.
### 5. First deploy (locally, with admin creds)
@ -138,12 +139,16 @@ the `SiteUrl` output.
## Adding staging / prod later
Separate accounts: deploy this stack there with `-c envName=staging`,
`-c deployBranch=<branch>`, and `-c apiOriginDomain=<that-env's-api-host>`; set
that repo's `AWS_DEPLOY_ROLE_ARN` secret; and add a job to `deploy.yml`. Because
the SPA calls `/api` same-origin, the API host is per-environment CloudFront
config (the `apiOriginDomain` context) — the build output is identical across
environments, so nothing env-specific gets baked into `vite build`.
Separate accounts: deploy this stack there with per-env `domainNames`,
`certificateArn`, `hostedZoneId`/`hostedZoneName` context; set that repo's
`AWS_DEPLOY_ROLE_ARN` secret; and add a job to `deploy.yml`.
Because the SPA calls the API directly at an absolute URL, **`VITE_API_URL` is
baked into `vite build`** — so each environment needs its own build with its own
API host (e.g. `https://api.staging.seahaven.com/api`). Set it per environment
in the deploy job (e.g. export `VITE_API_URL` before the build step) rather than
relying on the committed `.env.production` (which carries the dev value). The
backend must also allow CORS from each frontend origin.
## Notes

View file

@ -9,14 +9,7 @@ const envName = app.node.tryGetContext("envName") ?? "dev";
const githubRepo = app.node.tryGetContext("githubRepo") ?? "Sea-Haven-Industries/shoc-frontend-new";
const deployBranch = app.node.tryGetContext("deployBranch") ?? "dev";
// API origin the CloudFront /api behavior proxies to.
// Option A (interim, default): the HTTP-only console.seahavenind.com backend.
// Option B: -c apiOriginDomain=<seahaven.com API host> -c apiOriginProtocol=https
const apiOriginDomain = app.node.tryGetContext("apiOriginDomain") ?? "console.seahavenind.com";
const apiOriginProtocol =
app.node.tryGetContext("apiOriginProtocol") === "https" ? "https" : "http";
// Custom domain (Option B). Comma-separated, e.g. -c domainNames=console.seahaven.com
// Custom domain. Comma-separated, e.g. -c domainNames=dev.seahaven.com
// The ACM cert MUST be in us-east-1 in the SAME account this stack deploys to.
const domainNames = (app.node.tryGetContext("domainNames") ?? "")
.split(",")
@ -32,8 +25,6 @@ const stack = new FrontendStack(app, `shoc-frontend-${envName}`, {
envName,
githubRepo,
deployBranch,
apiOriginDomain,
apiOriginProtocol,
domainNames,
certificateArn,
hostedZoneId,

View file

@ -13,8 +13,6 @@
"//": "dev environment (account 396287094661). CI runs `cdk deploy` with no -c flags, so these live here.",
"domainNames": "dev.seahaven.com",
"certificateArn": "arn:aws:acm:us-east-1:396287094661:certificate/2b78e74f-7b65-4b82-a413-7a498b102f00",
"apiOriginDomain": "api.dev.seahaven.com",
"apiOriginProtocol": "https",
"hostedZoneId": "Z07671212N75U4YLPWZR8",
"hostedZoneName": "dev.seahaven.com"
}

View file

@ -16,19 +16,8 @@ export interface FrontendStackProps extends StackProps {
/** Git branch whose pushes may deploy (OIDC sub is scoped to this ref). */
readonly deployBranch: string;
/**
* Hostname of the backend API. CloudFront proxies `/api/*` to it (see
* `apiOriginProtocol`) so the SPA can call it same-origin at `/api`.
*/
readonly apiOriginDomain: string;
/**
* Protocol CloudFront uses to reach the API origin.
* - "http" (Option A, interim): HTTP-only backend (console.seahavenind.com)
* - "https" (Option B): the seahaven.com HTTPS backend
*/
readonly apiOriginProtocol: "http" | "https";
/**
* Custom domain(s) for the distribution, e.g. ["console.seahaven.com"].
* Empty = serve on the default *.cloudfront.net domain (Option A).
* Custom domain(s) for the distribution, e.g. ["dev.seahaven.com"].
* Empty = serve on the default *.cloudfront.net domain.
*/
readonly domainNames: string[];
/**
@ -68,15 +57,12 @@ export class FrontendStack extends Stack {
envName,
githubRepo,
deployBranch,
apiOriginDomain,
apiOriginProtocol,
domainNames,
certificateArn,
hostedZoneId,
hostedZoneName,
} = props;
const useHttpsApiOrigin = apiOriginProtocol === "https";
const hasCustomDomain = domainNames.length > 0;
if (hasCustomDomain && !certificateArn) {
throw new Error(
@ -97,11 +83,9 @@ export class FrontendStack extends Stack {
autoDeleteObjects: true,
});
// SPA client-side routing, scoped to the S3 (default) behavior only:
// rewrite extensionless paths (e.g. /work-orders) to /index.html. This is
// done with a CloudFront Function rather than distribution-wide
// customErrorResponses, because those are global and would also turn
// legitimate /api 403/404 responses into the SPA shell.
// SPA client-side routing: rewrite extensionless paths (e.g. /work-orders)
// to /index.html so deep links resolve. Done with a CloudFront Function
// rather than customErrorResponses so real asset 404s stay 404s.
const spaRewrite = new cloudfront.Function(this, "SpaRewrite", {
comment: "SPA routing: rewrite extensionless paths to /index.html",
code: cloudfront.FunctionCode.fromInline(
@ -119,7 +103,9 @@ export class FrontendStack extends Stack {
),
});
// --- CloudFront: S3 (SPA) default behavior + /api proxy behavior -------
// --- CloudFront: serves the static SPA from S3 -------------------------
// The SPA calls the backend directly at its absolute HTTPS URL
// (VITE_API_URL, cross-origin), so CloudFront hosts only static content.
const distribution = new cloudfront.Distribution(this, "Distribution", {
comment: `SeaHaven SHOC frontend (${envName})`,
defaultRootObject: "index.html",
@ -148,27 +134,6 @@ export class FrontendStack extends Stack {
},
],
},
additionalBehaviors: {
// Proxy API calls to the backend. The SPA calls same-origin `/api/...`
// over HTTPS; CloudFront forwards to the origin over HTTP (Option A) or
// HTTPS (Option B). Either way there's no mixed content or CORS.
"/api/*": {
origin: new origins.HttpOrigin(apiOriginDomain, {
protocolPolicy: useHttpsApiOrigin
? cloudfront.OriginProtocolPolicy.HTTPS_ONLY
: cloudfront.OriginProtocolPolicy.HTTP_ONLY,
httpPort: 80,
httpsPort: 443,
}),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
// Forward everything the viewer sent except Host (CloudFront sets Host
// to the origin domain so the backend's routing isn't confused).
originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
compress: true,
},
},
});
// --- GitHub Actions OIDC deploy role -----------------------------------