Vireo Architecture
System Overview
Vireo is built as a modular monolith with clear boundaries between business domains, designed to support future microservice extraction while maintaining development velocity. The system follows domain-driven design principles with event-driven communication patterns.
High-Level Architecture
┌─────────────────────────────────────────────────────────────────┐
│ FRONTEND LAYER │
├─────────────────────────────────────────────────────────────────┤
│ Next.js Dashboard │ Mobile PWA │ Swagger API Documentation │
│ React 18.2 │ (Planned) │ REST Endpoints │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────┴─────────┐
┌─────────────────────────────────────────────────────────────────┐
│ API GATEWAY LAYER │
├─────────────────────────────────────────────────────────────────┤
│ NestJS REST API │ Firebase Auth Guard │ Permission Guards │
│ 25+ Controllers │ Request Context │ Audit Interceptors │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────┴─────────┐
┌─────────────────────────────────────────────────────────────────┐
│ BUSINESS LOGIC LAYER │
├─────────────────────────────────────────────────────────────────┤
│ CRM │ Finance │ Events │ Forms │ Tasks │ Workflows │ Reporting │
│ Auth │ SKU │ Agents │ Automation │ Integrations │ Webhooks │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────┴─────────┐
┌─────────────────────────────────────────────────────────────────┐
│ DATA LAYER │
├─────────────────────────────────────────────────────────────────┤
│ PostgreSQL DB │ Prisma ORM │ Redis Cache │ Cloud Storage │
│ 1,578 line │ Connection │ BullMQ │ File Uploads │
│ Schema │ Pooling │ Queues │ │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────┴─────────┐
┌─────────────────────────────────────────────────────────────────┐
│ INTEGRATION LAYER │
├─────────────────────────────────────────────────────────────────┤
│ Google OAuth │ Stripe Payments │ Gmail │ Drive │ Sheets │
│ GoMethod │ Otter.ai │ Twilio │ Plaid │ Sage 50 │ AgentField │
└─────────────────────────────────────────────────────────────────┘
Technology Stack Details
Backend Stack
Core Framework
- NestJS 11.0 - Enterprise-grade TypeScript framework
- Dependency injection container
- Module system with clear boundaries
- Decorator-based architecture
- Built-in testing utilities
- OpenAPI/Swagger integration
Language & Runtime
- TypeScript 5.4 - Type-safe development
- Node.js 18+ - LTS runtime with modern features
- ts-node-dev - Development hot-reload
- ts-node - Production execution
Database & ORM
-
PostgreSQL - Primary relational database
- JSONB support for flexible metadata
- Full-text search capabilities
- Advanced indexing strategies
- Row-level security (future)
-
Prisma 6.18 - Next-generation ORM
- Type-safe database client
- Automatic migrations
- Connection pooling
- Query optimization
- 1,578-line schema with 60+ models
Authentication & Security
-
Firebase Admin SDK 13.5 - User authentication
- Google OAuth 2.0 integration
- Domain-restricted access
- Token verification
- Custom claims for permissions
-
Passport 0.7 - Authentication middleware
- Google OAuth strategy
- Custom Firebase strategy
- Session management
Queue System
-
BullMQ 5.65 - Distributed job queue
- Redis-backed persistence
- Job scheduling and retries
- Progress tracking
- Event notifications
-
IORedis 5.8 - Redis client
- Connection pooling
- Cluster support
- Lua script execution
External Services
-
Stripe 18.5 - Payment processing
- Webhooks for events
- Payment intents
- Customer management
- Invoice generation
-
Plaid 39.1 - Bank connections
- Account linking
- Transaction sync
- Balance checking
-
Google APIs 169.0 - Workspace integration
- Gmail API
- Google Drive API
- Google Sheets API
- Calendar API
-
Nodemailer 7.0 - Email sending
- SMTP transport
- HTML templates
- Attachment support
Validation & Transformation
- class-validator 0.14 - DTO validation
- class-transformer 0.5 - Object mapping
- zod (frontend) - Schema validation
Development Tools
- Jest 29.7 - Testing framework
- ESLint 8.56 - Code linting
- Prettier 3.2 - Code formatting
- TypeScript ESLint 7.0 - TypeScript linting
Frontend Stack
Core Framework
-
Next.js 14.2 - React framework
- App Router (not Pages Router)
- Server Components
- API Routes
- Image Optimization
- Automatic code splitting
-
React 18.2 - UI library
- Concurrent rendering
- Server Components
- Suspense boundaries
- Error boundaries
Styling System
-
Tailwind CSS 3.4 - Utility-first CSS
- Custom design tokens
- Dark/light theme support
- Responsive design
- JIT compilation
-
Custom Design System - Vireo UI Kit
- Typography tokens (Space Grotesk, Inter)
- Color primitives (60+ tokens)
- Spacing scale (4-64px)
- Border radius system
- Shadow definitions
- Motion/animation presets
- Data visualization colors
State Management
-
TanStack Query (React Query) - Server state
- Automatic caching
- Background refetching
- Optimistic updates
- Mutation management
- Infinite queries
-
React Context - Client state
- Auth context
- Theme context
- User preferences
Form Management
-
React Hook Form - Form state
- Uncontrolled inputs
- Validation integration
- Performance optimization
-
Zod - Schema validation
- Type inference
- Error messages
- Composition
UI Components
-
Custom Component Library - Built in-house
- Button, Input, Select variants
- Modal, Drawer, Popover
- Table with pagination
- Card, Badge, Tag
- Alert, Toast, Snackbar
- Skeleton loaders
- Empty states
-
Lucide React - Icon system (1000+ icons)
-
@dnd-kit - Drag and drop (Kanban, form builder)
-
Recharts 2.10 - Chart library
-
@visx - Advanced visualizations
-
React Dropzone 14.3 - File uploads
-
React Plaid Link 3.6 - Bank connection UI
Data Display
- TanStack Table - Advanced tables
- Server pagination
- Column sorting
- Filtering
- Row selection
- Column resizing
- Virtual scrolling (future)
Build Tools
- PostCSS 8.4 - CSS processing
- Autoprefixer 10.4 - Browser prefixes
- TypeScript 5.4 - Type checking
- ESLint - Code quality
- Prettier - Code formatting
Infrastructure Stack
Cloud Platform (Google Cloud Platform)
-
Cloud Run - Serverless containers
- Auto-scaling (0-1000 instances)
- Pay-per-request pricing
- No infrastructure management
- Automatic HTTPS
- Multi-region deployment
-
Cloud SQL - Managed PostgreSQL
- Automated backups
- Point-in-time recovery
- High availability
- Private IP networking
- Connection pooling via Prisma
-
Cloud Storage - Object storage
- File uploads (receipts, forms)
- Static asset hosting
- CDN integration
- Lifecycle policies
-
Secret Manager - Secret storage
- Encrypted at rest
- Versioned secrets
- IAM-controlled access
- Automatic rotation support
-
Cloud Build - CI/CD pipeline
- GitHub integration
- Docker image builds
- Multi-stage deployments
- Automated testing
-
Cloud Logging - Centralized logs
- Structured JSON logs
- Log-based metrics
- Real-time monitoring
-
Cloud Trace - Distributed tracing
- Request latency tracking
- Service dependencies
- Performance bottlenecks
Container & Deployment
-
Docker - Containerization
- Multi-stage builds
- Layer caching
- Security scanning
-
GitHub Actions (potential) - Workflow automation
-
Makefile - Development scripts
Monitoring & Observability
-
Structured Logging - JSON format
- Request/response logging
- Error tracking
- Performance metrics
- Audit trails
-
Health Checks - System status
/healthendpoint- Database connectivity
- Redis connectivity
- External service status
Database Architecture
Prisma Schema Overview
The database schema is defined in a comprehensive 1,578-line Prisma schema file containing 60+ models.
Core Database Models
Identity & Access
model User {
id: String (Primary Key)
email: String (Unique)
displayName: String?
photoURL: String?
provider: String
providerId: String
isActive: Boolean
createdAt: DateTime
updatedAt: DateTime
// Relations
permissions: UserPermission[]
auditLogs: AuditLog[]
interactions: Interaction[]
}
model UserPermission {
id: String (Primary Key)
userId: String (Foreign Key)
scope: String
department: String?
canRead: Boolean
canWrite: Boolean
canDelete: Boolean
canApprove: Boolean
createdAt: DateTime
user: User
}
model AuditLog {
id: String (Primary Key)
tableName: String (Indexed)
recordId: String (Indexed)
action: AuditAction (CREATE, UPDATE, DELETE)
oldValues: JSON?
newValues: JSON?
changedFields: String[]
userId: String? (Foreign Key)
sessionId: String?
ipAddress: String?
userAgent: String?
skuId: String?
reason: String?
source: String?
createdAt: DateTime (Indexed)
user: User?
}
CRM Models
model Contact {
id: String (Primary Key)
firstName: String?
lastName: String?
fullName: String (Indexed)
email: String? (Indexed)
phone: String?
mobilePhone: String?
workPhone: String?
organizationId: String? (Foreign Key)
assignedTo: String? (Indexed)
status: ContactStatus
type: String?
source: String?
tags: String[] (Indexed with GIN)
customFields: JSON
address: JSON
socialMedia: JSON
preferences: JSON
ivi: String? (Unique) // Legacy DonorSnap IPK
createdAt: DateTime
updatedAt: DateTime
// Relations
organization: Organization?
interactions: Interaction[]
donations: Donation[]
registrations: EventRegistration[]
tasks: TaskAssignment[]
segments: SegmentMember[]
mergedFrom: ContactMerge[]
mergedInto: ContactMerge[]
}
model Organization {
id: String (Primary Key)
name: String (Indexed)
type: OrganizationType
parentId: String? (Foreign Key)
level: Int (1-3 hierarchy)
description: String?
website: String?
address: JSON
metadata: JSON
isActive: Boolean
createdAt: DateTime
updatedAt: DateTime
// Relations
parent: Organization?
children: Organization[]
contacts: Contact[]
primaryContact: ContactOrgRole?
roles: ContactOrgRole[]
}
model ContactOrgRole {
id: String (Primary Key)
contactId: String (Foreign Key)
organizationId: String (Foreign Key)
role: String?
title: String?
department: String?
isPrimary: Boolean
isActive: Boolean
startDate: DateTime?
endDate: DateTime?
createdAt: DateTime
updatedAt: DateTime
contact: Contact
organization: Organization
}
model Interaction {
id: String (Primary Key)
contactId: String (Foreign Key, Indexed)
type: InteractionType (EMAIL, CALL, MEETING, NOTE, WHATSAPP)
subject: String
description: String?
outcome: String?
nextAction: String?
nextActionDate: DateTime?
userId: String (Foreign Key)
metadata: JSON
attachments: JSON?
createdAt: DateTime
contact: Contact
user: User
}
model Segment {
id: String (Primary Key)
name: String
description: String?
criteria: JSON (Filter rules)
isAutoRefresh: Boolean
refreshCron: String?
lastRefreshed: DateTime?
memberCount: Int
createdAt: DateTime
updatedAt: DateTime
members: SegmentMember[]
}
BSI (Builders Service Identifier) Models
model Sku {
id: String (Primary Key)
code: String (Unique, Indexed)
// Format: REGION-COUNTRY-PROJECT-TEAM-EVENT-SUFFIX
region: String (2 chars)
country: String (3 chars, ISO 3166-1 alpha-3)
projectId: String?
teamId: String?
eventId: String?
suffix: String?
description: String?
status: SkuStatus
metadata: JSON
createdAt: DateTime
updatedAt: DateTime
// Relations
project: Project?
team: Team?
event: Event?
budgets: Budget[]
donations: Donation[]
expenses: Expense[]
}
model Project {
id: String (Primary Key)
name: String
description: String?
status: ProjectStatus
startDate: DateTime?
endDate: DateTime?
budget: Decimal?
skuId: String? (Foreign Key)
managerId: String?
metadata: JSON
createdAt: DateTime
updatedAt: DateTime
sku: Sku?
teams: Team[]
budgets: Budget[]
accounts: ProjectAccount[]
}
model Team {
id: String (Primary Key)
name: String
description: String?
projectId: String? (Foreign Key)
skuId: String? (Foreign Key)
leaderId: String?
metadata: JSON
createdAt: DateTime
updatedAt: DateTime
project: Project?
sku: Sku?
members: TeamMember[]
events: Event[]
}
Finance Models
model ChartOfAccount {
id: String (Primary Key)
code: String (Unique)
accountCode: String?
name: String
accountName: String?
description: String?
section: String?
major: Int?
minor: Int?
accountType: AccountType (ASSET, LIABILITY, EQUITY, REVENUE, EXPENSE)
normalBalance: String?
parentId: String? (Foreign Key, hierarchical)
isActive: Boolean
isSystem: Boolean
tenantId: String? (Foreign Key)
createdAt: DateTime
updatedAt: DateTime
// Relations
parent: ChartOfAccount?
children: ChartOfAccount[]
journalLines: JournalLine[]
transactionLines: TransactionLine[]
invoiceLines: InvoiceLine[]
budgets: Budget[]
projectAccounts: ProjectAccount[]
}
model JournalEntry {
id: String (Primary Key)
entryNumber: String (Unique)
date: DateTime (Indexed)
description: String
reference: String?
totalDebit: Decimal
totalCredit: Decimal
isBalanced: Boolean
status: JournalStatus (DRAFT, POSTED, REVERSED)
userId: String
createdAt: DateTime
updatedAt: DateTime
lines: JournalLine[]
}
model JournalLine {
id: String (Primary Key)
journalEntryId: String (Foreign Key)
debitAccountId: String? (Foreign Key)
creditAccountId: String? (Foreign Key)
description: String?
debit: Decimal
credit: Decimal
lineNumber: Int
skuId: String?
metadata: JSON?
createdAt: DateTime
journalEntry: JournalEntry
debitAccount: ChartOfAccount?
creditAccount: ChartOfAccount?
}
model Budget {
id: String (Primary Key)
name: String
description: String?
budgetType: BudgetType (ANNUAL, PROJECT, DEPARTMENT)
accountId: String (Foreign Key)
skuId: String? (Foreign Key)
fiscalYear: Int
budgetYear: Int?
startDate: DateTime
endDate: DateTime
originalAmount: Decimal
revisedAmount: Decimal?
currentAmount: Decimal
status: BudgetStatus (DRAFT, ACTIVE, CLOSED)
projectId: String? (Foreign Key)
createdAt: DateTime
updatedAt: DateTime
createdBy: String
chartOfAccount: ChartOfAccount
sku: Sku?
project: Project?
budgetLines: BudgetLine[]
}
model Invoice {
id: String (Primary Key)
invoiceNumber: String (Unique)
customerId: String (Foreign Key - Contact or Org)
customerType: String (CONTACT, ORGANIZATION)
issueDate: DateTime
dueDate: DateTime
status: InvoiceStatus (DRAFT, SENT, PAID, OVERDUE, VOID)
subtotal: Decimal
taxAmount: Decimal?
discountAmount: Decimal?
totalAmount: Decimal
paidAmount: Decimal
balanceRemaining: Decimal
notes: String?
terms: String?
skuId: String?
createdAt: DateTime
updatedAt: DateTime
lines: InvoiceLine[]
payments: Payment[]
}
Events & Forms Models
model Event {
id: String (Primary Key)
name: String
description: String?
eventType: EventType
startDate: DateTime
endDate: DateTime?
location: String?
capacity: Int?
registrationDeadline: DateTime?
status: EventStatus (DRAFT, PUBLISHED, ACTIVE, COMPLETED, CANCELLED)
skuId: String? (Foreign Key)
teamId: String? (Foreign Key)
formId: String? (Foreign Key)
metadata: JSON
createdAt: DateTime
updatedAt: DateTime
sku: Sku?
team: Team?
form: Form?
registrations: EventRegistration[]
communications: Communication[]
pledges: Pledge[]
}
model EventRegistration {
id: String (Primary Key)
eventId: String (Foreign Key)
contactId: String? (Foreign Key)
email: String
firstName: String
lastName: String
phone: String?
status: RegistrationStatus
registrationDate: DateTime
paymentStatus: PaymentStatus
amountPaid: Decimal?
metadata: JSON
formSubmissionId: String?
createdAt: DateTime
updatedAt: DateTime
event: Event
contact: Contact?
payments: Payment[]
}
model Form {
id: String (Primary Key)
name: String
description: String?
formType: FormType (REGISTRATION, SURVEY, DONATION, CUSTOM)
schema: JSON (Form field definitions)
validation: JSON?
styling: JSON?
status: FormStatus (DRAFT, PUBLISHED, ARCHIVED)
isPublic: Boolean
tenantId: String? (Foreign Key)
createdAt: DateTime
updatedAt: DateTime
createdBy: String
submissions: FormSubmission[]
events: Event[]
}
model FormSubmission {
id: String (Primary Key)
formId: String (Foreign Key)
data: JSON (Submitted values)
contactId: String? (Foreign Key)
ipAddress: String?
userAgent: String?
status: SubmissionStatus
createdAt: DateTime
updatedAt: DateTime
form: Form
contact: Contact?
}
Task Management Models
model Task {
id: String (Primary Key)
title: String
description: String?
status: TaskStatus (TODO, IN_PROGRESS, REVIEW, DONE)
priority: TaskPriority (LOW, MEDIUM, HIGH, URGENT)
dueDate: DateTime?
projectId: String? (Foreign Key)
parentTaskId: String? (Foreign Key)
estimatedHours: Decimal?
actualHours: Decimal?
tags: String[]
metadata: JSON
createdAt: DateTime
updatedAt: DateTime
createdBy: String
project: Project?
parentTask: Task?
subtasks: Task[]
assignments: TaskAssignment[]
timeEntries: TimeEntry[]
comments: TaskComment[]
}
model TaskAssignment {
id: String (Primary Key)
taskId: String (Foreign Key)
userId: String? (Foreign Key)
contactId: String? (Foreign Key)
role: String?
createdAt: DateTime
task: Task
user: User?
contact: Contact?
}
model TimeEntry {
id: String (Primary Key)
taskId: String (Foreign Key)
userId: String (Foreign Key)
hours: Decimal
date: DateTime
description: String?
isBillable: Boolean
hourlyRate: Decimal?
status: TimeEntryStatus (DRAFT, SUBMITTED, APPROVED, REJECTED)
createdAt: DateTime
updatedAt: DateTime
task: Task
user: User
}
Workflow Models
model Workflow {
id: String (Primary Key)
name: String
description: String?
trigger: JSON (Trigger configuration)
nodes: JSON (Node definitions)
edges: JSON (Connection definitions)
status: WorkflowStatus (DRAFT, ACTIVE, PAUSED, ARCHIVED)
createdAt: DateTime
updatedAt: DateTime
createdBy: String
runs: WorkflowRun[]
}
model WorkflowRun {
id: String (Primary Key)
workflowId: String (Foreign Key)
status: RunStatus (RUNNING, COMPLETED, FAILED, CANCELLED)
startedAt: DateTime
completedAt: DateTime?
input: JSON?
output: JSON?
logs: JSON[]
errorMessage: String?
metadata: JSON
workflow: Workflow
}
Database Indexes
Key indexes for performance:
-- Contact indexes
CREATE INDEX idx_contact_fullname ON "Contact"(full_name);
CREATE INDEX idx_contact_email ON "Contact"(email);
CREATE INDEX idx_contact_assigned ON "Contact"(assigned_to);
CREATE INDEX idx_contact_tags ON "Contact" USING GIN(tags);
-- Interaction indexes
CREATE INDEX idx_interaction_contact ON "Interaction"(contact_id);
CREATE INDEX idx_interaction_created ON "Interaction"(created_at DESC);
-- Journal Entry indexes
CREATE INDEX idx_journal_entry_date ON "JournalEntry"(date DESC);
CREATE INDEX idx_journal_entry_status ON "JournalEntry"(status);
-- Audit Log indexes
CREATE INDEX idx_audit_log_table_record ON "AuditLog"(table_name, record_id);
CREATE INDEX idx_audit_log_user ON "AuditLog"(user_id);
CREATE INDEX idx_audit_log_created ON "AuditLog"(created_at DESC);
-- SKU indexes
CREATE INDEX idx_sku_code ON "Sku"(code);
CREATE INDEX idx_sku_project ON "Sku"(project_id);
Database Migrations
Prisma migrations are stored in prisma/migrations/ with timestamps and descriptive names:
prisma/migrations/
├── 20250901_init/
├── 20250915_add_ivi_field/
├── 20251120_bsi_backfill/
├── 20251201_workflow_tables/
└── 20260302_contact_tags/
Migration workflow:
# Development
npx prisma migrate dev --name description
# Production
npx prisma migrate deploy
API Architecture
Module Organization
The backend is organized into 24 modules following NestJS conventions:
src/modules/
├── agents/ # AI agent system
├── app.module.ts # Root application module
├── audit/ # Audit logging service
├── auth/ # Authentication & permissions
├── automation/ # Automation rules engine
├── configuration/ # System configuration
├── crm/ # Contact & organization management
├── dashboard/ # Dashboard widgets and KPIs
├── data-import/ # Data import services
├── database/ # Prisma service and health
├── finance/ # Financial management
├── forms/ # Form builder and submissions
├── health/ # Health check endpoints
├── integrations/ # External service integrations
├── journal-entries/ # Journal entry management
├── nextjs/ # Next.js integration (future)
├── notifications/ # Email and notification service
├── payments/ # Stripe payment processing
├── reporting/ # Report generation
├── shared/ # Shared utilities and DTOs
├── sku/ # BSI management
├── tasks/ # Task management
├── webhooks/ # Webhook receivers
└── workflows/ # Workflow automation engine
REST API Endpoints
25 controllers expose comprehensive REST APIs:
Authentication (/auth)
POST /auth/verify - Verify Firebase token
POST /auth/google - Google OAuth callback
GET /auth/me - Current user profile
GET /auth/users - List all users
GET /auth/users/:id - Get user details
PUT /auth/users/:id - Update user
POST /auth/users/invite - Invite new user
Permissions (/permissions)
GET /permissions/all - List all permissions
GET /permissions/user/:id - User permissions
PUT /permissions/user/:id - Update user permissions
POST /permissions/delegate - Delegate access
CRM - Contacts (/crm/contacts)
GET /crm/contacts - List contacts (paginated)
POST /crm/contacts - Create contact
GET /crm/contacts/:id - Get contact details
PUT /crm/contacts/:id - Update contact
DELETE /crm/contacts/:id - Delete contact
POST /crm/contacts/search - Advanced search
POST /crm/contacts/dedupe - Find duplicates
POST /crm/contacts/merge - Merge contacts
GET /crm/contacts/:id/interactions - Interaction history
POST /crm/contacts/:id/interactions - Log interaction
GET /crm/contacts/:id/donations - Donation history
CRM - Organizations (/crm/organizations)
GET /crm/organizations - List organizations
POST /crm/organizations - Create organization
GET /crm/organizations/:id - Get organization
PUT /crm/organizations/:id - Update organization
DELETE /crm/organizations/:id - Delete organization
GET /crm/organizations/:id/contacts - Linked contacts
POST /crm/organizations/:id/contacts - Link contact
CRM - Segments (/crm/segments)
GET /crm/segments - List segments
POST /crm/segments - Create segment
GET /crm/segments/:id - Get segment
PUT /crm/segments/:id - Update segment
DELETE /crm/segments/:id - Delete segment
POST /crm/segments/:id/refresh - Refresh members
GET /crm/segments/:id/export - Export segment
Finance - Chart of Accounts (/finance/chart-of-accounts)
GET /finance/chart-of-accounts - List accounts (tree)
POST /finance/chart-of-accounts - Create account
GET /finance/chart-of-accounts/:id - Get account
PUT /finance/chart-of-accounts/:id - Update account
DELETE /finance/chart-of-accounts/:id - Delete account
GET /finance/chart-of-accounts/:id/balance - Account balance
Finance - Journal Entries (/journal-entries)
GET /journal-entries - List entries
POST /journal-entries - Create entry
GET /journal-entries/:id - Get entry
PUT /journal-entries/:id - Update entry
DELETE /journal-entries/:id - Delete entry
POST /journal-entries/:id/post - Post entry
POST /journal-entries/:id/reverse - Reverse entry
Finance - Budgets (/finance/budgets)
GET /finance/budgets - List budgets
POST /finance/budgets - Create budget
GET /finance/budgets/:id - Get budget
PUT /finance/budgets/:id - Update budget
DELETE /finance/budgets/:id - Delete budget
GET /finance/budgets/:id/variance - Variance report
Finance - Invoices (/finance/invoices)
GET /finance/invoices - List invoices
POST /finance/invoices - Create invoice
GET /finance/invoices/:id - Get invoice
PUT /finance/invoices/:id - Update invoice
DELETE /finance/invoices/:id - Delete invoice
POST /finance/invoices/:id/send - Send invoice
POST /finance/invoices/:id/void - Void invoice
GET /finance/invoices/:id/pdf - Generate PDF
Events (/events)
GET /events - List events
POST /events - Create event
GET /events/:id - Get event
PUT /events/:id - Update event
DELETE /events/:id - Delete event
GET /events/:id/registrations - List registrations
POST /events/:id/register - Register attendee
GET /events/:id/export - Export attendee list
Forms (/forms)
GET /forms - List forms
POST /forms - Create form
GET /forms/:id - Get form
PUT /forms/:id - Update form
DELETE /forms/:id - Delete form
POST /forms/:id/publish - Publish form
GET /forms/:id/submissions - List submissions
POST /forms/:id/submit - Submit form (public)
Tasks (/tasks)
GET /tasks - List tasks
POST /tasks - Create task
GET /tasks/:id - Get task
PUT /tasks/:id - Update task
DELETE /tasks/:id - Delete task
POST /tasks/:id/assign - Assign task
POST /tasks/:id/comment - Add comment
GET /tasks/:id/time-entries - Time entries
POST /tasks/:id/time-entries - Log time
Workflows (/workflows)
GET /workflows - List workflows
POST /workflows - Create workflow
GET /workflows/:id - Get workflow
PUT /workflows/:id - Update workflow
DELETE /workflows/:id - Delete workflow
POST /workflows/:id/activate - Activate workflow
POST /workflows/:id/run - Manual run
GET /workflows/:id/runs - Run history
GET /workflows/:id/runs/:runId - Run details
Projects (/projects)
GET /projects - List projects
POST /projects - Create project
GET /projects/:id - Get project
PUT /projects/:id - Update project
DELETE /projects/:id - Delete project
GET /projects/:id/giving - Giving rollup
GET /projects/:id/budget - Budget summary
SKU (BSI) (/sku)
GET /sku - List SKUs
POST /sku - Create SKU
GET /sku/:code - Get SKU by code
PUT /sku/:code - Update SKU
DELETE /sku/:code - Delete SKU
GET /sku/:code/financials - Financial rollup
Reporting (/reporting)
GET /reporting/templates - List report templates
POST /reporting/templates - Create template
GET /reporting/templates/:id - Get template
POST /reporting/run - Run report
GET /reporting/exports/:id - Download export
POST /reporting/schedule - Schedule report
Data Import (/data-import)
POST /data-import/contacts - Import contacts CSV
POST /data-import/donations - Import donations CSV
POST /data-import/sage50 - Import Sage 50 data
GET /data-import/jobs - List import jobs
GET /data-import/jobs/:id - Job status
Payments (/payments)
POST /payments/stripe/webhook - Stripe webhook receiver
GET /payments/stripe/:id - Payment details
POST /payments/stripe/refund - Process refund
Health (/health)
GET /health - System health status
GET /health/db - Database connectivity
GET /health/redis - Redis connectivity
Request/Response Patterns
Standard Response Format
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: {
code: string;
message: string;
details?: any;
};
meta?: {
total?: number;
page?: number;
limit?: number;
timestamp: string;
};
}
Pagination
interface PaginatedRequest {
page?: number; // Default: 1
limit?: number; // Default: 20, max: 100
sortBy?: string;
sortOrder?: 'asc' | 'desc';
filters?: Record<string, any>;
}
interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
limit: number;
totalPages: number;
hasMore: boolean;
}
Error Responses
enum ErrorCode {
VALIDATION_ERROR = 'VALIDATION_ERROR',
AUTHENTICATION_ERROR = 'AUTHENTICATION_ERROR',
PERMISSION_DENIED = 'PERMISSION_DENIED',
NOT_FOUND = 'NOT_FOUND',
DUPLICATE_ENTRY = 'DUPLICATE_ENTRY',
INTERNAL_ERROR = 'INTERNAL_ERROR',
}
Authentication & Authorization
Firebase Auth Flow
1. User signs in with Google OAuth (frontend)
2. Firebase returns ID token
3. Frontend sends token to /auth/verify
4. Backend verifies token with Firebase Admin SDK
5. Backend looks up user permissions in database
6. Backend returns user profile + permissions
7. Frontend stores in auth context
8. Subsequent requests include Authorization: Bearer <token>
Permission Guard
@UseGuards(FirebaseAuthGuard, PermissionGuard)
@RequirePermission('crm', 'write')
@Post('/crm/contacts')
async createContact(@Body() dto: CreateContactDto) {
// Only accessible if user has 'crm:write' permission
}
Request Context
interface RequestContext {
user: {
id: string;
email: string;
displayName: string;
permissions: string[];
department?: string;
};
session: {
id: string;
ipAddress: string;
userAgent: string;
};
tenant?: {
id: string;
slug: string;
};
}
Audit Logging
All mutations are automatically logged via NestJS interceptor:
@Injectable()
export class AuditInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
const request = context.switchToHttp().getRequest();
const { user, method, url, body } = request;
return next.handle().pipe(
tap(response => {
this.auditService.log({
userId: user?.id,
action: method,
resource: url,
oldValues: request.originalData,
newValues: response,
ipAddress: request.ip,
userAgent: request.headers['user-agent'],
});
}),
);
}
}
Frontend Architecture
Next.js App Router Structure
frontend/src/app/
├── (auth)/ # Auth route group
│ ├── login/
│ │ └── page.tsx # Login page
│ └── layout.tsx # Auth layout (no app shell)
├── admin/ # Admin module
│ ├── permissions/
│ │ └── page.tsx
│ ├── users/
│ │ ├── page.tsx # User list
│ │ └── [id]/
│ │ └── page.tsx # User detail
│ └── layout.tsx
├── crm/ # CRM module
│ ├── contacts/
│ │ ├── page.tsx # Contact list
│ │ ├── [id]/
│ │ │ └── page.tsx # Contact detail
│ │ └── new/
│ │ └── page.tsx # Create contact
│ ├── organizations/
│ │ ├── page.tsx
│ │ └── [id]/
│ │ └── page.tsx
│ ├── segments/
│ │ └── page.tsx
│ └── layout.tsx
├── finance/ # Finance module
│ ├── chart-of-accounts/
│ │ └── page.tsx
│ ├── journal-entries/
│ │ ├── page.tsx
│ │ └── [id]/
│ │ └── page.tsx
│ ├── budgets/
│ │ └── page.tsx
│ ├── invoices/
│ │ └── page.tsx
│ └── layout.tsx
├── events/ # Events module
│ ├── page.tsx # Event list
│ ├── [id]/
│ │ ├── page.tsx # Event detail
│ │ └── registrations/
│ │ └── page.tsx
│ └── layout.tsx
├── forms/ # Forms module
│ ├── page.tsx # Form list
│ ├── builder/
│ │ └── [id]/
│ │ └── page.tsx # Form builder
│ └── layout.tsx
├── tasks/ # Tasks module
│ ├── page.tsx # Kanban board
│ └── [id]/
│ └── page.tsx # Task detail
├── workflows/ # Workflows module
│ ├── page.tsx # Workflow list
│ ├── builder/
│ │ └── [id]/
│ │ └── page.tsx # Workflow builder
│ └── runs/
│ └── [id]/
│ └── page.tsx # Run details
├── sku/ # BSI management
│ ├── page.tsx
│ └── [code]/
│ └── page.tsx
├── elt-dashboard/ # ELT dashboard
│ └── page.tsx
├── dashboard/ # User dashboard
│ └── page.tsx
├── page.tsx # Home/landing
├── layout.tsx # Root layout
└── error.tsx # Error boundary
Component Library Structure
frontend/src/components/
├── auth/
│ ├── AuthProvider.tsx # Auth context provider
│ ├── AuthErrorBoundary.tsx # Auth error handling
│ └── ProtectedRoute.tsx # Route guard
├── layout/
│ ├── AppLayout.tsx # Main app shell
│ ├── AppHeader.tsx # Top navigation
│ ├── AppSidebar.tsx # Left rail navigation
│ ├── ModuleHero.tsx # Page header component
│ └── Breadcrumbs.tsx # Breadcrumb navigation
├── ui/
│ ├── Button.tsx # Button variants
│ ├── Input.tsx # Text input
│ ├── Select.tsx # Dropdown select
│ ├── Modal.tsx # Modal dialog
│ ├── Drawer.tsx # Side drawer
│ ├── Popover.tsx # Popover component
│ ├── Table.tsx # Data table
│ ├── Card.tsx # Card container
│ ├── Badge.tsx # Status badge
│ ├── Tag.tsx # Tag chip
│ ├── Alert.tsx # Alert banner
│ ├── Toast.tsx # Toast notification
│ ├── Skeleton.tsx # Loading skeleton
│ ├── EmptyState.tsx # Empty placeholder
│ ├── Spinner.tsx # Loading spinner
│ ├── Tabs.tsx # Tab navigation
│ ├── Accordion.tsx # Accordion panel
│ └── Progress.tsx # Progress bar
├── forms/
│ ├── FormBuilder.tsx # Drag-drop form builder
│ ├── FormField.tsx # Form field wrapper
│ ├── FormPreview.tsx # Form preview
│ └── FormSubmit.tsx # Public form view
├── crm/
│ ├── ContactCard.tsx # Contact summary card
│ ├── ContactTable.tsx # Contact data table
│ ├── ContactSearch.tsx # Advanced search
│ ├── ContactMerge.tsx # Merge interface
│ ├── InteractionLog.tsx # Interaction history
│ └── SegmentBuilder.tsx # Segment criteria
├── finance/
│ ├── CoATree.tsx # Account hierarchy tree
│ ├── JournalEntryForm.tsx # Entry creation
│ ├── BudgetVariance.tsx # Variance chart
│ └── InvoicePreview.tsx # Invoice PDF preview
├── workflows/
│ ├── WorkflowBuilder.tsx # Node-based editor
│ ├── WorkflowNode.tsx # Node component
│ ├── WorkflowInspector.tsx # Node properties
│ └── WorkflowRunLog.tsx # Run history
├── tasks/
│ ├── KanbanBoard.tsx # Kanban view
│ ├── TaskCard.tsx # Task card
│ ├── TaskDetail.tsx # Task detail panel
│ └── TimeEntryForm.tsx # Time logging
├── charts/
│ ├── LineChart.tsx # Line chart
│ ├── BarChart.tsx # Bar chart
│ ├── PieChart.tsx # Pie chart
│ ├── TreeMap.tsx # Tree map (visx)
│ └── Sunburst.tsx # Sunburst chart
├── providers/
│ ├── Providers.tsx # All providers wrapper
│ ├── ThemeProvider.tsx # Theme context
│ └── QueryProvider.tsx # React Query config
└── utils/
├── ChunkErrorHandler.tsx # Chunk loading recovery
└── ErrorBoundary.tsx # React error boundary
State Management Patterns
Server State (TanStack Query)
// Query hook
function useContacts(filters: ContactFilters) {
return useQuery({
queryKey: ['crm', 'contacts', 'list', filters],
queryFn: () => apiClient.get('/crm/contacts', { params: filters }),
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
});
}
// Mutation hook
function useCreateContact() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateContactDto) =>
apiClient.post('/crm/contacts', data),
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries(['crm', 'contacts', 'list']);
toast.success('Contact created successfully');
},
onError: (error) => {
toast.error(error.message);
},
});
}
Client State (React Context)
// Auth context
interface AuthContextValue {
user: User | null;
permissions: string[];
isLoading: boolean;
signIn: () => Promise<void>;
signOut: () => Promise<void>;
hasPermission: (scope: string, action: string) => boolean;
}
const AuthContext = createContext<AuthContextValue>(null);
export function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be within AuthProvider');
return context;
}
Design System (Vireo UI Kit)
Typography Tokens
:root {
/* Font families */
--font-sans: "Space Grotesk", "Inter", system-ui, sans-serif;
--font-mono: "IBM Plex Mono", "Menlo", monospace;
/* Type scale (rem) */
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.125rem; /* 18px */
--text-xl: 1.25rem; /* 20px */
--text-2xl: 1.5rem; /* 24px */
--text-3xl: 1.75rem; /* 28px */
--text-4xl: 2rem; /* 32px */
--text-5xl: 2.5rem; /* 40px */
--text-6xl: 3rem; /* 48px */
/* Line heights */
--leading-tight: 1.25;
--leading-normal: 1.5;
--leading-relaxed: 1.75;
/* Font weights */
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
}
Color Tokens
:root {
/* Base surfaces (dark theme) */
--surface-0: #05060a; /* Darkest background */
--surface-1: #0c1018; /* App background */
--surface-2: #111827; /* Card background */
--surface-3: #1c2535; /* Elevated element */
--surface-4: #263144; /* Hover state */
/* Text colors */
--text-strong: #f8fbff; /* Primary text */
--text-muted: #b7c4d6; /* Secondary text */
--text-subtle: #8090a8; /* Tertiary text */
--text-inverse: #0b1020; /* On light background */
/* Accent colors */
--accent-primary: #5b8dff; /* Primary brand blue */
--accent-secondary: #7ae8c3; /* Secondary teal */
--accent-warn: #f0b429; /* Warning yellow */
--accent-error: #ff6b6b; /* Error red */
--accent-success: #4ade80; /* Success green */
/* Borders */
--border-strong: #2f3b52;
--border-muted: #1f2937;
/* Data visualization */
--viz-1: #7dd3fc; /* Sky blue */
--viz-2: #a78bfa; /* Purple */
--viz-3: #34d399; /* Emerald */
--viz-4: #f472b6; /* Pink */
--viz-5: #fbbf24; /* Amber */
}
Spacing Scale
:root {
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-5: 1.25rem; /* 20px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-10: 2.5rem; /* 40px */
--space-12: 3rem; /* 48px */
--space-16: 4rem; /* 64px */
}
Border Radius
:root {
--radius-xs: 4px;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--radius-xl: 18px;
--radius-full: 9999px;
}
Shadows
:root {
--shadow-sm: 0 4px 12px rgba(0, 0, 0, 0.15);
--shadow-md: 0 12px 30px rgba(0, 0, 0, 0.24);
--shadow-lg: 0 24px 48px rgba(0, 0, 0, 0.30);
}
Motion
:root {
--ease-standard: cubic-bezier(0.33, 0.11, 0.15, 1);
--dur-fast: 120ms;
--dur-med: 220ms;
--dur-slow: 340ms;
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
Deployment Architecture
Google Cloud Platform Infrastructure
┌─────────────────────────────────────────────────────────────┐
│ CLOUD LOAD BALANCER │
│ HTTPS (managed SSL certificates) │
└─────────────────────────────────────────────────────────────┘
│
┌───────────────────┴───────────────────┐
│ │
┌───────▼──────────┐ ┌─────────▼────────┐
│ CLOUD RUN │ │ CLOUD RUN │
│ vireo-api │ │ vireo-frontend │
│ │ │ │
│ Autoscaling: │ │ Autoscaling: │
│ Min: 0 │ │ Min: 0 │
│ Max: 100 │ │ Max: 50 │
│ │ │ │
│ Memory: 512MB │ │ Memory: 256MB │
│ CPU: 1 │ │ CPU: 1 │
│ Timeout: 300s │ │ Timeout: 60s │
└───────┬──────────┘ └─────────┬────────┘
│ │
│ ┌────────────────────────────┘
│ │
└────┬────┘
│
┌────────▼────────┐
│ CLOUD SQL │
│ PostgreSQL │
│ │
│ Private IP │
│ Auto backups │
│ HA config │
│ 10GB storage │
└────────┬────────┘
│
┌────────▼─────────────────────────────┐
│ │
┌───▼───────────┐ ┌─────────────┐ ┌────▼─────────┐
│ CLOUD STORAGE │ │ REDIS │ │ SECRET │
│ Buckets │ │ (MemStore) │ │ MANAGER │
│ │ │ │ │ │
│ - Uploads │ │ - Caching │ │ - API Keys │
│ - Receipts │ │ - BullMQ │ │ - DB Creds │
│ - Exports │ │ - Sessions │ │ - Stripe │
└───────────────┘ └─────────────┘ └──────────────┘
Cloud Run Configuration
vireo-api Service
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: vireo-api
annotations:
run.googleapis.com/launch-stage: BETA
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/minScale: '0'
autoscaling.knative.dev/maxScale: '100'
run.googleapis.com/cpu-throttling: 'false'
run.googleapis.com/cloudsql-instances: vireo-dev:us-central1:vireo-db
spec:
containerConcurrency: 80
timeoutSeconds: 300
containers:
- image: gcr.io/vireo-dev/vireo-api:latest
ports:
- containerPort: 3000
resources:
limits:
cpu: '1'
memory: 512Mi
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: database-url
key: latest
- name: NODE_ENV
value: production
vireo-frontend Service
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: vireo-frontend
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/minScale: '0'
autoscaling.knative.dev/maxScale: '50'
spec:
containerConcurrency: 80
timeoutSeconds: 60
containers:
- image: gcr.io/vireo-dev/vireo-frontend:latest
ports:
- containerPort: 3001
resources:
limits:
cpu: '1'
memory: 256Mi
env:
- name: NEXT_PUBLIC_API_URL
value: https://api.vireo.buildersintl.com
CI/CD Pipeline (Cloud Build)
Backend Build (cloudbuild.yaml)
steps:
# Install dependencies
- name: 'node:18'
entrypoint: npm
args: ['ci']
# Run tests
- name: 'node:18'
entrypoint: npm
args: ['run', 'test:ci']
# Build TypeScript
- name: 'node:18'
entrypoint: npm
args: ['run', 'build']
# Build Docker image
- name: 'gcr.io/cloud-builders/docker'
args:
- 'build'
- '-t'
- 'gcr.io/$PROJECT_ID/vireo-api:$SHORT_SHA'
- '-t'
- 'gcr.io/$PROJECT_ID/vireo-api:latest'
- '.'
# Push to Container Registry
- name: 'gcr.io/cloud-builders/docker'
args:
- 'push'
- 'gcr.io/$PROJECT_ID/vireo-api:latest'
# Deploy to Cloud Run
- name: 'gcr.io/cloud-builders/gcloud'
args:
- 'run'
- 'deploy'
- 'vireo-api'
- '--image'
- 'gcr.io/$PROJECT_ID/vireo-api:latest'
- '--region'
- 'us-central1'
- '--platform'
- 'managed'
- '--allow-unauthenticated'
# Run database migrations
- name: 'node:18'
entrypoint: npx
args: ['prisma', 'migrate', 'deploy']
env:
- 'DATABASE_URL=${_DATABASE_URL}'
timeout: 1200s
Frontend Build (cloudbuild-frontend.yaml)
steps:
# Install dependencies
- name: 'node:18'
entrypoint: npm
dir: 'frontend'
args: ['ci']
# Type check
- name: 'node:18'
entrypoint: npm
dir: 'frontend'
args: ['run', 'type-check']
# Build Next.js
- name: 'node:18'
entrypoint: npm
dir: 'frontend'
args: ['run', 'build']
# Build Docker image
- name: 'gcr.io/cloud-builders/docker'
dir: 'frontend'
args:
- 'build'
- '-t'
- 'gcr.io/$PROJECT_ID/vireo-frontend:$SHORT_SHA'
- '-t'
- 'gcr.io/$PROJECT_ID/vireo-frontend:latest'
- '.'
# Push to Container Registry
- name: 'gcr.io/cloud-builders/docker'
args:
- 'push'
- 'gcr.io/$PROJECT_ID/vireo-frontend:latest'
# Deploy to Cloud Run
- name: 'gcr.io/cloud-builders/gcloud'
args:
- 'run'
- 'deploy'
- 'vireo-frontend'
- '--image'
- 'gcr.io/$PROJECT_ID/vireo-frontend:latest'
- '--region'
- 'us-central1'
- '--platform'
- 'managed'
- '--allow-unauthenticated'
timeout: 1200s
Environment Configuration
Environment Variables (.env.example)
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/vireo
# Redis
REDIS_URL=redis://localhost:6379
# Firebase
FIREBASE_PROJECT_ID=vireo-dev
FIREBASE_CLIENT_EMAIL=firebase-adminsdk@vireo-dev.iam.gserviceaccount.com
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
# Stripe
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
# Email
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=notifications@buildersintl.com
SMTP_PASS=app-specific-password
SMTP_FROM="Vireo <notifications@buildersintl.com>"
# Google Sheets
GOOGLE_SHEETS_CLIENT_EMAIL=sheets-service@vireo-dev.iam.gserviceaccount.com
GOOGLE_SHEETS_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
# Workflows
WORKFLOW_ALERT_EMAIL=alerts@buildersintl.com
WORKFLOW_FINANCE_WRITE=true
# AI/AgentField
AGENTFIELD_API_URL=https://agentfield.api.example.com
AGENTFIELD_API_KEY=af_xxx
AGENTFIELD_TIMEOUT_MS=12000
AGENTFIELD_INSIGHTS_SKILL=donor_insights
# Feature Flags
ALLOW_DASHBOARD_FALLBACK=false
FORMS_INLINE_FILE_STORAGE=false
# Frontend
NEXT_PUBLIC_API_URL=http://localhost:3000
NEXT_PUBLIC_FIREBASE_API_KEY=xxx
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=vireo-dev.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=vireo-dev
Deployment Process
- Code Commit - Developer pushes to
mainbranch - Build Trigger - Cloud Build automatically triggers
- Test Execution - Unit tests and type checking
- Docker Build - Multi-stage Docker image creation
- Image Push - Push to Google Container Registry
- Cloud Run Deploy - Zero-downtime deployment
- Migration Run - Prisma migrations applied
- Health Check - Automatic rollback if unhealthy
- Traffic Shift - Gradual rollout to new version
Monitoring & Logging
Structured Logging
import { Logger } from '@nestjs/common';
const logger = new Logger('ContactService');
logger.log('Contact created', {
contactId: contact.id,
userId: user.id,
timestamp: new Date().toISOString(),
metadata: {
source: 'api',
ipAddress: request.ip,
},
});
logger.error('Failed to create contact', {
error: error.message,
stack: error.stack,
userId: user.id,
input: dto,
});
Health Check Endpoint
@Controller('health')
export class HealthController {
@Get()
async check() {
return {
status: 'ok',
timestamp: new Date().toISOString(),
services: {
database: await this.checkDatabase(),
redis: await this.checkRedis(),
firebase: await this.checkFirebase(),
},
};
}
}
Integration Architecture
External Service Integrations
Stripe Payment Processing
// Webhook handler
@Post('/payments/stripe/webhook')
async handleStripeWebhook(@Body() body, @Headers('stripe-signature') sig) {
const event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
switch (event.type) {
case 'payment_intent.succeeded':
await this.processPayment(event.data.object);
break;
case 'invoice.paid':
await this.markInvoicePaid(event.data.object);
break;
case 'customer.subscription.created':
await this.createSubscription(event.data.object);
break;
}
}
Google Workspace APIs
// Gmail integration
async sendEmail(to: string, subject: string, body: string) {
const gmail = google.gmail({ version: 'v1', auth: this.auth });
const message = [
`To: ${to}`,
`Subject: ${subject}`,
'Content-Type: text/html; charset=utf-8',
'',
body,
].join('\n');
const encoded = Buffer.from(message).toString('base64url');
await gmail.users.messages.send({
userId: 'me',
requestBody: { raw: encoded },
});
}
// Sheets integration
async readSheet(spreadsheetId: string, range: string) {
const sheets = google.sheets({ version: 'v4', auth: this.auth });
const response = await sheets.spreadsheets.values.get({
spreadsheetId,
range,
});
return response.data.values;
}
Plaid Bank Connections
async linkBankAccount(publicToken: string) {
const response = await this.plaidClient.itemPublicTokenExchange({
public_token: publicToken,
});
const accessToken = response.access_token;
const itemId = response.item_id;
// Store access token securely
await this.savePlaidToken(itemId, accessToken);
return { itemId };
}
async syncTransactions(itemId: string) {
const accessToken = await this.getPlaidToken(itemId);
const response = await this.plaidClient.transactionsSync({
access_token: accessToken,
});
for (const transaction of response.added) {
await this.createTransaction(transaction);
}
}
Sage 50 CSV Import
async importSage50Data(file: Express.Multer.File) {
const records = await this.parseCsv(file.buffer);
const job = await this.createImportJob({
type: 'sage50',
totalRecords: records.length,
status: 'processing',
});
for (const record of records) {
try {
await this.processFinancialRecord(record);
job.successCount++;
} catch (error) {
job.errors.push({ record, error: error.message });
job.errorCount++;
}
}
await this.updateImportJob(job.id, {
status: 'completed',
completedAt: new Date(),
});
return job;
}
Security Architecture
Authentication Flow
- User clicks "Sign in with Google"
- Firebase redirects to Google OAuth consent
- User grants permission
- Google returns to Firebase with authorization code
- Firebase exchanges code for ID token
- Frontend sends ID token to
/auth/verify - Backend verifies token signature with Firebase Admin SDK
- Backend checks user exists and is active
- Backend looks up permissions from database
- Backend returns user profile + JWT (optional)
- Frontend stores auth state in context
- All API requests include
Authorization: Bearer <token>
Permission System
Permission Scopes
enum PermissionScope {
// Admin
MANAGE_USERS = 'admin:manage_users',
MANAGE_PERMISSIONS = 'admin:manage_permissions',
SYSTEM_CONFIG = 'admin:system_config',
// CRM
CRM_READ = 'crm:read',
CRM_WRITE = 'crm:write',
CRM_DELETE = 'crm:delete',
CRM_MERGE = 'crm:merge',
CRM_EXPORT = 'crm:export',
// Finance
FINANCE_READ = 'finance:read',
FINANCE_WRITE = 'finance:write',
FINANCE_APPROVE = 'finance:approve',
FINANCE_CLOSE = 'finance:close',
// Events
EVENTS_READ = 'events:read',
EVENTS_WRITE = 'events:write',
EVENTS_PUBLISH = 'events:publish',
// Forms
FORMS_READ = 'forms:read',
FORMS_BUILD = 'forms:build',
FORMS_PUBLISH = 'forms:publish',
// Reports
REPORTS_READ = 'reports:read',
REPORTS_CREATE = 'reports:create',
REPORTS_SCHEDULE = 'reports:schedule',
// Workflows
WORKFLOWS_READ = 'workflows:read',
WORKFLOWS_BUILD = 'workflows:build',
WORKFLOWS_ACTIVATE = 'workflows:activate',
}
Role Definitions
const ROLES = {
SUPER_ADMIN: [
'admin:*',
'crm:*',
'finance:*',
'events:*',
'forms:*',
'reports:*',
'workflows:*',
],
ELT: [
'crm:read',
'finance:read',
'finance:approve',
'events:read',
'reports:*',
'workflows:read',
],
FINANCE_MANAGER: [
'finance:*',
'crm:read',
'reports:read',
'reports:create',
],
DEVELOPMENT_COORDINATOR: [
'crm:*',
'events:read',
'reports:read',
'reports:create',
],
EVENTS_COORDINATOR: [
'events:*',
'forms:*',
'crm:read',
'crm:write',
'reports:read',
],
CONTENT_MANAGER: [
'forms:*',
'events:read',
'events:write',
'crm:read',
],
CONTRIBUTOR: [
'crm:read',
'events:read',
'reports:read',
],
};
Audit Trail
Every mutation is logged with:
- User ID and email
- Timestamp
- Table and record ID
- Action (CREATE, UPDATE, DELETE)
- Old values (before)
- New values (after)
- Changed fields list
- IP address and user agent
- Session ID
- BSI code (if applicable)
- Reason (for sensitive changes)
SELECT
al.created_at,
u.email as user_email,
al.table_name,
al.action,
al.changed_fields,
al.old_values,
al.new_values,
al.ip_address
FROM audit_logs al
LEFT JOIN users u ON al.user_id = u.id
WHERE al.table_name = 'Contact'
AND al.record_id = 'contact_123'
ORDER BY al.created_at DESC;
Data Encryption
- In Transit: All connections use TLS 1.3
- At Rest: Cloud SQL encryption, Secret Manager encryption
- Secrets: Stored in Google Secret Manager, never in code
- PII: Sensitive fields (SSN, salary) field-level encrypted (future)
Performance Optimization
Database Query Optimization
- Prisma query optimization with
selectandinclude - Proper indexes on frequently queried columns
- Connection pooling (Prisma default: 10 connections)
- Query result caching in Redis for expensive queries
- Pagination on all list endpoints (default 20, max 100)
Frontend Performance
- Next.js automatic code splitting by route
- React Server Components for faster initial loads
- TanStack Query caching (5min stale, 10min cache)
- Optimistic updates for instant UI feedback
- Virtual scrolling for large lists (future)
- Image optimization with Next.js Image component
- Lazy loading for below-the-fold content
Caching Strategy
- Redis: Hot data (user permissions, session data)
- Browser: Static assets with long cache headers
- CDN: Cloud Storage serves media files
- API: TanStack Query client-side caching
- Database: Prisma query result cache
Monitoring Metrics
- API response time (p50, p95, p99)
- Database query time
- Error rate by endpoint
- Request throughput
- Cloud Run instance count
- Memory and CPU usage
- Active user count
Future Architecture Considerations
Multi-Tenancy
- Add
tenant_idto all tables - Row-level security in PostgreSQL
- Tenant-aware Prisma middleware
- Domain-based routing
- Isolated data per tenant
Microservices Extraction
Potential services to extract:
- Auth Service - Identity and permissions
- Notification Service - Email, SMS, push
- Reporting Service - Report generation and scheduling
- Integration Service - External API orchestration
- Workflow Service - Automation engine
Mobile App Architecture
- React Native app with shared component library
- Offline-first with local SQLite
- Background sync when online
- Push notifications via Firebase Cloud Messaging
- Biometric authentication
Advanced Analytics
- Data warehouse (BigQuery) for historical analysis
- ETL pipelines from operational DB
- Business intelligence tools (Looker, Metabase)
- Machine learning models for predictions
Summary
Vireo's architecture is built for:
- Scalability - Cloud Run auto-scaling, stateless design
- Maintainability - Modular monolith with clear boundaries
- Security - OAuth, RBAC, audit logging, encryption
- Performance - Caching, query optimization, CDN
- Reliability - Automated testing, health checks, monitoring
- Future Growth - Multi-tenancy ready, microservice-extractable
With 1,578 lines of Prisma schema, 24 NestJS modules, 25 API controllers, 89 Next.js page components, and comprehensive documentation, Vireo represents a production-ready enterprise system built with modern best practices.