AWS Builder Center
Building a Real-Time Dispatch Console with AWS Serverless Architecture

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  offered  status 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) and  viewers  (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.py
resp = 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.py
def 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 leaks
views.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.py
def 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.js
const 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 Hub
zoom: 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} , and  PATCH /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 in  admins  or the pickup is  confirmed / 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  offered  pickups. When Twilio sends the DTMF webhook to  /voice/response , the pickup record transitions to  confirmed  in 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.js  backed by  sessionStorage . 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  admins  Cognito 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_table  DynamoDB 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 SUPPRESS

aws cognito-idp admin-set-user-password \
--user-pool-id <UserPoolId> \
--username ops@ecolink.internal \
--password 'EcoLink2026!Aws' \
--permanent

aws 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
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article