import { authTables } from "@convex-dev/auth/server"; import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; export default defineSchema({ ...authTables, users: defineTable({ // Convex Auth fields name: v.optional(v.string()), image: v.optional(v.string()), email: v.optional(v.string()), emailVerificationTime: v.optional(v.number()), phone: v.optional(v.string()), phoneVerificationTime: v.optional(v.number()), isAnonymous: v.optional(v.boolean()), // custom fields username: v.optional(v.string()), imageId: v.optional(v.id("_storage")), // Phone linking fields phoneNumberHash: v.optional(v.string()), phoneLinkedAt: v.optional(v.number()), phoneVerified: v.optional(v.boolean()), accessToken: v.optional(v.string()), refreshToken: v.optional(v.string()), expiresAt: v.optional(v.number()), }) .index("email", ["email"]) .index("by_phoneHash", ["phoneNumberHash"]), gmailSyncStates: defineTable({ userId: v.id("users"), status: v.optional(v.string()), phase: v.optional(v.string()), pageToken: v.optional(v.union(v.string(), v.null())), promotionsProcessed: v.optional(v.number()), namespaceCleared: v.optional(v.boolean()), jobStartedAt: v.optional(v.number()), lastHistoryId: v.optional(v.string()), lastSyncedAt: v.optional(v.number()), lastError: v.optional(v.string()), totalPrimaryCount: v.optional(v.number()), totalSecondaryCount: v.optional(v.number()), totalSentCount: v.optional(v.number()), totalPromotionsCount: v.optional(v.number()), }).index("by_user", ["userId"]), userMemorySummaries: defineTable({ userId: v.id("users"), summary: v.string(), lastUpdatedAt: v.number(), }).index("by_user", ["userId"]), // Phone link tokens for authentication phoneLinkTokens: defineTable({ token: v.string(), phoneNumberHash: v.string(), phoneNumber: v.optional(v.string()), // Store actual phone for linking channel: v.string(), // "sms", "whatsapp", etc. userId: v.optional(v.id("users")), createdAt: v.number(), expiresAt: v.number(), used: v.boolean(), }) .index("by_token", ["token"]) .index("by_phoneHash", ["phoneNumberHash"]), // Rate limiting for phone number operations phoneRateLimits: defineTable({ phoneNumberHash: v.string(), attempts: v.array(v.float64()), lastAttempt: v.number(), resetAt: v.number(), }).index("by_phone", ["phoneNumberHash"]), // Phone verifications for OTP tracking phoneVerifications: defineTable({ userId: v.id("users"), phoneNumber: v.string(), status: v.union( v.literal("pending"), v.literal("verified"), v.literal("failed"), ), createdAt: v.number(), lastAttempt: v.optional(v.number()), verifiedAt: v.optional(v.number()), }) .index("by_user_phone", ["userId", "phoneNumber"]) .index("by_phone", ["phoneNumber"]), // Verification sessions for secure OTP validation verificationSessions: defineTable({ phoneNumber: v.string(), verificationId: v.string(), // Surge verification ID verified: v.boolean(), expiresAt: v.number(), // Unix timestamp usedAt: v.optional(v.number()), // Prevent reuse }) .index("by_phone", ["phoneNumber"]) .index("by_verification_id", ["verificationId"]), // Message store for all SMS/WhatsApp messages messages: defineTable({ phoneNumber: v.string(), // Keep for sending replies (consider encrypting) phoneNumberHash: v.string(), // For lookups body: v.string(), channel: v.string(), // "sms", "whatsapp", etc. direction: v.string(), // "inbound" or "outbound" timestamp: v.number(), processed: v.boolean(), // Optional fields userId: v.optional(v.id("users")), // Link to user if known metadata: v.optional(v.any()), // For extra data }) .index("by_phone", ["phoneNumberHash", "timestamp"]) .index("by_processed", ["processed", "timestamp"]), });