What problems does a custom CRM web interface solve?
Imagine: you have 50 sales managers, each keeping clients in Excel or in their heads. Deals are lost, leads are not processed, reports take weeks to compile. A web interface for a custom CRM solves this – but only if it is designed for your business processes, not a template.
Our team has implemented over 15 CRM systems for B2B companies over 6 years, so we know typical mistakes and how to avoid them. We develop CRM web interfaces from scratch on React and Laravel: from prototype to production. In this article, we'll break down the typical architecture, key entities, and pitfalls.
The main task is not to code buttons, but to build logic: who sees which deals, how the funnel moves, which notifications are critical. Without this, even a beautiful interface is useless. The project starts with an audit of current processes – in 2-3 days we gather requirements and propose an architecture.
One of our clients – an electronics distributor – was losing up to 15% of leads because managers did not see the call history. Solution: an interface with automatic logging of all activities. One month after implementation, churn dropped to 3%.
Why is a custom CRM more profitable than off-the-shelf solutions?
Ready-made CRMs (amoCRM, Bitrix24) offer a standard set of features, but their adaptation to business specifics often requires expensive modifications and limits flexibility. Custom development on React and Laravel allows:
- Optimize the funnel for specific sales stages: for example, for a distributor – stages "Quote Request", "Price Negotiation", "Shipment".
- Integrate any external software: telephony, 1C, messengers, email campaigns.
- Scale functionality without regard to licensing restrictions.
At the same time, the total cost of ownership of a custom CRM within a year turns out to be lower than the monthly fee for an off-the-shelf solution with a similar set of integrations. According to our data, companies save up to 40% on licenses when switching to a custom development.
Typical set of entities and their implementation
In a custom CRM, five main entities are distinguished. For each, we implement a separate module with CRUD, filtering, and relationships.
| Entity | Purpose | Example fields |
|---|---|---|
| Contacts | Individuals with interaction history | name, email, phone, source |
| Companies | Legal entities to which contacts are linked | name, inn, address |
| Deals | Potential and active sales with statuses | amount, stage_id, responsible_id |
| Tasks | Assignments with deadlines linked to deals/contacts | title, due_date, assignee |
| Activities | Communication log (calls, emails, meetings) | type, body, happened_at |
Custom development on React and Laravel adapts to business changes 2-3 times faster than ready-made CRM systems. For example, adding a new field or funnel stage takes hours, not days.
How do we design a CRM for your business?
The work process is divided into six stages:
- Analytics – interviews with key users, identifying scenarios and pain points. (2-3 days)
- Prototyping – interactive mockups in Figma approved by the client. (3-5 days)
- Backend development – Laravel (PHP 8.3+) with RESTful API, PostgreSQL, Redis for cache and queues.
- Frontend development – React 18 + TypeScript, TanStack Table for tables, @dnd-kit for funnel.
- Integrations – telephony (Asterisk, Miko), email (SMTP/API), messengers (Telegram, WhatsApp).
- Testing and deployment – CI/CD, load testing, monitoring.
For authorization, we use Laravel Policies – a standard mechanism that provides flexible access control: a manager sees only their deals, a supervisor sees all deals in their department.
Database architecture and API
The database is built on PostgreSQL with a normalized schema. Example main tables:
CREATE TABLE contacts (
id BIGSERIAL PRIMARY KEY,
company_id BIGINT REFERENCES companies(id),
name VARCHAR(255) NOT NULL,
email VARCHAR(255),
phone VARCHAR(50),
source VARCHAR(64),
responsible_id BIGINT REFERENCES users(id),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE deals (
id BIGSERIAL PRIMARY KEY,
contact_id BIGINT REFERENCES contacts(id),
company_id BIGINT REFERENCES companies(id),
title VARCHAR(255) NOT NULL,
amount DECIMAL(14,2),
currency CHAR(3) DEFAULT 'RUB',
stage_id BIGINT REFERENCES pipeline_stages(id),
responsible_id BIGINT REFERENCES users(id),
closed_at DATE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
The API is built on RESTful principles. Example deals resource:
GET /api/deals?stage_id=2&responsible_id=5
POST /api/deals
PATCH /api/deals/{id}
DELETE /api/deals/{id}
POST /api/deals/{id}/move # change stage
{
"id": 1042,
"stage_id": 4,
"stage": { "id": 4, "name": "Negotiations" },
"updated_at": "2024-01-15T14:22:00Z",
"activity": {
"id": 3891,
"type": "stage_change",
"body": "Stage changed: Qualification -> Negotiations",
"user_id": 12
}
}
Funnel with drag-and-drop
The sales funnel is visualized as a Kanban board. We use the @dnd-kit library for dragging cards between columns. Optimistic update is a key technique: the card instantly changes position without waiting for a server response. On error, we roll back.
import { DndContext, DragEndEvent, closestCenter } from '@dnd-kit/core';
import { SortableContext } from '@dnd-kit/sortable';
const Pipeline: React.FC<{ stages: Stage[]; deals: Deal[] }> = ({ stages, deals }) => {
const moveDeal = useMutation({
mutationFn: ({ dealId, stageId }) => api.patch(`/deals/${dealId}/move`, { stage_id: stageId }),
onMutate: async ({ dealId, stageId }) => {
await queryClient.cancelQueries({ queryKey: ['deals'] });
const prev = queryClient.getQueryData(['deals']);
queryClient.setQueryData(['deals'], (old: Deal[]) =>
old.map(d => d.id === dealId ? { ...d, stage_id: stageId } : d)
);
return { prev };
},
onError: (_, __, context) => queryClient.setQueryData(['deals'], context?.prev),
});
return (
<DndContext collisionDetection={closestCenter} onDragEnd={moveDeal.mutate}>
<div className="flex gap-4 overflow-x-auto p-4">
{stages.map(stage => (
<KanbanColumn key={stage.id} stage={stage}
deals={deals.filter(d => d.stage_id === stage.id)} />
))}
</div>
</DndContext>
);
};
Access rights management
CRM requires granular permissions. In Laravel this is implemented through Policy classes and global model scopes.
class DealPolicy {
public function view(User $user, Deal $deal): bool {
if ($user->hasRole('admin')) return true;
if ($user->hasRole('team_lead')) {
return $deal->responsible->team_id === $user->team_id;
}
return $deal->responsible_id === $user->id;
}
}
class Deal extends Model {
public function scopeVisibleTo(Builder $query, User $user): Builder {
if ($user->hasRole('admin')) return $query;
if ($user->hasRole('team_lead')) {
return $query->whereHas('responsible', fn($q) =>
$q->where('team_id', $user->team_id)
);
}
return $query->where('responsible_id', $user->id);
}
}
What is included in custom CRM development
The project cost includes:
- Technical specification and interactive interface prototype
- Backend development (Laravel + PostgreSQL + Redis)
- Frontend development (React 18 + TypeScript)
- Integration with telephony (Asterisk, Miko), email (SMTP/API), messengers
- API documentation and user manual
- Employee training (2-3 hours)
- 3 months of warranty support after launch
Additionally, we can connect a mobile version (PWA or React Native) and set up BI analytics.
Timeline and cost estimation
MVP with funnel, contact/deal cards, tasks, and basic analytics takes 4–6 weeks. Adding integrations (telephony, email), advanced reports, and roles takes another 3–4 weeks. Mobile version adds 3–4 weeks.
Cost is calculated individually after analyzing your business processes. Contact us to discuss your project and get a preliminary estimate. Order a demo of our CRM architecture on your data – it takes no more than an hour.







