Dashboard Deployment
Dev Environment on AWS
The Amply Dashboard is deployed as a static SPA to S3 + CloudFront, with auto-deploy from GitHub Actions using OIDC authentication (no long-lived credentials).
Architecture
┌──────────────────────────────────────────────────────────────┐
│ GitHub Actions │
│ push to 'main' → Lint → Typecheck → Build → Deploy to S3 │
│ (OIDC auth to AWS, no stored credentials) │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ AWS (eu-central-1) │
│ │
│ ┌──────────────┐ ┌──────────────────────────────────┐ │
│ │ S3 │ │ CloudFront │ │
│ │ amply-dev- │◄────│ dashboard.dev.amply-impact.org │ │
│ │ frontend │ OAC │ SPA routing (403/404 → index) │ │
│ └──────────────┘ └──────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Route 53 │ │
│ │ A record → CF │ │
│ └─────────────────────┘ │
│ │
│ API calls from browser: │
│ dashboard.dev.amply-impact.org → api.dev.amply-impact.org │
└──────────────────────────────────────────────────────────────┘
Dev Environment
| Aspect | Details |
|---|---|
| URL | https://dashboard.dev.amply-impact.org/dashboard/ |
| Backend API | https://api.dev.amply-impact.org/v1 |
| Deploy trigger | Push to main branch |
| Auth method | GitHub OIDC → AWS IAM role |
| GitHub repo | StayMeaty/amply-dashboard |
AWS Resources
S3
| Resource | Details |
|---|---|
| Bucket | amply-dev-frontend |
| Region | eu-central-1 |
| Public access | Blocked (CloudFront OAC only) |
| Content path | /dashboard/ |
| Versioning | Disabled |
The bucket is private. CloudFront accesses it via Origin Access Control (OAC), not a public bucket policy.
CloudFront
| Resource | Details |
|---|---|
| Distribution ID | E28IX5PKX6HT9N |
| Domain | dlglnald08jfq.cloudfront.net |
| Alias | dashboard.dev.amply-impact.org |
| OAC ID | E24GM2YWKHG9EA |
| Default root | dashboard/index.html |
| Cache policy | CachingOptimized (658327ea-f89d-4fab-a63d-7e88639e58f6) |
| Price class | PriceClass_100 (US, Canada, Europe) |
| HTTP version | HTTP/2 + HTTP/3 |
| TLS | TLSv1.2_2021 minimum |
SPA routing: Custom error responses rewrite 403 and 404 to /dashboard/index.html with HTTP 200, enabling client-side routing.
ACM Certificate
| Resource | Details |
|---|---|
| ARN | arn:aws:acm:us-east-1:640168439659:certificate/79b635dd-145d-4b1a-ada5-805c7fd6beab |
| Domain | dashboard.dev.amply-impact.org |
| Region | us-east-1 (CloudFront requirement) |
| Validation | DNS (Route 53) |
Route 53
| Record | Type | Target |
|---|---|---|
dashboard.dev.amply-impact.org | A (Alias) | CloudFront E28IX5PKX6HT9N |
IAM
The existing amply-dev-github-actions-role is shared between backend and dashboard:
Trust policy allows both repos:
repo:StayMeaty/amply-backend:*
repo:StayMeaty/amply-dashboard:*
Permissions (inline policy amply-dev-deploy-policy):
| Statement | Actions | Resource |
|---|---|---|
S3DeployFrontend | s3:PutObject, s3:DeleteObject, s3:ListBucket, s3:GetBucketLocation | amply-dev-frontend and amply-dev-frontend/* |
CloudFrontInvalidation | cloudfront:CreateInvalidation | Distribution E28IX5PKX6HT9N |
ECRAuth | ecr:GetAuthorizationToken | * (backend) |
ECRPush | ECR push actions | amply-backend repo (backend) |
ECS | ECS update/describe | * (backend) |
ECSPassRole | iam:PassRole | ECS execution role (backend) |
CI/CD Pipeline
Workflow file: .github/workflows/deploy-dev.yml
Pipeline Flow
Push to main
│
▼
┌─────────────────┐
│ Lint & Typecheck│ ← ESLint + tsc --noEmit
└────────┬────────┘
│
▼
┌─────────────────┐
│ Build & Deploy │ ← npm run build (VITE_API_URL baked in)
│ │ ← OIDC auth to AWS
│ │ ← Two-pass S3 sync
│ │ ← CloudFront invalidation
└─────────────────┘
Two-Pass S3 Sync Strategy
The deployment uses two separate aws s3 sync commands with different cache headers:
Pass 1 — Static assets (JS, CSS, images):
- Excludes
*.htmland*.json - Cache:
public, max-age=31536000, immutable(1 year) - Uses
--deleteto remove old assets - Safe because filenames contain content hashes (e.g.,
index-D4J36Lm_.js)
Pass 2 — HTML and JSON:
- Only includes
*.htmland*.json - Cache:
public, max-age=0, must-revalidate - No
--deleteflag (avoids race condition with pass 1)
CloudFront invalidation only targets /dashboard/index.html since all other files are content-hashed and naturally cache-bust.
Build-Time Environment Variables
| Variable | Value | Injected By |
|---|---|---|
VITE_API_URL | https://api.dev.amply-impact.org/v1 | Workflow env |
Vite replaces import.meta.env.VITE_API_URL at build time. The API URL is embedded in the JS bundle — it is not configurable at runtime.
CORS Configuration
The backend must accept requests from the dashboard origin. The ECS task definition includes:
CORS_ORIGINS=["https://dashboard.dev.amply-impact.org","http://localhost:3000","http://localhost:5173"]
This is set as an environment variable on the backend container in the amply-backend-dev task definition (revision 17+).
Local Development
# Clone and install
git clone https://github.com/StayMeaty/amply-dashboard.git
cd amply-dashboard
npm install
# Copy env and configure
cp .env.example .env
# Edit .env if you want to point to a different API
# Run dev server
npm run dev
# → http://localhost:5173/dashboard/
The dev server at localhost:5173 is included in the backend CORS origins.
Vite Base Path
vite.config.ts sets base: '/dashboard/', which means:
- All built assets have paths prefixed with
/dashboard/ - The
dist/output containsindex.html,assets/, etc. - Files are synced to
s3://amply-dev-frontend/dashboard/ - The app is accessed at
/dashboard/(not root/)
This matches the production pattern where the dashboard lives at dashboard.amply-impact.org.
Related: