Building a Multi-Stakeholder Reporting Infrastructure: Executive Dashboards via AWS Lambda & SES
Over the past development session, we built and deployed a comprehensive executive reporting system designed to provide leadership visibility across four distinct business entities (JADA, QueenofSanDiego, QuickDumpNow, DangerousCentaur) plus three ancillary domains. This post details the technical architecture, deployment strategy, and the reasoning behind key decisions.
What Was Built
We created a multi-perspective reporting framework that generates five distinct executive reports, each tailored to a specific stakeholder lens:
- CEO Report: Asset inventory, shortfall analysis, KPI gaps, 30-day action plan
- CTO Report: Stack-by-stack security audit, cost analysis, UX/dev cycle gaps
- CFO Report: Burn rate modeling, capital deployment framework, break-even analysis
- CMO Report: Channel visibility matrix, OTA sequencing, 30/60/90 milestones
- Accounting Report: Revenue recognition issues, chart of accounts, expense audit
Additionally, we identified three supporting domains requiring parallel reporting: 3028 51st St Rental (real estate asset), Expert Yacht Delivery (logistics operations), and DangerousCentaur Client Portfolio (billing audit). This expanded scope ensures no operational blind spots.
Technical Implementation: Report Generation & Delivery
File Structure & Report Scripts
The core reporting logic resides in two Python modules:
/Users/cb/Documents/repos/tools/send_exec_reports.py— Primary report generator and SES dispatcher/Users/cb/Documents/repos/tools/send_exec_reports_2.py— Secondary variant (used for batch iterations)
Both scripts load configuration from repos.env, which contains verified SES sender addresses and recipient email lists. The scripts iterate over stakeholder personas, generate contextual markdown/text reports, and dispatch via AWS SES.
Email Delivery via AWS SES
We leverage AWS Simple Email Service for reliability and deliverability:
# Pseudocode pattern (secrets omitted)
from boto3 import client as boto_client
ses_client = boto_client('ses', region_name='us-west-2')
message = {
'Subject': {'Data': f'Executive Report: {stakeholder_role}'},
'Body': {'Text': {'Data': report_content}}
}
ses_client.send_email(
Source='admin@queenofsandiego.com', # Verified sender
Destination={'ToAddresses': ['c.b.ladd@gmail.com'], 'BccAddresses': ['admin@queenofsandiego.com']},
Message=message
)
Why SES? Unlike third-party email services, SES integrates natively with IAM roles, scales to thousands of messages, and costs ~$0.10 per 1,000 emails. For executive reporting on a weekly/monthly cadence, this is negligible infrastructure expense.
All sender addresses are pre-verified in the SES console. This prevents deliverability issues and ensures emails land in primary inboxes rather than spam.
Infrastructure & DevOps Integration
Lambda Function Deployment
The primary application workload is housed in:
/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py
This Lambda function powers the Ship Captain Crew tool (a charter management system) but also serves as the execution environment for triggered reports. We iterated on this function extensively throughout the session to handle:
- Event checklist creation and state management
- JWT token generation for magic-link authentication
- EventBridge cron rule integration (push-to-book nudges)
- Executive report generation on-demand or scheduled
Deployment workflow:
# Syntax validation before deploy
python -m py_compile lambda_function.py
# Package and deploy to AWS
zip -r lambda_function.zip lambda_function.py
aws lambda update-function-code \
--function-name shipcaptaincrew \
--zip-file fileb://lambda_function.zip
Why Lambda? Serverless architecture eliminates server management overhead. For report generation (typically <15 second runtime), we pay only for actual compute. Combined with SES, the full reporting stack costs <$1/month in infrastructure.
Frontend & Static Assets
The Ship Captain Crew frontend lives at:
/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/frontend/index.html
This HTML file includes timing panels, checklist management UI, and JWT-based authentication flows. After edits, we deploy to S3 and invalidate CloudFront:
# Deploy frontend to S3
aws s3 cp index.html s3://queenofsandiego-assets/shipcaptaincrew/index.html
# Invalidate CloudFront cache (distribution ID omitted for security)
aws cloudfront create-invalidation \
--distribution-id [DIST_ID] \
--paths "/*"
CloudFront caches static assets globally, reducing latency for end users across geographies. Cache invalidation ensures new code reaches browsers within seconds rather than waiting for TTL expiry.
Key Decisions & Rationale
Report Personalization by Stakeholder Lens
Rather than a single generic report, we generate five distinct documents. Why?
- CEO cares about profitability, asset utilization, and revenue gaps. Technical details distract.
- CTO cares about security posture, scalability, and dev velocity. Business metrics are secondary.
- CFO cares about cash flow, burn rate, and unit economics. Everything else is context.
- CMO cares about customer acquisition channels and go-to-market velocity.
- Accounting cares about transaction tracking and reconciliation.
This multi-report approach prevents information overload and ensures each stakeholder receives actionable intelligence within their domain of responsibility.
Batch Email Delivery with BCC Pattern
We route all reports to c.b.ladd@gmail.com with admin@queenofsandiego.com on BCC. Why not send directly to each stakeholder's personal email?
- Centralized archival: One inbox for all reports ensures audit trail and historical comparison.
- Shared visibility: BCC keeps stakeholders aware of what others are receiving (transparency).
- Deliverability: Reduces risk of any single report bouncing due to recipient email issues.
Avoiding Hardcoded Credentials
All AWS credentials are managed via IAM roles attached to Lambda execution. No credentials appear in code or environment variables visible in logs. SES sender addresses are hardcoded (