import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; export default defineSchema({ userProfiles: defineTable({ username: v.string(), clientSalt: v.string(), encryptedMasterKey: v.string(), hashedAuthKey: v.string(), publicIdentityKey: v.string(), publicSigningKey: v.string(), encryptedPrivateKeys: v.string(), isAdmin: v.boolean(), status: v.optional(v.string()), displayName: v.optional(v.string()), avatarStorageId: v.optional(v.id("_storage")), aboutMe: v.optional(v.string()), customStatus: v.optional(v.string()), }).index("by_username", ["username"]), categories: defineTable({ name: v.string(), position: v.number(), }).index("by_position", ["position"]), channels: defineTable({ name: v.string(), type: v.string(), // 'text' | 'voice' | 'dm' categoryId: v.optional(v.id("categories")), topic: v.optional(v.string()), position: v.optional(v.number()), }).index("by_name", ["name"]) .index("by_category", ["categoryId"]), messages: defineTable({ channelId: v.id("channels"), senderId: v.id("userProfiles"), ciphertext: v.string(), nonce: v.string(), signature: v.string(), keyVersion: v.number(), replyTo: v.optional(v.id("messages")), editedAt: v.optional(v.number()), pinned: v.optional(v.boolean()), }).index("by_channel", ["channelId"]) .index("by_channel_pinned", ["channelId", "pinned"]), messageReactions: defineTable({ messageId: v.id("messages"), userId: v.id("userProfiles"), emoji: v.string(), }) .index("by_message", ["messageId"]) .index("by_message_user_emoji", ["messageId", "userId", "emoji"]), channelKeys: defineTable({ channelId: v.id("channels"), userId: v.id("userProfiles"), encryptedKeyBundle: v.string(), keyVersion: v.number(), }) .index("by_channel", ["channelId"]) .index("by_user", ["userId"]) .index("by_channel_and_user", ["channelId", "userId"]), roles: defineTable({ name: v.string(), color: v.string(), position: v.number(), permissions: v.any(), // JSON object of permissions isHoist: v.boolean(), }), userRoles: defineTable({ userId: v.id("userProfiles"), roleId: v.id("roles"), }) .index("by_user", ["userId"]) .index("by_role", ["roleId"]) .index("by_user_and_role", ["userId", "roleId"]), invites: defineTable({ code: v.string(), encryptedPayload: v.string(), createdBy: v.id("userProfiles"), maxUses: v.optional(v.number()), uses: v.number(), expiresAt: v.optional(v.number()), // timestamp keyVersion: v.number(), }).index("by_code", ["code"]), dmParticipants: defineTable({ channelId: v.id("channels"), userId: v.id("userProfiles"), }) .index("by_channel", ["channelId"]) .index("by_user", ["userId"]), typingIndicators: defineTable({ channelId: v.id("channels"), userId: v.id("userProfiles"), username: v.string(), expiresAt: v.number(), // timestamp }).index("by_channel", ["channelId"]), voiceStates: defineTable({ channelId: v.id("channels"), userId: v.id("userProfiles"), username: v.string(), isMuted: v.boolean(), isDeafened: v.boolean(), isScreenSharing: v.boolean(), isServerMuted: v.boolean(), watchingStream: v.optional(v.id("userProfiles")), }) .index("by_channel", ["channelId"]) .index("by_user", ["userId"]), channelReadState: defineTable({ userId: v.id("userProfiles"), channelId: v.id("channels"), lastReadTimestamp: v.number(), }) .index("by_user", ["userId"]) .index("by_channel", ["channelId"]) .index("by_user_and_channel", ["userId", "channelId"]), serverSettings: defineTable({ afkChannelId: v.optional(v.id("channels")), afkTimeout: v.number(), // seconds (default 300 = 5 min) }), });