courtmitra / packages /db /src /schema.ts
Hetansh Waghela
feat: Full Clerk Google OAuth + user profiles + combined API/Worker deploy
f69e867
Raw
History Blame
37 kB
import {
pgTable,
uuid,
text,
jsonb,
boolean,
integer,
bigint,
numeric,
timestamp,
date,
vector,
uniqueIndex,
index,
customType,
} from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
/** tsvector DB-only column type (Drizzle has no built-in; we use customType). */
const tsvector = customType<{ data: string }>({
dataType() {
return "tsvector";
},
});
/**
* Phase 1 schema (PHASE_1 §5, arch §9 + §30). Enums are stored as `text`; the
* canonical vocabularies live in @courtmitra/contracts and are validated in the
* app/domain layer. Every change here must be a Drizzle migration (CLAUDE.md #8).
*
* Embedding dimension targets OpenAI text-embedding-3-small (1536).
*/
const EMBEDDING_DIM = 1536;
// Reusable column builders.
const id = () => uuid("id").primaryKey().defaultRandom();
const createdAt = () => timestamp("created_at", { withTimezone: true }).defaultNow().notNull();
const updatedAt = () => timestamp("updated_at", { withTimezone: true }).defaultNow().notNull();
// ============================================================================
// Swarm / entity resolution (Phase 2 Module 1)
// ============================================================================
export const entities = pgTable(
"entities",
{
id: id(),
normalizedName: text("normalized_name").notNull(),
aliases: text("aliases").array(), // e.g. {"Acme Pvt Ltd", "Acme", "acme.com"}
domain: text("domain").notNull(), // consumer, payment, cyber_fraud
identifiers: jsonb("identifiers"), // { gst: "...", website: "...", socialHandles: [...] }
createdAt: createdAt(),
},
(t) => [
uniqueIndex("entities_normalized_name_domain_uq").on(t.normalizedName, t.domain),
index("entities_domain_idx").on(t.domain),
],
);
export const swarms = pgTable(
"swarms",
{
id: id(),
entityId: uuid("entity_id")
.notNull()
.references(() => entities.id),
issueDomain: text("issue_domain").notNull(), // consumer, payment, cyber_fraud
patternSignature: jsonb("pattern_signature"), // shared evidence/wording/route features
caseCount: integer("case_count").default(0).notNull(),
totalAmount: numeric("total_amount"),
status: text("status").default("active").notNull(), // active, resolved, archived
createdAt: createdAt(),
updatedAt: updatedAt(),
},
(t) => [
index("swarms_entity_idx").on(t.entityId),
index("swarms_domain_idx").on(t.issueDomain),
index("swarms_status_idx").on(t.status),
],
);
export const swarmMembers = pgTable(
"swarm_members",
{
swarmId: uuid("swarm_id")
.notNull()
.references(() => swarms.id),
missionId: uuid("mission_id")
.notNull()
.references(() => missions.id),
consentGrantId: uuid("consent_grant_id")
.notNull()
.references(() => consentGrants.id), // REQUIRED: per-mission aggregation consent
contribution: jsonb("contribution"), // what this mission contributes to the swarm dossier
joinedAt: createdAt(),
},
(t) => [
uniqueIndex("swarm_members_swarm_mission_uq").on(t.swarmId, t.missionId),
index("swarm_members_mission_idx").on(t.missionId),
],
);
// ============================================================================
// ASR / Transcripts (Phase 2 Module 2)
// ============================================================================
export const transcripts = pgTable("transcripts", {
id: id(),
evidenceItemId: uuid("evidence_item_id")
.notNull()
.references(() => evidenceItems.id),
language: text("language"), // detected language code (e.g. "hi", "ta", "en")
text: text("text").notNull(), // transcribed text
model: text("model").notNull(), // "whisper-1", "whisper-cpp-base", etc.
confidence: numeric("confidence"), // 0.0–1.0
durationMs: integer("duration_ms"), // audio duration in milliseconds
createdAt: createdAt(),
});
// ============================================================================
// Translation / localized texts (Phase 2 Module 3)
// ============================================================================
export const localizedTexts = pgTable("localized_texts", {
id: id(),
resourceType: text("resource_type").notNull(), // 'message', 'snapshot', 'prompt', 'evidence'
resourceId: uuid("resource_id").notNull(), // FK to the source row
languageCode: text("language_code").notNull(), // 'hi', 'ta', 'en', etc.
originalText: text("original_text").notNull(), // always preserved (CLAUDE.md: never lose original)
normalizedText: text("normalized_text"), // cleaned/transliterated form
translatedText: text("translated_text"), // English working form (for internal use)
modelVersion: text("model_version"), // 'llm-gemini', 'indictrans2-1b'
confidence: numeric("confidence"),
createdAt: createdAt(),
}, (t) => [
index("localized_texts_resource_idx").on(t.resourceType, t.resourceId),
index("localized_texts_language_idx").on(t.languageCode),
]);
// ============================================================================
// Identity / session
// ============================================================================
export const users = pgTable("users", {
id: id(),
displayName: text("display_name"),
email: text("email"),
avatarUrl: text("avatar_url"),
phone: text("phone"),
fullName: text("full_name"),
preferredLanguage: text("preferred_language").default("en"),
riskFlags: jsonb("risk_flags"),
createdAt: createdAt(),
updatedAt: updatedAt(),
});
export const userIdentities = pgTable(
"user_identities",
{
id: id(),
userId: uuid("user_id")
.notNull()
.references(() => users.id),
provider: text("provider").notNull(), // whatsapp, telegram, google, email, phone, social
providerSubject: text("provider_subject").notNull(),
isVerified: boolean("is_verified").default(false).notNull(),
metadata: jsonb("metadata"),
createdAt: createdAt(),
},
(t) => [uniqueIndex("user_identities_provider_subject_uq").on(t.provider, t.providerSubject)],
);
export const sessions = pgTable("sessions", {
id: id(),
userId: uuid("user_id")
.notNull()
.references(() => users.id),
sessionType: text("session_type").notNull(), // web, whatsapp, magic_link, oauth_callback
channel: text("channel"),
token: text("token"),
expiresAt: timestamp("expires_at", { withTimezone: true }),
createdAt: createdAt(),
});
export const accountLinkChallenges = pgTable("account_link_challenges", {
id: id(),
userId: uuid("user_id").references(() => users.id),
challengeType: text("challenge_type").notNull(), // magic_link, whatsapp_code
destination: text("destination").notNull(),
codeHash: text("code_hash").notNull(),
consumedAt: timestamp("consumed_at", { withTimezone: true }),
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
createdAt: createdAt(),
});
export const oauthTokens = pgTable("oauth_tokens", {
id: id(),
userId: uuid("user_id")
.notNull()
.references(() => users.id),
provider: text("provider").notNull(),
accessTokenRef: text("access_token_ref").notNull(), // encrypted ref, never plaintext
refreshTokenRef: text("refresh_token_ref"),
scopes: jsonb("scopes"),
expiresAt: timestamp("expires_at", { withTimezone: true }),
createdAt: createdAt(),
});
// ============================================================================
// Conversation
// ============================================================================
export const conversations = pgTable(
"conversations",
{
id: id(),
userId: uuid("user_id").references(() => users.id),
channel: text("channel").notNull(), // whatsapp, telegram, web, email
externalThreadId: text("external_thread_id"),
activeMissionId: uuid("active_mission_id"),
sessionState: jsonb("session_state"),
lastMessageAt: timestamp("last_message_at", { withTimezone: true }),
createdAt: createdAt(),
},
// Fix 6: unique index prevents duplicate conversations on concurrent ingest.
(t) => [uniqueIndex("conversations_channel_thread_uq").on(t.channel, t.externalThreadId)],
);
export const conversationWindows = pgTable("conversation_windows", {
id: id(),
conversationId: uuid("conversation_id")
.notNull()
.references(() => conversations.id),
// WhatsApp 24h free service window.
openedAt: timestamp("opened_at", { withTimezone: true }).notNull(),
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
createdAt: createdAt(),
});
export const messageTemplates = pgTable("message_templates", {
id: id(),
channel: text("channel").notNull(),
name: text("name").notNull(),
language: text("language").default("en").notNull(),
providerTemplateName: text("provider_template_name"), // no hardcoded ids — looked up by name
body: text("body").notNull(),
variables: jsonb("variables"),
createdAt: createdAt(),
});
export const messages = pgTable(
"messages",
{
id: id(),
conversationId: uuid("conversation_id")
.notNull()
.references(() => conversations.id),
missionId: uuid("mission_id"),
direction: text("direction").notNull(), // inbound, outbound
channel: text("channel").notNull(),
channelMessageId: text("channel_message_id"),
idempotencyKey: text("idempotency_key"),
body: text("body"),
mediaRefs: jsonb("media_refs"),
rawPayload: jsonb("raw_payload"),
createdAt: createdAt(),
},
(t) => [
uniqueIndex("messages_idempotency_key_uq").on(t.idempotencyKey),
index("messages_conversation_idx").on(t.conversationId, t.createdAt),
],
);
// ============================================================================
// Mission
// ============================================================================
export const missions = pgTable("missions", {
id: id(),
userId: uuid("user_id")
.notNull()
.references(() => users.id),
title: text("title").notNull(),
issueDomain: text("issue_domain").notNull(), // consumer, payment, cyber_fraud
state: text("state").notNull().default("created"),
jurisdiction: jsonb("jurisdiction"),
opposingParty: jsonb("opposing_party"),
reliefRequested: jsonb("relief_requested"),
facts: jsonb("facts"),
currentRoutePlanId: uuid("current_route_plan_id"),
safetyLevel: text("safety_level").notNull().default("normal"), // normal, elevated, emergency
createdAt: createdAt(),
updatedAt: updatedAt(),
});
export const missionEvents = pgTable(
"mission_events",
{
id: id(),
missionId: uuid("mission_id")
.notNull()
.references(() => missions.id),
eventType: text("event_type").notNull(),
actorType: text("actor_type").notNull(), // user, ai, system, responder, admin
actorId: text("actor_id"),
payload: jsonb("payload"),
createdAt: createdAt(),
},
(t) => [index("mission_events_mission_idx").on(t.missionId, t.createdAt)],
);
export const missionStatusSnapshots = pgTable(
"mission_status_snapshots",
{
id: id(),
missionId: uuid("mission_id")
.notNull()
.references(() => missions.id),
state: text("state").notNull(),
substate: text("substate"),
headline: text("headline").notNull(),
blockedReason: text("blocked_reason"),
nextUserAction: text("next_user_action"),
nextSystemAction: text("next_system_action"),
primaryButton: text("primary_button"),
danger: boolean("danger").default(false).notNull(),
confidence: numeric("confidence"),
createdAt: createdAt(),
},
(t) => [index("mission_status_snapshots_mission_idx").on(t.missionId, t.createdAt)],
);
export const missionLocks = pgTable("mission_locks", {
missionId: uuid("mission_id").primaryKey(),
lockedBy: text("locked_by").notNull(),
lockedAt: timestamp("locked_at", { withTimezone: true }).defaultNow().notNull(),
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
});
// ============================================================================
// Evidence
// ============================================================================
export const files = pgTable(
"files",
{
id: id(),
missionId: uuid("mission_id").references(() => missions.id),
uploaderUserId: uuid("uploader_user_id").references(() => users.id),
storageKey: text("storage_key").notNull(),
originalFilename: text("original_filename"),
mimeType: text("mime_type"),
sha256: text("sha256"),
sizeBytes: bigint("size_bytes", { mode: "number" }),
virusScanStatus: text("virus_scan_status").default("pending").notNull(), // pending, clean, infected, skipped
processingStatus: text("processing_status").default("pending").notNull(), // pending, processing, done, failed
retentionPolicy: text("retention_policy").default("standard").notNull(),
deletedAt: timestamp("deleted_at", { withTimezone: true }),
createdAt: createdAt(),
},
(t) => [index("files_mission_idx").on(t.missionId)],
);
export const evidenceItems = pgTable("evidence_items", {
id: id(),
missionId: uuid("mission_id")
.notNull()
.references(() => missions.id),
fileId: uuid("file_id").references(() => files.id),
uploaderUserId: uuid("uploader_user_id").references(() => users.id),
type: text("type").notNull(), // receipt, screenshot, chat, email, pdf, audio, video, id_doc, other
captureDate: timestamp("capture_date", { withTimezone: true }),
sourceChannel: text("source_channel"),
piiLevel: text("pii_level").default("unknown").notNull(),
admissibilityNotes: jsonb("admissibility_notes"),
createdAt: createdAt(),
});
export const evidenceParsedOutputs = pgTable(
"evidence_parsed_outputs",
{
id: id(),
evidenceItemId: uuid("evidence_item_id")
.notNull()
.references(() => evidenceItems.id),
parserName: text("parser_name").notNull(),
parserVersion: text("parser_version").notNull(),
extractedText: text("extracted_text"),
extractedEntities: jsonb("extracted_entities"),
extractedDates: jsonb("extracted_dates"),
extractedMoney: jsonb("extracted_money"),
// Fix 5: parties column to round-trip ParsedEvidenceDto faithfully.
extractedParties: jsonb("extracted_parties"),
ocrConfidence: numeric("ocr_confidence"),
pageSpans: jsonb("page_spans"),
createdAt: createdAt(),
},
// Fix 7: unique index prevents duplicate rows on Inngest step replay.
(t) => [
uniqueIndex("evidence_parsed_outputs_item_parser_uq").on(
t.evidenceItemId,
t.parserName,
t.parserVersion,
),
],
);
export const evidenceClaims = pgTable("evidence_claims", {
id: id(),
missionId: uuid("mission_id")
.notNull()
.references(() => missions.id),
evidenceItemId: uuid("evidence_item_id").references(() => evidenceItems.id),
claimText: text("claim_text").notNull(),
claimType: text("claim_type"),
supportLevel: text("support_level").notNull(), // explicit, inferred, weak, contradicted
sourceSpan: jsonb("source_span"),
createdAt: createdAt(),
});
// ============================================================================
// Legal / RAG (curated, small)
// ============================================================================
export const legalSources = pgTable("legal_sources", {
id: id(),
sourceType: text("source_type").notNull(), // act, rule, regulator_doc, govt_page, court_judgment, policy
title: text("title").notNull(),
tier: text("tier").notNull(), // tier1, tier2, tier3
jurisdiction: text("jurisdiction").default("IN"),
authority: text("authority"),
canonicalUrl: text("canonical_url"),
versionLabel: text("version_label"),
effectiveFrom: date("effective_from"),
effectiveTo: date("effective_to"),
sha256: text("sha256"),
fetchedAt: timestamp("fetched_at", { withTimezone: true }),
metadata: jsonb("metadata"),
createdAt: createdAt(),
});
export const legalChunks = pgTable(
"legal_chunks",
{
id: id(),
sourceId: uuid("source_id")
.notNull()
.references(() => legalSources.id),
chunkNo: integer("chunk_no").notNull(),
actName: text("act_name"),
sectionNo: text("section_no"),
heading: text("heading"),
text: text("text").notNull(),
tokenCount: integer("token_count"),
pageStart: integer("page_start"),
pageEnd: integer("page_end"),
embedding: vector("embedding", { dimensions: EMBEDDING_DIM }),
/**
* DB-generated tsvector for FTS — populated via GENERATED ALWAYS AS
* (see migration 0002_legal_indexes.sql). Drizzle treats this as a
* read-only column; writes are handled by Postgres.
*/
textTsv: tsvector("text_tsv").generatedAlwaysAs(
sql`to_tsvector('english', coalesce(text, ''))`,
),
metadata: jsonb("metadata"),
createdAt: createdAt(),
},
(t) => [
index("legal_chunks_source_idx").on(t.sourceId),
index("legal_chunks_tsv_idx").using("gin", t.textTsv),
index("legal_chunks_embedding_idx").using("ivfflat", t.embedding.op("vector_cosine_ops")),
],
);
export const legalRetrievalLogs = pgTable("legal_retrieval_logs", {
id: id(),
missionId: uuid("mission_id")
.notNull()
.references(() => missions.id),
query: text("query").notNull(),
retrievedChunkIds: uuid("retrieved_chunk_ids").array(),
rerankedChunkIds: uuid("reranked_chunk_ids").array(),
scores: jsonb("scores"),
modelVersions: jsonb("model_versions"),
createdAt: createdAt(),
});
export const legalClaimVerifications = pgTable("legal_claim_verifications", {
id: id(),
missionId: uuid("mission_id")
.notNull()
.references(() => missions.id),
claim: text("claim").notNull(),
supported: boolean("supported").notNull(),
citedChunkIds: uuid("cited_chunk_ids").array(),
quoteSpans: jsonb("quote_spans"),
verifierScore: numeric("verifier_score"),
failureReason: text("failure_reason"),
createdAt: createdAt(),
});
// ============================================================================
// Route / action
// ============================================================================
export const routeRules = pgTable("route_rules", {
id: id(),
issueDomain: text("issue_domain").notNull(),
jurisdictionFilter: jsonb("jurisdiction_filter"),
triggerConditions: jsonb("trigger_conditions"),
priority: integer("priority").default(0).notNull(),
routeGraph: jsonb("route_graph").notNull(),
requiredEvidence: jsonb("required_evidence"),
deadlines: jsonb("deadlines"),
sourceCitations: uuid("source_citations").array(),
socialTemplate: text("social_template"),
stopAndRefer: jsonb("stop_and_refer"),
active: boolean("active").default(true).notNull(),
createdAt: createdAt(),
updatedAt: updatedAt(),
});
export const authorities = pgTable("authorities", {
id: id(),
name: text("name").notNull(),
authorityType: text("authority_type").notNull(), // regulator, company, govt_dept, court, ombudsman, police, platform
jurisdiction: jsonb("jurisdiction"),
contactChannels: jsonb("contact_channels"),
officialUrls: jsonb("official_urls"),
escalationPolicy: jsonb("escalation_policy"),
verificationStatus: text("verification_status").default("unverified").notNull(),
lastVerifiedAt: timestamp("last_verified_at", { withTimezone: true }),
createdAt: createdAt(),
});
export const routePlans = pgTable("route_plans", {
id: id(),
missionId: uuid("mission_id")
.notNull()
.references(() => missions.id),
summary: text("summary"),
steps: jsonb("steps").notNull(),
citations: jsonb("citations"),
riskNotes: jsonb("risk_notes"),
expectedTimeline: jsonb("expected_timeline"),
confidence: numeric("confidence"),
supersededAt: timestamp("superseded_at", { withTimezone: true }),
createdAt: createdAt(),
});
export const consentGrants = pgTable("consent_grants", {
id: id(),
missionId: uuid("mission_id")
.notNull()
.references(() => missions.id),
userId: uuid("user_id")
.notNull()
.references(() => users.id),
actionType: text("action_type").notNull(),
actionPreviewHash: text("action_preview_hash").notNull(),
consentText: text("consent_text").notNull(),
channel: text("channel").notNull(),
grantedAt: timestamp("granted_at", { withTimezone: true }),
expiresAt: timestamp("expires_at", { withTimezone: true }),
revokedAt: timestamp("revoked_at", { withTimezone: true }),
createdAt: createdAt(),
});
export const actions = pgTable(
"actions",
{
id: id(),
missionId: uuid("mission_id")
.notNull()
.references(() => missions.id),
routePlanId: uuid("route_plan_id").references(() => routePlans.id),
actionType: text("action_type").notNull(),
title: text("title").notNull(),
description: text("description"),
target: jsonb("target"),
inputPayload: jsonb("input_payload"),
canonicalPayloadHash: text("canonical_payload_hash"),
previewStorageKey: text("preview_storage_key"),
consentRequired: boolean("consent_required").default(true).notNull(),
consentGrantId: uuid("consent_grant_id").references(() => consentGrants.id),
status: text("status").notNull().default("proposed"),
proofState: text("proof_state"),
proofRef: jsonb("proof_ref"),
idempotencyKey: text("idempotency_key"),
riskLevel: text("risk_level").default("low").notNull(),
createdBy: text("created_by").notNull(), // user, ai, system, admin
createdAt: createdAt(),
updatedAt: updatedAt(),
},
(t) => [
uniqueIndex("actions_idempotency_key_uq").on(t.idempotencyKey),
index("actions_mission_idx").on(t.missionId),
],
);
export const actionAttempts = pgTable("action_attempts", {
id: id(),
actionId: uuid("action_id")
.notNull()
.references(() => actions.id),
adapterName: text("adapter_name").notNull(),
attemptNo: integer("attempt_no").notNull(),
requestPayloadHash: text("request_payload_hash"),
responsePayload: jsonb("response_payload"),
providerReference: text("provider_reference"),
status: text("status").notNull(),
errorCode: text("error_code"),
errorMessage: text("error_message"),
startedAt: timestamp("started_at", { withTimezone: true }),
finishedAt: timestamp("finished_at", { withTimezone: true }),
});
export const submissions = pgTable("submissions", {
id: id(),
missionId: uuid("mission_id")
.notNull()
.references(() => missions.id),
routePlanId: uuid("route_plan_id").references(() => routePlans.id),
actionId: uuid("action_id").references(() => actions.id),
adapterName: text("adapter_name"),
targetAuthorityId: uuid("target_authority_id").references(() => authorities.id),
targetContact: jsonb("target_contact"),
status: text("status").notNull(),
proofState: text("proof_state").notNull(),
externalReference: text("external_reference"),
receiptStorageKey: text("receipt_storage_key"),
payloadHash: text("payload_hash"),
error: jsonb("error"),
sentAt: timestamp("sent_at", { withTimezone: true }),
createdAt: createdAt(),
});
// ============================================================================
// Portal assist sessions (Phase 2 Module 5)
// ============================================================================
export const portalAssistSessions = pgTable("portal_assist_sessions", {
id: id(),
missionId: uuid("mission_id")
.notNull()
.references(() => missions.id),
actionId: uuid("action_id")
.notNull()
.references(() => actions.id),
portalUrl: text("portal_url").notNull(),
state: text("state").notNull().default("packet_ready"), // PortalAssistState
formFields: jsonb("form_fields").default([]), // { selector, label, value, filled }[]
screenshotStorageKey: text("screenshot_storage_key"),
receiptStorageKey: text("receipt_storage_key"),
errorMessage: text("error_message"),
createdAt: createdAt(),
updatedAt: updatedAt(),
}, (t) => [
index("portal_assist_sessions_mission_idx").on(t.missionId),
index("portal_assist_sessions_state_idx").on(t.state),
]);
// ============================================================================
// Company Resolver Portal (Phase 2 Module 6)
// ============================================================================
export const organizations = pgTable("organizations", {
id: id(),
normalizedName: text("normalized_name").notNull().unique(),
websiteDomain: text("website_domain"),
officialEmails: text("official_emails").array(), // verified contact emails
verificationStatus: text("verification_status").notNull().default("unverified"), // unverified, pending, verified, rejected
metadata: jsonb("metadata"),
createdAt: createdAt(),
updatedAt: updatedAt(),
}, (t) => [
index("organizations_domain_idx").on(t.websiteDomain),
index("organizations_status_idx").on(t.verificationStatus),
]);
export const organizationMembers = pgTable("organization_members", {
id: id(),
organizationId: uuid("organization_id")
.notNull()
.references(() => organizations.id),
userId: uuid("user_id")
.notNull()
.references(() => users.id),
email: text("email").notNull(),
role: text("role").notNull().default("member"), // admin, member, viewer
verificationMethod: text("verification_method"), // domain_email, magic_link, manual
verifiedAt: timestamp("verified_at", { withTimezone: true }),
createdAt: createdAt(),
}, (t) => [
uniqueIndex("org_members_org_user_uq").on(t.organizationId, t.userId),
index("org_members_user_idx").on(t.userId),
]);
export const resolverThreads = pgTable("resolver_threads", {
id: id(),
missionId: uuid("mission_id")
.notNull()
.references(() => missions.id),
organizationId: uuid("organization_id")
.notNull()
.references(() => organizations.id),
status: text("status").notNull().default("open"), // open, awaiting_user, resolved, closed
maskedSummary: text("masked_summary"), // PII-free summary shown to company
createdAt: createdAt(),
updatedAt: updatedAt(),
}, (t) => [
uniqueIndex("resolver_threads_mission_org_uq").on(t.missionId, t.organizationId),
index("resolver_threads_org_idx").on(t.organizationId),
]);
export const resolverMessages = pgTable("resolver_messages", {
id: id(),
threadId: uuid("thread_id")
.notNull()
.references(() => resolverThreads.id),
senderType: text("sender_type").notNull(), // company, user, system
senderId: uuid("sender_id"), // userId or organizationId
body: text("body").notNull(),
visibility: text("visibility").notNull().default("both"), // both, user_only, resolver_only, public
createdAt: createdAt(),
}, (t) => [
index("resolver_messages_thread_idx").on(t.threadId, t.createdAt),
]);
// ============================================================================
// Social publishing (Phase 2 Module 7)
// ============================================================================
export const socialAccounts = pgTable("social_accounts", {
id: id(),
platform: text("platform").notNull(), // threads, instagram, x
providerUserId: text("provider_user_id"),
accessTokenRef: text("access_token_ref"), // encrypted ref, never plaintext (CLAUDE.md)
refreshTokenRef: text("refresh_token_ref"),
tokenExpiresAt: timestamp("token_expires_at", { withTimezone: true }),
scopes: text("scopes").array(),
status: text("status").notNull().default("active"), // active, expired, revoked
metadata: jsonb("metadata"),
createdAt: createdAt(),
}, (t) => [
index("social_accounts_platform_idx").on(t.platform),
]);
export const socialPosts = pgTable("social_posts", {
id: id(),
missionId: uuid("mission_id")
.notNull()
.references(() => missions.id),
actionId: uuid("action_id")
.notNull()
.references(() => actions.id),
socialAccountId: uuid("social_account_id").references(() => socialAccounts.id),
consentGrantId: uuid("consent_grant_id")
.notNull()
.references(() => consentGrants.id),
platform: text("platform").notNull(), // threads, instagram, x
draftText: text("draft_text").notNull(),
redactedText: text("redacted_text").notNull(),
legalRiskReview: jsonb("legal_risk_review"),
cardStorageKey: text("card_storage_key"), // generated image storage key
externalPostId: text("external_post_id"),
status: text("status").notNull().default("draft"), // draft, approved, published, failed, copy_ready
postUrl: text("post_url"),
proofState: text("proof_state").notNull().default("draft_only"),
publishedAt: timestamp("published_at", { withTimezone: true }),
createdAt: createdAt(),
}, (t) => [
index("social_posts_mission_idx").on(t.missionId),
index("social_posts_status_idx").on(t.status),
]);
// ============================================================================
// eCourts / Case lookups (Phase 2 Module 8)
// ============================================================================
export const caseLookups = pgTable("case_lookups", {
id: id(),
missionId: uuid("mission_id")
.notNull()
.references(() => missions.id),
cnrNumber: text("cnr_number").notNull(),
caseNumber: text("case_number"),
courtName: text("court_name"),
petitioner: text("petitioner"),
respondent: text("respondent"),
caseStatus: text("case_status"), // pending, disposed, stayed, transferred
nextHearingDate: date("next_hearing_date"),
lastOrderDate: date("last_order_date"),
lastOrderSummary: text("last_order_summary"),
rawData: jsonb("raw_data"),
source: text("source").notNull(), // surepass, manual
lookupStatus: text("lookup_status").default("pending").notNull(), // pending, success, failed, not_found
errorMessage: text("error_message"),
createdAt: createdAt(),
updatedAt: updatedAt(),
}, (t) => [
index("case_lookups_mission_idx").on(t.missionId),
index("case_lookups_cnr_idx").on(t.cnrNumber),
]);
// ============================================================================
// Public / resolution
// ============================================================================
export const publicCasePages = pgTable(
"public_case_pages",
{
id: id(),
missionId: uuid("mission_id")
.notNull()
.references(() => missions.id),
slug: text("slug").notNull(),
visibility: text("visibility").default("private_link").notNull(), // Phase 1: private_link only
redactedSummary: text("redacted_summary"),
consentGrantId: uuid("consent_grant_id").references(() => consentGrants.id),
takenDownAt: timestamp("taken_down_at", { withTimezone: true }),
createdAt: createdAt(),
},
(t) => [uniqueIndex("public_case_pages_slug_uq").on(t.slug)],
);
export const redactionSpans = pgTable("redaction_spans", {
id: id(),
sourceType: text("source_type").notNull(), // public_page, email_body, social_post
sourceId: uuid("source_id").notNull(),
start: integer("start").notNull(),
end: integer("end").notNull(),
piiType: text("pii_type").notNull(),
replacement: text("replacement").notNull(),
reviewed: boolean("reviewed").default(false).notNull(),
createdAt: createdAt(),
});
export const followups = pgTable("followups", {
id: id(),
missionId: uuid("mission_id")
.notNull()
.references(() => missions.id),
type: text("type").notNull(), // reminder, escalation, deadline
dueAt: timestamp("due_at", { withTimezone: true }).notNull(),
status: text("status").default("scheduled").notNull(), // scheduled, completed, escalated, snoozed, cancelled
payload: jsonb("payload"),
createdAt: createdAt(),
});
export const resolutionOutcomes = pgTable("resolution_outcomes", {
id: id(),
missionId: uuid("mission_id")
.notNull()
.references(() => missions.id),
outcomeType: text("outcome_type").notNull(),
amountRecovered: numeric("amount_recovered"),
userSatisfaction: integer("user_satisfaction"),
routeEffectiveness: jsonb("route_effectiveness"),
anonymizedPublicSummary: text("anonymized_public_summary"),
publishConsent: boolean("publish_consent").default(false).notNull(),
createdAt: createdAt(),
});
// ============================================================================
// Reliability
// ============================================================================
export const outboxEvents = pgTable(
"outbox_events",
{
id: id(),
eventType: text("event_type").notNull(),
aggregateType: text("aggregate_type").notNull(),
aggregateId: uuid("aggregate_id").notNull(),
payload: jsonb("payload"),
status: text("status").default("pending").notNull(), // pending, processing, sent, failed, dead_letter
attempts: integer("attempts").default(0).notNull(),
nextAttemptAt: timestamp("next_attempt_at", { withTimezone: true }).defaultNow().notNull(),
lockedAt: timestamp("locked_at", { withTimezone: true }),
createdAt: createdAt(),
},
(t) => [index("outbox_events_status_idx").on(t.status, t.nextAttemptAt)],
);
export const idempotencyKeys = pgTable("idempotency_keys", {
key: text("key").primaryKey(),
requestHash: text("request_hash").notNull(),
responseHash: text("response_hash"),
status: text("status").default("in_progress").notNull(),
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
createdAt: createdAt(),
});
export const webhookEvents = pgTable(
"webhook_events",
{
id: id(),
provider: text("provider").notNull(), // whatsapp, telegram, resend
providerEventId: text("provider_event_id").notNull(),
payload: jsonb("payload"),
processedAt: timestamp("processed_at", { withTimezone: true }),
createdAt: createdAt(),
},
(t) => [uniqueIndex("webhook_events_provider_event_uq").on(t.provider, t.providerEventId)],
);
export const auditLogs = pgTable("audit_logs", {
id: id(),
actorType: text("actor_type").notNull(),
actorId: text("actor_id"),
action: text("action").notNull(),
resourceType: text("resource_type").notNull(),
resourceId: text("resource_id"),
before: jsonb("before"),
after: jsonb("after"),
ipHash: text("ip_hash"),
userAgentHash: text("user_agent_hash"),
createdAt: createdAt(),
});
// ============================================================================
// Safety / compliance
// ============================================================================
export const humanReviewItems = pgTable("human_review_items", {
id: id(),
missionId: uuid("mission_id").references(() => missions.id),
reviewType: text("review_type").notNull(), // stop_and_refer, defamation_risk, low_confidence, manual_submit
priority: text("priority").default("normal").notNull(), // low, normal, high, urgent
status: text("status").default("open").notNull(), // open, in_review, resolved, dismissed
context: jsonb("context"),
createdAt: createdAt(),
updatedAt: updatedAt(),
});
export const consentNotices = pgTable("consent_notices", {
id: id(),
userId: uuid("user_id").references(() => users.id),
version: text("version").notNull(),
channel: text("channel").notNull(),
ackedAt: timestamp("acked_at", { withTimezone: true }),
createdAt: createdAt(),
});
export const dataSubjectRequests = pgTable("data_subject_requests", {
id: id(),
userId: uuid("user_id")
.notNull()
.references(() => users.id),
type: text("type").notNull(), // export, erase
status: text("status").default("pending").notNull(),
dueAt: timestamp("due_at", { withTimezone: true }),
resultRef: jsonb("result_ref"),
createdAt: createdAt(),
});
export const notificationPrefs = pgTable("notification_prefs", {
id: id(),
userId: uuid("user_id")
.notNull()
.references(() => users.id),
channel: text("channel").notNull(),
timezone: text("timezone").default("Asia/Kolkata").notNull(),
quietHours: jsonb("quiet_hours"),
enabled: boolean("enabled").default(true).notNull(),
createdAt: createdAt(),
});
export const emailEvents = pgTable(
"email_events",
{
id: id(),
submissionId: uuid("submission_id").references(() => submissions.id),
type: text("type").notNull(), // delivered, bounced, complained, replied
providerEventId: text("provider_event_id"),
payload: jsonb("payload"),
createdAt: createdAt(),
},
(t) => [uniqueIndex("email_events_provider_event_uq").on(t.providerEventId)],
);
export const takedownRequests = pgTable(
"takedown_requests",
{
id: id(),
missionId: uuid("mission_id").notNull().references(() => missions.id),
actionId: uuid("action_id").notNull().references(() => actions.id),
intermediaryName: text("intermediary_name").notNull(),
grievanceOfficerName: text("grievance_officer_name"),
grievanceOfficerEmail: text("grievance_officer_email").notNull(),
contentUrl: text("content_url").notNull(),
legalGrounds: text("legal_grounds").notNull(),
status: text("status").default("pending").notNull(), // pending, sent, acknowledged, complied, rejected
complianceDeadline: timestamp("compliance_deadline", { withTimezone: true }),
compliedAt: timestamp("complied_at", { withTimezone: true }),
createdAt: createdAt(),
updatedAt: updatedAt(),
}
);