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
- Tech Stack
- Server Architecture
- Client Architecture
- Database Architecture
- Authentication & Authorization
- File Structure Standards
- Development Patterns
- Security Patterns
- Testing Architecture
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 managementClient 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 componentsComponent 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
- Multi-tenancy: Every entity (except User/Auth tables) has
organisationId - Soft relationships: Use nullable foreign keys where appropriate
- Audit fields: Include
createdAtandupdatedAton all entities - 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) β BankTransactionQuery 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 hooksFile Naming Conventions
- Components: PascalCase (e.g.,
AnalyticsDashboard.tsx) - Services: camelCase with
.service.tssuffix - Repositories: camelCase with
.repository.tssuffix - Types: camelCase with
.types.tssuffix - API Routes:
route.tsin feature directories - Pages:
page.tsxin 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.mdfile 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.mdfile for the guidelines.