// The hub's tables: Better Auth's (people, sessions, passwords, signing keys, device sign-ins) and the hub's own // (linked IMVU accounts, link codes, the audit log). Better Auth's are as its CLI generates them for the plugins // used (username, admin, device-authorization, bearer, jwt), with the hub's linkCap added to user. import { sql } from "drizzle-orm"; import { bigint, boolean, customType, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, uniqueIndex, } from "drizzle-orm/pg-core"; const createdAt = () => timestamp("created_at", { withTimezone: true }).notNull().defaultNow(); const updatedAt = () => timestamp("updated_at", { withTimezone: true }) .notNull() .defaultNow() .$onUpdate(() => new Date()); // A person: one T5DE account. export const user = pgTable( "user", { id: text("id").primaryKey(), name: text("name").notNull(), email: text("email").notNull().unique(), emailVerified: boolean("email_verified").notNull().default(false), image: text("image"), createdAt: createdAt(), updatedAt: updatedAt(), username: text("username").unique(), displayUsername: text("display_username"), role: text("role").notNull().default("user"), banned: boolean("banned").default(false), banReason: text("ban_reason"), banExpires: timestamp("ban_expires", { withTimezone: true }), // How many IMVU accounts the person may link; moderators raise it on request. linkCap: integer("link_cap").notNull().default(10), }, (t) => [uniqueIndex("user_username_lower").on(sql`lower(${t.username})`)], ); export const session = pgTable( "session", { id: text("id").primaryKey(), expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), token: text("token").notNull().unique(), createdAt: createdAt(), updatedAt: updatedAt(), ipAddress: text("ip_address"), userAgent: text("user_agent"), userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), impersonatedBy: text("impersonated_by"), }, (t) => [index("session_user").on(t.userId)], ); export const account = pgTable( "account", { id: text("id").primaryKey(), accountId: text("account_id").notNull(), providerId: text("provider_id").notNull(), userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), accessToken: text("access_token"), refreshToken: text("refresh_token"), idToken: text("id_token"), accessTokenExpiresAt: timestamp("access_token_expires_at", { withTimezone: true }), refreshTokenExpiresAt: timestamp("refresh_token_expires_at", { withTimezone: true }), scope: text("scope"), password: text("password"), createdAt: createdAt(), updatedAt: updatedAt(), }, (t) => [index("account_user").on(t.userId)], ); export const verification = pgTable( "verification", { id: text("id").primaryKey(), identifier: text("identifier").notNull(), value: text("value").notNull(), expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), createdAt: createdAt(), updatedAt: updatedAt(), }, (t) => [index("verification_identifier").on(t.identifier)], ); export const jwks = pgTable("jwks", { id: text("id").primaryKey(), publicKey: text("public_key").notNull(), privateKey: text("private_key").notNull(), createdAt: createdAt(), expiresAt: timestamp("expires_at", { withTimezone: true }), alg: text("alg"), crv: text("crv"), }); // A T5DE proxy signing in: it shows userCode, the person approves it on the website. export const deviceCode = pgTable("device_code", { id: text("id").primaryKey(), deviceCode: text("device_code").notNull(), userCode: text("user_code").notNull(), userId: text("user_id").references(() => user.id, { onDelete: "cascade" }), expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), status: text("status").notNull(), lastPolledAt: timestamp("last_polled_at", { withTimezone: true }), pollingInterval: integer("polling_interval"), clientId: text("client_id"), scope: text("scope"), createdAt: createdAt(), updatedAt: updatedAt(), }); // A linked IMVU account. Linking it again from another person moves it (the most recent proof wins). export const imvuLink = pgTable( "imvu_link", { imvuId: bigint("imvu_id", { mode: "number" }).primaryKey(), userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), // What catalogs and rooms see instead of the person, so a person's accounts can't be connected. pairwiseId: text("pairwise_id").notNull().unique(), username: text("username").notNull(), linkedAt: timestamp("linked_at", { withTimezone: true }).notNull().defaultNow(), showBadge: boolean("show_badge").notNull().default(false), showPresence: boolean("show_presence").notNull().default(false), listRooms: boolean("list_rooms").notNull().default(false), }, (t) => [index("imvu_link_user").on(t.userId)], ); // An IMVU room a T5DE user entered whose owner is a linked account, as IMVU last described it. Listed on the hub // only if its owner chose to list their rooms and IMVU says it's public and listed. export const room = pgTable( "room", { instanceId: text("instance_id").primaryKey(), // "-" ownerImvuId: bigint("owner_imvu_id", { mode: "number" }).notNull(), name: text("name").notNull(), description: text("description").notNull().default(""), imageURL: text("image_url").notNull().default(""), privacy: text("privacy").notNull(), type: text("type").notNull(), contentRating: text("content_rating").notNull().default(""), accessPass: boolean("access_pass").notNull().default(false), occupancy: integer("occupancy").notNull().default(0), capacity: integer("capacity").notNull().default(0), expired: boolean("expired").notNull().default(false), checkedAt: timestamp("checked_at", { withTimezone: true }).notNull().defaultNow(), seenAt: timestamp("seen_at", { withTimezone: true }).notNull().defaultNow(), }, (t) => [index("room_owner").on(t.ownerImvuId)], ); // An age check by one provider: of a person, or of one of their linked IMVU accounts. Only the outcome: never a // birth date, document, photo or name (providers that see those keep them). export const ageCheck = pgTable( "age_check", { id: text("id").primaryKey(), userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), // Set when the check is of one IMVU account (IMVU's own verification); null when it's of the person. imvuId: bigint("imvu_id", { mode: "number" }), provider: text("provider").notNull(), // "imvu", "didit", "wallet", ... method: text("method").notNull().default(""), // e.g. "imvu_age_verified", "id_document", "wallet_mdoc" status: text("status").notNull(), // "pending", "complete", "failed", "revoked" overEighteen: boolean("over_eighteen").notNull().default(false), providerRef: text("provider_ref"), // the provider's session or transaction ID createdAt: createdAt(), checkedAt: timestamp("checked_at", { withTimezone: true }), expiresAt: timestamp("expires_at", { withTimezone: true }), }, (t) => [ index("age_check_user").on(t.userId), index("age_check_imvu").on(t.imvuId), uniqueIndex("age_check_provider_ref").on(t.provider, t.providerRef), ], ); // A code the person is to put in an IMVU account's tagline, proving it's theirs. export const linkChallenge = pgTable( "link_challenge", { userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), imvuId: bigint("imvu_id", { mode: "number" }).notNull(), username: text("username").notNull(), code: text("code").notNull(), createdAt: createdAt(), }, (t) => [primaryKey({ columns: [t.userId, t.imvuId] })], ); export const audit = pgTable( "audit", { id: serial("id").primaryKey(), at: timestamp("at", { withTimezone: true }).notNull().defaultNow(), actorId: text("actor_id").references(() => user.id, { onDelete: "set null" }), action: text("action").notNull(), target: text("target").notNull().default(""), detail: jsonb("detail").$type>().notNull().default({}), }, (t) => [index("audit_at").on(t.at)], ); // ---- Products (docs/products.md) ---- export type FileEntry = { name: string; sha256: string; size: number }; /** The public info of the IMVU product a revision derives from (IMVU's API), when it was looked up. */ export type ParentInfo = { found: boolean; // false: IMVU shows no such product publicly image: string | null; // https, on *.imvu.com categories: { id: number; name: string }[]; // outermost first gender: string; // "female", "male" or "" checkedAt: string; // ISO time }; // The hub's own signing keys (Ed25519) for the tokens catalogs and proxies check. The newest is used. export const signingKey = pgTable("signing_key", { id: text("id").primaryKey(), // the JWK's kid publicKey: jsonb("public_key").$type>().notNull(), privateKey: jsonb("private_key").$type>().notNull(), createdAt: createdAt(), }); // A catalog: a server hosting product files. Hidden until a moderator approves it. export const catalog = pgTable( "catalog", { id: text("id").primaryKey(), ownerId: text("owner_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), name: text("name").notNull(), url: text("url").notNull(), // its public URL contact: text("contact").notNull(), // takedown and legal contact visibility: text("visibility").notNull().default("public"), // "public", "private" membership: text("membership").notNull().default("open"), // "open", "approval", "invite" status: text("status").notNull().default("pending"), // "pending", "approved", "rejected" keyHash: text("key_hash").notNull(), // sha256 of its key, hex createdAt: createdAt(), }, (t) => [uniqueIndex("catalog_url").on(t.url)], ); // A product: its info, owned by its creator. Its files are in revisions. export const product = pgTable( "product", { id: text("id").primaryKey(), // 10 of a-z2-7 creatorId: text("creator_id") .notNull() .references(() => user.id, { onDelete: "restrict" }), name: text("name").notNull(), description: text("description").notNull().default(""), // HTML, already cleaned rating: text("rating").notNull(), // "general", "adult": the highest of its revisions, or raised by the creator latest: integer("latest").notNull().default(0), // the newest revision's number // Deleted by its creator: no longer listed or added to libraries; those who have it keep it, and rooms keep it. deletedAt: timestamp("deleted_at", { withTimezone: true }), // Removed by moderation: gone everywhere (no page, no files, out of libraries and rooms). removedAt: timestamp("removed_at", { withTimezone: true }), removedReason: text("removed_reason"), createdAt: createdAt(), updatedAt: updatedAt(), }, (t) => [index("product_creator").on(t.creatorId), index("product_created").on(t.createdAt)], ); // A revision: a product's files. Never changes (but can be retracted). export const productRevision = pgTable( "product_revision", { productId: text("product_id") .notNull() .references(() => product.id, { onDelete: "cascade" }), number: integer("number").notNull(), files: jsonb("files").$type().notNull(), rating: text("rating").notNull(), parent: bigint("parent", { mode: "number" }).notNull().default(0), // the IMVU product it derives from parentInfo: jsonb("parent_info").$type(), // null until looked up (parent > 0) notes: text("notes").notNull().default(""), token: text("token").notNull(), // the hub's t5de-revision token uploadedBy: text("uploaded_by").references(() => user.id, { onDelete: "set null" }), retracted: boolean("retracted").notNull().default(false), createdAt: createdAt(), }, (t) => [primaryKey({ columns: [t.productId, t.number] })], ); // Which catalogs host which revisions. export const hosting = pgTable( "hosting", { catalogId: text("catalog_id") .notNull() .references(() => catalog.id, { onDelete: "cascade" }), productId: text("product_id").notNull(), number: integer("number").notNull(), createdAt: createdAt(), }, (t) => [primaryKey({ columns: [t.catalogId, t.productId, t.number] }), index("hosting_product").on(t.productId)], ); // A person's library: products they got, optionally staying on a revision. export const library = pgTable( "library", { userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), productId: text("product_id") .notNull() .references(() => product.id, { onDelete: "cascade" }), stayOn: integer("stay_on"), addedAt: timestamp("added_at", { withTimezone: true }).notNull().defaultNow(), }, (t) => [primaryKey({ columns: [t.userId, t.productId] })], ); // ---- T5DE-hosted rooms (docs/hosted-rooms.md) ---- /** A scene item, as the client's room_contents: [furniture instance ID, product ID (IMVU's or the hub's), placement]. */ export type SceneItem = [number, number | string, string]; // A room hosted by the hub: its instance ID is "2100000000-", its chat ID 2100000000 + n. Who's in it is kept in // memory only (apps/api/src/realtime.ts). Closing it keeps the row, so its number is never reused. export const hostedRoom = pgTable( "hosted_room", { n: serial("n").primaryKey(), ownerId: text("owner_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), // One of the owner's linked IMVU accounts, shown as the room's owner (the client lets it set the scene). ownerImvuId: bigint("owner_imvu_id", { mode: "number" }).notNull(), name: text("name").notNull(), description: text("description").notNull().default(""), // plain text roomPid: bigint("room_pid", { mode: "number" }).notNull(), // the room shell, an IMVU product rating: text("rating").notNull().default("general"), // "general", "adult" maxUsers: integer("max_users").notNull().default(10), scene: jsonb("scene").$type().notNull().default([]), sceneHash: text("scene_hash").notNull().default(""), closed: boolean("closed").notNull().default(false), // Phase 2 (docs/hosted-rooms.md): "public" or "private" (unlisted; joined by the owner, moderators and invitees). privacy: text("privacy").notNull().default("public"), // Up to 20 IMVU accounts that may boot and ban (and invite to a private room), with their names as known then. moderators: jsonb("moderators").$type().notNull().default([]), // The room's image (hosted_room_image), by the sha256 of its bytes; null: none. imageHash: text("image_hash"), createdAt: createdAt(), updatedAt: updatedAt(), }, (t) => [index("hosted_room_owner").on(t.ownerId), index("hosted_room_created").on(t.createdAt)], ); /** A hosted room's moderator: an IMVU account, and its name as known when it was added ("" if unknown). */ export type RoomModerator = { imvuId: number; username: string }; const bytea = customType<{ data: Buffer; driverData: Buffer | Uint8Array }>({ dataType: () => "bytea", fromDriver: (v) => Buffer.from(v), }); // A hosted room's image, as uploaded (PNG or JPEG, checked by its magic bytes, up to 512 KB). export const hostedRoomImage = pgTable("hosted_room_image", { n: integer("n") .primaryKey() .references(() => hostedRoom.n, { onDelete: "cascade" }), contentType: text("content_type").notNull(), // "image/png", "image/jpeg" data: bytea("data").notNull(), hash: text("hash").notNull(), // sha256, hex updatedAt: updatedAt(), }); // IMVU accounts banned from a hosted room; until null: for good. export const hostedRoomBan = pgTable( "hosted_room_ban", { n: integer("n") .notNull() .references(() => hostedRoom.n, { onDelete: "cascade" }), imvuId: bigint("imvu_id", { mode: "number" }).notNull(), byImvuId: bigint("by_imvu_id", { mode: "number" }).notNull(), until: timestamp("until", { withTimezone: true }), createdAt: createdAt(), }, (t) => [primaryKey({ columns: [t.n, t.imvuId] })], ); // A person's favorite hosted rooms. export const hostedRoomFavorite = pgTable( "hosted_room_favorite", { userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), n: integer("n") .notNull() .references(() => hostedRoom.n, { onDelete: "cascade" }), createdAt: createdAt(), }, (t) => [primaryKey({ columns: [t.userId, t.n] })], ); // A person's rating of a hosted room, 1-5. export const hostedRoomRating = pgTable( "hosted_room_rating", { userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), n: integer("n") .notNull() .references(() => hostedRoom.n, { onDelete: "cascade" }), score: integer("score").notNull(), updatedAt: updatedAt(), }, (t) => [primaryKey({ columns: [t.userId, t.n] }), index("hosted_room_rating_room").on(t.n)], ); // A person's latest visit to each hosted room (room.join), with the IMVU account they joined as. 50 are kept. export const hostedRoomVisit = pgTable( "hosted_room_visit", { userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), n: integer("n") .notNull() .references(() => hostedRoom.n, { onDelete: "cascade" }), imvuId: bigint("imvu_id", { mode: "number" }).notNull(), at: timestamp("at", { withTimezone: true }).notNull().defaultNow(), }, (t) => [primaryKey({ columns: [t.userId, t.n] }), index("hosted_room_visit_at").on(t.userId, t.at)], ); // ---- Decorations over IMVU rooms (docs/products.md, "Decorations") ---- /** A decoration: [furniture instance ID, hub product ID, placement], as the client's room_contents. */ export type DecorationItem = [number, string, string]; // T5 products placed in an IMVU room by its owner. IMVU keeps the room without them (T5DE's proxy strips them from // what the client saves); the proxies of T5DE users in the room add them back. Hosted rooms keep theirs in // hosted_room.scene instead. export const roomDecoration = pgTable( "room_decoration", { instanceId: text("instance_id").primaryKey(), // IMVU's room instance, "-" ownerImvuId: bigint("owner_imvu_id", { mode: "number" }).notNull(), // the number before the dash contents: jsonb("contents").$type().notNull(), updatedAt: updatedAt(), }, (t) => [index("room_decoration_owner").on(t.ownerImvuId)], );