Skip to content

📊 Database Schema

📋 Overview

The MyPit platform uses a comprehensive database schema built on Convex with PostgreSQL engine under the hood. This ensures a modern serverless database that provides real-time capabilities, TypeScript type safety, and automatic scaling. The schema is designed to support verifiable credentials, digital identity, privacy-preserving data sharing, and collaborative education features.

📖 Complete Schema Definition

Below is the complete Convex schema definition with all tables, indexes, and validation rules:

typescript
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

const schema = defineSchema({
    // ===== AUTHENTICATION & USER MANAGEMENT =====
    
    users: defineTable({
        email: v.optional(v.string()),
        emailVerificationTime: v.optional(v.number()),
        image: v.optional(v.string()),
        isAnonymous: v.optional(v.boolean()),
        name: v.optional(v.string()),
        phone: v.optional(v.string()),
        phoneVerificationTime: v.optional(v.number()),
        did: v.optional(v.string()),
    })
        .index("by_email", ["email"])
        .index("by_phone", ["phone"]),

    profile: defineTable({
        name: v.optional(v.string()),
        email: v.optional(v.string()),
        phone: v.optional(v.string()),
        profilePhoto: v.optional(v.string()),
        address: v.optional(v.any()),
        userId: v.id("users"),
        identifier: v.optional(v.string()),
        credentials: v.optional(v.any()),
        updatedAt: v.optional(v.number()),
        createdAt: v.optional(v.number()),
        bio: v.optional(v.string()),
        gender: v.optional(v.string()),
        dateOfBirth: v.optional(v.string()),
        orgId: v.optional(v.string()),
    })
        .index("by_email", ["email"])
        .index("by_userId", ["userId"]),

    roleApplications: defineTable({
        applicantId: v.id("users"),
        requestedRole: v.union(
            v.literal("issuer"),
            v.literal("holder"),
            v.literal("verifier"),
            v.literal("data-consumer")
        ),
        status: v.union(
            v.literal("pending"),
            v.literal("under-review"),
            v.literal("approved"),
            v.literal("rejected"),
            v.literal("cancelled")
        ),
        fileUrl: v.optional(v.string()),
        reason: v.optional(v.string()),
        organization: v.optional(v.string()),
        contactEmail: v.optional(v.string()),
        contactPhone: v.optional(v.string()),
        experience: v.optional(v.string()),
        notes: v.optional(v.string()),
        fileId: v.optional(v.id("_storage")),
        documentName: v.optional(v.string()),
        documentSize: v.optional(v.string()),
        rejectionReason: v.optional(v.string()),
        updatedAt: v.optional(v.number()),
        appliedAt: v.number(),
        reviewedBy: v.optional(v.id("users")),
        reviewedAt: v.optional(v.number()),
    })
        .index("by_applicant", ["applicantId"])
        .index("by_role", ["requestedRole"])
        .index("by_status", ["status"]),

    // ===== BADGE MANAGEMENT SYSTEM =====
    
    badgeTemplates: defineTable({
        name: v.string(),
        type: v.union(
            v.literal("mobility"),
            v.literal("achievement"),
            v.literal("healthcare"),
            v.literal("finance")
        ),
        validityPeriod: v.optional(v.number()),
        isVerified: v.optional(v.boolean()),
        description: v.optional(v.string()),
        criteria: v.optional(v.string()),
        verifiedBy: v.optional(v.id("users")),
        verifiedAt: v.optional(v.number()),
        imageUrl: v.optional(v.string()),
        backgroundColor: v.optional(v.string()),
        textColor: v.optional(v.string()),
        price: v.optional(v.number()),
        isActive: v.optional(v.boolean()),
        orgId: v.optional(v.string()),
        ownerId: v.id("users"),
        createdAt: v.optional(v.number()),
        updatedAt: v.optional(v.number()),
    })
        .index("by_type", ["type"])
        .index("by_owner", ["ownerId"])
        .index("by_org", ["orgId"])
        .index("by_type_and_active", ["type", "isActive"])
        .index("by_owner_and_active", ["ownerId", "isActive"])
        .searchIndex("search_templates", {
            searchField: "name",
            filterFields: ["type", "isActive", "ownerId", "orgId"]
        }),

    holders: defineTable({
        name: v.string(),
        email: v.string(),
        phone: v.optional(v.string()),
        memo: v.optional(v.string()),
        createdAt: v.optional(v.number()),
        updatedAt: v.optional(v.number()),
        ownerId: v.id("users"),
        issuerDid: v.optional(v.string()),
        holderDid: v.optional(v.string()),
        holderId: v.optional(v.id("users")),
    })
        .index("by_email", ["email"])
        .index("by_owner", ["ownerId"])
        .index("by_holder", ["holderId"])
        .searchIndex("search_holders", {
            searchField: "name",
            filterFields: ["ownerId", "email"]
        }),

    issuedBadges: defineTable({
        templateId: v.id("badgeTemplates"),
        recipientId: v.id("holders"),
        issuerId: v.id("users"),
        type: v.union(
            v.literal("mobility"),
            v.literal("achievement"),
            v.literal("healthcare"),
            v.literal("finance")
        ),
        zkp: v.optional(v.any()),
        badgeData: v.optional(v.any()),
        vc: v.optional(v.any()),
        status: v.union(
            v.literal("issued"),
            v.literal("pending"),
            v.literal("expired")
        ),
        issuedAt: v.optional(v.number()),
        expiredAfter: v.optional(v.number()),
        expiresAt: v.optional(v.number()),
        issuerDid: v.optional(v.string()),
        holderDid: v.optional(v.string()),
        updatedAt: v.optional(v.number()),
    })
        .index("by_recipient", ["recipientId"])
        .index("by_template", ["templateId"])
        .index("by_issuer", ["issuerId"])
        .index("by_status", ["status"])
        .index("by_recipient_and_status", ["recipientId", "status"])
        .index("by_issuer_and_status", ["issuerId", "status"])
        .index("by_template_and_status", ["templateId", "status"])
        .searchIndex("search_badges", {
            searchField: "recipientId",
            filterFields: ["issuerId", "status", "recipientId"]
        }),

    badgeCollection: defineTable({
        name: v.string(),
        description: v.optional(v.string()),
        views: v.optional(v.number()),
        ownerId: v.id("users"),
        badgeIds: v.array(v.id("issuedBadges")),
        isShared: v.optional(v.boolean()),
        sharedUrl: v.optional(v.string()),
        updatedAt: v.optional(v.number()),
        isProfilePublic: v.optional(v.boolean()),
    })
        .index("by_owner", ["ownerId"])
        .index("by_shared", ["isShared"]),

    // ===== PRIVACY & DATA MANAGEMENT =====
    
    dataRequests: defineTable({
        requesterId: v.id("users"),
        issuerId: v.id("users"),
        purpose: v.string(),
        dataType: v.union(
            v.literal("mobility"),
            v.literal("achievement"),
            v.literal("healthcare"),
            v.literal("finance")
        ),
        dataSizeKB: v.number(),
        usagePeriodDays: v.number(),
        budget: v.number(),
        notes: v.optional(v.string()),
        status: v.union(
            v.literal("pending"),
            v.literal("approved"),
            v.literal("rejected"),
            v.literal("purchased")
        ),
        requestedAt: v.number(),
        updatedAt: v.optional(v.number()),
        reviewedBy: v.optional(v.id("users")),
        reviewedAt: v.optional(v.number()),
        rejectionReason: v.optional(v.string()),
        purchasedAt: v.optional(v.number()),
    })
        .index("by_requester", ["requesterId"])
        .index("by_issuer", ["issuerId"])
        .index("by_status", ["status"])
        .index("by_dataType", ["dataType"])
        .index("by_requester_and_status", ["requesterId", "status"])
        .index("by_issuer_and_status", ["issuerId", "status"]),

    sandboxes: defineTable({
        requesterId: v.id("users"),
        issuerId: v.id("users"),
        purpose: v.string(),
        dataType: v.union(
            v.literal("mobility"),
            v.literal("achievement"),
            v.literal("healthcare"),
            v.literal("finance")
        ),
        dataSizeKB: v.number(),
        usagePeriodDays: v.number(),
        budget: v.number(),
        status: v.union(
            v.literal("pending"),
            v.literal("approved"),
            v.literal("rejected"),
            v.literal("purchased")
        ),
        requestedAt: v.number(),
        availableAt: v.optional(v.number()),
        purchasedAt: v.optional(v.number()),
        resourceId: v.optional(v.string()),
        baseUrl: v.optional(v.string()),
        sandboxStatus: v.optional(v.union(
            v.literal("creating"),
            v.literal("running"),
            v.literal("stopped"),
            v.literal("failed")
        )),
        isDataReady: v.optional(v.boolean()),
    })
        .index("by_requester", ["requesterId"])
        .index("by_issuer", ["issuerId"])
        .index("by_status", ["status"])
        .index("by_sandbox_status", ["sandboxStatus"]),

    // ===== EDUCATION MODULE =====
    
    classes: defineTable({
        name: v.string(),
        description: v.optional(v.string()),
        ownerId: v.id("users"),
        inviteCode: v.string(),
        isArchived: v.optional(v.boolean()),
    })
        .index("by_owner", ["ownerId"])
        .index("by_inviteCode", ["inviteCode"])
        .searchIndex("search_classes", {
            searchField: "name",
            filterFields: ["ownerId", "isArchived"]
        }),

    students: defineTable({
        userId: v.optional(v.id("users")),
        name: v.string(),
        email: v.optional(v.string()),
        externalRef: v.optional(v.string()),
    })
        .index("by_user", ["userId"])
        .index("by_email", ["email"])
        .searchIndex("search_students", {
            searchField: "name",
            filterFields: ["email"]
        }),

    classStudents: defineTable({
        classId: v.id("classes"),
        studentId: v.id("students"),
    })
        .index("by_class", ["classId"])
        .index("by_student", ["studentId"])
        .index("by_class_and_student", ["classId", "studentId"]),

    // ===== TEAMS & COLLABORATION =====
    
    teams: defineTable({
        name: v.string(),
        description: v.optional(v.string()),
        creatorId: v.id("users"),
        classId: v.optional(v.id("classes")),
        inviteCode: v.string(),
        isPublic: v.optional(v.boolean()),
    })
        .index("by_creator", ["creatorId"])
        .index("by_class", ["classId"])
        .index("by_inviteCode", ["inviteCode"])
        .searchIndex("search_teams", {
            searchField: "name",
            filterFields: ["creatorId", "classId", "isPublic"]
        }),

    teamMembers: defineTable({
        teamId: v.id("teams"),
        userId: v.id("users"),
        classId: v.optional(v.id("classes")),
        role: v.union(v.literal("creator"), v.literal("member")),
        joinedAt: v.optional(v.number()),
        canCreateDoc: v.optional(v.boolean()),
        canEditDoc: v.optional(v.boolean()),
        canShareDoc: v.optional(v.boolean()),
        canExportDoc: v.optional(v.boolean()),
    })
        .index("by_team", ["teamId"])
        .index("by_user", ["userId"])
        .index("by_team_and_user", ["teamId", "userId"])
        .index("by_class", ["classId"]),

    documents: defineTable({
        teamId: v.id("teams"),
        title: v.string(),
        description: v.optional(v.string()),
        creatorId: v.id("users"),
        prosemirrorId: v.string(),
        content: v.optional(v.string()),
        lastModified: v.optional(v.number()),
        lastModifiedBy: v.optional(v.id("users")),
    })
        .index("by_team", ["teamId"])
        .index("by_creator", ["creatorId"])
        .index("by_prosemirror", ["prosemirrorId"])
        .searchIndex("search_documents", {
            searchField: "title",
            filterFields: ["teamId", "creatorId"]
        }),

    documentViews: defineTable({
        documentId: v.id("documents"),
        userId: v.id("users"),
        isViewing: v.optional(v.boolean()),
        lastUpdated: v.optional(v.number()),
    })
        .index("by_document", ["documentId"])
        .index("by_user", ["userId"])
        .index("by_document_and_user", ["documentId", "userId"]),

    messages: defineTable({
        teamId: v.id("teams"),
        authorId: v.id("users"),
        content: v.string(),
        type: v.union(v.literal("text"), v.literal("image")),
        storageId: v.optional(v.id("_storage")),
    })
        .index("by_team", ["teamId"])
        .index("by_author", ["authorId"]),

    joinRequests: defineTable({
        teamId: v.id("teams"),
        userId: v.id("users"),
        classId: v.optional(v.id("classes")),
        status: v.union(
            v.literal("pending"),
            v.literal("approved"),
            v.literal("rejected")
        ),
        requestedAt: v.optional(v.number()),
        respondedAt: v.optional(v.number()),
        respondedBy: v.optional(v.id("users")),
    })
        .index("by_team", ["teamId"])
        .index("by_user", ["userId"])
        .index("by_status", ["status"]),

    // ===== API MANAGEMENT & INTEGRATION =====
    
    api: defineTable({
        name: v.string(),
        owner: v.id("users"),
        hash: v.optional(v.string()),
        isVerified: v.optional(v.boolean()),
        permissions: v.optional(v.object({
            read: v.optional(v.boolean()),
            write: v.optional(v.boolean()),
            delete: v.optional(v.boolean()),
        })),
        secret: v.optional(v.string()),
        updatedAt: v.optional(v.number()),
    })
        .index("by_owner", ["owner"])
        .index("by_hash", ["hash"]),

    apiLogs: defineTable({
        apiId: v.id("api"),
        userId: v.optional(v.id("users")),
        apiName: v.optional(v.string()),
        requestMethod: v.union(
            v.literal("GET"),
            v.literal("POST"),
            v.literal("PUT"),
            v.literal("DELETE")
        ),
        requestBody: v.optional(v.any()),
        responseBody: v.optional(v.any()),
        requestHeaders: v.optional(v.any()),
        responseHeaders: v.optional(v.any()),
        requestTime: v.optional(v.number()),
        responseTime: v.optional(v.number()),
        status: v.union(
            v.literal("success"),
            v.literal("error"),
            v.literal("timeout")
        ),
        errorMessage: v.optional(v.string()),
        updatedAt: v.optional(v.number()),
    })
        .index("by_api", ["apiId"])
        .index("by_user", ["userId"])
        .index("by_status", ["status"])
        .index("by_method", ["requestMethod"]),

    // ===== SYSTEM LOGS & MONITORING =====
    
    verificationLogs: defineTable({
        id: v.string(),
        result: v.optional(v.any()),
        zkpResult: v.optional(v.any()),
    })
        .index("by_id", ["id"]),

    sysLogs: defineTable({
        threshold: v.optional(v.number()),
        numberOfShares: v.optional(v.number()),
        nodesInfo: v.optional(v.any()),
        tokenSecret: v.optional(v.any()),
    }),

    rateLimitLog: defineTable({
        userId: v.id("users"),
        action: v.string(),
        timestamp: v.number(),
    })
        .index("by_user_and_action", ["userId", "action"])
        .index("by_timestamp", ["timestamp"]),

    paymentLogs: defineTable({
        buyer: v.id("users"),
        issuer: v.id("users"),
        holder: v.optional(v.id("holders")),
        purpose: v.string(),
        amount: v.number(),
        usagePeriodDays: v.number(),
        dataSizeKB: v.number(),
        updatedAt: v.optional(v.number()),
    })
        .index("by_buyer", ["buyer"])
        .index("by_issuer", ["issuer"])
        .index("by_holder", ["holder"]),

    sandboxLogs: defineTable({
        buyer: v.id("users"),
        issuer: v.id("users"),
        holder: v.optional(v.id("holders")),
        activityName: v.union(
            v.literal("sandbox-creation"),
            v.literal("sandbox-deletion"),
            v.literal("sandbox-login"),
            v.literal("sandbox-error"),
            v.literal("sandbox-code-execution")
        ),
        amount: v.optional(v.number()),
        usagePeriodDays: v.optional(v.number()),
        dataSizeKB: v.optional(v.number()),
        updatedAt: v.optional(v.number()),
    })
        .index("by_buyer", ["buyer"])
        .index("by_issuer", ["issuer"])
        .index("by_holder", ["holder"])
        .index("by_activity", ["activityName"]),
});

export default schema;

🔍 Individual Table ERDs & Details

Below are focused ERD diagrams for each table showing their direct relationships and key fields.


🔐 Authentication & User Management

Users Table

Purpose: Core user authentication and identity management with Convex Auth integration.

Key Features:

  • Optional email/phone with verification timestamps
  • Anonymous user support via isAnonymous
  • DID (Decentralized Identifier) for blockchain identity
  • Indexes: email, phone

Connected Tables: profile, roleApplications, badgeTemplates, holders, issuedBadges, classes, teams, dataRequests, sandboxes, api


Profile Table

Purpose: Extended user profile information and organizational context.

Key Features:

  • One-to-one relationship with users
  • Organization linking via orgId
  • Flexible credentials storage
  • Indexes: email, userId

Role Applications Table

Purpose: Manage user requests for platform roles (issuer, holder, verifier, data-consumer).

Workflow: pending → under-review → approved/rejected/cancelled Indexes: applicantId, requestedRole, status


🏅 Badge Management System

Badge Templates Table

Purpose: Reusable badge definitions created by issuers.

Key Features:

  • Types: mobility, achievement, healthcare, finance
  • Pricing model for badge issuance
  • Verification system by admin users
  • Organization grouping
  • Comprehensive indexing by type, owner, org, status

Holders Table

Purpose: Badge recipients managed by issuers.

Key Features:

  • Dual identity: managed by issuer (ownerId) and linked to user (holderId)
  • DID integration for decentralized identity
  • Full-text search capability
  • Connected to payment and activity logs

Issued Badges Table

Purpose: Actual badge instances issued to holders.

Key Features:

  • Status tracking: issued, pending, expired
  • Zero-knowledge proof (ZKP) support
  • W3C Verifiable Credentials (VC) standard
  • Comprehensive indexing for queries by recipient, issuer, template, status

Badge Collection Table

Purpose: Curated collections of badges for sharing and display.

Key Features:

  • Portfolio showcase functionality
  • View tracking for analytics
  • Public sharing with unique URLs
  • Profile integration toggle

🔒 Privacy & Data Management

Data Requests Table

Purpose: Privacy-preserving data access requests between consumers and issuers.

Key Features:

  • Privacy-first data access model
  • Budget-based pricing
  • Multi-stage approval workflow
  • Data type categorization
  • Comprehensive indexing for dashboard queries

Sandboxes Table

Purpose: Secure containerized environments for privacy-preserving data analysis.

Key Features:

  • Kubernetes Job integration via resourceId
  • Dual status tracking: request status + container status
  • Shamir's Secret Sharing for data security
  • Complete audit trail via logs

🎓 Education Module

Classes Table

Purpose: Learning management system for educators and institutions.

Key Features:

  • Unique invite codes for enrollment
  • Archive functionality for completed classes
  • Full-text search by name
  • Team collaboration integration

Students Table

Purpose: Student registry with optional user account linkage.

Key Features:

  • Hybrid model: platform users + external students
  • External reference system for non-platform students
  • Full-text search capability

Class Students Table

Purpose: Many-to-many enrollment relationship.

Key Features:

  • Simple join table
  • Uniqueness enforcement via compound index
  • Efficient roster and enrollment queries

👥 Teams & Collaboration

Teams Table

Purpose: Collaborative workspaces for projects and classes.

Key Features:

  • Class integration (optional)
  • Invite-based joining
  • Public/private visibility
  • Full-text search capability

Team Members Table

Purpose: Team membership with granular permissions.

Key Features:

  • Role hierarchy: creator, member
  • Granular document permissions
  • Class context tracking
  • Complex indexing for multi-dimensional queries

Documents Table

Purpose: Collaborative document editing with real-time sync.

Key Features:

  • Notion-like editor integration via prosemirrorId
  • Real-time collaboration tracking
  • Version control with modification timestamps
  • Full-text search by title

Messages Table

Purpose: Team communication and chat.

Key Features:

  • Text and image message support
  • File attachment via Convex storage
  • Team-scoped conversations

Join Requests Table

Purpose: Team join request workflow management.

Workflow: User requests → Creator reviews → pending → approved/rejected


🔗 API Management & Logging

API Table

Purpose: API key management for external integrations.

Key Features:

  • Granular permissions: read, write, delete
  • API key hashing for security
  • Verification system
  • Complete request logging

API Logs Table

Purpose: Comprehensive API request/response logging and analytics.

Key Features:

  • Complete request/response capture
  • Performance tracking
  • Error logging and analysis
  • Method-based analytics

📊 System Logs & Security

Rate Limit Log Table

Purpose: Abuse prevention and rate limiting tracking.

Use Case: Track action frequency per user to enforce rate limits and prevent abuse.


Verification Logs Table

Purpose: Badge and credential verification audit trail.

Key Features:

  • Verification outcome tracking
  • Zero-knowledge proof result storage
  • Audit trail for compliance

Sys Logs Table

Purpose: Shamir's Secret Sharing system configuration and monitoring.

Key Features:

  • Cryptographic threshold configuration
  • Node distribution tracking
  • Security token management

Payment Logs Table

Purpose: Financial transaction tracking for data monetization.

Key Features:

  • Complete transaction audit
  • Data usage tracking
  • Multi-party payment model

Sandbox Logs Table

Purpose: Detailed sandbox activity and resource usage tracking.

Activity Types:

  • sandbox-creation
  • sandbox-deletion
  • sandbox-login
  • sandbox-error
  • sandbox-code-execution

Document Views Table

Purpose: Real-time document collaboration and presence tracking.

Key Features:

  • Real-time presence indicators
  • Collaboration analytics
  • User activity tracking

📈 Database Relationship Summary

Core Relationships

User-Centric:

  • usersprofile (1:1)
  • usersroleApplications (1:many as applicant)
  • usersbadgeTemplates (1:many as creator)
  • usersholders (1:many as manager)
  • usersissuedBadges (1:many as issuer)
  • usersclasses (1:many as teacher)
  • usersteams (1:many as creator)

Badge System:

  • badgeTemplatesissuedBadges (1:many)
  • holdersissuedBadges (1:many)
  • issuedBadgesbadgeCollection (many:many via array)

Education System:

  • classesstudents (many:many via classStudents)
  • classesteams (1:many)

Team Collaboration:

  • teamsteamMembers (1:many)
  • teamsdocuments (1:many)
  • teamsmessages (1:many)
  • documentsdocumentViews (1:many)

Privacy & Sandboxes:

  • dataRequestssandboxes (1:1)
  • sandboxessandboxLogs (1:many)
  • sandboxespaymentLogs (1:many)

API & Logging:

  • apiapiLogs (1:many)
  • All entities → respective logs (1:many)

🔍 Index Strategy Summary

Primary Indexes

  • Identity: email, phone, userId
  • Ownership: ownerId, creatorId, applicantId
  • Status: status, isActive, isVerified
  • Temporal: createdAt, updatedAt, timestamp

Compound Indexes

  • Type + Status: ["type", "isActive"]
  • Owner + Time: ["ownerId", "createdAt"]
  • Team + User: ["teamId", "userId"]
  • Status Combinations: ["requesterId", "status"]

Search Indexes

  • Full-text: holders, badges, teams, classes, documents, students

This comprehensive schema supports all MyPit platform features while maintaining optimal query performance and data integrity.

Released under the Dao Solution License.