Claude-skill-registry database-schema-guide
VaultCPA database schema reference with 50+ Prisma models, relationships, and query patterns. Use when working with database models, designing features, or understanding data structure.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/database-schema-guide" ~/.claude/skills/majiayu000-claude-skill-registry-database-schema-guide && rm -rf "$T"
skills/data/database-schema-guide/SKILL.mdVaultCPA Database Schema Guide
Version: 2.0 Last Updated: January 2026 Schema Location:
server/prisma/schema.prisma
This Skill provides a comprehensive guide to VaultCPA's PostgreSQL database schema, including model relationships, common patterns, and query examples.
Quick Navigation
- Schema Overview
- Core Models
- Common Query Patterns
- Relationship Diagrams
- Migration Patterns
- Data Model Decisions
For detailed model references, see:
- Core Models Reference - Organization, User, Client
- Compliance Models - Alerts, Nexus, Risk
- Workflow Models - Tasks, Decisions, Documents
Schema Overview
Model Categories
Tenant & Identity (4 models)
- Organization - Root tenant entity
- User - Team members with CPA credentials
- Permission - Role-based access control
- ApiKey - API authentication
Core Business (12 models)
- Client - Primary data subject
- ClientState - Per-state tracking
- BusinessProfile - Business details
- BusinessLocation - Physical locations
- Contact - Client contacts
- GeographicDistribution - Revenue by region
- RevenueBreakdown - Categorized revenue
- CustomerDemographics - Customer analytics
- ClientRevenueHistory - Historical revenue
- StateTaxInfo - State tax thresholds
- OrganizationMetadata - Custom org data
- PerformanceMetric - Business metrics
Compliance & Risk (10 models)
- Alert - Multi-purpose alerts
- NexusAlert - State tax nexus specific
- NexusActivity - Activity tracking
- RiskFactor - Risk assessments
- ComplianceStandard - Compliance frameworks
- RegulatoryChange - Law changes
- DataProcessing - Processing records
- AuditLog - System audit trail
- AuditTrail - Business audit trail
- Notification - In-app notifications
Workflow & Decisions (7 models)
- Task - Workflow tasks
- TaskStep - Task breakdown
- ProfessionalDecision - High-stakes decisions
- DecisionTable - Decision audit
- Document - File management
- AdvisoryDocument - Client advice
- Comment - Collaborative notes
Communication (3 models)
- Consultation - Client meetings
- Communication - Contact log
- ClientCommunication - Interaction tracking
Tax & Doctrine (4 models)
- DoctrineRule - Tax rules with versioning
- DoctrineApproval - Approval workflow
- DoctrineVersionEvent - Change history
- DoctrineImpactMetrics - Rule impact
System & Integration (9 models)
- Integration - Third-party connections
- Webhook - Webhook configs
- WebhookDelivery - Delivery tracking
- GeneratedDashboard - Custom dashboards
- Template - Reusable content
- Report - Scheduled reports
- ActivityFeed - Team activity
- DataProcessing - Processing jobs
Total: 50+ models
Core Models
Organization (Tenant Root)
model Organization { id String @id @default(uuid()) slug String @unique name String subscriptionTier String @default("trial") subscriptionStatus String @default("active") // Relationships - ALL data scoped to organization users User[] clients Client[] alerts Alert[] tasks Task[] // ... 30+ more relationships }
Key Points:
- Root of multi-tenant hierarchy
- Every other model references organizationId
- Subscription and billing tracked here
- Custom settings stored in JSON fields
Common Queries:
// Get organization with user count const org = await prisma.organization.findUnique({ where: { id: orgId }, include: { _count: { select: { users: true, clients: true } } } }); // Get all orgs expiring soon const expiring = await prisma.organization.findMany({ where: { subscriptionExpiresAt: { lte: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days } } });
User (Team Members)
model User { id String @id @default(uuid()) organizationId String @map("organization_id") email String passwordHash String @map("password_hash") role String // MANAGING_PARTNER, TAX_MANAGER, STAFF_ACCOUNTANT, SYSTEM_ADMIN // CPA Credentials cpaLicense String? @map("cpa_license") cpaState String? @map("cpa_state") cpaExpiration DateTime? @map("cpa_expiration") // Relationships organization Organization @relation(fields: [organizationId], references: [id]) assignedTasks Task[] @relation("AssignedUser") createdTasks Task[] @relation("CreatedByUser") }
Key Points:
- Scoped to organization
- Role determines dashboard access
- CPA credentials for compliance tracking
- Audit trail through created/assigned relationships
Common Queries:
// Get user with permissions const user = await prisma.user.findFirst({ where: { email, organizationId }, include: { organization: true, permissions: true } }); // Get all CPAs in org const cpas = await prisma.user.findMany({ where: { organizationId, cpaLicense: { not: null } } });
Client (Primary Business Entity)
model Client { id String @id @default(uuid()) organizationId String @map("organization_id") name String status String @default("prospect") riskLevel String @default("low") // Relationships organization Organization @relation(fields: [organizationId], references: [id]) alerts Alert[] nexusAlerts NexusAlert[] tasks Task[] decisions ProfessionalDecision[] clientStates ClientState[] revenueHistory ClientRevenueHistory[] }
Key Points:
- Central entity for all client data
- Risk level drives compliance workflows
- State-specific data in related tables
- Extensive relationships (20+ related models)
Common Queries:
// Get client with all nexus alerts const client = await prisma.client.findFirst({ where: { id: clientId, organizationId }, include: { nexusAlerts: { where: { status: 'ACTIVE' }, orderBy: { createdAt: 'desc' } }, clientStates: true, revenueHistory: { orderBy: { year: 'desc' }, take: 3 // Last 3 years } } }); // Get high-risk clients const highRisk = await prisma.client.findMany({ where: { organizationId, riskLevel: { in: ['HIGH', 'CRITICAL'] } }, include: { _count: { select: { alerts: true } } } });
Alert (Multi-Purpose Alerts)
model Alert { id String @id @default(uuid()) organizationId String @map("organization_id") clientId String? @map("client_id") type String // NEXUS, COMPLIANCE, RISK, DOCUMENT, DEADLINE severity String // CRITICAL, HIGH, MEDIUM, LOW status String @default("pending") message String // Polymorphic relationships client Client? @relation(fields: [clientId], references: [id]) consultation Consultation? @relation(fields: [consultationId], references: [id]) }
Key Points:
- Generic alert system for all alert types
- Polymorphic - can relate to different entities
- Status workflow: pending → acknowledged → in_progress → resolved
- Severity determines urgency
NexusAlert (State Tax Nexus Specific)
model NexusAlert { id String @id @default(uuid()) organizationId String @map("organization_id") clientId String @map("client_id") state String type String // SALES_TAX, INCOME_TAX, FRANCHISE_TAX, PAYROLL severity String // RED, ORANGE, YELLOW threshold Decimal? currentAmount Decimal? // Doctrine integration appliedDoctrineRuleId String? @map("applied_doctrine_rule_id") doctrineRule DoctrineRule? @relation(fields: [appliedDoctrineRuleId], references: [id]) }
Key Points:
- Specialized for tax nexus alerts
- Links to doctrine rules for professional judgment
- Tracks threshold vs actual amounts
- Color-coded severity (RED/ORANGE/YELLOW)
ProfessionalDecision (High-Stakes Decisions)
model ProfessionalDecision { id String @id @default(uuid()) organizationId String @map("organization_id") clientId String @map("client_id") decisionType String riskLevel String financialExposure Decimal? // Decision content question String analysis String conclusion String supportingEvidence Json @default("{}") // Peer review reviewStatus String @default("pending") reviewedBy String? reviewedAt DateTime? // Audit trail createdById String @map("created_by_id") createdBy User @relation("DecisionCreator", fields: [createdById], references: [id]) }
Key Points:
- Documents high-stakes professional judgments
- Peer review workflow built-in
- Financial exposure tracking
- Complete audit trail for liability protection
DoctrineRule (Tax Doctrine with Versioning)
model DoctrineRule { id String @id @default(uuid()) organizationId String? @map("organization_id") clientId String? @map("client_id") scope String // FIRM, OFFICE, CLIENT version Int @default(1) status String // DRAFT, PENDING_APPROVAL, ACTIVE, DISABLED // Rule content title String description String taxType String states String[] // Array of state codes // Versioning previousVersionId String? versionEvents DoctrineVersionEvent[] approvals DoctrineApproval[] impactMetrics DoctrineImpactMetrics[] }
Key Points:
- Reusable tax position rules
- Versioned for compliance
- Scoped to firm/office/client level
- Approval workflow integration
- Impact tracking for audit purposes
Common Query Patterns
Pattern 1: Multi-Tenant Filtering
// ALWAYS include organizationId const clients = await prisma.client.findMany({ where: { organizationId: req.user.organizationId // Required! } }); // With additional filters const activeClients = await prisma.client.findMany({ where: { organizationId: req.user.organizationId, status: 'ACTIVE', riskLevel: { in: ['HIGH', 'CRITICAL'] } } });
Pattern 2: Pagination
const page = 1; const limit = 20; const clients = await prisma.client.findMany({ where: { organizationId }, skip: (page - 1) * limit, take: limit, orderBy: { createdAt: 'desc' } }); const total = await prisma.client.count({ where: { organizationId } }); const pages = Math.ceil(total / limit);
Pattern 3: Efficient Relationships (Avoid N+1)
// ❌ BAD - N+1 query const clients = await prisma.client.findMany({ where: { organizationId } }); for (const client of clients) { const alerts = await prisma.alert.findMany({ where: { clientId: client.id } }); } // ✅ GOOD - Single query with include const clients = await prisma.client.findMany({ where: { organizationId }, include: { alerts: { where: { status: 'PENDING' } } } });
Pattern 4: Selective Field Loading
// Only fetch needed fields const clients = await prisma.client.findMany({ where: { organizationId }, select: { id: true, name: true, riskLevel: true, _count: { select: { alerts: true } } } });
Pattern 5: Transactions
const result = await prisma.$transaction(async (tx) => { // Create client const client = await tx.client.create({ data: { ...clientData, organizationId } }); // Create onboarding alert const alert = await tx.alert.create({ data: { type: 'ONBOARDING', clientId: client.id, organizationId } }); // Audit log await tx.auditLog.create({ data: { action: 'CLIENT_CREATED', resourceId: client.id, userId: req.user.id, organizationId } }); return { client, alert }; });
Pattern 6: Soft Deletes
// Instead of deleting, mark as deleted await prisma.client.update({ where: { id: clientId }, data: { deletedAt: new Date() } }); // Filter out deleted records const activeClients = await prisma.client.findMany({ where: { organizationId, deletedAt: null } });
Relationship Diagrams
Core Entity Relationships
Organization (Tenant Root) │ ├─► User (Team members) │ └─► Task (Assigned work) │ ├─► Client (Business entity) │ ├─► Alert (All alert types) │ ├─► NexusAlert (Tax nexus specific) │ ├─► Task (Client work) │ ├─► ProfessionalDecision (Judgments) │ ├─► ClientState (Per-state data) │ ├─► RevenueHistory (Historical data) │ ├─► Consultation (Meetings) │ └─► Document (Files) │ ├─► DoctrineRule (Tax rules) │ ├─► DoctrineApproval (Approval workflow) │ ├─► DoctrineVersionEvent (Version history) │ └─► NexusAlert (Applied to alerts) │ └─► Integration (Third-party) ├─► Webhook (Event configs) └─► WebhookDelivery (Delivery log)
Alert Workflow
Alert Created (PENDING) │ ├─► Acknowledged (USER_ACTION) │ └─► In Progress (WORK_STARTED) │ ├─► Resolved (COMPLETED) │ └─► Dismissed (NOT_APPLICABLE) │ └─► Escalated (CRITICAL_SEVERITY) └─► Consultation Created
Decision Workflow
ProfessionalDecision Created (DRAFT) │ ├─► Submitted for Review (PENDING_REVIEW) │ ├─► Approved (APPROVED) │ │ └─► Active (Applied to clients) │ │ │ └─► Rejected (REJECTED) │ └─► Back to Draft (REVISIONS_NEEDED) │ └─► Archived (ARCHIVED)
Migration Patterns
Safe Migration Strategy
1. Additive Changes (Safe)
// Add optional field model Client { newField String? // Optional } // Deploy migration // Backfill data if needed // Make required in next migration
2. Renaming Fields (Zero Downtime)
// Step 1: Add new field model Client { oldName String newName String? } // Step 2: Dual-write in application // Step 3: Backfill data // Step 4: Switch reads to newName // Step 5: Remove oldName
3. Breaking Changes (Requires Downtime)
// Changing field type or removing required field // Plan maintenance window // Run migration during low-traffic period
Migration Commands
# Development - interactive cd server npx prisma migrate dev --name add_client_risk_level # Production - automated npx prisma migrate deploy # Check status npx prisma migrate status # Generate Prisma client npx prisma generate # View data npx prisma studio
Data Model Decisions
Why Separate NexusAlert from Alert?
Reason: Specialized tax nexus tracking with doctrine rule integration
Alert - Generic (all types: compliance, risk, document) NexusAlert - Tax nexus specific (threshold tracking, doctrine rules)
Benefits:
- Cleaner schema (nexus-specific fields don't clutter Alert)
- Better query performance (smaller Alert table)
- Doctrine integration without affecting other alerts
Why ClientState Table?
Reason: Per-state tracking for multi-state clients
Client (parent) └─► ClientState[] (one per state where client operates) ├─► state: "CA" ├─► hasNexus: true ├─► registeredForSalesTax: true └─► lastFilingDate: 2024-01-15
Benefits:
- Scalable (clients can operate in 1-50 states)
- Clean queries (get all CA clients, get client's states)
- Historical tracking per state
Why JSON Fields?
Used for flexible, schema-less data:
model Organization { settings Json @default("{}") // Customizable settings branding Json @default("{}") // Logo, colors, themes features Json @default("{}") // Feature flags }
Use JSON when:
- Data structure varies by tenant
- Frequent schema changes needed
- Non-queryable configuration data
Use separate tables when:
- Need to query/filter on field
- Foreign key relationships needed
- Data integrity constraints required
Audit Trail Strategy
Two-Level Approach:
AuditLog - System-level (all actions, auto-generated) ├─► user_id, action, resource_type, resource_id, timestamp AuditTrail - Business-level (important business events) ├─► decision_id, event_type, description, user_id, timestamp
Why both?
- AuditLog: Complete system history for debugging
- AuditTrail: Business events for compliance/audit
Index Strategy
Critical Indexes
// Multi-tenant queries @@index([organizationId]) // Common lookups @@index([organizationId, status]) @@index([organizationId, createdAt(sort: Desc)]) // Relationship indexes (auto-created by Prisma) // Fields used in @relation get indexes automatically // Composite indexes for common queries @@index([organizationId, clientId, type])
When to Add Indexes
- WHERE clauses - Fields frequently used in filters
- ORDER BY - Fields used for sorting
- JOIN operations - Foreign key fields
- Multi-column queries - Composite indexes
Index Performance Check
-- Check query performance EXPLAIN ANALYZE SELECT * FROM clients WHERE organization_id = '...'; -- See table indexes SELECT * FROM pg_indexes WHERE tablename = 'clients'; -- Index usage stats SELECT schemaname, tablename, indexname, idx_scan FROM pg_stat_user_indexes ORDER BY idx_scan DESC;
Common Mistakes to Avoid
- ❌ Missing organizationId filter → Data leakage
- ❌ N+1 queries → Use include/select
- ❌ No pagination → Memory issues with large datasets
- ❌ Not using transactions → Data inconsistency
- ❌ Forgetting indexes → Slow queries as data grows
- ❌ Using findUnique without unique constraint → Runtime errors
- ❌ Not cascading deletes → Orphaned records
Quick Reference
Get Organization
const org = await prisma.organization.findUnique({ where: { id: orgId } });
Get User with Org
const user = await prisma.user.findFirst({ where: { email, organizationId }, include: { organization: true } });
Get Client with Alerts
const client = await prisma.client.findFirst({ where: { id: clientId, organizationId }, include: { alerts: { where: { status: 'PENDING' } }, nexusAlerts: true } });
Create with Audit Trail
const client = await prisma.client.create({ data: { ...clientData, organizationId, createdById: req.user.id, createdAt: new Date() } });
For detailed model specifications, see:
- core-models.md - Complete field definitions
- compliance-models.md - Alert and risk models
- workflow-models.md - Task and decision models
When using this Skill:
- Always verify organizationId filtering
- Use appropriate query patterns for performance
- Follow migration best practices
- Refer to relationship diagrams for data modeling