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-edgethat 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.
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 inrft-codes, writes a row torft-clicks, sets the attribution token, and returns a302. For a sign-up or conversion webhook: verifies the provider signature against the secret, and enqueues a typed job onrft-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 therft-codesreferrer 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. Readsrft-clicksfor the most recent valid click within the window, resolves the referred person to a durable identity, callsrft-screen, and opens a pending item inrft-referrals(or leaves the sign-up unattributed).rft-rewarder— SQS-triggered on conversion jobs. Flips the referralpending → rewardedwith a conditional write, re-checksrft-screen, writes one entry per side torft-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 overrft-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. Queriesrft-referralsfor pending items whose window has elapsed with no conversion and marks themexpired, 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— PKcode, with a GSI onreferrer_idto find a customer’s active code; holds the owner, the pinned reward terms and window, and astatus(active/retired).rft-clicks— PKattribution_id(the token), SKclick_ts, with a TTL equal to the window so expired clicks vanish and can’t attribute; queried newest-first for last touch.rft-referrals— PKreferred_id(the referred person’s durable identity), one item per referred customer so repeat sign-ups collapse to one; carriesreferrer_id,code,state(pending/rewarded/held/rejected/expired), the pinned terms, and the identity fingerprints.rft-rewards— PKreferral_id, SKside(referrer/friend), the exactly-once ledger of what was actually paid.rft-audit— PKevent_id, SKts, 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 NONEat 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-sweepatrate(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-5via the Global cross-Region inference profile, invoked only byrft-minterandrft-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-edgeis 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-2throughout; Bedrock Haiku 4.5 via Global inference, called only at mint and conversion.