Part 7 of 7 · Referral tracker series ~11 min read

Engineering reference: the referral tracker architecture

This is the referral tracker with the friendly labels removed: the real resource names, the runtime, the table key schemas, the single public Function URL that both redirects and receives webhooks, the exactly-once ledger, and the IAM scope. If you want to build it rather than understand it, start here.

Key takeaways

  • Six Lambda functions, all Python 3.14 on arm64, with the sign-up and conversion webhooks buffered through one SQS queue with a dead-letter queue.
  • One public surface: a single Lambda Function URL on rft-edge that serves the link redirect and takes the sign-up and conversion webhooks — no API Gateway.
  • Five DynamoDB tables, all on-demand: codes, a TTL’d click log, referrals keyed on the referred identity, a rewards ledger, and an append-only audit.
  • Exactly-once payout is a property of two writes: a code claim, and a conditional flip of the referral from pending to rewarded.
  • One Bedrock model, Claude Haiku 4.5 via Global cross-Region inference, called only by the minter and the rewarder. Single region, eu-west-2.

The architecture, for engineers

This is the same system as Part 1 with the friendly labels removed and the real resources named. Everything is in one region, eu-west-2 (London), in one account. There is no API Gateway, no NAT Gateway, and nothing always-on; the only inbound surface is one Lambda Function URL that both redirects link clicks and receives the store’s webhooks, outbound messaging goes through SNS and SES, and slow work is buffered on a single SQS queue.

Engineering architecture: the edge Function URL, SQS, six Lambdas, five DynamoDB tables, Bedrock, SNS and SES, and the expiry sweep An external box at the top, the store and checkout, posts link clicks, sign-up events, and conversion webhooks to a single Lambda Function URL, rft-edge, inside a dotted AWS account container labelled eu-west-2. rft-edge resolves the code in the DynamoDB codes table rft-codes, writes each click to the DynamoDB click log rft-clicks with a TTL equal to the window, 302-redirects the visitor, and for sign-up and conversion events verifies the signature with a secret from Secrets Manager and enqueues a typed job on the SQS queue rft-jobs with dead-letter queue rft-jobs-dlq. From the queue, three processing Lambdas run, all Python 3.14 arm64: rft-minter claims a code in rft-codes with a conditional write and makes a Bedrock Claude Haiku 4.5 call to write the invite; rft-attributor reads rft-clicks for the last-touch click and opens a pending referral keyed on the referred identity in the referrals table rft-referrals; and rft-rewarder flips that referral from pending to rewarded with a conditional write, writes one entry per side to the rewards ledger rft-rewards, makes a Bedrock call for the two thank-you notes, and sends via SNS SMS or SES email. rft-screen is invoked by the attributor and rewarder to run the self-referral, duplicate, and velocity checks, emailing held cases to your team via SES. On the left, EventBridge Scheduler drives rft-sweep, which expires pending referrals in rft-referrals whose last-touch window has elapsed. The five DynamoDB tables sit in a column on the right: rft-codes keyed by code, rft-clicks keyed by attribution id and click timestamp with a TTL, rft-referrals keyed by referred id, rft-rewards keyed by referral id and side, and rft-audit keyed by event id and timestamp. Secrets Manager holds two provider secrets. Bedrock is called only by rft-minter and rft-rewarder. Store & checkout clicks, sign-up + conversion webhooks AWS account · eu-west-2 rft-edge Function URL · redirect + webhooks events in rft-jobs SQS + rft-jobs-dlq EventBridge Scheduler 1 rule rft-sweep expire pending referrals rft-minter claim code + Bedrock invite rft-attributor last-touch → pending referral rft-rewarder flip + ledger + Bedrock thanks rft-screen self-ref, dupe, velocity Secrets Manager 2 provider secrets Bedrock Haiku 4.5, Global SNS (SMS) reward text SES thank-you / review Store reward API fulfil credit rft-codes PK code · GSI referrer rft-clicks PK attrib_id, SK ts · TTL rft-referrals PK referred_id rft-rewards PK referral_id, SK side rft-audit PK event_id, SK ts All Lambdas: Python 3.14, arm64 · CloudWatch Logs 7-day retention · AWS Budgets cost alarm · one Function URL, no API Gateway, no NAT
Fig 7. The referral tracker drawn for engineers: one Function URL on rft-edge that redirects and receives webhooks, an SQS-buffered set of six Lambdas, five DynamoDB tables, Bedrock called only by the minter and rewarder, SNS and SES for messaging, and one scheduled expiry sweep. One region, one account, no API Gateway.

Lambda functions

Six functions, all Python 3.14 on arm64, all with CloudWatch Logs at 7-day retention. Each does one job and hands off; the SQS queue (rft-jobs, with rft-jobs-dlq as its dead-letter queue after five attempts) decouples the store’s webhooks from the slower model, ledger, and store-API calls. The link redirect is the one path that is not queued — it runs synchronously inside rft-edge so the click is logged and the 302 returned in milliseconds.

  • rft-edge — the only public surface. Backs the single Lambda Function URL and handles three event kinds. For a link click (GET /r/{code}): resolves the code in rft-codes, writes a row to rft-clicks, sets the attribution token, and returns a 302. For a sign-up or conversion webhook: verifies the provider signature against the secret, and enqueues a typed job on rft-jobs. For a mint request: enqueues a mint job. Nothing slow happens here.
  • rft-minter — SQS-triggered on mint jobs. Reuses the customer’s active code via the rft-codes referrer GSI or claims a new one with a conditional write, pins the reward terms and window, makes the single Bedrock call for the invite, injects the real URL, and returns the link.
  • rft-attributor — SQS-triggered on sign-up jobs. Reads rft-clicks for the most recent valid click within the window, resolves the referred person to a durable identity, calls rft-screen, and opens a pending item in rft-referrals (or leaves the sign-up unattributed).
  • rft-rewarder — SQS-triggered on conversion jobs. Flips the referral pending → rewarded with a conditional write, re-checks rft-screen, writes one entry per side to rft-rewards, calls the store reward API to fulfil, makes the single Bedrock call for the two thank-you notes, and sends via SNS or SES.
  • rft-screen — invoked synchronously by the attributor and rewarder. Runs the deterministic self-referral, duplicate, and velocity checks over rft-referrals, rft-clicks, and the identity fields; returns block / hold / pass, and on a hold emails your team via SES with the rule that fired and the evidence.
  • rft-sweep — scheduled. Queries rft-referrals for pending items whose window has elapsed with no conversion and marks them expired, so a code shared into the void is tidied away rather than lingering as an open liability.

Data stores, schedules, and messaging

  • DynamoDB (all on-demand). rft-codes — PK code, with a GSI on referrer_id to find a customer’s active code; holds the owner, the pinned reward terms and window, and a status (active / retired). rft-clicks — PK attribution_id (the token), SK click_ts, with a TTL equal to the window so expired clicks vanish and can’t attribute; queried newest-first for last touch. rft-referrals — PK referred_id (the referred person’s durable identity), one item per referred customer so repeat sign-ups collapse to one; carries referrer_id, code, state (pending / rewarded / held / rejected / expired), the pinned terms, and the identity fingerprints. rft-rewards — PK referral_id, SK side (referrer / friend), the exactly-once ledger of what was actually paid. rft-audit — PK event_id, SK ts, append-only, holding each event and the facts a decision was made on.
  • Function URL. One, on rft-edge, with provider signature verification in-function for the webhooks; AuthType NONE at the edge because authenticity is enforced by the shared secret, not by IAM, and the redirect route is public by nature. No API Gateway.
  • SNS and SES. SNS sends the reward-confirmation SMS where a mobile is the contact; SES sends the invite (if delivered by the business rather than pasted by the customer), the thank-you emails, and the fraud-review email to the team from a verified domain with DKIM.
  • EventBridge Scheduler. One rule — rft-sweep at rate(1 hour), expiring pending referrals past their window; the granularity is loose because expiry is not time-critical.
  • Secrets Manager. Two secrets — the webhook signing secret and the store/reward API key — fetched at call time, never in env vars or the settings doc.
  • Bedrock. Model id anthropic.claude-haiku-4-5 via the Global cross-Region inference profile, invoked only by rft-minter and rft-rewarder.

Exactly-once and failure handling

Two properties carry the whole design, and both are enforced by DynamoDB conditions rather than by hope. Codes are unique because minting claims each with attribute_not_exists(code); a collision fails the write and the minter draws again. Payouts happen once because the rewarder flips rft-referrals from pending to rewarded under ConditionExpression state = pending; the first conversion webhook wins the flip, and any duplicate finds the condition false and stops before touching the ledger. Keying rft-referrals on the referred person’s durable identity does the third piece of work — the same human signing up under many emails resolves to one item, so they can only ever be rewarded once. The rft-rewards ledger adds belt-and-braces with a conditional put per (referral_id, side).

Failure handling is ordinary and boring, which is the point. The webhook-driven work runs from rft-jobs; a handler that throws is retried by SQS and, after five attempts, parked in rft-jobs-dlq for inspection rather than lost or looped. The store reward API is called after the ledger write, so a fulfilment that fails can be retried idempotently against a ledger that already knows the payout is owed — the credit is never double-issued because the ledger, not the API call, is the source of truth. Bedrock is best-effort on both paths: if the invite or thank-you call is slow or errors, a fixed template goes out and the referral is unaffected, because the model only ever wrote words.

IAM scope and region

Each function gets its own execution role scoped to exactly what it touches, no wildcards. rft-edge can read rft-codes, write rft-clicks, read the signing secret, and send to rft-jobs — it cannot call Bedrock, SNS, or the store API, and it cannot touch rft-referrals or the ledger. rft-minter can conditionally write rft-codes and invoke Bedrock, nothing more. rft-attributor can read rft-clicks, write rft-referrals, and invoke rft-screen. rft-rewarder is the only role that can flip a referral, write rft-rewards, call the store reward API, publish to SNS, and invoke Bedrock — and it cannot delete from any table. rft-screen reads its inputs and sends via SES only. rft-sweep can query and update rft-referrals and has no inbound surface at all. Everything runs in eu-west-2; the only cross-Region path is Bedrock’s Global inference profile, which routes the model call for capacity and is not a data store. An AWS Budgets alarm watches the monthly spend — with a runaway loop (a webhook storm, a code being sprayed) the thing most likely to move it, a Budgets alert is the cheapest early warning that something is running hot, and it pairs with a CloudWatch alarm on the rft-jobs-dlq depth.

That’s the whole system: a redirect that logs a click, a queue that carries the rest, six small functions, five tables, and two conditional writes that make the money move exactly once. It turns a customer’s offhand recommendation into a link you can follow and a reward you can trust — and it does it for less than the price of the coffee the referrer and their friend might have had while they talked you up.

Design rules that shaped the build

  • One job per function. Six small Lambdas beat one that does everything; the queue decouples the slow calls from the public edge.
  • One public surface. Only rft-edge is reachable from outside, on a single Function URL that both redirects and takes webhooks.
  • Exactly-once by condition. A code claim and a pending-to-rewarded flip, both conditional writes, are what make the payout happen once.
  • Least privilege, per role. Only the rewarder can move money; only the minter and rewarder can call Bedrock; the edge can’t reach either.
  • The ledger is the truth. Fulfilment is retried against rft-rewards, so a failed store call never double-credits.
  • One region, one model. eu-west-2 throughout; Bedrock Haiku 4.5 via Global inference, called only at mint and conversion.
All posts