-
Notifications
You must be signed in to change notification settings - Fork 11
API Architecture
The EPIC (Environmental Assessment Information Catalogue) platform is a microservices-based application deployed on OpenShift that provides public access to environmental assessment projects in British Columbia. The platform consists of multiple services working together through a combination of direct routing and reverse proxy patterns.
graph TB
Route["OpenShift Routes<br/>eagle-{env}.apps.silver.devops.gov.bc.ca"]
Route -->|/api direct| API["eagle-api<br/>Node.js/Express<br/>Port 3000"]
Route -->|/ root| RProxy["rproxy<br/>nginx<br/>Port 8080"]
API --> MongoDB[(MongoDB)]
RProxy --> Public["eagle-public<br/>Angular<br/>Port 8080"]
RProxy --> Admin["eagle-admin<br/>Angular<br/>Port 8080"]
RProxy --> Analytics["penguin-analytics<br/>Node.js<br/>Port 3000"]
Analytics --> TimescaleDB[("TimescaleDB<br/>PostgreSQL")]
style Route fill:#e1f5ff
style API fill:#fff4e6
style RProxy fill:#f3e5f5
style MongoDB fill:#e8f5e9
style TimescaleDB fill:#e8f5e9
Technology: Node.js/Express, MongoDB, Mongoose ODM
Port: 3000
Repository: bcgov/eagle-api
The main backend API service providing RESTful endpoints for:
- Public project information (
/api/public/*) - Protected admin operations (
/api/*- requires Keycloak authentication) - Runtime configuration for frontends (
/api/config) - Document management and search
- User management and authorization
Key Features:
- Keycloak JWT authentication for admin endpoints
- MongoDB for data persistence
- S3/Minio integration for document storage
- Comprehensive search capabilities
- RESTful API design following OpenAPI specification
Technology: Angular 21 (standalone components), TypeScript
Port: 8080 (nginx)
Repository: bcgov/eagle-public
Public-facing Angular application providing:
- Browse environmental assessment projects
- View project details, documents, and milestones
- Search functionality
- Responsive design for mobile/desktop
- Anonymous analytics tracking
Deployment: 2-stage Docker build
- Build stage: Node compiles Angular app
- Runtime stage: nginx serves static files
Technology: Angular (standalone components), TypeScript
Port: 8080 (nginx)
Repository: bcgov/eagle-admin
Administrative interface for authorized users:
- Keycloak SSO integration
- Project management (CRUD operations)
- Document upload and management
- User and role management
- Authenticated analytics tracking
Authentication Flow:
- User redirects to Keycloak login
- Keycloak returns JWT token
- Frontend includes JWT in Authorization header
- eagle-api validates JWT for protected endpoints
Technology: Node.js/Express, TimescaleDB (PostgreSQL + time-series)
Port: 3000
Repository: bcgov/penguin-analytics
Dedicated analytics service for tracking user interactions:
- Anonymous event ingestion
- Time-series data storage
- Event schema validation
- Metabase dashboard integration
- Batch and single-event endpoints
See: Analytics Architecture for detailed analytics documentation.
Technology: nginx (OpenShift S2I build)
Port: 8080
Repository: bcgov/eao-nginx
Central reverse proxy providing:
- Path-based routing to backend services
- HTTP caching layer
- Security headers
- Optional HTTP Basic Auth per location
- Single entry point for multiple services
The platform uses two distinct routing patterns that work together:
OpenShift Route Configuration:
- Host:
eagle-{env}.apps.silver.devops.gov.bc.ca - Path:
/api - Target:
eagle-api:3000 - TLS: Edge termination
Request Flow:
sequenceDiagram
participant Browser as User Browser
participant Route as OpenShift Route
participant API as eagle-api:3000
Browser->>Route: GET /api/projects
Note over Route: Direct match on /api path
Route->>API: Forward request
API-->>Route: Response
Route-->>Browser: Response
OpenShift Route Configuration:
- Host:
eagle-{env}.apps.silver.devops.gov.bc.ca - Path:
/(root - catches all non-/api paths) - Target:
rproxy:8080 - TLS: Edge termination
rproxy nginx Routing Rules:
| Path | Target Service | Purpose |
|---|---|---|
/ |
eagle-public:8080 |
Public frontend |
/public |
eagle-public:8080 |
Alternative public access |
/admin/ |
eagle-admin:8080 |
Admin frontend |
/analytics |
penguin-analytics-api:3000 |
Analytics events |
Request Flow Example (accessing admin):
sequenceDiagram
participant Browser as User Browser
participant Route as OpenShift Route
participant RProxy as rproxy:8080
participant Admin as eagle-admin:8080
Browser->>Route: GET /admin/
Note over Route: Root / path matches
Route->>RProxy: Forward to rproxy
Note over RProxy: location /admin/ rule
RProxy->>Admin: proxy_pass to :8080
Admin-->>RProxy: Angular app
RProxy-->>Route: Response
Route-->>Browser: Angular app
The /api path has both routing configurations:
-
Direct OpenShift route to eagle-api (explicit
/apipath) -
rproxy location block that could also proxy
/api
The direct route takes precedence at the OpenShift ingress level.
Performance:
- Eliminates extra nginx hop for frequent API calls
- Reduces latency for data-heavy operations
- Direct TLS termination at OpenShift edge
Simplicity:
- Cleaner authentication header passing (Keycloak JWT)
- No nginx proxy_pass header manipulation needed
- Easier to debug connection issues
Independence:
- API can scale independently from rproxy
- API restarts don't affect rproxy
- Separate resource allocation and monitoring
Backend Communication:
- API handles sensitive operations (auth, authorization, data modification)
- Direct path provides better security audit trail
- Reduces complexity in request chain
rproxy is used for:
- Frontend applications (eagle-public, eagle-admin) - static file serving with caching
- Analytics - separate microservice integration
- Path-based routing - multiple services on same domain
- Caching layer - reduce load on backend services
- Security headers - centralized CSP, HSTS, etc.
eagle-public:
// Via ConfigService
API_LOCATION: 'https://eagle-dev.apps.silver.devops.gov.bc.ca'
API_PATH: '/api/public'
// HTTP calls
this.http.get(`${API_LOCATION}${API_PATH}/projects`)
↓
https://eagle-dev.apps.silver.devops.gov.bc.ca/api/public/projects
↓
OpenShift Route (direct /api)
↓
eagle-api:3000/api/public/projectseagle-admin:
// Authenticated calls include JWT
this.http.get(`${API_LOCATION}${API_PATH}/projects`, {
headers: { Authorization: `Bearer ${jwt}` }
})
↓
https://eagle-dev.apps.silver.devops.gov.bc.ca/api/projects
↓
eagle-api:3000/api/projects
↓
JWT validation via Keycloak// Via AnalyticsService
ANALYTICS_API_URL: '/analytics'
// Event tracking
this.http.post(`${ANALYTICS_API_URL}`, eventData)
↓
https://eagle-dev.apps.silver.devops.gov.bc.ca/analytics
↓
OpenShift Route (root /)
↓
rproxy:8080
↓
location /analytics { proxy_pass penguin-analytics-api:3000/analytics; }
↓
penguin-analytics-api:3000/analytics
↓
TimescaleDB storageBoth frontends use the ConfigService pattern for runtime configuration:
// At application bootstrap
ConfigService.init()
↓
HTTP GET /api/config
↓
eagle-api:3000/api/config
↓
Returns: {
ENVIRONMENT: 'dev',
API_LOCATION: 'https://eagle-dev.apps.silver.devops.gov.bc.ca',
API_PATH: '/api',
ANALYTICS_API_URL: '/analytics',
KEYCLOAK_URL: '...',
// ... other config
}
↓
Frontend initializes services with runtime configSee: Configuration Management for detailed configuration documentation.
Every query in eagle-api uses .collation({ locale: 'en', strength: 2 }) for case-insensitive string matching. All indexes on the epic collection must be created with the same collation — otherwise MongoDB ignores them and falls back to a full collection scan of 79,000+ documents.
The four performance indexes (added in migration 20260408000000-addCollatedIndexes) are:
| Index | Fields | Covers |
|---|---|---|
idx_schemaName_en |
{_schemaName:1} |
All schema-filtered queries |
idx_document_by_project |
{_schemaName:1, project:1} |
Document/CommentPeriod lists |
idx_comment_by_period |
{_schemaName:1, period:1} |
Comment queries by period |
idx_recentActivity_active_pinned_date |
{_schemaName:1, active:1, pinned:-1, dateAdded:-1} |
Homepage recent activity |
All are created with collation: { locale: 'en', strength: 2 }.
Utils.runDataQuery() (the generic aggregation builder) runs $lookup stages before $sort/$limit. This is fine for paginated list queries, but is expensive for small-result endpoints — every joined document in the collection is processed before pagination discards most of them.
Rule: If an endpoint only needs a small fixed number of results (e.g., 4 homepage items from 2,400+ active records), write a custom pipeline that places $sort+$limit before any $lookup stages. See api/controllers/recentActivity.js for the reference implementation, which reduced response time from ~1,700 ms to ~200 ms.
// ✅ Fast — limit before lookup
$match → $sort → $limit(4) → $lookup → $lookup → ...
// ❌ Slow — lookup before limit
$match → $lookup(all 2,400 docs) → $sort → $limit(4)
- Configuration Management - ConfigService pattern and environment variables
- Analytics Architecture - Penguin Analytics integration
- API Deployment - Deployment workflows for eagle-api
- Helm Charts - Helm chart configuration
Eagle Documentation
Operations
Architecture
Configuration
Help
Repositories
Environments