Live App →

Deployment

Sentinel runs on AWS ECS Fargate with CloudFront CDN, Route 53 DNS, and an Application Load Balancer. The frontend is a Next.js standalone application packaged as a Docker container. Build, test, image push, ECS rollout, and CloudFront cache invalidation are all driven by the Jenkins pipeline defined in Jenkinsfile at the repository root.


Infrastructure Overview

graph TB
    DNS["Route 53<br/>sentinel.centricitywealth.tech"] --> CF["CloudFront CDN<br/>SSL + Cache + WAF"]
    CF --> ALB["ALB<br/>Health Checks"]
    ALB --> ECS1["ECS Task 1<br/>Next.js Standalone"]
    ALB --> ECS2["ECS Task N<br/>Next.js Standalone"]
    ECS1 --> BFF["Studio Backend (BFF)"]
    ECS2 --> BFF

    classDef primary fill:#dbeafe,stroke:#3b82f6,color:#1e293b
    classDef success fill:#d1fae5,stroke:#10b981,color:#1e293b
    classDef dark fill:#1e293b,stroke:#334155,color:#f8fafc

    class DNS,CF,ALB primary
    class ECS1,ECS2 success
    class BFF dark

Network Architecture

A detailed view of the VPC layout, showing how traffic flows from users through the CDN and load balancer into private ECS tasks:

graph TB
    subgraph Internet
        Users["Users / Browsers"]
        DNS["Route 53<br/>sentinel.centricitywealth.tech"]
    end

    subgraph AWS["AWS ap-south-1"]
        CF["CloudFront CDN<br/>SSL Termination + WAF"]
        subgraph VPC["VPC 10.0.0.0/16"]
            subgraph Public["Public Subnets"]
                ALB["Application<br/>Load Balancer"]
            end
            subgraph Private["Private Subnets"]
                ECS1["ECS Task 1<br/>Next.js :3000"]
                ECS2["ECS Task 2<br/>Next.js :3000"]
            end
        end
        ECR["ECR<br/>Container Registry"]
        CW["CloudWatch<br/>Logs + Metrics"]
    end

    subgraph Backend["Backend Services"]
        BFF["Studio BFF<br/>studio-backend-dev"]
    end

    Users --> DNS
    DNS --> CF
    CF --> ALB
    ALB --> ECS1
    ALB --> ECS2
    ECS1 --> BFF
    ECS2 --> BFF
    ECS1 -.-> CW
    ECS2 -.-> CW
    ECR -.->|Pull image| ECS1
    ECR -.->|Pull image| ECS2

    classDef primary fill:#dbeafe,stroke:#3b82f6,color:#1e293b
    classDef secondary fill:#e0e7ff,stroke:#6366f1,color:#1e293b
    classDef neutral fill:#f1f5f9,stroke:#64748b,color:#1e293b
    classDef dark fill:#1e293b,stroke:#334155,color:#f8fafc

    class Users,DNS,CF,ALB primary
    class ECS1,ECS2 secondary
    class ECR,CW neutral
    class BFF dark

Service Map

Component Resource Details
DNS Route 53 sentinel.centricitywealth.tech (prod), sentinel-dev.centricitywealth.tech (dev)
CDN CloudFront Distribution EQEH6ZAHOVUNL, SSL termination, /* cache invalidation
Load Balancer ALB Health check on port 3000, target group with ECS tasks
Compute ECS Fargate 256 CPU / 512 MB memory, auto-scaling up to 2 (dev) or 10 (prod)
Container Registry ECR <account-id>.dkr.ecr.ap-south-1.amazonaws.com/cwt-dev/sentinel
Backend Studio BFF studio-backend-dev.centricitywealth.tech – all API calls route here

Jenkins Pipeline

Jenkinsfile at the repo root is the source of truth for the release flow. The same image-build and deploy commands are also exposed locally as cwt-* Make targets so engineers can reproduce a stage on a workstation when debugging.

Branch Policy

Branch Target environment Promotion
dev cwt-dev Automatic on merge
main cwt-prod Manual approval gate

Pipeline Stages

graph LR
    Checkout["Checkout"] --> Install["npm ci"]
    Install --> Lint["Lint + Type Check"]
    Lint --> Test["Playwright"]
    Test --> Audit["npm audit<br/>+ outdated"]
    Audit --> Build["Docker Build<br/>Multi-stage"]
    Build --> Push["ECR Push"]
    Push --> Deploy["ECS Force Deploy<br/>Rolling update"]
    Deploy --> Invalidate["CloudFront<br/>Cache Invalidation"]
    Invalidate --> Verify["services-stable wait"]

    classDef primary fill:#dbeafe,stroke:#3b82f6,color:#1e293b
    classDef success fill:#d1fae5,stroke:#10b981,color:#1e293b
    classDef warning fill:#fef3c7,stroke:#f59e0b,color:#1e293b

    class Checkout,Install,Lint,Test,Audit primary
    class Build,Push,Deploy,Verify success
    class Invalidate warning

The pipeline performs each stage in order; any failure short-circuits the run. ECR/ECS/CloudFront credentials are referenced by Jenkins credential IDs configured on the controller — no secrets are committed to the repository.


Local Make Targets

The Makefile mirrors the Jenkins stages so developers can run any step locally with the same command shape.

Target Purpose
dev Start the Next.js dev server
build Produce the Next.js standalone build
test Run the Playwright suite (Chromium project)
lint Run ESLint
type-check Run tsc --noEmit strict type check
audit Run dependency audits (npm audit, npm outdated)
cwt-ecr-login Authenticate Docker against the shared CWT ECR registry
cwt-docker-build Build the production image from the root Dockerfile
cwt-docker-push Push tagged + latest image to ECR
cwt-deploy Force a new ECS deployment and wait for services-stable

Docker Build

The Dockerfile sits at the repository root and uses a multi-stage build optimised for Next.js standalone output. The Jenkins pipeline runs the equivalent of:

docker build --platform linux/amd64 \
  --build-arg NEXT_PUBLIC_STUDIO_API_URL=https://studio-backend-dev.centricitywealth.tech \
  --build-arg NEXT_PUBLIC_ENABLE_MOCKS=false \
  -f Dockerfile -t cwt-dev/sentinel:latest .

Key details:

  • NEXT_PUBLIC_* variables are baked at build time into the Docker image (Next.js inlines them during compilation)
  • Server-only variables (STUDIO_API_URL, SESSION_SECRET) are provided at runtime via the ECS task definition
  • The final image uses Next.js standalone output for a minimal container size

Environment Variables

Environment Variables Flow

Build-time variables are baked into the JavaScript bundle and cannot change after deployment. Runtime variables are read by the Node.js server at startup.

graph TD
    subgraph BuildTime["Build Time (docker build)"]
        NB["next build"]
        NB --> BP1["NEXT_PUBLIC_STUDIO_API_URL<br/>Inlined into JS bundle"]
        NB --> BP2["NEXT_PUBLIC_ENABLE_MOCKS<br/>Inlined into JS bundle"]
    end

    subgraph RunTime["Runtime (ECS Task Start)"]
        TS["Task Definition<br/>Environment"]
        TS --> RP1["STUDIO_API_URL<br/>Server-side only"]
        TS --> RP2["SESSION_SECRET<br/>From Secrets Manager"]
        TS --> RP3["NODE_ENV / PORT<br/>Process config"]
    end

    BP1 -->|"Shipped to browser"| Browser["Client Browser"]
    RP1 -->|"Never exposed"| Server["Node.js Server"]
    RP2 -->|"Never exposed"| Server

    classDef warning fill:#fef3c7,stroke:#f59e0b,color:#1e293b
    classDef success fill:#d1fae5,stroke:#10b981,color:#1e293b
    classDef neutral fill:#f1f5f9,stroke:#64748b,color:#1e293b
    classDef danger fill:#fee2e2,stroke:#ef4444,color:#1e293b
    classDef dark fill:#1e293b,stroke:#334155,color:#f8fafc

    class NB,BP1,BP2 warning
    class TS,RP1,RP2,RP3 success
    class Browser danger
    class Server dark

Build-Time Variables (NEXT_PUBLIC_*)

These are embedded into the JavaScript bundle during next build and cannot be changed at runtime.

Variable Description Dev Value
NEXT_PUBLIC_STUDIO_API_URL Studio Backend URL for all client-side API calls https://studio-backend-dev.centricitywealth.tech
NEXT_PUBLIC_ENABLE_MOCKS Enable mock data mode false

Runtime Variables (ECS Task Definition)

These are available to the Node.js server at runtime and are never exposed to the browser.

Variable Description Dev Value
STUDIO_API_URL Studio Backend URL for server-side auth https://studio-backend-dev.centricitywealth.tech
SESSION_SECRET Cookie encryption key (32+ characters) Stored in AWS Secrets Manager
NODE_ENV Node environment production
PORT Container port 3000

Production Gating

app/src/instrumentation.ts runs once on Next.js boot. When NODE_ENV=production or APP_ENV=prod, it fails fast if any of STUDIO_API_URL, NEXT_PUBLIC_STUDIO_API_URL, or SESSION_SECRET are missing or pointing at localhost. The container exits before serving traffic if the configuration is unsafe.


Health Checks

Container Health

The ALB performs HTTP GET requests to port 3000 on each ECS task. A healthy response (HTTP 200) keeps the task in the target group.

# Check target health
aws elbv2 describe-target-health \
  --target-group-arn <TARGET_GROUP_ARN>

Service Health

# Check ECS service status
aws ecs describe-services \
  --cluster cwt-dev-cluster \
  --services sentinel

# List running tasks
aws ecs list-tasks \
  --cluster cwt-dev-cluster \
  --service-name sentinel

Application Health Endpoint

# Studio Backend health
curl https://studio-backend-dev.centricitywealth.tech/api/v1/health/

Local Reproduction Steps

To reproduce the pipeline against cwt-dev from a workstation (debugging only — production deploys are owned by Jenkins):

# 1. Install + checks
cd app && npm ci
npm run lint
npx tsc --noEmit
npx playwright test --project=chromium

# 2. Dependency audit
npm audit --audit-level=high
npm outdated

# 3. Build + push (uses Make targets)
cd ..
make cwt-ecr-login
make cwt-docker-build
make cwt-docker-push

# 4. Force ECS rollout
make cwt-deploy

Rollback

To roll back to a previous version, pin the ECS task definition to the previous image tag and re-run make cwt-deploy (or trigger the equivalent Jenkins job parameters):

# 1. List recent ECR images
aws ecr describe-images \
  --repository-name cwt-dev/sentinel \
  --query 'imageDetails | sort_by(@, &imagePushedAt) | [-5:]'

# 2. Update the ECS task definition to use a previous image tag,
#    then force a new deployment
aws ecs update-service \
  --cluster cwt-dev-cluster \
  --service sentinel \
  --force-new-deployment

Monitoring

CloudWatch Logs

# Stream logs in real time
aws logs tail /ecs/sentinel-frontend-dev --follow

# Search for errors
aws logs filter-log-events \
  --log-group-name /ecs/sentinel-frontend-dev \
  --filter-pattern "ERROR"

Security

Practice Implementation
Network isolation ECS tasks run in private subnets
HTTPS only CloudFront redirects HTTP to HTTPS
Server-only secrets STUDIO_API_URL and SESSION_SECRET are never exposed to the browser
Cookie-based auth Auth tokens in encrypted HttpOnly cookies, not localStorage
IAM least privilege Task roles have minimal required permissions
Secrets management SESSION_SECRET stored in AWS Secrets Manager; pulled by Jenkins via credential IDs
Security headers X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy
Production gating instrumentation.ts rejects missing or localhost values for STUDIO_API_URL, NEXT_PUBLIC_STUDIO_API_URL, and SESSION_SECRET

Domains

Stage Domain URL
Dev sentinel-dev.centricitywealth.tech https://sentinel-dev.centricitywealth.tech
Prod sentinel.centricitywealth.tech https://sentinel.centricitywealth.tech