Building a Vendor Referral Tracking System: Lambda, DynamoDB, and CloudFront Integration

This post documents the complete implementation of a referral tracking infrastructure for the JADA vendor program, deployed across AWS Lambda, DynamoDB, and CloudFront. The system captures vendor-driven traffic, logs clicks server-side, and attributes conversions back to specific vendor partners.

What Was Built

A lightweight referral endpoint that:

  • Accepts vendor codes via GET /ref/{code} requests
  • Logs each click to DynamoDB with vendor name, timestamp, and running click count
  • Redirects guests to the primary domain (queenofsandiego.com) with UTM parameters for GA4 attribution
  • Supports seven active vendors: Solare, Smallgoods, Puesto, Board & Brew, Whole Foods, Total Wine, and Gianni Buonomo Vintners

Technical Architecture

Lambda Handler Routing

The referral endpoint lives in the existing SCC Lambda function deployed at /tmp/scc-deployed/lambda_function.py. The handler implements path-based routing before the authorization gate:

def lambda_handler(event, context):
    path = event.get('rawPath', '')
    
    # Public routes (no auth required)
    if path.startswith('/ref/'):
        return handle_referral_click(event)
    
    # Protected routes require auth token
    auth_token = event['headers'].get('authorization', '')
    if not validate_auth(auth_token):
        return {'statusCode': 401, 'body': 'Unauthorized'}
    
    # Protected endpoint logic follows...

This pattern ensures the referral endpoint is publicly accessible while keeping dashboard and admin APIs protected. The key insight: authorization gates should be applied selectively, not globally to all routes.

DynamoDB Click Logging

The handle_referral_click function extracts the vendor code from the path and records the click:

def handle_referral_click(event):
    code = event['rawPath'].split('/ref/')[-1]
    vendor_name = VENDOR_CODES.get(code)
    
    if not vendor_name:
        return {'statusCode': 404, 'body': 'Vendor not found'}
    
    # Log to DynamoDB
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('vendor-referrals')
    table.update_item(
        Key={'vendor_code': code},
        UpdateExpression='SET click_count = click_count + :inc, last_click = :ts',
        ExpressionAttributeValues={
            ':inc': 1,
            ':ts': int(time.time())
        }
    )
    
    # Redirect with UTM params
    return {
        'statusCode': 302,
        'headers': {
            'Location': f'https://queenofsandiego.com/?utm_source={code}&utm_medium=referral'
        }
    }

DynamoDB provides atomic increment operations, eliminating race conditions when multiple clicks arrive simultaneously. The UpdateExpression syntax ensures accurate click counting without read-before-write logic.

CloudFront and API Gateway Configuration

Behavior Routing

The CloudFront distribution for shipcaptaincrew.queenofsandiego.com includes two behaviors:

  • Primary behavior (path /*): Routes to the origin serving dashboard HTML and static assets
  • /ref/* behavior (created new): Routes exclusively to the API Gateway integration, with Compress: true and cache TTL set to 0

CloudFront distribution ID: E1ABC2DEF3GHIJ (exact ID withheld for security). The behavior ordering matters—more specific paths must be evaluated first. The /ref/* behavior was inserted before the catch-all /*.

API Gateway Route Configuration

The SCC API Gateway includes a catch-all route that maps ALL paths to the Lambda function:

Route: $default (matches any path and method)
Integration: Lambda function scc-lambda-prod
Payload format: 2.0 (ALB/Lambda proxy format v2)
Authorization: NONE (referral endpoint must be public)

The Lambda function itself handles routing logic internally. This design avoids the complexity of managing dozens of API Gateway routes while keeping authorization decisions in Lambda where context is available.

Deployment Process and Challenges

Initial Authorization Block

The first deployment attempt failed because the API Gateway had a global authorizer attached to the $default route. Referral requests were rejected before reaching Lambda. Solution: remove the authorizer from the $default route, move authorization logic into Lambda, and apply it selectively to protected endpoints only.

CloudFront Cache Invalidation

After deploying the /ref/* behavior to CloudFront, initial tests returned 403 Forbidden. The issue: CloudFront cache behavior conflicted with the new routing. Solution: explicitly invalidate the /ref/* path pattern:

aws cloudfront create-invalidation \
  --distribution-id E1ABC2DEF3GHIJ \
  --paths "/ref/*"

CloudFront distributions typically take 60-90 seconds to propagate behavior changes globally. Polling the deployment status before testing prevents false negatives.

Testing the Redirect Chain

End-to-end testing verifies the complete flow:

curl -i -L https://shipcaptaincrew.queenofsandiego.com/ref/boardbrew

# Expected flow:
# 1. 302 Found (from Lambda)
# 2. Location: https://queenofsandiego.com/?utm_source=boardbrew&utm_medium=referral
# 3. Final 200 OK from primary domain

Using -L follows redirects; -i shows response headers. This confirms both the Lambda redirect logic and the DynamoDB side-effect.

Key Design Decisions

  • DynamoDB over RDS: Atomic increment operations and millisecond-scale latency justify a NoSQL approach for simple click counting. Relational databases would require locking or eventual consistency tradeoffs.
  • Pre-auth Routing in Lambda: Instead of managing multiple API Gateway routes and authorizers, centralized routing in Lambda simplifies deployment and keeps all business logic in one place.
  • CloudFront Behavior Specificity: A dedicated /ref/* behavior with zero cache TTL ensures redirects are always fresh while avoiding cache misses on the dashboard. Path-based routing at the CDN level is more efficient than forcing all traffic through Lambda.
  • Vendor Code Mapping: A simple dictionary (VENDOR_CODES) in Lambda maps short codes to full vendor names. This is faster than DynamoDB lookups and keeps configuration in code for easy auditing.

Monitoring and Next Steps

Click counts are queryable directly from the vendor-referrals DynamoDB table. CloudWatch Logs captures Lambda invocations with full event context, enabling debugging of malformed requests or edge cases.

Future enhancements could include:

  • Dashboard card showing