6-domain audit (API, web, mobile, lambdas, infra, QA) with fixes: API security: scope internal API key middleware to allowed paths only, return 401 on invalid key instead of falling through, remove unvalidated JWT code path, sanitize error messages, add UpdateProposal validator, remove status field from UpdateProposalRequest to prevent over-posting, log swallowed exceptions in ProposalService. Infrastructure: enforce SSL on all S3 buckets, encrypt SQS queues, enable optional MFA on Cognito, add API Gateway access logging. Lambdas: fix _retry_request undefined variable across all 4 Lambdas, re-raise exceptions in pdf-extract/pdf-generate instead of swallowing, add idempotency guard to suggestions Lambda. Web: add ErrorBoundary, add auth loading state to ProtectedRoute, add mutation error toasts in AdminWorkspace, fix dead Cognito link. Mobile: add mutex to offline queue processing, distinguish permanent vs retryable failures, register all screens for both roles, log sync errors. Swagger/OpenAPI: add Swashbuckle with JWT bearer security definition, add ProducesResponseType attributes to key endpoints. Includes AUDIT-REPORT.md with complete findings and CLAUDE.md project instructions. |
||
|---|---|---|
| .github | ||
| api | ||
| infra | ||
| lambdas | ||
| mobile | ||
| scripts | ||
| shared/api-contracts | ||
| web | ||
| .gitignore | ||
| AUDIT-REPORT.md | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| HANDOFF-2026-05-22.md | ||
| README.md | ||
| RETROSPECTIVE-2026-05-19.md | ||
Proposal System
Internal proposal management platform for Sea Haven Industries. Dispatchers submit proposal requests, AI generates draft line items from historical data via Bedrock RAG, admins review and approve in a pricing workspace, and the system produces branded PDFs for delivery.
Architecture Overview
Monorepo with five primary services:
- .NET 8 API -- Clean Architecture REST API hosted on Lambda behind API Gateway (JWT-authorized) with Function URL for internal access
- React 19 Web -- MUI v7 admin/dispatcher workspace served via CloudFront + S3
- React Native Mobile -- iOS-first field app for dispatchers (offline-capable)
- Python Lambdas -- PDF extraction, PDF generation, library ingestion, AI suggestions, AOSS index provisioning
- CDK Infrastructure -- Three TypeScript stacks managing all AWS resources
Repository Structure
proposal-system/
├── api/ .NET 8 Web API (Lambda-hosted, EF Core + PostgreSQL)
├── web/ React 19 + MUI v7 + Vite frontend
├── mobile/ React Native 0.85 iOS app
├── lambdas/ Python 3.12 processing functions (arm64)
├── infra/ CDK TypeScript (3 stacks)
├── shared/ TypeScript API contracts (shared between web + mobile)
├── scripts/ Post-deploy and utility scripts
├── .github/ CI/CD workflows
└── docker-compose.yml
Tech Stack
| Component | Technologies |
|---|---|
| API | .NET 8, ASP.NET Core, EF Core + Npgsql, FluentValidation, Cognito JWT, Amazon.Lambda.AspNetCoreServer |
| Web | React 19, TypeScript, MUI v7, Vite, Redux Toolkit, TanStack Query, axios |
| Mobile | React Native CLI 0.85, React 19, React Native Paper, React Navigation, react-native-app-auth (PKCE), amazon-cognito-identity-js (SRP), Keychain, offline draft queue |
| Lambdas | Python 3.12, arm64, pdfplumber, reportlab, httpx, boto3 |
| Infrastructure | CDK TypeScript (aws-cdk-lib 2.253.1) |
| AI/RAG | Bedrock Knowledge Base (Titan Embeddings v2), OpenSearch Serverless, Claude via Bedrock Runtime |
| Auth | Cognito User Pool + Google OAuth IdP (groups: dispatchers, admins, sysadmins) |
AWS Resources
All resources are in us-east-1 (account 328440206208).
| CDK Stack | Key Resources |
|---|---|
proposal-system-foundation |
RDS PostgreSQL 15 (t4g.small), S3 buckets, SQS queue + DLQ, Cognito user pool, Secrets Manager |
proposal-system-compute |
API Gateway HTTP API (JWT authorizer), .NET 8 API Lambda + Function URL, Python Lambdas (pdf-extract, pdf-generate, library-ingest, suggestions, oss-index-creator), OpenSearch Serverless collection, Bedrock KB |
proposal-system-frontend |
CloudFront distribution (S3 OAC) |
| Resource Type | Names |
|---|---|
| S3 Buckets | proposal-system-uploads, proposal-system-generated, proposal-system-library, seahaven-ios-certificates |
| SQS | proposal-system-jobs (720s visibility, reportBatchItemFailures) + proposal-system-jobs-dlq (message body filtering by jobType) |
| Secrets | proposal-system/db-credentials, proposal-system/internal-api-key |
Local Development
Prerequisites
- .NET 8 SDK
- Node.js 24+
- Python 3.12
- PostgreSQL 16 (via docker-compose or native)
Database
docker compose up -d # starts PostgreSQL on port 5432
# database: proposalsystem, password: localdev
API
cd api
dotnet restore
dotnet run --project src/ProposalSystem.Api
# runs on http://localhost:5000
In development mode (DevMode=true in appsettings.Development.json):
- JWT auth uses a local symmetric HMAC key (no Cognito required)
- S3 service returns fake presigned URLs
- SQS publisher logs messages without sending
Web Frontend
cd web
npm install
npm run dev
# runs on http://localhost:5173, proxies /api to localhost:5000
When VITE_COGNITO_CLIENT_ID is not set, the login screen shows role-selector buttons for local development.
Infrastructure
cd infra
npm install
npx cdk synth
CI/CD
CI (on pull request to main)
Five parallel jobs calling org reusable workflows:
| Job | Workflow | What it checks |
|---|---|---|
| .NET Build & Test | ci-dotnet.yaml |
Restore, build, test the API solution |
| Web Frontend Check | ci-typescript-cdk.yaml |
TypeScript typecheck for web |
| Mobile Typecheck | ci-typescript-cdk.yaml |
TypeScript typecheck for mobile |
| Python Lint | ci-python-sam.yaml |
ruff check + format on lambdas/ |
| CDK Synth | ci-typescript-cdk.yaml |
Synthesize CDK stacks (includes .NET publish) |
Deploy (on push to main)
Calls cd-cdk.yaml reusable workflow:
- Publishes .NET 8 API and Python Lambdas
- Runs
cdk deploy --all - Executes
scripts/post-deploy.sh(builds web, syncs to S3, invalidates CloudFront)
Deploy uses OIDC role githubdeploy-proposal-system. Concurrency group prevents parallel deploys.
Mobile Deploy
Workflow: deploy-mobile.yaml -- builds and uploads to TestFlight via cd-mobile-ios.yaml reusable workflow on macos-26.
Triggers:
- Automatic: push to
mainwith changes inmobile/** - Manual:
workflow_dispatchfor on-demand builds
Mobile iOS
The iOS app uses Fastlane with match for code signing. Certificates and profiles are stored in the seahaven-ios-certificates S3 bucket (versioning enabled, public access blocked).
Build and upload to TestFlight is handled by the cd-mobile-ios.yaml reusable workflow. Required secrets:
| Secret | Purpose |
|---|---|
AWS_DEPLOY_ROLE_ARN |
OIDC role for match S3 access |
MATCH_PASSWORD |
Decryption passphrase for signing assets |
ASC_KEY_ID |
App Store Connect API key ID |
ASC_ISSUER_ID |
App Store Connect issuer |
ASC_KEY_CONTENT |
App Store Connect API key (base64) |
Authentication & Authorization
Two-layer auth architecture with defense-in-depth:
| Path | Authorizer | Authentication |
|---|---|---|
External clients → API Gateway /{proxy+} |
Cognito JWT authorizer (web + mobile client IDs) | .NET JWT middleware (ValidateAudience=true) |
/api/health |
None (public) | None |
/api/auth/callback, /api/auth/dev-login |
None (unauthenticated) | None (pre-auth endpoints) |
| Internal Lambdas → Function URL | None (NONE auth type) | Internal API key (X-Internal-Api-Key header, value from Secrets Manager) |
Role-based access: Cognito groups (dispatchers, admins, sysadmins) map to API roles via cognito:groups claim. Dispatchers can only see their own proposals (ownership enforced in service layer). VendorProposals and GeneratedPdfs endpoints restricted to admins/sysadmins.
Internal API key: Python Lambdas call the .NET API via a Lambda Function URL (bypasses API Gateway JWT check). The InternalApiKeyMiddleware validates the key and assigns the admins role to the synthetic identity.
Data Flow
- Dispatcher submits proposal request (web or mobile)
- API creates proposal record (with advisory-locked number generation), publishes SQS message
- If vendor PDF attached:
pdf-extractLambda parses and structures data - Suggestions Lambda queries Bedrock KB for similar proposals, generates line items via Claude
- Admin reviews/edits line items in pricing workspace
- On approval:
pdf-generateLambda creates branded PDF - On send:
library-ingestLambda adds approved proposal to KB for future matching
Failed SQS messages are reported via batchItemFailures and retried up to 3 times before moving to the DLQ.