Skip to content
CHRISTOPHER KILO.

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

Next.jsReactTypeScriptTailwind CSS

Ingress

ZodCloudflare TurnstileVercel OIDC

AWS cloud

Step FunctionsLambdaDynamoDBSQSSSMIAMCloudWatchCDK

Lead systems

HubSpot CRMResend

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.

  1. Visitor
  2. NovaTech form
  3. Next.js Route Handler
  4. Turnstile
  5. Step Functions
  6. DynamoDB
  7. HubSpot
  8. SQS
  9. 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

  1. Visitor
  2. Next.js
  3. Turnstile
  4. OIDC
  5. StartExecution

Workflow

  1. DynamoDB
  2. HubSpot Lambda
  3. HubSpot
  4. SQS
  5. Notification Lambda
  6. Resend

Ingress and workflow

The browser stays on Next.js. AWS starts after the request is accepted.

Shared entry

  1. Visitor
  2. NovaTech / Next.js

Public ingress

Same-origin HTTP. Secrets, ARNs, and provider tokens never go to the browser.

  1. NovaTech form
  2. POST /api/novatech/inquiries
  3. Zod + Turnstile
  4. Vercel OIDC
  5. states:StartExecution

Durable workflow

DynamoDB claims the submission. HubSpot is the CRM record. Email is isolated from that write.

  1. Step Functions
  2. DynamoDB claim
  3. HubSpot contact + deal + note
  4. SQS notifications
  5. 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

  1. 01Move rate limiting to shared storage so multiple instances agree
  2. 02Add alertable monitoring beyond allowlisted console logs
  3. 03Finish mobile-menu background inertness
  4. 04Use a verified Resend sending domain if visitor mail must reach arbitrary inboxes

Explore the public demo

Open the demonstration site, then return here via the demo chrome or browser Back.