AI
ClaudeCode
Mario
Architecture

BizWiz Architecture Documentation

This document provides actionable architectural guidance for the BizWiz Invoice Bank Statement Processor. Follow these patterns and principles when implementing new features or maintaining existing code.

Table of Contents

Project Overview

BizWiz is a multi-tenant financial management application that processes invoices, bank transactions, and provides analytics for businesses. The application supports role-based access control with three user types: OWNER, ADMIN, and USER.

Core Features

  • Multi-tenant organization management
  • Invoice processing with PDF and manual entry
  • Bank transaction import via CSV
  • Automated invoice-transaction matching
  • Financial analytics and reporting
  • Employee management and payslip processing
  • Service account (client/supplier) management

Tech Stack

Frontend

  • Framework: Next.js 15.1.0 with App Router
  • Language: TypeScript (strict mode)
  • Styling: TailwindCSS + Radix UI components
  • Forms: React Hook Form + Zod validation
  • Charts: Recharts
  • State Management: React hooks + Context API
  • Notifications: Sonner

Backend

  • Runtime: Node.js with Next.js API Routes
  • Database: PostgreSQL with Prisma ORM
  • Authentication: NextAuth.js with Prisma adapter
  • File Storage: S3-compatible (Linode Object Storage)
  • AI Integration: OpenAI API for PDF processing

Development Tools

  • Testing: Jest (unit) + Cypress (E2E)
  • Linting: ESLint
  • Package Manager: pnpm

Server Architecture

Layered Architecture Pattern

The backend follows a strict layered architecture:

API Routes (app/api/) β†’ Services (lib/services/) β†’ Repositories (lib/repositories/) β†’ Database (Prisma)

1. API Routes Layer (app/api/)

Pattern: Each API route handles HTTP requests and delegates business logic to services.

// Example: app/api/invoices/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth.config";
import { invoiceService } from "@/lib/services/invoice.service";
 
export async function GET(request: NextRequest) {
  // 1. Authentication check
  const session = await getServerSession(authOptions);
  if (!session?.user?.organisationId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
 
  // 2. Extract parameters
  const searchParams = request.nextUrl.searchParams;
 
  try {
    // 3. Delegate to service layer
    const result = await invoiceService.getInvoices(
      session.user.organisationId,
      {
        /* parsed params */
      },
    );
 
    return NextResponse.json(result);
  } catch (error) {
    // 4. Error handling
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 },
    );
  }
}

Rules for API Routes:

  • Always check authentication and organization access
  • Extract and validate input parameters
  • Delegate business logic to services
  • Return consistent error responses
  • Use appropriate HTTP status codes

2. Services Layer (lib/services/)

Pattern: Services contain business logic and orchestrate repository calls.

// Example: lib/services/invoice.service.ts
export const invoiceService = {
  async createInvoice(
    data: CreateInvoiceInput,
    organisationId: string,
  ): Promise<Invoice> {
    // 1. Validate business rules
    if (!data.customerId && !data.supplierId) {
      throw new Error("Invoice must have either customer or supplier");
    }
 
    // 2. Coordinate multiple repository calls if needed
    const invoice = await invoiceRepository.create({
      ...data,
      organisationId,
    });
 
    // 3. Trigger side effects (matching, notifications, etc.)
    await matchingService.attemptAutoMatch(invoice.id, organisationId);
 
    return invoice;
  },
};

Rules for Services:

  • Validate business rules before database operations
  • Coordinate multiple repository calls
  • Handle complex business logic
  • Trigger side effects and integrations
  • Throw descriptive errors for business rule violations

3. Repositories Layer (lib/repositories/)

Pattern: Repositories handle data access and provide organization-scoped queries.

// Example: lib/repositories/invoice.repository.ts
export const invoiceRepository = {
  async findByOrganisation(organisationId: string): Promise<Invoice[]> {
    return prisma.invoice.findMany({
      where: { organisationId },
      include: {
        customer: true,
        supplier: true,
        matches: {
          include: {
            bankTransaction: true,
          },
        },
      },
      orderBy: { createdAt: "desc" },
    });
  },
 
  async create(data: CreateInvoiceData): Promise<Invoice> {
    return prisma.invoice.create({ data });
  },
};

Rules for Repositories:

  • Always filter by organizationId for data isolation
  • Use Prisma relations to avoid N+1 queries
  • Provide consistent error handling
  • Include necessary relations based on use case

Multi-Tenancy Implementation

Organization Scoping: Every data operation must be scoped to the user's organization.

// βœ… Correct - organization-scoped query
const transactions = await prisma.bankTransaction.findMany({
  where: {
    organisationId: session.user.organisationId,
    // other filters...
  },
});
 
// ❌ Incorrect - could leak data between organizations
const transactions = await prisma.bankTransaction.findMany({
  where: {
    // other filters without organisationId
  },
});

API Route Structure

app/api/
β”œβ”€β”€ auth/                    # Authentication endpoints
β”‚   β”œβ”€β”€ [...nextauth]/       # NextAuth.js configuration
β”‚   └── register/           # User registration
β”œβ”€β”€ analytics/              # Analytics and reporting
β”‚   β”œβ”€β”€ overview/           # Dashboard metrics
β”‚   β”œβ”€β”€ expenses/           # Expense analytics
β”‚   └── clients/            # Client analytics
β”œβ”€β”€ invoices/               # Invoice management
β”‚   β”œβ”€β”€ route.ts           # CRUD operations
β”‚   └── upload/            # PDF upload
β”œβ”€β”€ bank-transactions/      # Transaction management
β”‚   β”œβ”€β”€ route.ts           # CRUD operations
β”‚   └── upload-csv/        # CSV import
β”œβ”€β”€ transactions/           # Transaction processing
β”‚   β”œβ”€β”€ categorize/        # AI categorization
β”‚   β”œβ”€β”€ detect-transfers/  # Transfer detection
β”‚   └── auto-match-service-accounts/
└── organisation/           # Organization management
    └── users/             # User management

Client Architecture

Next.js App Router Structure

The frontend uses Next.js App Router with server-side rendering and client components.

app/
β”œβ”€β”€ layout.tsx              # Root layout with navigation
β”œβ”€β”€ page.tsx               # Landing page
β”œβ”€β”€ dashboard/             # Dashboard pages
β”œβ”€β”€ invoices/              # Invoice management
β”œβ”€β”€ transactions/          # Transaction management
β”œβ”€β”€ employees/             # Employee management
β”œβ”€β”€ service-accounts/      # Client/supplier management
└── components/            # Feature components
    β”œβ”€β”€ Analytics/         # Analytics components
    β”œβ”€β”€ Dashboard/         # Dashboard components
    β”œβ”€β”€ InvoiceProcessing/ # Invoice-related components
    └── auth/              # Authentication components

Component Architecture

Pattern: Feature-based component organization with shared UI components.

// Feature Component Example
interface AnalyticsDashboardProps {
  metrics: OverviewMetrics;
  userRole: string;
  organisationId: string;
  matchInvoicesAction: () => Promise<ActionResult>;
  onDataRefresh: () => Promise<void>;
}
 
export function AnalyticsDashboard({
  metrics,
  userRole,
  organisationId,
  matchInvoicesAction,
  onDataRefresh,
}: AnalyticsDashboardProps) {
  // Component implementation
}

Rules for Components:

  • Use TypeScript interfaces for all props
  • Implement proper loading and error states
  • Follow accessibility guidelines
  • Use server actions for mutations
  • Implement proper form validation with Zod

State Management Patterns

Local State: Use React hooks for component-specific state.

const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

Global State: Use Context API for authentication and organization data.

// Session context is provided by NextAuth SessionProvider
import { useSession } from "next-auth/react";
 
export function useAuthenticatedUser() {
  const { data: session, status } = useSession();
  return {
    user: session?.user,
    isLoading: status === "loading",
    isAuthenticated: !!session?.user,
  };
}

Database Architecture

Schema Design Principles

  1. Multi-tenancy: Every entity (except User/Auth tables) has organisationId
  2. Soft relationships: Use nullable foreign keys where appropriate
  3. Audit fields: Include createdAt and updatedAt on all entities
  4. Proper indexing: Index frequently queried fields and foreign keys

Data Relationships

Organisation (1:N) β†’ User
Organisation (1:N) β†’ ServiceAccount
Organisation (1:N) β†’ Invoice
Organisation (1:N) β†’ BankTransaction
Organisation (1:N) β†’ BankAccount

Invoice (N:M) β†’ BankTransaction (through InvoiceBankTransactionMatch)
ServiceAccount (1:N) β†’ Invoice (as customer or supplier)
ServiceAccount (1:N) β†’ BankTransaction
BankAccount (1:N) β†’ BankTransaction

Query Patterns

Always include organization scoping:

// βœ… Correct pattern
const data = await prisma.invoice.findMany({
  where: {
    organisationId,
    status: "PENDING",
  },
  include: {
    customer: true,
    matches: {
      include: {
        bankTransaction: true,
      },
    },
  },
});

Authentication & Authorization

NextAuth.js Configuration

Session Strategy: JWT with custom user fields.

// lib/auth.config.ts
export const authOptions: NextAuthOptions = {
  providers: [
    CredentialsProvider({
      async authorize(credentials) {
        // Validate credentials and return user with role and organisationId
        return {
          id: user.id,
          email: user.email,
          name: user.name,
          role: user.role,
          organisationId: user.organisationId,
        };
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.role = user.role;
        token.organisationId = user.organisationId;
      }
      return token;
    },
    async session({ session, token }) {
      session.user.role = token.role;
      session.user.organisationId = token.organisationId;
      return session;
    },
  },
};

Middleware Protection

Route Protection: Protect all routes except public pages.

// middleware.ts
export default withAuth(
  async function middleware(req) {
    const token = req.nextauth.token;
 
    // Redirect users without organisation to setup page
    if (token && !token.organisationId) {
      if (pathname !== "/create-organisation") {
        return NextResponse.redirect(new URL("/create-organisation", req.url));
      }
    }
 
    return NextResponse.next();
  },
  {
    callbacks: {
      authorized: ({ token }) => !!token,
    },
  },
);

Role-Based Access Control

Frontend Role Checking:

// Component-level role checking
{(user.role === 'OWNER' || user.role === 'ADMIN') && (
  <AdminOnlyComponent />
)}

Backend Permission Validation:

// API route permission checking
if (!session?.user?.organisationId) {
  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
 
if (session.user.role === "USER" && requiredRole !== "USER") {
  return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}

File Structure Standards

Directory Organization

/
β”œβ”€β”€ app/                    # Next.js App Router
β”‚   β”œβ”€β”€ api/               # API routes
β”‚   β”œβ”€β”€ components/        # Feature components
β”‚   └── [feature]/         # Feature pages
β”œβ”€β”€ components/            # Shared UI components
β”‚   └── ui/               # Radix UI components
β”œβ”€β”€ lib/                   # Business logic
β”‚   β”œβ”€β”€ services/         # Business services
β”‚   β”œβ”€β”€ repositories/     # Data access
β”‚   β”œβ”€β”€ types/           # TypeScript types
β”‚   β”œβ”€β”€ utils/           # Utility functions
β”‚   └── validations/     # Zod schemas
β”œβ”€β”€ prisma/               # Database schema
β”œβ”€β”€ cypress/              # E2E tests
β”œβ”€β”€ __tests__/           # Unit tests
└── hooks/               # Custom React hooks

File Naming Conventions

  • Components: PascalCase (e.g., AnalyticsDashboard.tsx)
  • Services: camelCase with .service.ts suffix
  • Repositories: camelCase with .repository.ts suffix
  • Types: camelCase with .types.ts suffix
  • API Routes: route.ts in feature directories
  • Pages: page.tsx in feature directories

Development Patterns

Error Handling

API Routes:

try {
  const result = await service.performOperation();
  return NextResponse.json(result);
} catch (error) {
  console.error("Operation failed:", error);
 
  if (error instanceof ValidationError) {
    return NextResponse.json({ error: error.message }, { status: 400 });
  }
 
  return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}

Client Components - NOT PREFERRED, PREFER SERVER COMPONENTS:

const [error, setError] = useState<string | null>(null);
 
const handleAction = async () => {
  try {
    setError(null);
    await performAction();
  } catch (err) {
    setError(err instanceof Error ? err.message : "Unknown error");
  }
};

Data Fetching Patterns

Server Components (preferred for initial data):

export default async function Page() {
  const session = await getServerSession(authOptions);
  const data = await service.getData(session.user.organisationId);
 
  return <Component data={data} />;
}

Client Components (for dynamic data):

export function DynamicComponent() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
 
  useEffect(() => {
    fetchData().then(setData).finally(() => setLoading(false));
  }, []);
 
  if (loading) return <LoadingSpinner />;
  return <DataDisplay data={data} />;
}

Server Actions Pattern

// Server action in page component
async function updateDataAction(formData: FormData): Promise<ActionResult> {
  "use server";
 
  const session = await getServerSession(authOptions);
  if (!session?.user?.organisationId) {
    return { success: false, message: "Unauthorized" };
  }
 
  try {
    await service.updateData(/* validated data */);
    revalidatePath("/current-path");
    return { success: true, message: "Update successful" };
  } catch (error) {
    return { success: false, message: "Update failed" };
  }
}

Security Patterns

Input Validation

Always validate with Zod:

const CreateInvoiceSchema = z
  .object({
    amount: z.number().positive(),
    issueDate: z.string().transform((str) => new Date(str)),
    customerId: z.string().optional(),
    supplierId: z.string().optional(),
  })
  .refine((data) => data.customerId || data.supplierId, {
    message: "Either customer or supplier must be specified",
  });

Organization Isolation

Repository Pattern:

async findByOrganisation(organisationId: string, filters: any) {
  return prisma.entity.findMany({
    where: {
      organisationId, // Always required
      ...filters
    }
  });
}

Sensitive Data Handling

  • Never log passwords or tokens
  • Use environment variables for secrets
  • Implement proper file upload validation
  • Sanitize user inputs before database operations

IMPORTANT - GENERAL IMPLEMENTATION GUIDELINES

When implementing new features:

  • Always start with backend implementation first!. Check the CONVENTIONS-BACKEND.md file for the guidelines.

  • Test the backend implementation with unit tests and E2E tests. Only continue with the frontend implementation if the backend is working as expected.

  • Continue with the frontend implementation only if the backend is working as expected. Check the CONVENTIONS-FRONTEND.md file for the guidelines.