Developer Docs

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: getOrCreateAlias handles 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:

  1. Info card explaining the anonymity system in plain language
  2. Alias table (expandable) listing all aliases with Malet scope and creation date
  3. 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 shareSupportContact with 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:

  1. Try findOne for existing alias
  2. If not found, create with the generated alias
  3. 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