Ко мне регулярно приходят с одной и той же задачей: данные лежат в PostgreSQL - заказы из WooCommerce, лиды с Tilda-форм, статусы посылок СДЭК - а руководителю нужен наглядный дашборд, а не выгрузка в Excel раз в неделю. BI дашборд с подключением к Postgres на Vue закрывает это через живой API поверх базы и компоненты визуализации во фронте. Разбираю весь путь: от архитектуры и прав доступа до графиков и обновления данных в реальном времени - так, как делаю это в реальных проектах.
Архитектура: почему Vue не подключается к PostgreSQL напрямую
Первый вопрос от клиентов - можно ли Vue-компоненту напрямую дёрнуть базу и вывести данные без бэкенда. Нет, и дело не в прихоти разработчика. Драйвер pg работает только в среде Node.js, а строка подключения с логином и паролем от боевой базы в браузерном бандле - готовая дыра для любого, кто откроет DevTools и посмотрит исходники бандла. Между фронтом и Postgres всегда стоит промежуточный слой: он держит соединение с базой и отдаёт наружу только JSON по HTTP или WebSocket.
На практике использую три варианта такого слоя, в зависимости от бюджета и сложности логики:
| Вариант | Время запуска | Гибкость запросов | Когда беру для проекта |
|---|---|---|---|
| Node.js/Express + pg | 2-3 дня на базовый API | Полная - любые агрегаты и джойны | Дашборды со сложной бизнес-логикой и правами по ролям |
| PostgREST | несколько часов | Ограничена структурой таблиц и представлений | Быстрый MVP или внутренний прототип |
| Supabase (self-hosted) | около дня с настройкой | Средняя, через RLS-политики | Когда у команды уже есть инстанс Supabase под другие задачи |
Для дашбордов с бизнес-логикой - расчёт конверсии, сведение данных из нескольких таблиц, права по ролям сотрудников - беру Express с сырыми SQL-запросами: полный контроль над тем, что уходит в базу и что возвращается на фронт. PostgREST хорош для внутреннего прототипа, когда за пару часов нужно показать заказчику, что данные вообще можно визуализировать. Supabase self-hosted имеет смысл, если у команды уже крутится инстанс под другие задачи - разворачивать его только ради одного дашборда избыточно.
Backend-слой на Node.js: пул соединений и REST-эндпоинты для дашборда
Бэкенд для дашборда не готовит десятки эндпоинтов на все случаи жизни - обычно хватает 5-8 маршрутов под конкретные виджеты: продажи по дням, топ товаров, воронка заказов, остатки по складам. Начинаю с пула соединений - создавать новый клиент pg на каждый запрос ошибка, которая укладывает базу при первой же нагрузке от нескольких открытых вкладок дашборда.
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.PG_HOST,
port: 5432,
user: process.env.PG_USER,
password: process.env.PG_PASSWORD,
database: process.env.PG_DATABASE,
ssl: { rejectUnauthorized: false },
max: 10,
idleTimeoutMillis: 30000,
});
module.exports = pool;
Дальше - сами эндпоинты. Каждый принимает параметры фильтра (диапазон дат, филиал, категорию) и возвращает уже агрегированный результат, а не сырые строки для агрегации на фронте - Postgres считает суммы и группировки быстрее, чем JS в браузере.
app.get('/api/sales/daily', async (req, res) => {
const { from, to } = req.query;
const { rows } = await pool.query(
`SELECT date_trunc('day', created_at) AS day, SUM(total) AS revenue
FROM orders
WHERE created_at BETWEEN $1 AND $2
GROUP BY 1
ORDER BY 1`,
[from, to]
);
res.json(rows);
});
Пароль и хост базы храню в переменных окружения, .env в .gitignore, доступ к продовой базе - только через VPN или белый список IP. Для CORS разрешаю конкретный домен фронта, а не звёздочку.
Права доступа к PostgreSQL: read-only роль и защита подключения
Дашборд ничего не пишет в базу - только читает. Поэтому первое, что делаю на новом проекте: завожу отдельную роль с правами SELECT и подключаю бэкенд именно под ней, а не под основным пользователем приложения.
psql -U postgres -d shop -c "CREATE ROLE dashboard_ro LOGIN PASSWORD 'strong_password';"
psql -U postgres -d shop -c "GRANT CONNECT ON DATABASE shop TO dashboard_ro;"
psql -U postgres -d shop -c "GRANT USAGE ON SCHEMA public TO dashboard_ro;"
psql -U postgres -d shop -c "GRANT SELECT ON ALL TABLES IN SCHEMA public TO dashboard_ro;"
psql -U postgres -d shop -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO dashboard_ro;"
Плюс sslmode=require в строке подключения, если база не в одной приватной сети с сервером бэкенда, и закрытый порт 5432 наружу - Postgres слушает только localhost или адрес внутри VPC, наружу торчит исключительно HTTP от Node. Шаблоны таких SQL-скриптов под разные сценарии (read-only роль, отдельная схема для аналитики, ограничение по таблицам) собраны в подборке готовых скриптов - беру оттуда основу и адаптирую под структуру конкретной базы.
Бесплатный материал
🎁 Полезный скрипт в подарок
Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.
Без спама. Отписка в 1 клик.
Компоненты BI-дашборда на Vue: графики, таблицы, фильтры
На фронте держу общую логику в Pinia-сторе: один раз запросил данные за период - переиспользую их между графиком, таблицей и карточками с итоговыми цифрами, вместо того чтобы дёргать API из каждого компонента отдельно.
<script setup>
import { ref, onMounted } from 'vue';
import * as echarts from 'echarts';
import axios from 'axios';
const chartRef = ref(null);
onMounted(async () => {
const { data } = await axios.get('/api/sales/daily', {
params: { from: '2026-06-01', to: '2026-07-01' },
});
const chart = echarts.init(chartRef.value);
chart.setOption({
xAxis: { type: 'category', data: data.map(d => d.day) },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: data.map(d => d.revenue) }],
});
});
</script>
Библиотеку графиков выбираю под задачу, а не по умолчанию:
| Библиотека | Размер бандла | Гибкость | Когда выбираю |
|---|---|---|---|
| ECharts | около 1 MB | Максимальная: тепловые карты, sankey, графы связей | Сложные аналитические дашборды с десятком типов графиков |
| Chart.js | около 200 KB | Хватает на большинство типовых задач | Простой дашборд с линиями, столбцами и пирогами |
| ApexCharts | около 500 KB | Хорошая анимация и зум из коробки | Когда важен внешний вид без глубокой кастомизации |
Для аналитических дашбордов с тепловыми картами, sankey-диаграммами воронки продаж или графами связей беру ECharts - тяжелее в бандле, зато не упираюсь в ограничения при усложнении визуализации на середине проекта. Для простого дашборда с парой линий и столбцов Chart.js закрывает задачу без лишнего веса.
Обновление данных в реальном времени: polling, WebSocket и LISTEN/NOTIFY
Не каждому дашборду нужен realtime - для еженедельной сводки хватает обновления раз в 5-10 минут через обычный setInterval с повторным запросом к API. Для операционных дашбордов (склад, статусы доставки СДЭК, очередь заказов) разница между «обновилось только что» и «обновится через пять минут» ощущается менеджером сразу.
Три уровня, от простого к сложному:
- Polling - фронт сам раз в N секунд перезапрашивает данные. Просто, но лишняя нагрузка на базу при большом числе открытых вкладок.
- WebSocket через Socket.io - бэкенд транслирует изменения всем подключённым клиентам, не дожидаясь их запроса.
- LISTEN/NOTIFY в Postgres - база сама сообщает бэкенду о новой записи через триггер, а тот пробрасывает событие в сокет.
const { Client } = require('pg');
const client = new Client({ /* параметры подключения */ });
await client.connect();
await client.query('LISTEN orders_channel');
client.on('notification', (msg) => {
io.emit('new-order', JSON.parse(msg.payload));
});
Похожий канал использовал на проекте с автоматизацией на n8n: тот же NOTIFY, что триггерит обновление дашборда, параллельно ловит aiogram-бот и присылает менеджеру алерт в Telegram, если выручка за день падает ниже порога - дашборд для наблюдения, бот для реакции, база одна.
Тяжёлые запросы: материализованные представления, индексы и стоимость проекта
Когда таблица заказов разрастается до пары миллионов строк - а это происходит быстрее, чем кажется на старте, особенно при интеграции с WooCommerce и СДЭК, - прямые агрегатные запросы на лету начинают выполняться по 8-12 секунд. Дашборд с таким временем отклика бесполезен: менеджер закрывает вкладку раньше, чем дождётся графика.
На одном из проектов решил это материализованным представлением: тяжёлая агрегация считается не при каждом открытии дашборда, а по расписанию - REFRESH MATERIALIZED VIEW раз в ночь через сценарий в n8n. Запрос с 8-12 секунд упал до 150-200 миллисекунд, потому что дашборд читает уже готовую витрину, а не пересчитывает джойны трёх таблиц на каждый клик фильтра. Плюс индексы на created_at и status - без них даже витрина фильтруется медленно на большом объёме.
Такой дашборд под ключ - бэкенд с правами доступа, витрины данных, графики на Vue и деплой - у меня стоит от 90 000 ₽, срок обычно 3-4 недели в зависимости от числа источников данных и сложности агрегаций. Дороже выходит не сама визуализация, а сведение данных из разных систем в единую структуру, если до этого они нигде не пересекались.
Интерфейс аналитики и отчётов
Дашборд / BI
от 90 000 ₽
Подробнее →Частые вопросы
Можно ли использовать Supabase вместо своего бэкенда на Node.js?
Можно, если проект и так живёт в экосистеме Supabase - там уже есть auth, realtime из коробки и RLS-политики вместо ручных SQL-грантов. Для отдельного изолированного дашборда без остальной инфраструктуры Supabase разворачивать смысла обычно не вижу - Express с пулом соединений даёт тот же результат без лишнего сервиса на балансе.
Сколько данных выдерживает такой дашборд без тормозов?
На голых SQL-запросах без материализованных представлений комфортно держится таблица до 300-500 тысяч строк с индексами на нужных полях. После этого объёма закладываю витрины и кэш на уровне API - Redis на 1-5 минут для тяжёлых агрегатов, иначе каждый открытый дашборд превращается в отдельный тяжёлый запрос к базе.
Нужен ли выделенный сервер под бэкенд или хватит serverless-функций?
Для API поверх Postgres с пулом соединений постоянный процесс удобнее serverless - пул живёт между запросами, а не пересоздаётся на каждом холодном старте функции. Serverless имеет смысл только при низкой и редкой нагрузке, когда простаивающий сервер невыгоден.
Как быть, если данные лежат в нескольких базах Postgres одновременно?
Бэкенд держит отдельный pool на каждую базу и агрегирует результат уже на уровне Node перед отправкой на фронт, либо через dblink или foreign data wrapper на уровне самого Postgres, если базы в одной сети. Второй вариант проще поддерживать, но требует прав на настройку FDW на обеих сторонах.