Externalize LLM Configuration
The model name and per-token pricing were hardcoded across multiple files -- generator.py, lambda_process.py, main.py. This meant changing the model or updating pricing after an Anthropic price change required a code deploy. Moved both into a single SSM Parameter Store value as a JSON blob: {"model": "claude-opus-4-6", "input_price_per_mtok": 5.00, "output_price_per_mtok": 25.00}. The process Lambda reads it on each invocation, with a hardcoded fallback if SSM is unreachable. Now switching to a different Claude model or updating pricing is a single Parameter Store edit -- no deploy needed.
Also threaded the model parameter through generate_html() so it's no longer hardcoded at the SDK call site. The local dev server reads model config the same way for parity.
Pre-signed S3 URL Uploads
The biggest architectural change since launch. API Gateway has a ~10MB payload limit (6MB for Lambda proxy integrations with base64 encoding overhead), which meant users couldn't upload larger scientific papers. A 19MB PDF would simply fail.
Replaced the direct multipart upload with a three-step flow:
- Initiate: Frontend sends
POST /api/uploadwith{"filename": "paper.pdf"}. The upload Lambda validates the filename, checks the daily rate limit, generates a job ID, creates a pre-signed S3 PUT URL (5-minute expiry, content-type locked toapplication/pdf), writes anawaiting_uploadrecord to DynamoDB, and returns{job_id, upload_url}. - Upload: The browser PUTs the PDF directly to S3 using the pre-signed URL. The file never touches API Gateway or Lambda.
- Confirm: Frontend sends
POST /api/confirm/{job_id}. A new confirm Lambda verifies the S3 object exists viaHeadObject, flips the DynamoDB status toprocessing, and invokes the process Lambda asynchronously.
This required a new Lambda function (lambda_confirm.py), CloudFormation additions (ConfirmFunction, API Gateway route and integration, Lambda permission), an IAM policy update (s3:HeadObject), and S3 CORS configuration on the ShareIt bucket to allow cross-origin PUT from nowigetit.us. The deploy script now packages lambda_confirm.py and applies the CORS config as an idempotent step.
The upload Lambda shrank dramatically -- no more multipart parsing, no more s3.put_object, no more lambda_client.invoke. Timeout dropped from 30s to 10s, memory from 256MB to 128MB.
The frontend change is transparent to the user. All three steps happen during the same "Uploading PDF" stepper phase. Same drag-and-drop, same progress stepper, same error messages. The only visible difference: files up to 50MB now work where they previously failed. Verified end-to-end with a 19MB PDF -- uploaded via pre-signed URL, confirmed, processed by Claude in ~3.7 minutes, and the generated page appeared on the live site.