スキル一覧に戻る
galactivale

vaultcpa-coding-standards

by galactivale

0🍴 0📅 2026年1月2日
GitHubで見るManusで実行

SKILL.md


name: vaultcpa-coding-standards description: VaultCPA project coding standards, conventions, and best practices for Next.js, React, TypeScript, Express, and Prisma. Use when reviewing code, writing new features, or setting up development patterns. allowed-tools: Read, Grep, Glob

VaultCPA Coding Standards

Version: 2.0 Last Updated: January 2026

This Skill defines the coding standards, architectural patterns, and best practices for the VaultCPA tax and accounting platform.

Table of Contents

  1. Multi-Tenant Architecture (CRITICAL)
  2. Frontend Standards (Next.js/React/TypeScript)
  3. Backend Standards (Express/Prisma)
  4. Database Patterns
  5. Security Requirements
  6. Testing Standards
  7. Code Review Checklist

Multi-Tenant Architecture (CRITICAL)

⚠️ Golden Rule: EVERY Query Must Filter by organizationId

This is the most critical security requirement in VaultCPA.

Data leakage between organizations is a catastrophic failure. Every database query MUST include organizationId filtering.

✅ Correct Patterns

// Backend - Prisma queries
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,  // ← ALWAYS FIRST
    status: 'ACTIVE'
  }
});

// Related data queries
const alerts = await prisma.alert.findMany({
  where: {
    organizationId: req.user.organizationId,
    clientId: clientId  // Already scoped to org
  }
});

// Count queries
const totalClients = await prisma.client.count({
  where: {
    organizationId: req.user.organizationId
  }
});

❌ WRONG - Data Leakage

// NEVER DO THIS - Exposes all organizations' data
const clients = await prisma.client.findMany();

// WRONG - Missing organizationId in where clause
const clients = await prisma.client.findMany({
  where: { status: 'ACTIVE' }
});

// WRONG - Only filtering at service layer
// Must filter at database query level
const allClients = await prisma.client.findMany();
return allClients.filter(c => c.organizationId === orgId);

Middleware Pattern

// All protected routes use authMiddleware
router.get('/api/clients', authMiddleware, async (req, res) => {
  // req.user.organizationId is guaranteed to exist
  const clients = await prisma.client.findMany({
    where: { organizationId: req.user.organizationId }
  });
  res.json({ success: true, data: clients });
});

Code Review Requirement

Every PR touching database queries must:

  1. Verify organizationId filter is present
  2. Test with multiple organizations to confirm isolation
  3. Add a comment in review confirming multi-tenant isolation

Frontend Standards

Project Structure

app/
├── (app)/                    # Main app routes (layout wrapper)
├── (auth)/                   # Authentication routes
├── dashboard/                # Role-based dashboards
│   ├── managing-partner/     # Executive dashboard
│   ├── tax-manager/          # Tax management
│   ├── staff-accountant/     # Staff work dashboard
│   └── system-admin/         # Admin dashboard
└── api/                      # API routes/proxies

components/
├── auth/                     # Authentication components
├── charts/                   # Data visualization
├── home/                     # Dashboard cards
├── layout/                   # Layout components
├── navbar/                   # Navigation
└── sidebar/                  # Sidebar navigation

Next.js App Router Patterns

Server vs Client Components

// Default to Server Components (no 'use client')
// app/dashboard/page.tsx
export default async function Dashboard() {
  const data = await fetchData(); // Can fetch directly
  return <DashboardLayout data={data} />;
}

// Use 'use client' only when needed
// components/InteractiveChart.tsx
'use client';
import { useState } from 'react';

export default function InteractiveChart() {
  const [filter, setFilter] = useState('all');
  // Client-side interactivity
}

Use Client Components when you need:

  • useState, useEffect, or other React hooks
  • Event handlers (onClick, onChange)
  • Browser APIs (localStorage, window)
  • Third-party libraries that require client-side rendering

Dynamic Routes

// app/dashboard/clients/[id]/page.tsx
interface PageProps {
  params: { id: string };
  searchParams?: { [key: string]: string | string[] | undefined };
}

export default async function ClientDetailPage({ params }: PageProps) {
  const clientId = params.id;
  // Fetch and render client data
}

TypeScript Standards

No any Types

// ❌ WRONG
function processData(data: any) {
  return data.value;
}

// ✅ Correct - Use specific types
interface ClientData {
  id: string;
  name: string;
  revenue: number;
}

function processData(data: ClientData) {
  return data.revenue;
}

// ✅ Correct - Use generics for flexibility
function processData<T extends { value: number }>(data: T) {
  return data.value;
}

// ✅ Correct - Use unknown when type is truly unknown
function processData(data: unknown) {
  if (typeof data === 'object' && data !== null && 'value' in data) {
    return (data as { value: number }).value;
  }
  throw new Error('Invalid data structure');
}

Interface Naming

// Props interfaces - suffix with Props
interface AlertCardProps {
  alert: NexusAlert;
  onDismiss: () => void;
}

// Data models - no suffix
interface Client {
  id: string;
  name: string;
  organizationId: string;
}

// API responses - suffix with Response
interface AlertsResponse {
  success: boolean;
  data: NexusAlert[];
  pagination?: PaginationInfo;
}

React Component Standards

Component Structure

'use client';
import { useState, useEffect } from 'react';
import { Card, Button } from '@nextui-org/react';

// 1. Interfaces at top
interface ClientListProps {
  initialClients: Client[];
  organizationId: string;
}

// 2. Component function
export default function ClientList({ initialClients, organizationId }: ClientListProps) {
  // 3. State declarations
  const [clients, setClients] = useState(initialClients);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // 4. Effects
  useEffect(() => {
    // Effect logic
  }, [/* dependencies */]);

  // 5. Event handlers
  const handleRefresh = async () => {
    setLoading(true);
    try {
      const response = await fetch('/api/clients');
      const data = await response.json();
      setClients(data.data);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred');
    } finally {
      setLoading(false);
    }
  };

  // 6. Render helpers (if needed)
  const renderClient = (client: Client) => (
    <Card key={client.id}>
      <h3>{client.name}</h3>
    </Card>
  );

  // 7. Early returns for loading/error states
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  // 8. Main render
  return (
    <div>
      {clients.map(renderClient)}
    </div>
  );
}

Event Handler Naming

// Use handle* for event handlers
const handleClick = () => {};
const handleSubmit = () => {};
const handleChange = (value: string) => {};

// Use on* for props
interface ButtonProps {
  onClick: () => void;
  onSubmit: (data: FormData) => void;
}

NextUI Component Usage

import {
  Card,
  CardBody,
  CardHeader,
  Button,
  Input,
  Select,
  SelectItem,
  Modal,
  ModalContent
} from '@nextui-org/react';

// Use NextUI components consistently
<Card className="bg-white/5 border border-white/10">
  <CardBody>
    <Button color="primary" variant="flat">
      Action
    </Button>
  </CardBody>
</Card>

Tailwind CSS Standards

// Use Tailwind utility classes
<div className="flex items-center justify-between p-4 bg-gray-800 rounded-lg">

// Group related utilities
<div className="
  flex items-center gap-4        // Layout
  p-6 rounded-lg                 // Spacing & shape
  bg-white/5 border border-white/10  // Colors
  hover:bg-white/10 transition-colors // Interactions
">

// Use clsx for conditional classes
import clsx from 'clsx';

<div className={clsx(
  'px-4 py-2 rounded',
  isActive ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700',
  isDisabled && 'opacity-50 cursor-not-allowed'
)}>

Form Handling with Formik

import { Formik, Form, Field } from 'formik';
import * as Yup from 'yup';

const validationSchema = Yup.object({
  name: Yup.string().required('Name is required'),
  email: Yup.string().email('Invalid email').required('Email is required')
});

export default function ClientForm() {
  return (
    <Formik
      initialValues={{ name: '', email: '' }}
      validationSchema={validationSchema}
      onSubmit={async (values, { setSubmitting }) => {
        try {
          await saveClient(values);
        } finally {
          setSubmitting(false);
        }
      }}
    >
      {({ errors, touched, isSubmitting }) => (
        <Form>
          <Field name="name" as={Input} />
          {errors.name && touched.name && <span>{errors.name}</span>}

          <Button type="submit" isLoading={isSubmitting}>
            Save
          </Button>
        </Form>
      )}
    </Formik>
  );
}

Backend Standards

Express Route Structure

// Standard route pattern
const express = require('express');
const router = express.Router();
const { authMiddleware } = require('../middleware/auth');
const { validate } = require('../middleware/validation');
const clientController = require('../controllers/clientController');

// GET list (with pagination)
router.get(
  '/api/clients',
  authMiddleware,
  clientController.list
);

// POST create
router.post(
  '/api/clients',
  authMiddleware,
  validate(clientSchema),
  clientController.create
);

// GET by ID
router.get(
  '/api/clients/:id',
  authMiddleware,
  clientController.getById
);

// PUT update
router.put(
  '/api/clients/:id',
  authMiddleware,
  validate(clientUpdateSchema),
  clientController.update
);

// DELETE
router.delete(
  '/api/clients/:id',
  authMiddleware,
  clientController.delete
);

module.exports = router;

Controller Pattern

// controllers/clientController.js
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

const clientController = {
  async list(req, res, next) {
    try {
      const { organizationId } = req.user;
      const { page = 1, limit = 20, status } = req.query;

      const where = { organizationId };
      if (status) where.status = status;

      const clients = await prisma.client.findMany({
        where,
        skip: (page - 1) * limit,
        take: parseInt(limit),
        orderBy: { createdAt: 'desc' }
      });

      const total = await prisma.client.count({ where });

      res.json({
        success: true,
        data: clients,
        pagination: {
          page: parseInt(page),
          limit: parseInt(limit),
          total,
          pages: Math.ceil(total / limit)
        }
      });
    } catch (error) {
      next(error);
    }
  },

  async create(req, res, next) {
    try {
      const { organizationId } = req.user;
      const data = {
        ...req.body,
        organizationId,
        createdById: req.user.id
      };

      const client = await prisma.client.create({ data });

      res.status(201).json({
        success: true,
        data: client
      });
    } catch (error) {
      next(error);
    }
  },

  async getById(req, res, next) {
    try {
      const { organizationId } = req.user;
      const { id } = req.params;

      const client = await prisma.client.findFirst({
        where: {
          id,
          organizationId  // Critical: prevent cross-org access
        }
      });

      if (!client) {
        return res.status(404).json({
          success: false,
          error: 'Client not found'
        });
      }

      res.json({
        success: true,
        data: client
      });
    } catch (error) {
      next(error);
    }
  }
};

module.exports = clientController;

API Response Format

// Success response
{
  success: true,
  data: { /* resource or array */ }
}

// Success with pagination
{
  success: true,
  data: [/* resources */],
  pagination: {
    page: 1,
    limit: 20,
    total: 150,
    pages: 8
  }
}

// Error response
{
  success: false,
  error: "Descriptive error message"
}

// Validation error response
{
  success: false,
  error: "Validation failed",
  details: [
    { field: "email", message: "Invalid email format" },
    { field: "name", message: "Name is required" }
  ]
}

Input Validation (Joi)

const Joi = require('joi');

const clientSchema = Joi.object({
  name: Joi.string().required().min(2).max(100),
  email: Joi.string().email().required(),
  phone: Joi.string().optional().pattern(/^\d{10}$/),
  riskLevel: Joi.string().valid('LOW', 'MEDIUM', 'HIGH', 'CRITICAL').required(),
  metadata: Joi.object().optional()
});

// In routes
const { validate } = require('../middleware/validation');
router.post('/api/clients', authMiddleware, validate(clientSchema), controller.create);

Error Handling

// Custom error class
class ApiError extends Error {
  constructor(message, statusCode = 500) {
    super(message);
    this.statusCode = statusCode;
    this.name = 'ApiError';
  }
}

// Error handler middleware (last middleware)
app.use((err, req, res, next) => {
  console.error('[ERROR]', err);

  if (err instanceof ApiError) {
    return res.status(err.statusCode).json({
      success: false,
      error: err.message
    });
  }

  // Prisma errors
  if (err.code === 'P2002') {
    return res.status(409).json({
      success: false,
      error: 'Resource already exists'
    });
  }

  // Default error
  res.status(500).json({
    success: false,
    error: process.env.NODE_ENV === 'production'
      ? 'Internal server error'
      : err.message
  });
});

Database Patterns

Prisma Query Patterns

// Always include organizationId
const client = await prisma.client.findFirst({
  where: {
    id: clientId,
    organizationId: req.user.organizationId
  }
});

// Include related data
const client = await prisma.client.findFirst({
  where: { id: clientId, organizationId },
  include: {
    alerts: {
      where: { status: 'PENDING' },
      orderBy: { createdAt: 'desc' }
    },
    decisions: true
  }
});

// Select specific fields only
const clients = await prisma.client.findMany({
  where: { organizationId },
  select: {
    id: true,
    name: true,
    riskLevel: true
  }
});

// Pagination
const clients = await prisma.client.findMany({
  where: { organizationId },
  skip: (page - 1) * limit,
  take: limit,
  orderBy: { createdAt: 'desc' }
});

Audit Trail Pattern

// Create with audit trail
const decision = await prisma.professionalDecision.create({
  data: {
    ...decisionData,
    organizationId,
    createdById: req.user.id,
    createdAt: new Date()
  }
});

// Update with audit trail
const updated = await prisma.professionalDecision.update({
  where: { id: decisionId },
  data: {
    ...updates,
    updatedById: req.user.id,
    updatedAt: new Date()
  }
});

Transaction Pattern

const result = await prisma.$transaction(async (tx) => {
  // Create client
  const client = await tx.client.create({
    data: { ...clientData, organizationId }
  });

  // Create initial alert
  const alert = await tx.alert.create({
    data: {
      type: 'ONBOARDING',
      clientId: client.id,
      organizationId
    }
  });

  // Log audit trail
  await tx.auditLog.create({
    data: {
      action: 'CLIENT_CREATED',
      resourceId: client.id,
      userId: req.user.id,
      organizationId
    }
  });

  return { client, alert };
});

Security Requirements

Authentication

// JWT middleware - verify and attach user
const authMiddleware = async (req, res, next) => {
  try {
    const token = req.headers.authorization?.replace('Bearer ', '');

    if (!token) {
      return res.status(401).json({
        success: false,
        error: 'Authentication required'
      });
    }

    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded; // Contains id, organizationId, role
    next();
  } catch (error) {
    res.status(401).json({
      success: false,
      error: 'Invalid token'
    });
  }
};

Password Handling

const bcrypt = require('bcryptjs');

// Hash password before storing
const hashedPassword = await bcrypt.hash(password, 10);

// Never store plain text
await prisma.user.create({
  data: {
    email,
    password: hashedPassword,  // ALWAYS hashed
    organizationId
  }
});

// Verify password
const isValid = await bcrypt.compare(inputPassword, user.password);

Input Sanitization

// Sanitize user input to prevent XSS
const sanitizeHtml = require('sanitize-html');

const cleanContent = sanitizeHtml(userInput, {
  allowedTags: ['b', 'i', 'em', 'strong'],
  allowedAttributes: {}
});

Environment Variables

// NEVER commit secrets to git
// Use .env files (in .gitignore)

// .env
DATABASE_URL=postgresql://...
JWT_SECRET=your_secret_here_min_32_chars
JWT_REFRESH_SECRET=different_secret

// Access via process.env
const secret = process.env.JWT_SECRET;

// Validate required variables on startup
if (!process.env.JWT_SECRET) {
  throw new Error('JWT_SECRET is required');
}

Testing Standards

Unit Test Structure (Jest)

// __tests__/nexusDetector.test.js
const { SalesNexusDetector } = require('../services/SalesNexusDetector');

describe('SalesNexusDetector', () => {
  let detector;

  beforeEach(() => {
    detector = new SalesNexusDetector();
  });

  describe('California Economic Nexus', () => {
    it('should trigger RED alert when revenue exceeds $500k', () => {
      const result = detector.detect({ CA: 650000 });

      expect(result).toHaveLength(1);
      expect(result[0]).toMatchObject({
        severity: 'RED',
        stateCode: 'CA',
        threshold: 500000
      });
    });
  });
});

Integration Test (Supertest)

const request = require('supertest');
const app = require('../app');

describe('POST /api/clients', () => {
  it('should create client with valid data', async () => {
    const response = await request(app)
      .post('/api/clients')
      .set('Authorization', `Bearer ${validToken}`)
      .send({
        name: 'Test Client',
        email: 'test@example.com'
      })
      .expect(201);

    expect(response.body.success).toBe(true);
    expect(response.body.data.name).toBe('Test Client');
  });

  it('should enforce multi-tenant isolation', async () => {
    // Create client in org 1
    await createClient({ organizationId: 'org1' });

    // Try to access with org 2 token
    const response = await request(app)
      .get('/api/clients')
      .set('Authorization', `Bearer ${org2Token}`)
      .expect(200);

    // Should not see org 1's client
    expect(response.body.data).toHaveLength(0);
  });
});

Code Review Checklist

Security ✓

  • Multi-tenant isolation enforced (organizationId filter)
  • No hardcoded secrets or API keys
  • Passwords hashed with bcrypt
  • Input validation on all endpoints
  • SQL injection prevented (Prisma parameterization)
  • XSS prevented (React auto-escaping, input sanitization)

Code Quality ✓

  • TypeScript types defined (no any)
  • Error handling implemented
  • Consistent naming conventions
  • No console.log in production code
  • Comments explain "why", not "what"

Testing ✓

  • Tests added for new features
  • Critical paths covered (nexus detection, auth)
  • Multi-tenant isolation tested

VaultCPA Specifics ✓

  • Audit trail created for important operations
  • Tax calculations verified against thresholds
  • Professional decisions documented properly
  • Database migrations are reversible

Performance ✓

  • No N+1 query patterns
  • Pagination implemented for lists
  • Appropriate indexes exist
  • Large files handled efficiently

File Naming Conventions

Frontend:
- Components: PascalCase (ClientCard.tsx)
- Pages: lowercase (page.tsx, layout.tsx)
- Utilities: camelCase (formatCurrency.ts)
- Types: PascalCase (types.ts with PascalCase interfaces)

Backend:
- Routes: camelCase (clientRoutes.js)
- Controllers: camelCase (clientController.js)
- Services: PascalCase (SalesNexusDetector.js)
- Tests: *.test.js or *.spec.js

Common Anti-Patterns to Avoid

  1. ❌ Missing organizationId filter → Data leakage
  2. ❌ Using any type → Loss of type safety
  3. ❌ Inline business logic in routes → Hard to test
  4. ❌ Unhandled promise rejections → Server crashes
  5. ❌ Missing error boundaries → Poor UX
  6. ❌ N+1 queries → Performance issues
  7. ❌ Hardcoded configuration → Deployment problems
  8. ❌ Skipping validation → Security vulnerabilities

When using this Skill:

  1. Reference specific sections when reviewing code
  2. Cite examples when providing feedback
  3. Link to this Skill in PR comments for context
  4. Update this Skill when new patterns emerge

スコア

総合スコア

60/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です