Skip to content

Support, feedback and review: research findings that should shape the follow-up PRs #1265

Description

@teetangh

Durable findings from three research passes run 2026-08-27 alongside the #1195 support hub work. Recording them here so the follow-up PRs can cite evidence instead of re-deriving it. Related: #1021 (org-scoped support/feedback/quality signal), #739 (agentic support architecture).

1. Architecture verdict: keep Postgres, and the reason is control, not residency

The question was whether the support thread should move onto Stream Chat, since we already pay for Stream.

No. Three independent reasons:

  • Stream has no ticket primitive, and queryChannels has no indexed status / assignee / priority. An agent queue sorted by "oldest unassigned, breaching SLA" is not expressible.
  • Stream's own OSS helpdesk (Combase) keeps the record in MongoDB, not in Stream.
  • Org scoping would need Stream Teams, which is Elevate tier ($599+/mo) — for something the PR gets free from a select allowlist.

Sendbird Desk is the counter-shape that proves the rule: its Ticket carries status/priority/CSAT/SLA and exactly one field pointing at chat (channelUrl). Ably states the general principle outright — "History is not a database… an architectural anti-pattern."

The binding constraint on any third-party helpdesk vendor is not data residency — see §4 — it is that DPDP makes you answerable in two opposite directions for a processor you do not operate: cause it to erase per person (Act §8(7)(b)), and prove it retained a year (Rules 8(3)).

2. Our transport is correct; the flow shape is correct

Transport. Uber's in-app self-serve tree — the closest published analogue to a tap-through help flow — is a blocking synchronous RPC per step, with the server naming the next screen. Verbatim: "Today, we expect the user to wait while the flow is executing, with a visual indicator letting them know execution is in progress… push updates to the client (rather than having it be a blocking call, as it is today)." They file that as future work, not a defect. Websocket-first cases (Amazon Connect, Swiggy's messaging module) are websocket-first because they are genuinely chat products with humans in the loop.

Flow shape. lib/support/flows.ts is a data-shaped DAG (FlowDefinition{ entryNodeId, nodes: { id, kind, body, options[], action } }) that happens to be serialized as TypeScript. That is structurally the same object as Uber's JSON DAG and Airbnb's node-typed tree (Content / Contextual / Action). Storing it in code is a deliberate, documented tradeoff and at ~142 lines it is the right one. Moving it to rows later is a serialization change, not a rewrite — worth knowing before anyone proposes a rewrite.

When it becomes worth moving: both Airbnb and Uber built authoring platforms for one stated reason — removing engineers from flow edits. Airbnb: "needing engineers every time we had a product update or workflow change." Uber: "a means of expediting the authoring and maintainability of these decision trees was needed such that it did not require engineering effort." The trigger is ops wanting to edit copy, not node count.

Versioning, when we get there: Uber's gate is Draft → Shadow → Final → Live. Shadow runs in production taking no actions and showing the user nothing, randomly picking inputs; promotion requires ≥60% of nodes hit at least once and ≥24h in shadow; Final is immutable; one live version per policy; versions map to experiments.

3. Review design — two findings that are legally load-bearing

CSAT and public reviews must be separate objects. FTC 16 CFR §465.1(d) makes a bare star rating a "consumer review" — no text required. The preamble kills sentiment-gated solicitation explicitly: "solicitations made only to customers whom the business believes to be happy customers would not be 'generalized solicitations'." EU Recital 49 bans "publishing only positive reviews and deleting the negative ones."

The contamination filter must be sentiment-blind. The safe harbour is §465.7(b)(3) — reviews "wholly unrelated to the products or services offered". So "platform/support complaints don't become provider reviews" is permitted only if the same rule catches glowing platform praise identically. Fiverr's rule sits exactly there. Three published precedents for the fix: Fiverr removes reviews "that clearly concern Fiverr, the platform experience"; Uber statistically excludes ratings from riders who habitually rate low and those driven by traffic; Upwork quarantines feedback from flagged clients rather than adjudicating individual reviews.

D5 (CSAT ↔ thread) is answered by precedent. Uber binds CSAT to the terminal node of the flow execution, not to a ticket — and a ticket exists only on escalation (Airbnb creates one only when the wizard fails to deflect; Uber enters Cadence routing only on escalation). That matches what escalate() already does. So: attach CSAT to the terminal node, route low ratings to reason chips, and expose "Get help" only on the actionable reasons. Never auto-ticket a low star — it floods the queue and teaches users that rating low is the fast lane to a human.

Two-sided blind reveal raises volume and cuts retaliation but does not improve matching (Fradkin/Grewal/Holtz, Marketing Science 40(6):1013–1029, Airbnb experiment). Adopt it as fairness, not as a conversion play. Never incentivise reviews — the same programme found incentivised reviews more negative, with no revenue effect.

4. India: DPDP is not a localisation constraint, but Rule 8(3) is a retention floor

Verified against the official Gazette (No. 760, MeitY, 13 November 2025).

  • s.16 is a blacklist, the inverse of GDPR adequacy. Transfer is permitted by default and becomes unlawful only when the Government notifies a specific country. No country is notified. s.16 commences 13 May 2027.
  • Rule 15 restricts only making data available to a foreign State or its agencies — no approved-country list, no SCC equivalent, no TIA. Rule 13(4) localisation is SDF-only, category-by-category, and no category has been specified.
  • India has no general data-localisation mandate.
  • Rule 8(3) imposes a minimum ONE-YEAR retention floor on personal data, associated traffic data, and processing logs — "undertaken by it or on its behalf by a Data Processor". Official Illustration Case 2 is explicitly about a cloud processor. It opens "Without prejudice to sub-rules (1) and (2)", so unlike Rule 8(1)/(2) it does not depend on being a Third Schedule class (≥2 crore e-commerce / ≥50 lakh gaming / ≥2 crore social media) — it binds us, they do not.
    Scope correction: the retention is "for the purposes as specified in the Seventh Schedule", and the Seventh Schedule ([See rule 23(1) and 8(3)]) is a table of State-access purposes — sovereignty and security of India, performance of or disclosure under any law in force, and SDF-designation assessment — with an "Authorised person" column naming State officers. So 8(3) is a preservation-for-State-access duty, not a general business-records floor. It is still a floor, it still reaches processors, and it still constrains any "delete on request" design; it is simply narrower than a blanket retention mandate.
    The GDPR Art 17 tension is real but ordinary: Art 17(3)(b) carves out processing required by a Union or Member State legal obligation, which an Indian retention duty does not automatically satisfy for an EU data subject. Resolution is per-jurisdiction retention at row level, not a global TTL.
  • Rule 8(2) requires informing the data principal 48 hours before erasure completes — but it attaches to a Third Schedule erasure period, so it does not bind us today.

Design rule that falls out: RBI's "Storage of Payment System Data" (6 Apr 2018) is stricter than DPDP and is preserved by s.16(2). Keep payment-system data out of SupportMessage.body — reference an internal payment ID and resolve server-side. That single rule keeps the support store out of RBI scope.

A docs PR covering Rule 8(2)/(3) is in flight separately.

5. India competitor teardown — what is worth copying

Company Support shape Vendor
Practo In-app live agent chat (consumers); email (providers) In-house; KB on WordPress/WP Engine
Urban Company Server-driven decision tree (help-recovery service) In-house; Zendesk in SPF for the email leg
Unacademy Chat → ticket number → named officer → external council Freshdesk (India pod, fdind-)
Cult.fit KB-first, ticket behind login Freshdesk (US pod, fdus-)
Preplaced Intercom, gated behind login Intercom (fqbndbfp)
upGrad Phone/WhatsApp first + typed grievance form Freshchat
Topmate Email + web form Trengo (help centre currently dead)

Findings that should change what we build:

  • Practo computes an "Appointment Experience Score" from booking telemetry — no-shows, wait time, cancellation/reschedule rate, clinic ratings — entirely separate from written reviews. This is the strongest validation of the telemetry-derived quality signal: we own join/leave timestamps and nobody in the set uses them this way. Auto-detect provider absence at T+10 and offer rebook-or-refund without making the consultee argue.
  • Practo does NOT gate reviews on a platform booking ("Open Feedback"), enforcing reactively by takedown; only online consultations need appointment proof or a prescription, within 3 months. Worth weighing before we make appointmentId mandatory on ConsultantReview.
  • Practo publishes a score only above a threshold (≥10 recommendations, ≥5 appointment-experience) — stops one review defining a new consultant.
  • Urban Company rates bidirectionally and says in its Terms that ratings are an enforcement input — either side can be removed. That is the Upwork public-stars/private-score split expressed as a Terms clause.
  • Ticket-number-as-receipt is the connective tissue everyone else has and we do not. Unacademy's L1 chat issues a number that the L2 email and L3 form both consume. SupportTicket.id is a bare UUID — grep for ticketNumber|ticketRef|displayId|shortId returns nothing. Our escalation tells the user "our team will follow up here" and hands them no artifact.
  • The escalation ladder is a legal artifact in India, not a nicety. Every company in the set names a Grievance/Nodal Officer with a published SLA: Unacademy 24h ack / 7d resolve; Topmate 24h / 15d; upGrad 2 business days / 30d. CP (E-Commerce) Rules 2020 require ack ≤48h and redress ≤1 month; IT Rules 2021 tighten that to ack ≤24h / dispose ≤15 days for intermediaries. SupportTicket has no SLA fields today.
  • DpdpGrievance and SupportTicket being disconnected is CORRECT and now externally validated — Unacademy's Terms state their IT-Rules grievance officer "is not the point of contact for consumer grievances." Two pipes by design. Recording it so nobody "fixes" it later.
  • CCPA v. Ola (13 Oct 2024): the consumer must be able to choose refund mode — bank account or credit. Wallet-credit-only remedies are a regulatory risk; this lands on refundBookingPayment's credits rail.
  • CCPA Dark Patterns Guidelines 2023 name nagging, confirm-shaming ("are you sure you only want to give 3 stars?") and asymmetric star affordances. Constrains the review UI directly.

6. The metric we are missing

Every published support system has a deflection number — Swiggy's "bot efficacy percentage… the percentage of conversations resolved by the bot vs the support executives", Airbnb's "help 15% more people", Uber's terminal-node CSAT. We have no way to know what fraction of threads resolve without escalation. Cheap to add, and it is the number that tells us whether the tree works.

Not published anywhere (do not infer)

  • Amazon.com's retail help architecture. Nothing first-party exists; do not infer it from Amazon Connect.
  • Where Swiggy stores its tree, or whether it has an authoring UI. The widely-cited "relational database" claim is third-party analysis, not a Swiggy statement.
  • Support CSAT after ticket close — no company in the India set publishes one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions