Case study · Web Application
NovaTech Solutions
NovaTech Solutions is a fictional MSP marketing site with a real inquiry workflow behind the contact form. Submit the form and the lead is accepted immediately; CRM and email continue in the background.
Frontend
Ingress
AWS cloud
Lead systems
Project snapshot
At a glance
- Role
- Full-Stack / Cloud Developer
- Project type
- MSP marketing site with a durable inquiry workflow
- Frontend
- Next.js 16 · React 19 · TypeScript · Tailwind CSS
- Backend
- Next.js Route Handler · Zod
- Cloud
- AWS CDK · Step Functions · Lambda · DynamoDB · SQS
- Testing
- Inquiry-path unit tests · Playwright QA
- Architecture
- Turnstile and OIDC at ingress, then Step Functions → DynamoDB claim → HubSpot → SQS → Resend.
- Status
- Production-verified inquiry path · architecture demo
Problem
MSP sites often explain services well and then drop the inquiry into a void: no validation, no CRM, no spam control. I wanted the services → proof → contact journey to end in a durable lead, even with a fictional company identity.
Approach
Marketing content lives in typed constants so routes stay consistent. The form validates on the client, then a same-origin handler re-validates, checks Turnstile, and starts the workflow. The HTTP response does not wait for HubSpot or email.
How it works
A visitor submits an inquiry with a Turnstile token and a submission id. The Route Handler verifies the token, discards it, and calls states:StartExecution with a deterministic execution name. Step Functions claims the submission in DynamoDB, the HubSpot Lambda upserts a contact and creates a deal plus note, then SQS carries customer and staff jobs to a Lambda that sends Resend. If that name already exists, StartExecution returns ExecutionAlreadyExists and the API still answers 202. Email can fail after CRM success without rolling the lead back.
- Visitor
- NovaTech form
- Next.js Route Handler
- Turnstile
- Step Functions
- DynamoDB
- HubSpot
- SQS
- Resend
Architecture highlight
Inquiry workflow
The browser never talks to AWS. Turnstile stays on the Next.js edge. A StartExecution-only OIDC role is the only AWS identity Vercel receives.
Ingress
- Visitor
- Next.js
- Turnstile
- OIDC
- StartExecution
Workflow
- DynamoDB
- HubSpot Lambda
- HubSpot
- SQS
- Notification Lambda
- Resend
Ingress and workflow
The browser stays on Next.js. AWS starts after the request is accepted.
Shared entry
- Visitor
- NovaTech / Next.js
Public ingress
Same-origin HTTP. Secrets, ARNs, and provider tokens never go to the browser.
- NovaTech form
- POST /api/novatech/inquiries
- Zod + Turnstile
- Vercel OIDC
- states:StartExecution
Durable workflow
DynamoDB claims the submission. HubSpot is the CRM record. Email is isolated from that write.
- Step Functions
- DynamoDB claim
- HubSpot contact + deal + note
- SQS notifications
- Resend
Engineering decisions
Same-origin Route Handler, not a public AWS API
The browser posts to Next.js. That keeps Turnstile, Zod, and the 202 contract in one deployable app. Vercel is allowed to start the workflow, not to call HubSpot, SQS, or DynamoDB directly.
OIDC instead of long-lived AWS keys
Production and Preview assume the ingress role through the Vercel team issuer. The role can call only states:StartExecution on the NovaTech state machine. Local next dev keeps using the AWS CLI profile.
DynamoDB as idempotency authority
Deterministic execution names turn browser retries into ExecutionAlreadyExists. The table claim with attribute_not_exists is still the business duplicate guard, so a second execution does not create a second deal or a second note.
HubSpot as system of record
Sales needs a durable lead more than a pretty success toast. I upsert by email, open a deal, and attach a note with the message. Email can fail without undoing the CRM write.
Verification
Built, not mocked.
What was deployed, executed, or observed.
Deployed
The inquiry workflow was deployed through AWS CDK and successfully executed from the public Vercel application.
Tested
A production submission completed end-to-end, and repeating the same submission ID did not create a second workflow execution.
Secure
Vercel authenticates to AWS through a scoped OIDC role; no long-lived AWS credentials are stored in the deployment.
Observable
CloudTrail recorded the web-identity role assumption, and Step Functions exposed the completed execution path.
Integrated
The verified workflow completed its HubSpot CRM write and notification path.
Known limits
- Company identity and marketing statistics are fictional.
- Email is at-least-once; an unverified sending domain can fail for some inboxes.
Current state
Production inquiries on christopherkilo.com return 202 and continue asynchronously. HubSpot still receives the lead when visitor email cannot be delivered.
Implemented
- ✓ Shared Zod contract for client and API
- ✓ Cloudflare Turnstile at public ingress
- ✓ Vercel OIDC role limited to states:StartExecution
- ✓ Step Functions inquiry workflow with DynamoDB idempotency
- ✓ HubSpot contact upsert, deal, association, and note
- ✓ SQS notification jobs and asynchronous Resend
- ✓ Allowlisted structured logs without payload secrets
Demo / seeded
- △ Fictional company identity and illustrative testimonials/stats
- △ Dev Turnstile bypass token for local work
- △ Forced failure mode for UI QA
- △ Resend test sender — unverified visitor inboxes can fail permanently
What I learned
I learned to keep spam control and AWS identity on the HTTP edge, then let DynamoDB own idempotency. That split made retries, duplicate submissions, and email failure easy to explain: the visitor hears that the request was received, HubSpot keeps the lead, and Resend is allowed to fail without pretending the message already sent.
Next
- 01Move rate limiting to shared storage so multiple instances agree
- 02Add alertable monitoring beyond allowlisted console logs
- 03Finish mobile-menu background inertness
- 04Use a verified Resend sending domain if visitor mail must reach arbitrary inboxes