Skip to content

Data model

This page answers where every kind of user-facing data lives. Schema source of truth in the monorepo: apps/server/src/db/schema.ts.

Mental model: chat ≠ channel

SurfaceCanonical storeWhat the user sees
Support chat (messages, conversation state)Intercom conversations + partsHub → Support thread
TicketsIntercom tickets + partsHub → Tickets
AW Channel postsPostgreSQLHub → Channel feed
Comments on postsPostgreSQLComment thread under a post
CSAT / complaintsPostgreSQL (admin panel)Close → rating; complaint form

Support messages are not copied into Postgres as a message log. Channel comments are not stored in Intercom.

Ownership matrix

DataCanonical storeRelated PostgresObject storage (S3 / disk)Client
Support messages / conversationsIntercomconversation_reads, conversation_durationsChat attachment bytesIn-memory liveStore only
Tickets + ticket partsIntercomcsat_ratings (rated overlay)RN tickets-seen is session-local
CSAT ratingsPostgres csat_ratingsBest-effort Intercom tag/note
ComplaintsPostgres complaintsBest-effort Intercom note
Channel posts, buttons, media metaPostgres channels, channel_posts, post_attachments, post_buttonsSoft status lifecycleMedia / poster bytes
Channel commentsPostgres channel_commentscomment_attachments, reports, bans, mods, eventsComment image bytesIn-memory while thread open
Reactions / views / button clicksPostgres post_reaction_*, post_views, post_button_clicksDenormalized view_count on postsView dedupe in memory
Channel unreadPostgres channel_readsHub badge also client-derived
Devices / push tokensPostgres devicesRegistered from SDK props
Notification prefsPostgres notification_settings
userId ↔ Intercom contactPostgres user_contacts + Intercom contactContact profile attrs on IntercomOpaque to SDK
Reasons / topic listsPostgres reasonsServed as GET /api/config
Admin usersPostgres admin_usersscrypt password hash
Admin sessionSigned HMAC bearer (not a DB table)Revoked by deactivating usersessionStorage in admin SPA
Admin auditPostgres admin_logs
Webhook dedupPostgres processed_eventsAuto-delete after ~7 days
Host session JWTMinted by your appValidated with JWT_SECRET; not storedPassed as sessionToken

Layers

┌──────────────────────────────────────────────────────────┐
│ Intercom                                                  │
│  conversations, messages, tickets, contact profile/tags   │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ PostgreSQL (@aw-chat/server)                              │
│  maps, reads, CSAT, complaints, channel, comments, admin  │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ S3 (or UPLOAD_DIR)                                        │
│  chat / channel / comment file bytes                      │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ Client SDK                                                │
│  React state only — no durable chat/comment cache         │
└──────────────────────────────────────────────────────────┘

Support chat & tickets (Intercom)

  • Each support request is an Intercom conversation. The in-app UI shows one continuous thread; closed requests stay in history and a new request appends below.
  • Message bodies, operator HTML, and conversation state (open / closed / snoozed) live in Intercom.
  • Postgres only keeps:
    • conversation_reads — last-read watermark for unread badges.
    • conversation_durations — timing for admin analytics (on close webhook).
    • csat_ratings — whether this conversation/ticket was rated (and stars).
  • Attachments: uploaded via POST /api/files/upload → S3 (or disk) → public signed URL handed to Intercom as an attachment.

Channel comments (Postgres)

  • Table channel_comments (status: visible / hidden / deleted).
  • Images in comment_attachments → storage keys on S3/disk.
  • Moderation: comment_reports, comment_bans, comment_moderators, comment_moderation_events (DLP hits store a redacted snippet only).
  • Client APIs under /api/channel/.../comments — see Comments guide and API.

Channel posts (Postgres)

  • channels, channel_posts (draft / scheduled / published / hidden / deleted).
  • Media metadata in post_attachments; CTA buttons in post_buttons.
  • Engagement: reactions, views, clicks, channel_reads.

Files

PathBytesMetadata
Chat attachmentsS3 or UPLOAD_DIROwner-tagged key; Intercom holds the URL
Channel mediaS3 or diskpost_attachments.storage_key
Comment imagesS3 or diskcomment_attachments.storage_key

Public download: GET /files/:id?t=<hmac> (short-lived signature). Authenticated download: GET /api/files/:id. Production requires REQUIRE_S3=1.

Retention

WhatPolicy
Intercom conversations / tickets / contactsIntercom workspace retention (outside this app)
DATA_RETENTION_DAYS setDaily prune of post_button_clicks and admin_logs older than N days
processed_eventsRows older than ~7 days removed
post_views / reactions / comments / CSAT / complaintsNot auto-pruned
Local disk uploads without S3Ephemeral in containers unless you mount a volume

Default: DATA_RETENTION_DAYS empty → analytics/audit kept forever.

In-memory (not durable)

Per process (lost on restart; not shared across replicas):

  • Intercom read-coalescing cache
  • /api rate-limit counters
  • Admin login lockout map
  • Circuit-breaker state

Before running more than one API replica, move shared state to Redis — see Deployment.

Client persistence

The SDKs keep chat, tickets, channel, and comments in React state for the mounted session. Unmount / process kill drops that cache; the next open re-fetches from the backend (and Intercom via the backend). There is no AsyncStorage / localStorage chat history in the SDK.

AW Chat SDK — integration & platform handoff docs.