
Building a Real-Time Dispatch Console with AWS Serverless Architecture
EcoLink coordinates informal scrap recyclers, WhatsApp conversations, and privacy-shielded household dispatches across India with Amazon Location Service, Cognito RBAC, and serverless API Gateway telemetry. This is how the operations dashboard was built, how it integrates with the live backend, and what pushed back.
The control room problem
When an autonomous AI agent starts booking recycling pickups from WhatsApp photos and placing automated IVR calls to neighbourhood kabadiwalas, the physical world catches up quickly.
In an automated system, software failures look like silent queues:
- A recycler declines or lets the phone ring out. The AI has done its job by placing the call, but without human dispatch oversight, the household is left waiting indefinitely for a pickup slot that will never arrive.
- Privacy cannot depend on trust. Household addresses must never leak to recyclers before they accept an offer, nor should non-admin dashboard operators see unconfirmed household addresses or exact GPS coordinates on their screens.
- Logistics bottlenecks need spatial visibility. Knowing that twenty pickups are in
offeredstatus tells you very little. Knowing that nineteen of them are clustered in Indiranagar while the only available paper recyclers are stuck across the city in Koramangala tells you everything.
- Language and conversation context matter. When an operator needs to troubleshoot a stalled pickup, they need to see the exact path the Bedrock Strands agent took, what scrap rate was quoted, and the conversational history of that WhatsApp session.
The EcoLink Operations Dashboard was designed as the central dispatch console: a single-pane-of-glass interface connecting live API Gateway endpoints, DynamoDB tables, and Twilio voice telemetry with Amazon Location Service and Amazon Cognito authentication.
How we got to this solution
Internal operations consoles are frequently treated as afterthoughts—either cobbled together from internal admin templates or wired with broad, unrestricted database credentials. We treated the dashboard as an integral, production-grade microservice within our AWS CDK deployment.
This shaped the architecture:
- Strict Role-Based Access Control (RBAC) via Amazon Cognito. Operations staff sign in through Cognito User Pools with granular groups:
admins(who can manually reassign statuses and view sensitive addresses) andviewers(who observe live pipeline telemetry with household identities shielded).
- Enforcing the Address Rule at the API layer. The dashboard never trusts the browser to hide private data. If a pickup has not been explicitly accepted by a recycler, the backend sanitises the household’s address and geolocation before the JSON response leaves API Gateway.
- Amazon Location Service + MapLibre GL JS. Real-time spatial tracking of registered kabadiwalas, authorized CPCB drop-off hubs, and household pickup requests, rendered natively via AWS map tiles without third-party mapping SaaS dependencies.
- Two-Column Master-Detail inspection. Operators monitor live pipeline counters across the entire city cluster, filter by operational status (
requested,offered,confirmed,completed,declined), and pull up a slide-out drawer containing the Bedrock agent’s execution log and Twilio IVR call state.
- Live WhatsApp session diagnostics. Operators can inspect joined user tables to trace household language preferences (Hindi, English, Kannada), active conversation paths (
regular,ewaste,photo_flow), and pickup histories in real time.
The architecture
The dashboard is served as a decoupled frontend client talking directly to the production AWS API Gateway, authenticated by Cognito ID tokens and protected by dedicated Lambda authorizers.
Plain text
┌──────────────────────────────────────────────┐│ EcoLink Operations Console (Client) │└───────────────┬──────────────────────────────┘│Cognito ID Token │ Map Tiles (API Key)┌──────────────────────┴──────────────────────┐▼ ▼┌────────────────────────┐ ┌────────────────────────┐│ Amazon API Gateway │ │ Amazon Location Service││ (prod / 50 req/s) │ │ (ecolink-dispatch-map) │└───────────┬────────────┘ └────────────────────────┘│Cognito Authorizer┌──────────────┴──────────────┐▼ ▼┌───────────────┐ ┌───────────────┐│ DashboardFn │ │ PickupsFn │└───────┬───────┘ └───────┬───────┘│ │├─────────────────────────┼─────────────────────────┐▼ ▼ ▼┌───────────────┐ ┌───────────────┐ ┌───────────────┐│ EcoLink- │ │ EcoLink- │ │ EcoLink- ││ recyclers │ │ pickups │ │ users ││ (DynamoDB) │ │ (DynamoDB) │ │ (DynamoDB) │└───────────────┘ └───────────────┘ └───────────────┘│ │ │└─────────────────────────┼─────────────────────────┘▼┌───────────────┐│ EcoLink- ││ conversation-││ state (DDB) │└───────────────┘
Why each service is there
Service | Role in Dashboard | Why this choice |
|---|---|---|
Amazon Cognito | User Pool with admins and viewers groups, issuing short-lived JWT ID tokens | Native AWS identity management supporting password policies, group claims, and SRP/ USER_PASSWORD_AUTH without managing our own credential database. |
Amazon API Gateway | prod stage exposing /pickups , /dashboard/* , and /auth/login | Uses native CognitoUserPoolsAuthorizer , decoupling authenticated admin operations from agent API-key routes while enforcing throttling (50 req/s limit, 100 burst). |
AWS Lambda ( DashboardFn , PickupsFn , AuthFn ) | Python 3.12 handlers executing backend business logic | Serverless execution that scales to zero when operators are idle and joins DynamoDB records server-side in milliseconds. |
Amazon DynamoDB | pickups , recyclers , users , and conversation_state tables | On-demand billing with partition key lookups and secondary status indexes ( by_status GSI), returning live pickup pipelines and joined user sessions. |
Amazon Location Service | Map tile generation for the live dispatch canvas | Enterprise map tiles accessed securely via scoped API keys restricted to geo:GetMap* , keeping billing, security, and networking inside the same AWS account. |
MapLibre GL JS | Interactive client-side vector map renderer | Open-source, high-performance WebGL map library that directly ingests Amazon Location Service vector tile endpoints. |
AWS CDK (Python) | Complete stack infrastructure as code | Allows all Cognito user pool clients, groups, API Gateway authorizers, IAM roles, and DynamoDB table grants to be provisioned and destroyed deterministically. |
Real backend integration: How the pieces connect
1. Authentication and RBAC with Amazon Cognito
When an operator logs in, the client communicates with
POST /auth/login . Inside AuthFn , AWS Cognito's initiate_auth endpoint is called using the USER_PASSWORD_AUTH flow:Python
# backend/ecolink/handlers/auth.pyresp = client.initiate_auth(ClientId=client_id,AuthFlow="USER_PASSWORD_AUTH",AuthParameters={"USERNAME": email, "PASSWORD": password})result = resp["AuthenticationResult"]claims = _claims(result["IdToken"])groups = claims.get("cognito:groups") or []return {"id_token": result["IdToken"],"expires_in": result.get("ExpiresIn", 3600),"user": {"email": claims.get("email", email)},"group": "admins" if "admins" in groups else "viewers",}
The resulting
id_token is stored in the client's session storage. Subsequent requests to protected endpoints ( /pickups , /dashboard/recyclers , /dashboard/users ) pass this token in the Authorization header, where API Gateway's DashboardAuthorizer validates the cryptographic signature directly against the Cognito User Pool.2. The Address Rule inside dashboard_view.py
The dashboard must uphold the core privacy promise of EcoLink: a household's location is shielded until a recycler explicitly confirms the dispatch.
In
core/dashboard_view.py , views are constructed based on the caller's verified Cognito claims:Python
# backend/ecolink/core/dashboard_view.pydef build_views(repo: Any, pickups: list[dict[str, Any]], *, privileged: bool) -> list[dict[str, Any]]:views = []for pickup in pickups:show_private = privileged or address_visible(pickup.get("status"))view = pickup_view(pickup, privileged=privileged)if show_private:household = user(pickup["user_id"])if household.get("lat") is not None and household.get("lng") is not None:view["lat"], view["lng"] = household["lat"], household["lng"]else:view["address_text"] = "[Hidden until confirmed]"view["user_id"] = "User (protected)"# lat and lng remain omitted, preventing map pin leaksviews.append(view)return views
If an operator belongs to the
viewers group and a pickup is in requested or offered status, the address string is scrubbed and GPS coordinates are withheld. The frontend displays an active Household Privacy Shield indicator in the inspection drawer. The moment a kabadiwala presses 1 on their phone, the state machine transitions to confirmed , and the next refresh safely unlocks the physical address.3. Joining DynamoDB tables for WhatsApp user intelligence
Logistics operators need visibility into the WhatsApp conversation state without forcing our Lambdas to run slow relational queries. The
GET /dashboard/users endpoint performs a server-side join between the users table and the short-lived conversation_state table:Python
# backend/ecolink/handlers/dashboard.pydef users(event: dict, context: Any) -> dict[str, list[dict[str, Any]]]:repo = deps.get_repo()states = {row["user_id"]: _decode_state(row) for row in repo.list_conversation_states()}rows = []for user in sorted(repo.list_users(), key=lambda row: row.get("user_id", "")):uid = user["user_id"]state = states.get(uid, {})pickup_count = len(repo.list_pickups(user_id=uid, limit=100))rows.append({"user_id": uid,"name": user.get("name"),"lang": user.get("lang", state.get("lang", "en")),"pickup_count": pickup_count,"conversation_state": state,})return {"users": rows}
This populates the console's WhatsApp Users view, revealing the household's chosen language, active conversation stage, and total completed pickups.
4. Live dispatch canvas via Amazon Location Service
The interactive map renders the respective city's operational cluster using vector tiles hosted directly on Amazon Location Service:
JavaScript
// frontend/dashboard/app.jsconst cfg = window.ECOLINK_CONFIG.amazonLocation;const style = `https://maps.geo.${cfg.region}.amazonaws.com/maps/v0/maps/${cfg.mapName}/style-descriptor?key=${cfg.apiKey}`;alMap = new maplibregl.Map({container: 'map-canvas',style: style,center: [77.5946, 12.9716], // Bengaluru Hubzoom: 11});
The map displays color-coded markers distinguishing:
- Kabadiwala Partners (Emerald green markers displaying current availability and accepted categories).
- Authorised CPCB E-Waste / Battery Drop-Off Centers (Purple markers).
- Active Pickup Requests (Blue markers with live status badges). Clicking any map pin opens the master-detail drawer to inspect the pickup directly.
What fought back
1. The two-authorizer problem on API Gateway
A single API Gateway method cannot attach two distinct authorizers simultaneously. The WhatsApp agent Lambda needed to query and mutate pickups using a shared internal API key (via
TokenAuthorizer ), whereas the dashboard console needed to access pickups using Cognito JWT tokens.- The fix: We cleanly partitioned the REST hierarchy in
infra/ecolink_stack.py. The agent talks to/pickups(POST) and/pickups/{id}(PATCH) using the API key authorizer. The dashboard uses Cognito-protected twin routes (GET /pickups,GET /pickups/{id}, andPATCH /dashboard/pickups/{id}).
2. Map coordinates are addresses too
In early iterations,
address_text was replaced with "[Hidden until confirmed]" for unconfirmed pickups, but the DynamoDB pickup record still passed lat and lng to the browser so the map could display a marker.- The vulnerability: Anyone opening their browser's DevTools Network tab could read exact coordinates down to the street address, defeating the privacy shield.
- The fix: We unified location privacy in
dashboard_view.py. Coordinates and street text are governed by the exact same rule: unless the caller is inadminsor the pickup isconfirmed/completed, coordinates are stripped from the response entirely.
3. The raw ID token vs Bearer token gotcha
Standard REST clients often default to adding the
Bearer prefix in authorization headers ( Authorization: Bearer <token> ). However, Amazon API Gateway's native CognitoUserPoolsAuthorizer expects the raw JWT ID token string. Sending Bearer <token> results in an immediate, opaque 401 Unauthorized .- The fix: In
api.js, the API client passes the raw token directly:````javascript const res = await fetch(${this.baseUrl}/pickups, { headers: { Authorization: this.token } });plaintext const res = await fetch(${this.baseUrl}/pickups, { headers: { Authorization: this.token } });javascript const res = await fetch(${this.baseUrl}/pickups, { headers: { Authorization: this.token } });
Plain text
4. State synchronization during outbound voice IVR
When a pickup enters
offered status, Twilio is in the middle of placing an automated phone call to the recycler's handset. Recyclers can take anywhere from 10 to 45 seconds to answer and press 1 or 2 .- The issue: If the operator refreshed the dashboard mid-call, there was no indication of whether the dispatch was stuck, dialing, or declining.
- The fix: We added dedicated IVR telemetry banners and pulsing status badges in the UI for
offeredpickups. When Twilio sends the DTMF webhook to/voice/response, the pickup record transitions toconfirmedin DynamoDB, instantly resolving the drawer stepper to Step 3.
5. Session resilience across operator tab reloads
Operators in dispatch centers frequently refresh browser tabs or switch workstations. Relying on transient JavaScript state meant operators were logged out on every refresh, losing their active filters and drawer selections.
- The fix: We implemented an active session restoration loop in
api.jsbacked bysessionStorage. On page load,restoreSession()retrieves and validates the active token and role badge, seamlessly restoring the dashboard state without exposing credentials.
What we upgraded along the way
- Leaflet replaced with MapLibre GL JS & Amazon Location Service: Eliminated third-party map token dependencies and integrated map hosting directly into our AWS account and billing boundary.
- Real-time Pipeline Stepper: Replaced static status text with an active 4-stage progression indicator (
1. Requested → 2. Offered → 3. Confirmed → 4. Completed) that clearly indicates when an automated IVR call is underway.
- Administrative Force Overrides: Allowed operators in the
adminsCognito group to trigger state machine overrides (Force Confirm,Mark Complete,Cancel) directly from the drawer to recover from unanswered phone calls or deadlocks.
- Dynamic Scrap Valuation Aggregation: Configured the backend to pull current per-kg rates from the
rate_tableDynamoDB table and calculate estimated rupee payouts for mixed household scrap in real time.
Limitations we are honest about
- Polling vs WebSockets: The current console relies on on-demand refresh buttons rather than an open WebSocket connection through AWS IoT Core or API Gateway WebSockets. Under peak dispatch loads, operators must manually sync the view.
- Linear scans for recycler matching: Recycler listings currently run on a table scan across available partners. While performant for hundreds of partners across Bengaluru, scaling to tens of thousands of recyclers across India will require a spatial geohash index (e.g., DynamoDB Geo Library or Amazon OpenSearch Service).
- Static recycler depot locations: The dispatch map plots recyclers at their registered shop coordinates rather than tracking real-time vehicle GPS locations during physical collection runs.
What comes next
The next iteration of the operations console will introduce WebSocket push notifications via API Gateway to stream Twilio DTMF call events live into the UI without polling. We also plan to integrate an interactive scrap pricing editor directly into the console, allowing ops managers to adjust regional scrap indices dynamically in DynamoDB as wholesale market prices fluctuate across paper, copper, and e-waste.
Closing thoughts
AI agents are only as reliable as the operational infrastructure supporting them. While Amazon Bedrock and Twilio handle the frontline interaction with households and scrap dealers, the serverless operations console provides the guardrails that make the whole network safe to run.
By enforcing strict Cognito role-based access, shielding household addresses in code, and plotting dispatch activity across Amazon Location Service, EcoLink gives human operators complete command over an otherwise invisible logistics engine.
Running the operations console against the deployed stack
1. Create your operations user in Cognito:
Bash
aws cognito-idp admin-create-user \--user-pool-id <UserPoolId> \--username ops@ecolink.internal \--user-attributes Name=email,Value=ops@ecolink.internal Name=email_verified,Value=true \--message-action SUPPRESSaws cognito-idp admin-set-user-password \--user-pool-id <UserPoolId> \--username ops@ecolink.internal \--password 'EcoLink2026!Aws' \--permanentaws cognito-idp admin-add-user-to-group \--user-pool-id <UserPoolId> \--username ops@ecolink.internal \--group-name admins
2. Configure the frontend (
frontend/dashboard/config.js ):JavaScript
window.ECOLINK_CONFIG = {mockMode: false,baseUrl: 'https://<ApiGatewayId>.execute-api.<region>.amazonaws.com/prod',cognito: {userPoolId: '<UserPoolId>',clientId: '<UserPoolClientId>',region: '<region>'},amazonLocation: {region: '<region>',mapName: 'ecolink-dispatch-map',apiKey: '<AmazonLocationApiKey>'}};
3. Serve the console:
Bash
python scripts/serve_dashboard.py# Open http://localhost:8000/dashboard in your browser
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article