BoringOldCRM is a personal project born out of a desire to explore what a truly well-architected, modern CRM could look like — one that is opinionated about isolation and reliability but highly flexible in how each tenant models their data and business rules.
The name is intentional. Good software should be predictable and boring at the operational level, even when the engineering underneath is anything but. Every feature exists to solve a real problem rather than to be clever.
It is a fully functional application, publicly available at bocrm.rsalvador.dev.
This is a personal project built for learning and scratching the architecture itch — not for commercial use.
Dedicated DB namespace per tenant — cross-tenant leakage is structurally impossible with no row filters to forget.
Full CRM control via chat — search, create, update, generate docs, and bulk-import data from file attachments. Tenant admins pick their LLM by subscription tier.
20 configurable types — primitives, relationship links (document, asset, contact), and milestone workflow trackers — stored as JSONB, no deployments.
Unlimited derived values per entity — arithmetic, conditionals, status labels — auto-denormalized into table views with no deployments.
Tenant-defined DENY/WARN rules with dual-layer enforcement — client-side preview and server-side hard block — authored entirely in the UI with no deployments.
Per-record visibility modes with explicit user and group grants, enforced on every query — no hidden records slipping through list endpoints.
Generate slide decks, one-pagers, and CSV exports from live CRM data using tenant-defined style templates.
Outbox pattern guarantees zero lost events — email and in-app delivery via RabbitMQ with per-user preferences.
Per-tenant AI observations with real-time news context, surfacing pipeline signals and customer trends on the dashboard.
One-way event push to Slack, HubSpot, Webhooks, and Zapier — AES-256-GCM encrypted credentials, guaranteed at-least-once delivery via outbox.
Exposes full CRM read/write via Model Context Protocol — Claude Desktop or any MCP agent with the same tenant isolation and audit trail as the UI.
AI assistant enriched with real-time news and market data for context-aware pipeline recommendations and customer insights.
Described using the C4 model — four levels of zoom from system context down to code-level patterns.
Who uses the system and what external systems does it depend on. BOCRM serves three distinct user roles and integrates with external LLM providers, SMTP, and OIDC identity providers.
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '16px', 'fontFamily': 'Inter, ui-sans-serif, sans-serif', 'lineColor': '#a5b4fc', 'primaryTextColor': '#e2e4ea', 'primaryColor': '#1e2330', 'primaryBorderColor': '#6366f1', 'secondaryColor': '#252b3b', 'tertiaryColor': '#181c26', 'background': '#0d0f14', 'person_bg_color': '#1e2330', 'person_border_color': '#34d399', 'personFontSize': '16', 'personFontWeight': '600', 'system_bg_color': '#1a1f35', 'system_border_color': '#6366f1', 'systemFontSize': '16', 'external_system_bg_color': '#111827', 'external_system_border_color': '#4b5563', 'systemdb_bg_color': '#1a1f35', 'systemdb_border_color': '#fbbf24', 'systemqueue_bg_color': '#1a1f35', 'systemqueue_border_color': '#fb923c', 'boundaryfontsize': '14', 'messageFontSize': '13', 'messageFontWeight': '500'}}}%%
C4Context
title BOCRM — System Context
Person(user, "CRM User", "Sales · support · operations")
Person(admin, "Tenant Admin", "Fields · rules · users")
Person(sysadmin, "System Admin", "Tenants · platform config")
System_Boundary(platform, "BOCRM Platform") {
System(bocrm, "BOCRM", "Multi-tenant CRM")
}
SystemDb(db, "Postgres", "Tenant + shared schemas")
SystemQueue(mq, "RabbitMQ", "Async event bus")
System_Ext(llm, "LLM Providers", "Claude · OpenAI · Gemini")
System_Ext(email, "SMTP", "Email delivery")
System_Ext(oidc, "OIDC", "External SSO")
Rel(user, bocrm, "Uses", "HTTPS")
Rel(admin, bocrm, "Administers", "HTTPS")
Rel(sysadmin, bocrm, "Manages", "HTTPS")
Rel(bocrm, db, "Reads/Writes", "JDBC")
Rel(bocrm, mq, "Pub/Sub", "AMQP")
Rel(bocrm, llm, "AI prompts", "HTTPS")
Rel(bocrm, email, "Sends", "SMTP")
Rel(bocrm, oidc, "Tokens", "HTTPS")
UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="1")
The deployable units that make up the system. A React SPA communicates with a Spring Boot API, which coordinates with the data stores, message broker, and an OPA sidecar. Policy rules are pushed to OPA as Rego documents on every change and evaluated via HTTP on every write.
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '15px', 'fontFamily': 'Inter, ui-sans-serif, sans-serif', 'lineColor': '#a5b4fc', 'primaryTextColor': '#e2e4ea', 'primaryColor': '#1e2330', 'primaryBorderColor': '#6366f1', 'secondaryColor': '#252b3b', 'tertiaryColor': '#181c26', 'background': '#0d0f14', 'person_bg_color': '#1e2330', 'person_border_color': '#34d399', 'container_bg_color': '#1e2330', 'container_border_color': '#6366f1', 'containerFontSize': '15', 'containerdb_bg_color': '#1a1f35', 'containerdb_border_color': '#fbbf24', 'containerqueue_bg_color': '#1a1f35', 'containerqueue_border_color': '#fb923c', 'external_system_bg_color': '#111827', 'external_system_border_color': '#4b5563', 'boundaryfontsize': '14', 'messageFontSize': '13'}}}%%
C4Container
title BOCRM — Containers
Person(user, "CRM User / Admin", "Uses CRM and admin UI")
Person(sysadmin, "System Admin", "Platform management")
Container_Boundary(platform, "BOCRM Platform") {
Container(web, "Web App", "React 19 · Vite · TS", "SPA — CRM, dashboards, AI chat")
Container(api, "Backend API", "Spring Boot 4 · Java 21", "REST — auth, CRM, policies, AI")
Container(worker, "Background Worker", "Spring Boot", "Outbox, notifications, backups")
Container(ai, "AI Orchestrator", "Spring AI", "Chat tools, doc gen, insights")
ContainerDb(oltp, "Postgres OLTP", "Postgres 15", "Admin + per-tenant schemas")
ContainerDb(report, "Reporting Store", "Postgres", "Rollup aggregates")
ContainerQueue(mq, "RabbitMQ", "AMQP", "Event bus · at-least-once")
Container(opa, "OPA Sidecar", "Open Policy Agent", "Rego policy evaluation")
}
System_Ext(llm, "LLM Providers", "Claude · OpenAI · Gemini")
System_Ext(email, "SMTP", "Email delivery")
System_Ext(oidc, "OIDC", "External SSO")
Rel(user, web, "Uses", "HTTPS")
Rel(sysadmin, web, "Manages", "HTTPS")
Rel(web, api, "REST calls", "JSON/HTTPS")
Rel(api, oltp, "Reads/Writes", "JDBC")
Rel(api, mq, "Publishes", "AMQP")
Rel(worker, mq, "Consumes", "AMQP")
Rel(worker, oltp, "Reads/Writes", "JDBC")
Rel(worker, report, "Writes rollups", "JDBC")
Rel(worker, email, "Sends", "SMTP")
Rel(ai, llm, "LLM prompts", "HTTPS")
Rel(api, oidc, "Validates", "HTTPS")
Rel(api, opa, "Evaluates policies", "HTTP")
UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="1")
Each tenant is provisioned a dedicated isolated namespace (tenant_<id>).
Hibernate SCHEMA mode routes every request through a TenantContext ThreadLocal —
no shared tables, no partition keys, no query-level filters to miss.
The pattern is database-agnostic — currently implemented with PostgreSQL schemas, but Hibernate's
multi-tenancy layer means swapping to MySQL databases or SQL Server schemas requires only
configuration changes. Tenant bootstrapping runs a full Flyway migration on the new schema automatically.
Replace with a screenshot of the tenant admin panel or a diagram showing schema isolation. Good candidate: tenant setup flow or the schema-per-tenant diagram from your architecture docs.
The major modules inside the Spring Boot API. Each module owns its own domain, repository layer, and service boundary. Cross-cutting concerns (tenancy, auth, audit) are handled via Spring aspects and filters.
---
config:
layout: elk
theme: base
elk:
mergeEdges: true
nodePlacementStrategy: NETWORK_SIMPLEX
themeVariables:
fontSize: 14px
fontFamily: "Inter, ui-sans-serif, sans-serif"
primaryColor: "#1e2330"
primaryTextColor: "#e2e4ea"
primaryBorderColor: "#6366f1"
lineColor: "#a5b4fc"
secondaryColor: "#252b3b"
tertiaryColor: "#181c26"
background: "#0d0f14"
edgeLabelBackground: "#1e2330"
clusterBkg: "#181c26"
clusterBorder: "#6366f1"
nodeTextColor: "#e2e4ea"
---
flowchart TB
WEB(["Web App · React 19"])
subgraph API["Backend API — Spring Boot 4 · Java 21"]
direction TB
subgraph AUTHN["Auth & Tenancy"]
direction TB
AUTH["Auth · JWT · OIDC"]
TEN["Tenancy · Schema routing"]
SA["System Admin"]
TA["Tenant Admin"]
end
subgraph CRMD["CRM Domain"]
direction TB
CRM["CRM Core · CRUD · Search"]
ASS["Assets"]
DOC["Documents"]
BLK["Bulk Ops"]
end
subgraph EXT["Extensibility"]
direction TB
CF["Custom Fields · JSONB"]
CLF["Calculated Fields · CEL"]
PR["Policy Rules · OPA/Rego"]
OT["Opportunity Types"]
DT["Doc Templates"]
end
subgraph SEC["Access & Security"]
direction TB
AC["Access Control"]
UG["User Groups"]
AUD["Audit Log"]
end
subgraph AIG["AI & Insights"]
direction TB
CHAT["AI Chat · Spring AI"]
INS["Dashboard Insight"]
end
subgraph OPS["Operations"]
direction TB
NOT["Notifications"]
RPT["Reporting"]
OB["Outbox Publisher"]
BAK["Tenant Backup"]
end
%% Force vertical stacking of subgroups
AUTHN ~~~ CRMD
CRMD ~~~ EXT
EXT ~~~ SEC
SEC ~~~ AIG
AIG ~~~ OPS
end
WEB -->|REST| AUTH
AUTH --> TEN
CRM --> CF & PR & AC
API --> PG[("Postgres")]
OB --> MQ(["RabbitMQ"])
CHAT & INS --> LLM(["LLM Providers"])
classDef module fill:#1e2330,stroke:#6366f1,color:#e2e4ea
classDef external fill:#111827,stroke:#374151,color:#9ca3af
class AUTH,TEN,SA,TA,CRM,ASS,DOC,BLK,CF,CLF,PR,OT,DT,AC,UG,AUD,CHAT,INS,NOT,RPT,OB,BAK module
class WEB,PG,MQ,LLM external
---
config:
layout: elk
theme: base
elk:
mergeEdges: true
nodePlacementStrategy: NETWORK_SIMPLEX
themeVariables:
fontSize: 14px
fontFamily: "Inter, ui-sans-serif, sans-serif"
primaryColor: "#1e2330"
primaryTextColor: "#e2e4ea"
primaryBorderColor: "#7c3aed"
lineColor: "#a5b4fc"
secondaryColor: "#252b3b"
tertiaryColor: "#1a1530"
background: "#0d0f14"
edgeLabelBackground: "#1e2330"
clusterBkg: "#1a1530"
clusterBorder: "#7c3aed"
nodeTextColor: "#e2e4ea"
---
flowchart TB
API(["Backend API · REST"])
subgraph WEB["Web App — React 19 + Vite + TypeScript"]
direction TB
APIC["API Client · Axios"]
subgraph STORES["State — Zustand"]
direction TB
AUTHS["authStore"]
UIS["uiStore"]
CRMS["crmStore"]
FS["Field Stores"]
end
subgraph PAGES["Pages"]
direction TB
PC["CRM Pages"]
PA["Admin Pages"]
PSA["System Admin"]
PAU["Auth Pages"]
end
subgraph COMPS["Shared Components"]
direction TB
FORMS["Form Components"]
VIEWS["View Components"]
TABLE["DataTable"]
KAN["KanbanBoard"]
ASST["AssistantBar"]
CFI["CustomFieldInput"]
ACP["AccessControlPanel"]
end
STORES ~~~ PAGES
PAGES ~~~ COMPS
end
API -->|REST| APIC
PC & PA & ASST --> APIC
FORMS --> FS
PC --> AUTHS
ASST --> UIS
classDef comp fill:#1e2330,stroke:#7c3aed,color:#e2e4ea
classDef external fill:#111827,stroke:#374151,color:#9ca3af
class APIC,AUTHS,UIS,CRMS,FS,PC,PA,PSA,PAU,FORMS,VIEWS,TABLE,KAN,ASST,CFI,ACP comp
class API external
Key implementation patterns that underpin the system's reliability and extensibility.
TenantContext
A ThreadLocal populated by a Spring filter on every request. Hibernate's CurrentTenantIdentifierResolver reads it to route all queries to the correct schema — zero chance of cross-tenant bleed.
OutboxPublisher
An @TransactionalEventListener(BEFORE_COMMIT) writes domain events to an outbox table in the same transaction as the mutation. A separate scheduler polls and publishes to RabbitMQ, then marks delivered.
PolicyValidationService
Called in all five CRM services on CREATE/UPDATE/DELETE. Delegates to OpaEvaluator, which POSTs the entity context (input.entity.* + input.previous.* + input.operation) to the OPA sidecar. DENY violations throw immediately; WARNs are collected and returned. A separate evaluateOnly() path handles frontend pre-submit checks without throwing. Rego documents are kept in sync via OpaSyncService, which pushes updated policies after each DB commit.
CelEvaluator
Wraps Google's CEL-Java library. Field definitions carry a CEL expression string; the evaluator compiles it once, caches the program, and executes it against the record's field map on every write.
CustomFieldMapper
On read, deserializes the JSONB blob into a typed Map<String, FieldValue> using the tenant's field definitions as the schema. On write, validates types and serializes back. No generated columns, no EAV joins.
AiChatService
Uses Spring AI's ChatClient with a registered tool set covering all CRM operations. Tools are resolved at runtime based on the tenant's enabled modules. Confirm mode buffers tool calls for user approval before execution.
Replace with a sequence diagram or class diagram for one of the patterns above. Good candidate: the policy evaluation flow or the outbox publish sequence.
Tables are split across two isolation tiers. The shared schema holds platform-level data (tenants, platform users). Each tenant gets a fully isolated tenant_<id> namespace for all CRM data — no shared tables, no cross-tenant joins possible. Currently implemented with PostgreSQL schemas; the Hibernate multi-tenancy layer makes this switchable to other databases through configuration.
tenant_<id> schemaThe primary CRM entities. All reside in the tenant's isolated namespace — tenant_id columns exist in the physical tables but are omitted here for clarity since schema isolation already prevents cross-tenant access.
%%{init: {'theme': 'dark', 'themeVariables': {'fontSize': '14px', 'fontFamily': 'Inter, ui-sans-serif, sans-serif', 'primaryColor': '#1e2330', 'primaryBorderColor': '#6366f1', 'lineColor': '#a5b4fc', 'background': '#0d0f14', 'mainBkg': '#1e2330', 'nodeBorder': '#6366f1', 'clusterBkg': '#181c26', 'titleColor': '#e2e4ea'}, 'er': {'useMaxWidth': false, 'fontSize': 14, 'minEntityWidth': 500, 'entityPadding': 4}}}%%
erDiagram
CUSTOMER {
bigint id_ PK
varchar name
varchar status
bigint owner_id FK
varchar industry
varchar website
jsonb custom_data
timestamp created_at
}
CONTACT {
bigint id PK
bigint customer_id FK
varchar name
varchar email
varchar phone
varchar title
boolean is_primary
jsonb custom_data
}
OPPORTUNITY {
bigint id PK
bigint customer_id FK
varchar name
varchar stage
numeric value
numeric probability
date close_date
bigint owner_id FK
varchar opp_type_slug
jsonb custom_data
}
OPPORTUNITY_TYPE {
bigint id PK
varchar name
varchar slug UK
int display_order
}
OPPORTUNITY_CONTACT {
bigint opportunity_id FK
bigint contact_id FK
}
ACTIVITY {
bigint id PK
varchar subject
varchar type
timestamp due_at
bigint owner_id FK
varchar related_type
bigint related_id
varchar status
jsonb custom_data
}
ASSET {
bigint id PK
varchar name
varchar type
varchar serial_number
varchar status
bigint customer_id FK
bigint owner_id FK
jsonb custom_data
}
ASSET_OPPORTUNITY {
bigint asset_id FK
bigint opportunity_id FK
}
CUSTOMER ||--o{ CONTACT : "has"
CUSTOMER ||--o{ OPPORTUNITY : "has"
CUSTOMER ||--o{ ASSET : "has"
OPPORTUNITY_TYPE ||--o{ OPPORTUNITY : "typed as"
OPPORTUNITY ||--o{ OPPORTUNITY_CONTACT : ""
CONTACT ||--o{ OPPORTUNITY_CONTACT : ""
ASSET ||--o{ ASSET_OPPORTUNITY : ""
OPPORTUNITY ||--o{ ASSET_OPPORTUNITY : ""
tenant_<id> schemaTenant-controlled configuration tables: extensibility rules (custom fields, CEL calculated fields, OPA/Rego policy rules), access control, user groups, document templates, and saved filters.
%%{init: {'theme': 'dark', 'themeVariables': {'fontSize': '14px', 'fontFamily': 'Inter, ui-sans-serif, sans-serif', 'primaryColor': '#1e2330', 'primaryBorderColor': '#6366f1', 'lineColor': '#a5b4fc', 'background': '#0d0f14', 'mainBkg': '#1e2330', 'nodeBorder': '#6366f1', 'clusterBkg': '#181c26', 'titleColor': '#e2e4ea'}, 'er': {'useMaxWidth': false, 'fontSize': 14, 'minEntityWidth': 500, 'entityPadding': 4}}}%%
erDiagram
CUSTOM_FIELD_DEFINITION {
bigint id PK
varchar entity_type
varchar key
varchar label
varchar field_type
jsonb config_jsonb
int display_order
}
CALCULATED_FIELD_DEFINITION {
bigint id PK
varchar entity_type
varchar key
varchar label
text expression
int display_order
}
CALCULATED_FIELD_VALUE {
bigint id PK
varchar entity_type
bigint entity_id
bigint calc_field_id FK
jsonb value
timestamp created_at
}
POLICY_RULE {
bigint id PK
varchar entity_type
varchar name
text expression
varchar action
int display_order
}
CUSTOM_FIELD_DEFINITION ||--o{ CALCULATED_FIELD_DEFINITION : "references"
CALCULATED_FIELD_DEFINITION ||--o{ CALCULATED_FIELD_VALUE : "produces"
%%{init: {'theme': 'dark', 'themeVariables': {'fontSize': '14px', 'fontFamily': 'Inter, ui-sans-serif, sans-serif', 'primaryColor': '#1e2330', 'primaryBorderColor': '#6366f1', 'lineColor': '#a5b4fc', 'background': '#0d0f14', 'mainBkg': '#1e2330', 'nodeBorder': '#6366f1', 'clusterBkg': '#181c26', 'titleColor': '#e2e4ea'}, 'er': {'useMaxWidth': false, 'fontSize': 14, 'minEntityWidth': 500, 'entityPadding': 4}}}%%
erDiagram
RECORD_ACCESS_POLICY {
bigint id PK
varchar entity_type
bigint entity_id
bigint owner_id FK
varchar access_mode
}
RECORD_ACCESS_GRANT {
bigint id PK
varchar entity_type
bigint entity_id
varchar grantee_type
bigint grantee_id
varchar permission
}
USER_GROUP {
bigint id PK
varchar name
varchar description
}
USER_GROUP_MEMBERSHIP {
bigint group_id FK
bigint user_id FK
}
RECORD_ACCESS_POLICY ||--o{ RECORD_ACCESS_GRANT : "grants"
USER_GROUP ||--o{ USER_GROUP_MEMBERSHIP : "has"
USER_GROUP ||--o{ RECORD_ACCESS_GRANT : "grantee"
%%{init: {'theme': 'dark', 'themeVariables': {'fontSize': '14px', 'fontFamily': 'Inter, ui-sans-serif, sans-serif', 'primaryColor': '#1e2330', 'primaryBorderColor': '#6366f1', 'lineColor': '#a5b4fc', 'background': '#0d0f14', 'mainBkg': '#1e2330', 'nodeBorder': '#6366f1', 'clusterBkg': '#181c26', 'titleColor': '#e2e4ea'}, 'er': {'useMaxWidth': false, 'fontSize': 14, 'minEntityWidth': 500, 'entityPadding': 4}}}%%
erDiagram
DOCUMENT_TEMPLATE {
bigint id PK
varchar name
varchar template_type
jsonb style_json
boolean is_default
}
NOTIFICATION_TEMPLATE {
bigint id PK
varchar name
varchar notification_type
varchar subject
text body
}
SAVED_FILTER {
bigint id PK
bigint user_id FK
varchar name
varchar entity_type
jsonb filter_config_jsonb
}
DOCUMENT_TEMPLATE ||--o{ NOTIFICATION_TEMPLATE : "used by"
tenant_<id> schemaRuntime and operational tables: AI chat history, document storage, notifications, immutable audit trail, outbox events (at-least-once delivery to RabbitMQ), backup jobs, and token usage ledger.
%%{init: {'theme': 'dark', 'themeVariables': {'fontSize': '14px', 'fontFamily': 'Inter, ui-sans-serif, sans-serif', 'primaryColor': '#1e2330', 'primaryBorderColor': '#6366f1', 'lineColor': '#a5b4fc', 'background': '#0d0f14', 'mainBkg': '#1e2330', 'nodeBorder': '#6366f1', 'clusterBkg': '#181c26', 'titleColor': '#e2e4ea'}, 'er': {'useMaxWidth': false, 'fontSize': 14, 'minEntityWidth': 500, 'entityPadding': 4}}}%%
erDiagram
CHAT_MESSAGE {
bigint id PK
bigint user_id FK
varchar role
text content
varchar context_entity_type
bigint context_entity_id
timestamp created_at
}
TENANT_DOCUMENT {
bigint id PK
varchar name
varchar mime_type
varchar content_type
varchar linked_entity_type
bigint linked_entity_id
timestamp created_at
}
TOKEN_USAGE_LEDGER {
bigint id PK
bigint tokens_used
varchar usage_type
timestamp created_at
}
CHAT_MESSAGE ||--o{ TOKEN_USAGE_LEDGER : "consumes"
CHAT_MESSAGE ||--o{ TENANT_DOCUMENT : "generates"
%%{init: {'theme': 'dark', 'themeVariables': {'fontSize': '14px', 'fontFamily': 'Inter, ui-sans-serif, sans-serif', 'primaryColor': '#1e2330', 'primaryBorderColor': '#6366f1', 'lineColor': '#a5b4fc', 'background': '#0d0f14', 'mainBkg': '#1e2330', 'nodeBorder': '#6366f1', 'clusterBkg': '#181c26', 'titleColor': '#e2e4ea'}, 'er': {'useMaxWidth': false, 'fontSize': 14, 'minEntityWidth': 500, 'entityPadding': 4}}}%%
erDiagram
AUDIT_LOG {
bigint id PK
bigint user_id FK
varchar action
varchar entity_type
bigint entity_id
jsonb old_value
jsonb new_value
timestamp created_at
}
OUTBOX_EVENT {
bigint id PK
varchar event_type
jsonb payload_jsonb
int retry_count
timestamp published_at
timestamp created_at
}
NOTIFICATION_INBOX {
bigint id PK
bigint user_id FK
varchar message_type
varchar subject
boolean is_read
timestamp created_at
}
TENANT_BACKUP_JOB {
bigint id PK
varchar job_type
varchar status
boolean includes_data
varchar label
timestamp created_at
}
AUDIT_LOG ||--o{ OUTBOX_EVENT : "publishes"
OUTBOX_EVENT ||--o{ NOTIFICATION_INBOX : "delivers"