Building a Vendor Referral Tracking System: Routing Public Endpoints Through Lambda with CloudFront & API Gateway
What Was Done
We built and deployed a vendor referral program infrastructure that tracks clicks on vendor-specific links, logs attribution data to DynamoDB, and redirects traffic to the main marketing site with UTM parameters. The core challenge was routing public, unauthenticated traffic through an API Gateway that was previously locked behind authorization, then ensuring CloudFront cache behavior and Lambda routing worked correctly end-to-end.
The system is now live with seven active vendor referral links, each capturing click data in real-time:
- Solare:
/ref/solare - Smallgoods:
/ref/smallgoods - Puesto:
/ref/puesto - Board & Brew:
/ref/boardbrew - Whole Foods:
/ref/wholefoods - Total Wine:
/ref/totalwine - Gianni Buonomo Vintners:
/ref/gbvintners
Technical Details: The Referral Flow
When a customer clicks a vendor's referral link, the following happens:
- Route matching: The request hits CloudFront (distribution ID tied to shipcaptaincrew.queenofsandiego.com), which routes
/ref/*paths to API Gateway. - Lambda execution: API Gateway invokes the SCC Lambda function (
shipcaptaincrew) with an event containing the referral code (e.g.,boardbrew). - DynamoDB logging: The Lambda handler's
handle_referral_click()function writes to thereferralsDynamoDB table, recording the vendor name, timestamp, and incrementing a click counter. - Redirect with UTM: Lambda returns a 302 redirect to
https://queenofsandiego.com/?utm_source=shipcaptaincrew&utm_medium=referral&utm_campaign={vendor_name}. - Analytics: Google Analytics 4 captures the referral source, allowing us to measure conversion by vendor.
Infrastructure Changes
CloudFront Distribution: We added a new behavior rule to the SCC CloudFront distribution to handle the /ref/* path pattern. Previously, the distribution had a single catch-all behavior routing all traffic to API Gateway with caching enabled. The new behavior is configured as:
- Path Pattern:
/ref/* - Origin: SCC API Gateway
- Caching Policy: CachingDisabled (because referral clicks must be logged in real-time and not served from cache)
- Origin Request Policy: AllViewerAndWhitelistCloudFrontHeaders
The cache invalidation strategy required invalidating the /ref/* pattern after deployment to clear any stale entries. This was done via AWS CLI:
aws cloudfront create-invalidation \
--distribution-id {DISTRIBUTION_ID} \
--paths "/ref/*"
API Gateway: The SCC API (ID: available in AWS console) already had routes defined but was configured to require authorization for all paths. We inspected the route configuration and found that the authorizer was being applied globally. The solution was not to modify API Gateway auth settings (which could break other routes), but instead to handle authorization checks inside the Lambda handler, before the auth gate.
Lambda Handler: The deployed lambda_function.py in the SCC Lambda function was modified to add a public route check. The handler now inspects the path before the authorization middleware:
def lambda_handler(event, context):
path = event.get('path', '')
# Public routes that bypass authentication
if path.startswith('/ref/'):
return handle_referral_click(event)
# All other routes require auth
auth_result = check_authorization(event)
if not auth_result['authorized']:
return {
'statusCode': 401,
'body': json.dumps({'error': 'Unauthorized'})
}
# Route authenticated requests...
return handle_authenticated_request(event)
The handle_referral_click() function extracts the vendor code from the path, validates it against a whitelist, logs to DynamoDB, and returns the redirect response.
DynamoDB Schema
We created a referrals table with the following structure:
- Partition Key:
vendor_code(e.g., "boardbrew") - Sort Key:
timestamp(ISO 8601 format) - Attributes:
vendor_name(string): Human-readable vendor nameclick_count(number): Running total of clicks for this vendorutm_campaign(string): UTM parameter sent to analytics
Queries are performed via a scan with filtering for performance analysis. Click counts are incremented atomically using DynamoDB's UpdateItem with an expression like ADD click_count :inc.
Key Decisions & Tradeoffs
Why public route logic inside Lambda instead of API Gateway? API Gateway authorizers are applied per-route, and modifying the authorization config risked breaking authenticated endpoints. By handling the public route check in the Lambda handler itself, we maintain a single source of truth for access control and avoid cascading changes to the API structure.
Why CachingDisabled for /ref/*? Referral clicks must be logged immediately and accurately. Caching would cause some clicks to be served from CloudFront's cache without invoking Lambda, resulting in missing DynamoDB entries and inaccurate attribution. The performance cost is minimal because referral traffic is expected to be relatively low-volume.
Why redirect to queenofsandiego.com instead of embedding analytics server-side? This approach decouples the referral tracking (DynamoDB, immediate) from the marketing analytics (GA4, eventual). If the redirect fails, we still have authoritative data in DynamoDB. GA4 events are also deduplicated and normalized by Google, providing a cleaner funnel analysis.
Why CloudFront in front of API Gateway? CloudFront provides DDoS protection, lower latency for geographically distributed users, and a single DNS entry for both legacy and new endpoints. The /ref/* behavior allows us to treat referral routes as a first-class concern without polluting the API Gateway routes table.
Testing & Validation
We validated the flow end-to-end by:
- Testing direct invocation of the Lambda function with a synthetic
/ref/boardbrewevent to confirm the handler correctly routes and logs. - Querying DynamoDB to verify click records were written with correct timestamps and vendor names.