Files
tower/docs/DATA_MODELS.md
T
maaz519 d21bd6bcb4 feat: add Redis Stream handoff for AI dev + monitor all messages
- Publish every non-spam message to tower:stream:messages immediately
  after DB insert in ingest.processor — before any admin approval or
  routing decision
- Remove early tags.length === 0 drop in main.ts so no-rule-match
  messages are now stored and streamed (monitor-all behaviour)
- Wire classify worker, review/p1/dnc queues into main.ts
- Add streams/message-stream.ts with XADD wrapper (capped at ~50k entries)
- Add docs/DATA_MODELS.md — full data model reference for all 29 models

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 13:56:10 +05:30

37 KiB
Raw Blame History

TOWER — Data Model Reference

Source of truth: apps/api/prisma/schema.prisma Database: PostgreSQL 17 · ORM: Prisma · All IDs: cuid() · All timestamps: UTC


Table of Contents

  1. Entity Relationship Overview
  2. Enums
  3. Organisation Layer
  4. Tenancy & Admins
  5. WhatsApp Accounts & Groups
  6. Message Pipeline
  7. Member Identity & Consent
  8. Rules Engine
  9. Digest
  10. Community Content
  11. AI Draft Pipeline
  12. Audit
  13. Indexes Reference
  14. Relationship Map

1. Entity Relationship Overview

SuperAdmin (platform operator)

Organization ─────────────────────────────────────────────────┐
  ├── OrgAdmin[]      (national admins)                        │
  ├── OrgRule[]       (routing rules, cascade to all Tenants)  │
  └── Tenant[] ───────────────────────────────────────────────┘
        ├── Admin[]            (Dall admins)
        ├── TenantBot[]        (bot access grants)
        ├── TenantRule[]       (Dall-specific routing rules)
        ├── Group[]            (WhatsApp groups)
        │     └── Message[]
        │           ├── Approval?
        │           ├── ContentDraft[]
        │           └── Thread?
        ├── TowerUser[]        (members)
        │     ├── TowerSession[]
        │     ├── ConsentRecord[]
        │     ├── EventRsvp[]
        │     ├── SevaEntry[]
        │     ├── GamificationEvent[]
        │     ├── CircleMembership[]
        │     └── AskAIQuery[]
        ├── DigestConfig?
        ├── Digest[]
        ├── Event[]
        ├── SevaOpportunity[]
        ├── Circle[]
        ├── KnowledgeBoard[]
        ├── KnowledgeItem[]
        ├── GalleryAlbum[]
        ├── GalleryItem[]
        ├── ContentDraft[]
        └── AuditEvent[]

Account (global bot) ──── TenantBot ──── Tenant
                     └─── Group

2. Enums

OrgAdminRole

Value Meaning
ORG_OWNER Full access to organisation including billing/settings
ORG_ADMIN Manage chapters and org rules, cannot delete org

AdminRole

Tenant-level admin roles.

Value Meaning
OWNER Full tenant control, can delete tenant
ADMIN Manage groups, rules, messages, content
VIEWER Read-only access to messages and stats

AccountStatus

WhatsApp bot account status.

Value Meaning
ACTIVE Session connected and healthy
DISCONNECTED Session lost, awaiting reconnect or re-scan
BANNED Number banned by WhatsApp
PAIRING QR code displayed, waiting for scan

GroupClaimStatus

Lifecycle of a WhatsApp group in the system.

Value Meaning
PENDING_CLAIM Bot sees the group, no tenant has claimed it
CLAIMED A tenant admin has claimed it — tenantId is set
RELEASED Admin explicitly released the group
EXPIRED Claim token expired before being used

MessageStatus

Lifecycle of a single message through the pipeline.

Value Meaning
RAW Just ingested, not yet classified
PENDING Flagged or P1 — awaiting admin review
APPROVED Admin approved, ready to forward
REJECTED Admin rejected
DISTRIBUTED Forwarded to all target groups
DNC Do-Not-Classify — spam, no rule match, or rule said SKIP/REJECT
ARCHIVED Retained past distribution window

ApprovalDecision

Value Meaning
APPROVED Admin approved the message
REJECTED Admin rejected the message

ActorType

Who performed an audited action.

Value Meaning
ADMIN Tenant admin
SYSTEM Automated pipeline action
ADAPTER WhatsApp adapter (ingest events)
MEMBER Community member via portal

RuleMatchType

How a routing rule matches message content.

Value Meaning
HASHTAG Message contains the hashtag as a standalone token (e.g. #Event)
PREFIX Message text starts with the prefix
REACTION_EMOJI An emoji reaction on a message matches this value

RuleAction

What happens when a rule matches.

Value Meaning
FLAG Route to review queue — human decision required
AUTO_APPROVE Auto-approve if sender is a group admin, else FLAG
SKIP Silently discard — do not store
REJECT Store as DNC, do not process further
P1 Emergency priority — immediate admin alert

Precedence (highest wins when multiple rules match): SKIP > REJECT > P1 > AUTO_APPROVE > FLAG

ConsentScope

What a member has consented to for their messages.

Value Meaning
INGEST Messages may be ingested into TOWER
ARCHIVE Messages may be stored beyond the pipeline window
REPLICATE Messages may be forwarded to other groups
DISPLAY Messages may be shown in the member portal / digest

ConsentStatus

Value Meaning
GRANTED Consent is active
REVOKED Member revoked consent

MemberOptOutReason

Value Meaning
STOP_KEYWORD Member sent STOP in a group
SELF_PORTAL Member opted out via portal settings
ADMIN_ACTION Admin manually opted the member out

RsvpStatus

Value Meaning
GOING Member confirmed attendance
NOT_GOING Member declined
MAYBE Member is undecided

SevaStatus

Value Meaning
OPEN Accepting signups
CLOSED No longer accepting signups

SevaEntryStatus

Value Meaning
SIGNED_UP Member has signed up
COMPLETED Seva was completed — triggers gamification award
CANCELLED Member cancelled their signup

GamificationEventType

Points awarded per event type. Time-decay: full ≤30d, 50% 3190d, 25% >90d.

Value Points Trigger
HELPFUL_ANSWER 10 Admin marks a member's answer as helpful
SEVA_COMPLETED 20 SevaEntry status → COMPLETED
EVENT_ATTENDANCE 5 Member checks in / RSVPs GOING to an event
FAQ_APPROVED 15 A FAQ item attributed to a member is approved
WELCOME 3 Member completes onboarding
PROFILE_COMPLETED 5 Member fills name + hometown + at least one interest
BUSINESS_ADDED 5 Member adds a business to the bazaar (Sprint 12)

CircleRole

Value Meaning
MEMBER Regular circle member
LEAD Circle leader — can manage membership

KnowledgeItemStatus

Value Meaning
DRAFT Not visible to members
PUBLISHED Visible in member portal and Ask AI
ARCHIVED Hidden from portal, retained in DB

DraftType

Type of AI-proposed content draft.

Value Meaning
EVENT An event extracted from a WhatsApp message
SEVA_TASK A volunteer task extracted as an action item within an event
FAQ_ITEM A Q&A pair extracted from a WhatsApp message

DraftStatus

Value Meaning
PENDING_REVIEW Waiting for admin decision
APPROVED Admin approved — published record created
REJECTED Admin rejected — nothing published

3. Organisation Layer

Organization

Top-level entity. One per national body (e.g. UP Parivaar).

Field Type Notes
id String (cuid) PK
slug String URL-safe unique identifier
name String Display name
settings Json Org-level config (default {})
createdAt DateTime
updatedAt DateTime

Relations:

  • tenantsTenant[]
  • orgAdminsOrgAdmin[]
  • orgRulesOrgRule[]

OrgAdmin

Admin user at the organisation level.

Field Type Notes
id String (cuid) PK
organizationId String FK → Organization
email String Unique per org
passwordHash String bcrypt
name String? Display name
role OrgAdminRole ORG_OWNER | ORG_ADMIN
createdAt DateTime
updatedAt DateTime

Constraints: UNIQUE(organizationId, email)


OrgRule

Routing rule scoped to an organisation. Cascades to all Tenants under it. Evaluated before TenantRules — org rules are authoritative.

Field Type Notes
id String (cuid) PK
organizationId String FK → Organization
matchType RuleMatchType HASHTAG | PREFIX | REACTION_EMOJI
matchValue String e.g. #Event, URGENT:, 👍
action RuleAction What to do when matched
priority Int Lower = higher priority (default 0)
isActive Boolean Soft-disable without deleting
createdAt DateTime
updatedAt DateTime

Constraints: UNIQUE(organizationId, matchType, matchValue)


4. Tenancy & Admins

Tenant

One per Dall/chapter. The primary isolation boundary — every query is scoped by tenantId.

Field Type Notes
id String (cuid) PK
slug String Globally unique URL slug
name String Display name (e.g. "UP Parivaar Dallas")
isActive Boolean Inactive tenants drop all messages
isForwardingPaused Boolean Pause message forwarding without stopping ingest
settings Json Tenant-specific config (default {})
organizationId String? FK → Organization (null for standalone tenants)
createdAt DateTime
updatedAt DateTime

Relations: all content models reference Tenant via tenantId.


Admin

Tenant-level admin user.

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
email String Unique per tenant
passwordHash String bcrypt
role AdminRole OWNER | ADMIN | VIEWER
createdAt DateTime
updatedAt DateTime

Constraints: UNIQUE(tenantId, email)

Relations: claimedGroupsGroup[] (groups this admin claimed)


SuperAdmin

Platform-level operator. Can create and manage organisations, tenants, and bot accounts.

Field Type Notes
id String (cuid) PK
email String Globally unique
passwordHash String bcrypt
name String?
createdAt DateTime
updatedAt DateTime

AuditEvent

Append-only audit log. Written on every significant action across all realms.

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
actorType ActorType Who performed the action
actorId String? Admin/member ID (null for SYSTEM)
action String e.g. MESSAGE_APPROVED, DRAFT_APPROVED
resourceType String e.g. Message, ContentDraft
resourceId String ID of the affected record
payload Json Additional context (default {})
traceId String? Distributed trace ID
createdAt DateTime

Indexes: (tenantId, createdAt), (resourceType, resourceId)


5. WhatsApp Accounts & Groups

Account

A WhatsApp bot phone number. Global — not owned by any tenant. Access is granted per-tenant via TenantBot.

Field Type Notes
id String (cuid) PK
platform String "whatsapp" currently
jid String WhatsApp JID (phone@s.whatsapp.net) — unique per platform
sessionPath String Filesystem path to Baileys session files
displayName String? Bot's WhatsApp display name
status AccountStatus Current connection state
qrCode String? Base64 QR — set during PAIRING, cleared on ACTIVE
isBot Boolean Always true for bot accounts
pairingToken String? One-time token for admin to initiate pairing
pairingExpiresAt DateTime? Token expiry
createdAt DateTime
updatedAt DateTime

Constraints: UNIQUE(platform, jid)


TenantBot

Junction table granting a tenant access to a bot account.

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
accountId String FK → Account
isActive Boolean
createdAt DateTime

Constraints: UNIQUE(tenantId, accountId)


Group

A WhatsApp group detected by a bot. Goes through a claim lifecycle before being associated with a tenant.

Field Type Notes
id String (cuid) PK
tenantId String? FK → Tenant — null until CLAIMED
platform String "whatsapp"
platformId String WhatsApp group JID
name String Group display name
description String? Group description
isActive Boolean
accountId String? FK → Account (which bot sees this group)
claimStatus GroupClaimStatus PENDING_CLAIMCLAIMED | RELEASED | EXPIRED
claimedAt DateTime? When it was claimed
claimedByAdminId String? FK → Admin
claimExpiresAt DateTime? Claim token expiry
createdAt DateTime
updatedAt DateTime

Constraints: UNIQUE(platform, platformId)

Relations:

  • messagesMessage[]
  • syncRoutesFromSyncRoute[] (this group is a source)
  • syncRoutesToSyncRoute[] (this group is a target)
  • consentsConsentRecord[]
  • claimTokensGroupClaimToken[]
  • groupAccessesGroupAccess[]
  • threadsThread[]

SyncRoute

Defines forwarding: approved messages from sourceGroup are forwarded to targetGroup.

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
sourceGroupId String FK → Group
targetGroupId String FK → Group
isActive Boolean
createdAt DateTime

Constraints: UNIQUE(sourceGroupId, targetGroupId)


GroupClaimToken

One-time token sent inside a WhatsApp group to claim it for a tenant.

Field Type Notes
id String (cuid) PK
groupId String FK → Group
token String Unique random token
creatorJid String JID of admin who initiated the claim
expiresAt DateTime
consumedAt DateTime? Set when token is used
createdAt DateTime

GroupAccess

Grants a tenant read access to a group they do not own (shared group).

Field Type Notes
id String (cuid) PK
groupId String FK → Group
tenantId String FK → Tenant
grantedBy String Admin ID who granted access
createdAt DateTime

Constraints: UNIQUE(groupId, tenantId)


6. Message Pipeline

Message

Central record. Created by ingest.processor.ts when a WhatsApp message passes pre-ingest spam checks.

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
platform String "whatsapp"
platformMsgId String Platform-native message ID — unique per platform
sourceGroupId String FK → Group
senderJid String WhatsApp JID of sender
senderName String? Display name at time of send
senderTowerUserId String? FK → TowerUser (null if sender not onboarded)
content String Raw message text
mediaUrl String? URL of attached media (stored, not processed currently)
tags String[] Rule match values (e.g. ["#Event"])
status MessageStatus Pipeline state — see enum
expiresAt DateTime? RAW/DNC messages expire after 72h
quotedPlatformMsgId String? Platform ID of quoted/replied-to message
threadId String? FK → Thread (set after thread resolution)
createdAt DateTime
updatedAt DateTime

Constraints: UNIQUE(platform, platformMsgId)

Relations:

  • approvalApproval?
  • contentDraftsContentDraft[]

Status lifecycle:

ingest → RAW
classify → PENDING (if flagged/P1) | DNC (if spam/no match/SKIP)
admin approve → APPROVED
forward worker → DISTRIBUTED
admin reject → REJECTED
archive job → ARCHIVED

Thread

A group of related messages — resolved from quoted replies.

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
sourceGroupId String FK → Group
lastActivityAt DateTime Updated on each new message in thread
messageCount Int Denormalised count
topic String? LLM-inferred topic label
createdAt DateTime

Relations: messagesMessage[]


Approval

Records the admin decision for a message. One per message.

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
messageId String FK → Message (unique)
adminId String Admin who decided
decision ApprovalDecision APPROVED | REJECTED
notes String? Optional admin note
decidedAt DateTime

TowerUser

A community member. Phone number is never stored — only a salted SHA-256 hash.

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
phoneHash String SHA-256(E.164_phone + JWT_SECRET) — 64 hex chars
jid String WhatsApp JID — used for OTP delivery only
displayName String? Member's chosen name
avatar String? Profile image URL
hometown String? Where they're originally from
currentLocation String? Where they live now
interests String[] Self-declared interest tags
language String "en" | "hi" (default "en")
digestPreference String "daily" | "weekly" | "off" (default "daily")
directoryVisible Boolean Show in member directory (default true)
createdAt DateTime
updatedAt DateTime

Constraints: UNIQUE(tenantId, phoneHash) — one member per Dall

Relations:

  • consentsConsentRecord[]
  • optOutsMemberOptOut[]
  • sessionsTowerSession[]
  • messagesMessage[] (messages sent by this member)
  • rsvpsEventRsvp[]
  • sevaEntriesSevaEntry[]
  • gamificationEventsGamificationEvent[]
  • circleMembershipsCircleMembership[]
  • askAIQueriesAskAIQuery[]

TowerSession

JWT-backed member session. One session per login.

Field Type Notes
id String (cuid) PK
userId String FK → TowerUser
tokenHash String SHA-256 of the JWT — unique
expiresAt DateTime 30 days from creation
createdAt DateTime

Cookie name: tower_member_token (HttpOnly, SameSite=Lax)


ConsentRecord

GDPR-style consent per (member × group × scope set). Created during onboarding OTP flow.

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
groupId String FK → Group (which group consent applies to)
userId String FK → TowerUser
scopes ConsentScope[] Which processing activities are consented to
retentionDays Int How long to retain data (default 90)
policyVersion String Consent policy version string
status ConsentStatus GRANTED | REVOKED
proofEventId String Platform event ID proving consent was given
effectiveAt DateTime When consent became active
revokedAt DateTime? When revoked (null if still active)
createdAt DateTime

MemberOptOut

Records a member opting out of processing.

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
userId String FK → TowerUser
groupId String? Specific group opt-out (null = all groups)
reason MemberOptOutReason How they opted out
notes String?
createdAt DateTime

OtpChallenge

Short-lived OTP for member login. Delivered via WhatsApp DM.

Field Type Notes
id String (cuid) PK
tenantId String
jid String WhatsApp JID to deliver OTP to
phoneHash String Member identity hash
code String 6-digit OTP
scopes ConsentScope[] Scopes being consented to
retentionDays Int default 90
policyVersion String
groupId String Which group the member was in when they triggered onboarding
expiresAt DateTime 10 minutes from creation
consumedAt DateTime? Set when OTP is verified
sentAt DateTime? Set when WhatsApp DM is delivered
createdAt DateTime

8. Rules Engine

TenantRule

Tenant-level routing rule. Evaluated as fallback if no OrgRule matches.

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
matchType RuleMatchType HASHTAG | PREFIX | REACTION_EMOJI
matchValue String Pattern to match (e.g. #Event, URGENT:, 👍)
action RuleAction What to do when matched
priority Int Lower number = higher priority (default 0)
isActive Boolean Soft-disable
createdAt DateTime
updatedAt DateTime

Constraints: UNIQUE(tenantId, matchType, matchValue)

Rule evaluation order:

  1. Load OrgRules for this tenant's org (ordered by priority ASC)
  2. Load TenantRules (ordered by priority ASC)
  3. Run OrgRules first — if any match, use that effectiveAction (org is authoritative)
  4. If no OrgRule matched, run TenantRules
  5. Highest-precedence action wins across all matching rules

9. Digest

DigestConfig

One digest schedule per tenant. Controls when and where the daily digest is sent.

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant (unique) — one config per tenant
targetGroupJid String WhatsApp group to send digest to
targetAccountId String Bot account to send from
scheduleHour Int Hour (023, default 20)
scheduleMinute Int Minute (059, default 0)
isActive Boolean Enable/disable schedule
lastSentAt DateTime? Timestamp of last successful send
createdAt DateTime
updatedAt DateTime

Digest

A generated and sent digest. One per (tenant, date).

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
content String LLM-generated digest text
messageIds String[] IDs of messages included in this digest
digestDate DateTime The day this digest covers
sentAt DateTime When it was sent

Constraints: UNIQUE(tenantId, digestDate)


10. Community Content

Event

A community event. Populated either by admin approval of a ContentDraft (AI-extracted) or directly via admin API.

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
title String Event name
description String?
location String? Venue or address
startsAt DateTime Event start time
endsAt DateTime? Event end time
createdBy String Admin ID who created/approved it
isPublished Boolean Visible to members (default false)
createdAt DateTime
updatedAt DateTime

Relations: rsvpsEventRsvp[]


EventRsvp

A member's RSVP to an event. Upserted (one per member per event).

Field Type Notes
id String (cuid) PK
eventId String FK → Event (cascade delete)
userId String FK → TowerUser
status RsvpStatus GOING | NOT_GOING | MAYBE
note String? Optional member note
createdAt DateTime
updatedAt DateTime

Constraints: UNIQUE(eventId, userId)


SevaOpportunity

A volunteer opportunity. May be auto-extracted as an action item within an event message.

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
title String Task name
description String?
location String?
slots Int? Max volunteers (null = unlimited)
startsAt DateTime? When the task takes place
pointsAward Int Gamification points on completion (default 20)
status SevaStatus OPEN | CLOSED
createdBy String Admin ID
isPublished Boolean Visible to members (default false)
createdAt DateTime
updatedAt DateTime

Relations: entriesSevaEntry[]


SevaEntry

A member's signup for a seva opportunity.

Field Type Notes
id String (cuid) PK
opportunityId String FK → SevaOpportunity (cascade delete)
userId String FK → TowerUser
status SevaEntryStatus SIGNED_UPCOMPLETED | CANCELLED
hours Float? Hours logged on completion
note String?
completedAt DateTime? Set when status → COMPLETED
createdAt DateTime
updatedAt DateTime

Constraints: UNIQUE(opportunityId, userId)


GamificationEvent

Points ledger. Each record represents one award. Idempotent — duplicate awards are silently ignored (P2002 caught).

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
userId String FK → TowerUser
type GamificationEventType What triggered the award
points Int Points awarded at time of event
refType String? Type of the reference record (e.g. "SevaEntry")
refId String? ID of the reference record
createdAt DateTime

Constraints: UNIQUE(userId, type, refId) — prevents double-awarding the same source

Score calculation:

lifetimeScore = SUM(points)
recentScore   = SUM(points × decayFactor(ageDays))
  where decayFactor: ≤30d → 1.0, 3190d → 0.5, >90d → 0.25

Circle

An interest or affinity sub-group within a Dall. Members can join/leave circles.

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
name String Unique per tenant
description String?
isPublic Boolean Public = any member can join; private = invite-only (default true)
createdBy String Admin ID
createdAt DateTime
updatedAt DateTime

Constraints: UNIQUE(tenantId, name) Relations: membershipsCircleMembership[]


CircleMembership

Junction between a member and a circle.

Field Type Notes
id String (cuid) PK
circleId String FK → Circle (cascade delete)
userId String FK → TowerUser
role CircleRole MEMBER | LEAD
createdAt DateTime

Constraints: UNIQUE(circleId, userId)


KnowledgeBoard

A category/board that groups related FAQ items.

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
name String Board title
description String?
slug String URL-safe slug, unique per tenant
sortOrder Int Display order (default 0)
createdAt DateTime
updatedAt DateTime

Constraints: UNIQUE(tenantId, slug) Relations: itemsKnowledgeItem[]


KnowledgeItem

A single FAQ entry. Visible to members when status=PUBLISHED. Used as context source for Ask AI.

Field Type Notes
id String (cuid) PK
boardId String FK → KnowledgeBoard (cascade delete)
tenantId String FK → Tenant
question String The question
answer String The answer
tags String[] Topic tags for search/filter
status KnowledgeItemStatus DRAFT | PUBLISHED | ARCHIVED
sortOrder Int Display order within board (default 0)
createdBy String Admin ID
createdAt DateTime
updatedAt DateTime

GalleryAlbum

A photo album (event memories, chapter highlights).

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
title String Album name
description String?
coverUrl String? URL of cover photo
isPublished Boolean Visible to members (default false)
createdBy String Admin ID
createdAt DateTime
updatedAt DateTime

Relations: itemsGalleryItem[]


GalleryItem

A single photo within an album.

Field Type Notes
id String (cuid) PK
albumId String FK → GalleryAlbum (cascade delete)
tenantId String FK → Tenant
mediaUrl String Photo URL
caption String?
sortOrder Int Display order within album (default 0)
createdAt DateTime

AskAIQuery

Log of every Ask AI question and answer. Used for query history in the member portal.

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
userId String FK → TowerUser
question String Member's question
answer String LLM-generated answer
citations Json Array of citation objects (see shape below)
createdAt DateTime

Citations shape:

[
  {
    "messageId": "string",
    "snippet": "string",
    "senderName": "string",
    "sourceGroupName": "string",
    "approvedAt": 1234567890
  }
]

11. AI Draft Pipeline

ContentDraft

An AI-proposed community content item pending admin review. Created by the Event Extractor (tower.event.extract worker) or FAQ Curator (tower.faq.candidate worker).

Field Type Notes
id String (cuid) PK
tenantId String FK → Tenant
type DraftType EVENT | SEVA_TASK | FAQ_ITEM
status DraftStatus PENDING_REVIEW | APPROVED | REJECTED
sourceMessageId String FK → Message (the WhatsApp message that triggered extraction)
proposedJson Json AI-proposed content — shape depends on type (see below)
confidence Float? Extraction confidence 01
reviewedBy String? Admin ID who approved/rejected
reviewedAt DateTime? When the decision was made
publishedId String? ID of the created Event/SevaOpportunity/KnowledgeItem on approval
createdAt DateTime
updatedAt DateTime

proposedJson shapes by type:

// EVENT
{
  "title": "string",
  "date": "YYYY-MM-DD or null",
  "time": "HH:MM or null",
  "location": "string or null",
  "description": "string",
  "rsvpNeeded": true,
  "sevaActionItems": [
    { "title": "string", "slots": 10, "skills": ["cooking", "setup"] }
  ]
}

// SEVA_TASK (auto-created when EVENT has sevaActionItems)
{
  "title": "string",
  "slots": 5,
  "skills": ["string"],
  "date": "YYYY-MM-DD or null",
  "parentEventTitle": "string"
}

// FAQ_ITEM
{
  "question": "string",
  "answer": "string",
  "tags": ["string"]
}

On approval:

  • EVENT → creates Event record with isPublished=true
  • SEVA_TASK → creates SevaOpportunity record with isPublished=true
  • FAQ_ITEM → creates KnowledgeItem in default board (creates board if none exists)

Confidence scoring:

  • EVENT: filledFields / 5 (title, date, time, location, description)
  • SEVA_TASK: parent event confidence × 0.9
  • FAQ_ITEM: fixed 0.8

12. Audit

See Section 4 — AuditEvent.

All current action values:

Action Triggered by
AUTH_LOGIN Admin login
AUTH_LOGIN_FAILED Failed login attempt
AUTH_LOGOUT Admin logout
AUTH_SIGNUP New admin created
ROUTE_CREATED / ROUTE_DELETED SyncRoute changes
ACCOUNT_CREATED New bot account
MESSAGE_INGESTED Message stored
MESSAGE_APPROVED / REJECTED / FORWARDED / INDEXED Message lifecycle
BOT_INITIATED / PAIRED / REVEALED / REMOVED Bot lifecycle
GROUP_* Group claim/release/share lifecycle
MEMBER_ONBOARDED / OPT_OUT / OPT_IN / DELETED Member lifecycle
OTP_REQUESTED / OTP_VERIFIED Member auth
EVENT_CREATED / UPDATED / DELETED Event management
DIGEST_SENT Digest delivery
SEVA_CREATED / DELETED / COMPLETED Seva lifecycle
CIRCLE_CREATED / DELETED Circle management
DRAFT_APPROVED / DRAFT_REJECTED AI draft review

13. Indexes Reference

Model Index fields Purpose
OrgAdmin (organizationId) List admins by org
OrgRule (organizationId, isActive) Load active org rules
Account (isBot), (status) Find active bots
TenantBot (accountId) Find tenants per bot
Group (accountId), (tenantId), (claimStatus) Group lookups
GroupClaimToken (expiresAt) Expire old tokens
GroupAccess (tenantId) Find shared groups
Message (tenantId), (senderTowerUserId), (status, expiresAt), (threadId) Pipeline queries
Thread (tenantId), (sourceGroupId, lastActivityAt) Thread browsing
Approval (tenantId)
SyncRoute (tenantId)
TowerUser (phoneHash), (tenantId), (jid) Member lookup by phone/JID
TowerSession (userId), (expiresAt) Session cleanup
ConsentRecord (tenantId, groupId, userId), (status), (userId) Consent checks
MemberOptOut (tenantId, userId), (groupId), (userId) Opt-out checks
OtpChallenge (tenantId, jid), (expiresAt), (sentAt) OTP lookup + cleanup
TenantRule (tenantId, isActive), (tenantId, matchType) Rule engine
Digest (tenantId)
Event (tenantId, startsAt), (tenantId, isPublished) Event listing
EventRsvp (userId) Member RSVP history
SevaOpportunity (tenantId, status), (tenantId, isPublished) Seva listing
SevaEntry (userId) Member seva history
GamificationEvent (tenantId, userId), (userId, createdAt) Score calculation
Circle (tenantId, isPublic) Circle browser
CircleMembership (userId) Member's circles
AskAIQuery (tenantId, createdAt), (userId, createdAt) History
KnowledgeBoard (tenantId)
KnowledgeItem (tenantId, status), (boardId, status) FAQ listing
GalleryAlbum (tenantId, isPublished) Album listing
GalleryItem (albumId), (tenantId) Photo listing
ContentDraft (tenantId, status), (tenantId, type, status), (sourceMessageId) Draft review
AuditEvent (tenantId, createdAt), (resourceType, resourceId) Audit log

14. Relationship Map

Organization ──────── OrgAdmin (many)
             ──────── OrgRule (many)
             ──────── Tenant (many)

Tenant ─────────────── Admin (many)
       ─────────────── TenantBot (many) ──── Account (global, many Tenants)
       ─────────────── TenantRule (many)
       ─────────────── Group (many) ─────── Message (many)
       │                                          │
       │                                          ├── Approval (one)
       │                                          └── ContentDraft (many)
       │
       ├── TowerUser (many)
       │       ├── TowerSession (many)
       │       ├── ConsentRecord (many)
       │       ├── MemberOptOut (many)
       │       ├── EventRsvp (many) ────── Event (many, per Tenant)
       │       ├── SevaEntry (many) ────── SevaOpportunity (many, per Tenant)
       │       ├── GamificationEvent (many)
       │       ├── CircleMembership (many) ── Circle (many, per Tenant)
       │       └── AskAIQuery (many)
       │
       ├── DigestConfig (one)
       ├── Digest (many)
       ├── Event (many) ──────────── EventRsvp (many)
       ├── SevaOpportunity (many) ── SevaEntry (many)
       ├── Circle (many) ─────────── CircleMembership (many)
       ├── KnowledgeBoard (many) ─── KnowledgeItem (many)
       ├── GalleryAlbum (many) ───── GalleryItem (many)
       ├── ContentDraft (many) ────── Message (source)
       │         on approve →
       │           creates Event OR SevaOpportunity OR KnowledgeItem
       └── AuditEvent (many)