Identity and Accounts: Who Exactly Is the Person Commenting?

· tech

#war-story#live-commerce#identity

📑 Contents

The FSM in the last chapter worked out “who bought what” — except that “who” is only a string of digits Facebook handed us. They may never have registered on our platform, may log in tomorrow, may never log in at all. And stock has to be held for them right now. This chapter is the problem I called “the most underestimated in the series” back in the overview: who exactly is the person commenting.

The order comes first, the account second

Ordinary e-commerce runs register → log in → order. Live commerce turns that completely around: at the moment of ordering, the other party may be nothing at all — not a member, no app installed, never logged in. All you know is that Facebook says some user id typed 2601+1.

The solution we had is, in hindsight, textbook layering: identity and account are two different things.

  • An identity is a fact: an fb user, an ig user, an own-studio user — the platform says “this id left a comment”, and that’s true without anyone registering. The moment an FSM batch hits a key, the fb user entity is created on the spot and the order is attached to it.
  • An account is an aggregate: it appears the day the customer logs into the app. It doesn’t own orders, it claims them — pulling the orders of every identity bound to it into one view.
identity: a fact fb user (PSID) created on the spot, at comment time ig user own-studio user account: an aggregate account appears only after login claims identities' orders, 1:N binding (can be multiple) cart item (reserves stock) attached to the fb user, not the account fbmsgtocartitem msg id · bidding key id aggregate view: claim it, don't move it
Identity is a fact, account is an aggregate: an order stays attached to its identity forever, and the account only pulls its bound identities into one view.

The key payoff of that split: an order never has to move house. Binding, unbinding, adding another identity — all of it touches only the association, never the order. Everything that touches money and stock stays pinned to the fact it was born from.

Every column is an id: the data model we had

The cart table looked like this back then:

  • One cart item table, with content type + object id (a generic foreign key) marking the source. An order from a live-stream message and an order added by hand on the storefront share one table and one set of quantity-adjustment and checkout logic; only “where it came from” differs. The two carts from the overview have a data-model answer: one table, polymorphic source.
  • Message orders also got an association table, fbmsgtocartitem: (fb_user_id, fb_msg_id, cart_item_id, bidding_key_id) — every column an id. Which means every order is traceable: which person, which comment, which sale round. A complaint of “I definitely typed +2, why is it +1?” gets reconstructed by following the msg id; a host re-calling an item gets cleanly wiped by following the bidding key id.
  • Sharp eyes will notice fb_user_id is already reachable through fb_msg — putting it in the association table is a deliberate violation of 3NF: the message table is huge, and the hottest query in the system (LWW overwrites need “this person’s orders for this key”) can’t be made to travel through it. This is textbook-safe denormalisation, because what’s copied is an immutable column — a message’s author never changes, so the copy can never drift. The practical test for normalisation is hiding right here: copy an immutable column and the risk is near zero; copy a mutable one and you’ve signed a lifetime synchronisation contract.

Facebook won’t tell you who they are: ASID and PSID

The real boss fight is at binding. Facebook’s privacy design is that the same person has different ids on different surfaces: comments and Messenger give you a PSID (page-scoped id), while a customer logging into your app with Facebook gives you an ASID (app-scoped id), and neither can be derived from the other. That isn’t a bug, it’s a wall Facebook built: it doesn’t want you casually joining “the person who commented on a page” to “a member of your app”. The narrow door they leave open is the Business Mapping API (ids_for_pages / ids_for_apps), which requires the app and the page to sit under the same Business Manager and pass business verification — and coverage is never 100%.

So binding became a funnel, catching a batch at each layer:

every fb user who ordered by comment (100%) Layer 1: log into the app → auto ASID↔PSID mapping Business Mapping API · most bind here Layer 2: private-reply win notice + one-time token the notice is the binding chance · tap, log in, bound Layer 3: support, by hand (~1%) multiple bindings · the hard cases the remainder approaches 0 — but is never 0
The binding funnel: automatic mapping takes the bulk, the token in the win notice takes another batch, support closes out the tail.

Layer two is worth pausing on. The win notice had to be sent anyway (a private reply telling the customer “you got it”), so attach a link carrying a one-time token, and the moment they tap it and log in you hold both the PSID (the message channel) and the ASID (the login) — the binding is deterministic, no guessing. Turning a matching problem into a flow problem is the same move as turning “detecting intent to order” into a DB lookup in the last chapter: don’t force the hard problem, convert it into an easy one. Every completed sale automatically shrinks the unbound tail, and only what’s left goes to a human.

The reality of binding: a family member’s account

A real case: one customer called their orders from their own Facebook, but at checkout kept logging in on a desktop as a family member’s account. The orders hung off their fb user while the account belonged to someone else — and it was settled by support doing a multiple binding: attach both identities to the same account, and the orders were all there.

That case is the best possible testimony for the identity model:

  • identity ≠ person. One person shows up wearing several identities: their own Facebook, a family member’s account, a new id after switching platforms. There is no “person” entity in the system, only identities and aggregates.
  • account to identity has to be 1:N, and N grows. Multiple binding isn’t a workaround, it’s what the model was always supposed to look like.
  • Best of all: the notorious “account merge” problem is sidestepped entirely by the 1:N model. Two identities each accumulate orders, and later turn out to be one person — the traditional move is to merge two accounts, which is an irreversible data migration whose conflict handling is every bit as hair-raising as conflict resolution in multi-leader replication. In a model where orders hang off identities and the account only aggregates, the answer is one more association row: a single insert, revocable any time. Merging is moving house; aggregating is adding a nameplate — and anything you can solve with a nameplate should never involve moving house.

Binding is of course authorisation — attaching an identity to an account hands that identity’s orders to that account. Bind the wrong person and you’ve moved someone else’s orders. That risk line belongs to the risk chapter.

What the identity layer would change in a rebuild

Honestly, I’d keep almost all of this: the split is right, orders on identities is right, the funnel is right, 1:N is right. A rebuild adds three things:

  1. Promote identity into a named layer. Back then fb user was a concrete table, the Instagram equivalent was handled by a different colleague, and the own studio was a third thing again. A rebuild would define one identity interface first (source + external id + per-source metadata), with fb, ig and own-studio as instances. Naming looks like navel-gazing, but it decides whether the next platform to arrive is “one more identity” or “another parallel stack”.
  2. The binding association carries provenance: bound_via (auto / token / manual), timestamp, operator. Manual binding by support especially needs an audit trail — binding is authorisation, and the human channel is the easiest to get wrong and the hardest to hold anyone accountable for. That’s also where this chapter meets the permissions chapter and the risk chapter.
  3. Turn the funnel into a dashboard. Each layer’s binding rate is a product metric, not engineering trivia: a drop in the automatic layer means the API broke, a drop in the token layer means notifications aren’t landing, a backlog in the manual layer means support needs more people. What’s left goes into a ticket queue rather than scattered through support’s inbox. The 1% of manual work isn’t a design failure, it’s the funnel’s natural residue — but it has to be visible, queued and trackable.

Reflections

There is no “person” in the system, only identities

The deepest lesson here is admitting that a person is not an id. A designer’s most natural arrogance is assuming one person, one account, account equals human — and then reality shows you the customer checking out on a family member’s account, the customer with three Facebook profiles, the customer who never registers but orders every month. The model we had held up because from day one it never pretended to know a “person”: it recorded only “which identity did what”, left “are these identities the same person?” to bindings to express, and allowed the answer to be added to at any time. A humble data model outlives a clever one.

The platform’s wall sets your floor; flow design sets your ceiling

We couldn’t climb the ASID/PSID wall — the mapping Facebook won’t give you, you don’t get. But “attach a token to the win notice” meant most bindings never needed that door opened at all: the user walks over and connects the two identities themselves. Build cross-platform products long enough and you learn this: your identity model’s floor is set by what the platform is willing to give you; its ceiling is set by the paths your flows lay down for the user. Complaining about API limits is unproductive; turning every unavoidable touchpoint — notification, checkout, support — into a binding opportunity is not.

That 1% of manual work deserves to be treated as a real feature

What the funnel can’t catch was bound by support, one case at a time — including the family-account kind that a machine will never guess right. Here I want to credit something we did well: our support tooling was built as a real feature, and it was good. The team’s whole origin was a boss fed up with unusable third-party tools deciding to build something better; plenty of engineers came from Grindr and cared intensely about UX on top of engineering quality — internal tools were first-class citizens from day one, with proper interfaces for multiple binding and cart adjustments. So support’s difficulty was in the cases themselves (whether to bind a family account is a judgment call, not an operation), not in tools fighting them. That shaped a judgment I’ve held since: most companies treat internal tools as second-class, “good enough if it works” — but support’s operating speed is your complaint response time, and internal tooling UX is part of the external experience. An engineer’s instinct is to automate 1% down to 0.1%, but there will always be a last mile the system can’t catch, and designing the interface for “a human does this” in advance is the mark of a mature system. It’s the same thing I kept repeating later when leading an SRE team: the end point of automation isn’t replacing people, it’s leaving people only the work that deserves a person.