Support Ticket Anonymity
When a Visitor contacts a Malet's support team through uChat, the platform shields their real identity behind a pseudo-anonymous alias. The Malet Owner sees a stable, deterministic alias (e.g., Customer-A7x3B) instead of the Visitor's name, email, or phone number. If the Visitor needs more hands-on help โ a refund, delivery correction, or callback โ they can explicitly share their real contact info on a per-conversation basis via a one-way door mechanism.
This architecture ensures Malet Owners can track support history across conversations without mining real consumer identities.
Architecture Overview
The system consists of three entities in the nodes subgraph:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ SupportAlias Entity โ
โ โ
โ userId โโโโโโโโ real consumer ID โ
โ subjectId โโโโโ Malet/Org scope โ
โ alias โโโโโโโโโ Customer-XXXXX โ
โ โ
โ Unique index: (userId, subjectId) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ SupportContactShare Entity โ
โ โ
โ userId โโโโโโโโ consumer ID โ
โ issueId โโโโโโ conversation ID โ
โ sharedAt โโโโโโ timestamp โ
โ โ
โ Unique index: (userId, issueId) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Key Properties
- Deterministic: Same consumer always appears as the same alias on a given Malet (stable across sessions).
- Scoped: Aliases are per (userId, subjectId) pair โ a Visitor has different aliases on different Malets.
- Idempotent:
getOrCreateAliashandles race conditions via unique index + catch-and-retry. - One-way door: Once contact info is shared on a conversation, it cannot be revoked.
GraphQL API
Queries
# List all support aliases for the authenticated user
query MySupportAliases {
mySupportAliases {
userId
subjectId
alias
createdAt
}
}
Federated ResolveFields on User
The SupportProxyResolver extends the User type with federated fields that enforce privacy gates:
# Get alias for a specific Malet (creates if needed)
type User {
supportAlias(subjectId: ID!): String
# Real email โ only if consumer shared contact on this issue
supportContactEmail(issueId: ID!): String
# Real phone โ only if consumer shared contact on this issue
supportContactPhone(issueId: ID!): String
}
Mutations
# One-way door: share real contact info on a support conversation
mutation ShareSupportContact($issueId: ID!) {
shareSupportContact(issueId: $issueId) {
isNewShare
issueId
}
}
The issueId for the shareSupportContact mutation maps to the uChat conversation ID. When a Visitor opens a SUPPORT type conversation in uChat, that conversation's id is passed as the issueId.
Frontend Integration
Query Module
All GraphQL operations are defined in src/lib/queries/supportAlias.ts:
import {
MY_SUPPORT_ALIASES,
SHARE_SUPPORT_CONTACT,
type SupportAlias,
type MySupportAliasesResponse,
type ShareSupportContactResponse
} from '$lib/queries/supportAlias';
Settings โ Privacy Panel
The SupportAliasSection.svelte component renders in Settings โ Privacy & Security, between Content Visibility and Two-Factor Authentication. It shows:
- Info card explaining the anonymity system in plain language
- Alias table (expandable) listing all aliases with Malet scope and creation date
- Empty state for users with no support interactions yet
uChat Support Banner
When a Visitor opens a SUPPORT conversation in uChat, a banner appears between the header and the message area:
- ๐ก๏ธ "You're anonymous in this conversation" โ explains that the Malet Owner sees their alias
- "Share my contact info" button โ triggers
shareSupportContactwith the conversation ID - Browser
confirm()dialog warns this is a one-way door - After sharing, the button is replaced with a green "โ Contact shared" badge
Privacy Gate Flow
Visitor opens SUPPORT conversation
โ
โผ
Banner shows: "You're anonymous"
โ
โโโ Visitor sends messages โ Owner sees "Customer-A7x3B"
โ
โโโ Visitor clicks "Share my contact info"
โ โ
โ โผ
โ confirm() dialog warns of one-way door
โ โ
โ โผ
โ shareSupportContact(issueId: conversationId)
โ โ
โ โผ
โ Owner can now query supportContactEmail / supportContactPhone
โ
โโโ Visitor does nothing โ Owner never sees real identity
Backend Implementation
Alias Generation
Aliases use nanoid with a 5-character alphanumeric suffix:
const ALIAS_PREFIX = 'Customer';
const nanoid = customAlphabet(
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
5
);
// Result: "Customer-A7x3B"
Race Condition Handling
The getOrCreateAlias method handles concurrent requests gracefully:
- Try
findOnefor existing alias - If not found,
createwith the generated alias - If a
11000(duplicate key) error fires, another request won the race โ fetch the winning record
File Map
| File | Purpose |
|---|---|
apps/nodes/src/actors/user/support-alias.entity.ts |
SupportAlias Mongoose schema |
apps/nodes/src/actors/user/support-contact-share.entity.ts |
SupportContactShare schema |
apps/nodes/src/actors/user/support-alias.service.ts |
Alias lifecycle + contact share logic |
apps/nodes/src/actors/user/support-proxy.resolver.ts |
GraphQL resolver with @ResolveField gates |
src/lib/queries/supportAlias.ts |
Frontend GraphQL queries + types |
src/lib/components/settings/SupportAliasSection.svelte |
Settings alias viewer |
src/routes/uchat/+page.svelte |
Support anonymity banner + share button |
Related
- Privacy & Security APIs โ Content visibility matrix, profile privacy toggles, and GDPR data controls
- uChat Client SDK โ E2EE messaging infrastructure, conversation types, and
SUPPORTconversation flow - User Identity Resolution โ Shared
profileResolver.tsthat prevents raw UUID exposure across UI surfaces - Handle System & Sigil Taxonomy โ Pipe sigil taxonomy (
v|handle) used for user identification