Урок 6 · Запись данных во внешнюю БД

Слайд 15

06

Модуль 3 · Занятие 3

Запись данных во внешнюю БД

На место тестового приёмника встаёт функция, которая проверяет ключ, валидирует данные и пишет заявку в таблицу

Слайд 16

Выбор платформы

Supabase и Firebase

SupabaseFirebase
База данныхPostgreSQL: таблицы, SQL, связиFirestore: документы, NoSQL
Серверный кодEdge Functions (TypeScript / Deno)Cloud Functions (Node.js)
Права доступаRow Level Security — политики на SQLSecurity Rules
Удобство для курсаER-модель из урока 2 ложится один в одинСвязи придётся переводить в документы

В курсе берём Supabase: таблицы и связи, которые вы нарисовали на уроке 2, создаются в нём напрямую, без перепроектирования.

Слайд 17

Ключи доступа

Три ключа и их места

КлючЧто даётГде может лежать
anon (publishable)Публичный доступ, ограниченный политиками RLSВо frontend допустим — но только при строгом RLS
service_role (secret)Полный доступ, игнорирует RLSТолько на сервере, в секретах функции
TILDA_WEBHOOK_SECRETНаш собственный ключ: подтверждает, что запрос пришёл от насВ секретах функции и в настройках webhook в Tilda
Код в T123, скрытое поле, атрибут data- — это одно и то же место: код страницы. Он публичен.

Слайд 18

Архитектура

Безопасная схема

Tilda

Форма и страница. Отправляет POST с данными и ключом

→

Edge Function

Проверяет ключ, валидирует, обращается к базе с service_role

→

Postgres

Таблицы с RLS: снаружи закрыто, пишет только функция

Публичная логика — на странице

Показать форму, проверить заполнение, подставить source. Всё это видно посетителю, и это нормально.

Приватная логика — в функции

Проверить ключ, провалидировать данные, найти услугу, создать клиента и заявку, отсечь дубль, записать журнал.

Слайд 19

Таблицы и права

Схема базы: один файл SQL

Ниже — весь файл схемы. Скопируйте его отсюда, откройте в своём проекте Supabase SQL Editor → New query, вставьте и нажмите Run. Ответ Success. No rows returned значит, что четыре таблицы созданы.

-- Учебный кейс «Запись на услугу»: схема данных MVP.

-- ─── Справочник услуг ───────────────────────────────────────────────
create table public.services (
  id          bigint generated always as identity primary key,
  code        text not null unique,             -- машинное имя: 'consult'
  title       text not null,                    -- как в выпадающем списке Tilda
  price       numeric(10, 2),
  is_active   boolean not null default true,
  created_at  timestamptz not null default now()
);

-- ─── Клиенты (пользователи, оставившие заявку) ─────────────────────
create table public.clients (
  id          bigint generated always as identity primary key,
  email       text not null unique,
  name        text not null,
  phone       text,
  created_at  timestamptz not null default now()
);

-- ─── Статусы заявки ────────────────────────────────────────────────
create type public.request_status as enum ('new', 'confirmed', 'done', 'cancelled');

-- ─── Заявки ────────────────────────────────────────────────────────
create table public.requests (
  id            bigint generated always as identity primary key,
  client_id     bigint not null references public.clients (id),
  service_id    bigint references public.services (id),   -- null, если услуга не распознана
  preferred_date date,
  comment       text,
  status        public.request_status not null default 'new',
  source        text,                                     -- скрытое поле / UTM
  tilda_tranid  text unique,                              -- защита от дублей
  created_at    timestamptz not null default now()
);

create index requests_client_id_idx on public.requests (client_id);
create index requests_status_idx    on public.requests (status);

-- ─── Журнал входящих webhook-запросов ──────────────────────────────
create table public.webhook_log (
  id          bigint generated always as identity primary key,
  received_at timestamptz not null default now(),
  result      text not null check (result in ('ok', 'duplicate', 'invalid', 'unauthorized', 'error', 'test')),
  http_status int  not null,
  message     text,
  payload     jsonb,          -- тело запроса без секретного ключа
  request_id  bigint references public.requests (id)
);

-- ─── Безопасность ──────────────────────────────────────────────────
-- RLS включён и политик для anon/authenticated нет. Значит, через публичный
-- anon-ключ ни прочитать, ни записать ничего нельзя. Пишет только Edge
-- Function с service_role-ключом, а он хранится в секретах Supabase,
-- а не на странице Tilda.
alter table public.services    enable row level security;
alter table public.clients     enable row level security;
alter table public.requests    enable row level security;
alter table public.webhook_log enable row level security;

-- Справочник услуг можно показывать публично (например, в каталоге).
create policy "services are public" on public.services
  for select to anon, authenticated using (is_active);

-- ─── Стартовые данные ──────────────────────────────────────────────
insert into public.services (code, title, price) values
  ('consult', 'Консультация',        2000),
  ('audit',   'Аудит сайта',         7500),
  ('setup',   'Настройка под ключ', 15000);

В документации такой файл называют миграцией: это просто SQL с изменением схемы, который лежит в репозитории проекта, чтобы базу можно было собрать заново одной командой. Никакого отдельного инструмента для него не нужно — достаточно SQL Editor.

  • enable row level security без политик означает, что через anon-ключ ничего не прочитать и не записать
  • Единственное исключение — публичное чтение активных услуг: витрина должна работать без логина
  • tilda_tranid text unique — защита от дублей на уровне самой базы, а не кода
  • status — enum: недопустимое значение база просто не примет

Демонстрация. Прочитать requests через anon-ключ из консоли браузера. В ответ придёт пустой массив — RLS работает.

Своя схема

Как написать схему для своего проекта

Наша схема — пример учебного кейса. Для своего MVP порядок такой же, и начинается он не с SQL, а с диаграммы из урока 2: прямоугольник становится таблицей, строчка внутри — полем, стрелка — внешним ключом.

01

Выписать сущности. Существительные из сценария, у которых есть свои свойства и которые живут дольше одного действия

02

На каждую — болванка таблицы с двумя служебными полями: id и created_at

03

Добавить поля. Текст → text, цена → numeric, дата → date, да/нет → boolean, фиксированный список → enum

04

Связи. Внешний ключ ставится на стороне «много»: у заявки есть клиент, значит client_id лежит в заявках

05

Уникальность. Что не должно повторяться — email, номер заказа — помечаем unique. Это защита от дублей на уровне базы, а не проверка в коде

06

Права. enable row level security на каждую таблицу, политика — только там, где нужно публичное чтение

Заготовка на одну таблицу: скопируйте, переименуйте, оставьте нужные поля.

create table public.zayavki (
  id          bigint generated always as identity primary key,   -- служебный номер строки
  created_at  timestamptz not null default now(),                -- когда появилась

  name        text not null,                  -- текст, заполнять обязательно
  email       text not null unique,           -- обязательно и без повторов
  price       numeric(10, 2),                 -- число с копейками, можно не заполнять
  visit_date  date,                           -- дата без времени
  is_paid     boolean not null default false,  -- да/нет со значением по умолчанию
  client_id   bigint not null references public.clients (id)  -- связь: у заявки один клиент
);

alter table public.zayavki enable row level security;  -- закрыть от публичного ключа

Если схему пишет ИИ — а это нормальный способ, — прочитайте результат по этим шести пунктам. Чаще всего забывают последний: таблицы создаются без row level security, и данные оказываются открыты всем, у кого есть публичный ключ.

Слайд 20

Прослойка

Что делает функция, по шагам

01

Не POST → 405

02

Разобрать тело: urlencoded или JSON

03

test=test → 200 ok: это Tilda подключает webhook

04

Проверить ключ → иначе 401

05

Валидация и нормализация → иначе 400 со списком ошибок

06

upsert клиента по email: тот же человек — та же строка

07

insert заявки. Код ошибки 23505 — нарушен unique на tranid, значит дубль → 200 ok

08

Записать результат в webhook_log и ответить

Ключ проверяется до валидации данных: посторонний запрос не должен доходить до разбора полей.

Слайд 21

Деплой

Разворачиваем в Dashboard

00

supabase.com → Start your project: аккаунт по почте и паролю, письмо Confirm your signup. Организация создаётся сама. Затем New project: имя, Generate a password для базы (сохраните себе), Region — Europe. Три галочки Security оставляем как есть. Проект поднимается пару минут

01

Edge Functions → Secrets: TILDA_WEBHOOK_SECRET = длинная случайная строка (генерация — ниже). Сохраните её себе — дашборд потом покажет только дайджест

02

Deploy a new function → Via Editor, имя tilda-webhook: из имени собирается URL

03

Вставить index.ts, вторым файлом logic.ts → Deploy function

04

Settings функции → выключить Verify JWT

05

Скопировать URL: https://<ref>.supabase.co/functions/v1/tilda-webhook

Где взять строку. В консоли браузера (F12 → Console) на любой странице: [...crypto.getRandomValues(new Uint8Array(24))].map(b => b.toString(16).padStart(2, '0')).join('') — получится 48 символов. Альтернатива — менеджер паролей, 32+ символа латиницы и цифр. Онлайн-генераторы «random string» не используйте: секрет уйдёт на чужой сервер.

Шаг 4 обязателен. С включённой проверкой Supabase ждёт свой токен и отбивает запрос до вашего кода: Tilda получит 401, а в webhook_log не появится ни строки. Доступ закрывает не JWT, а ключ из шага 1. Терминал, CLI и Docker не нужны.

Слайд 22

Подключение

Проверяем запись в таблицу

01

В Tilda заменить URL тестового приёмника на URL функции и указать ключ

02

Отправить заявку с опубликованной страницы

03

Table Editor → requests: новая строка со статусом new, заполнены client_id и service_id

04

Table Editor → clients: клиент с нормализованным телефоном +7…

05

Отправить форму ещё раз с тем же email: клиент остался один, заявок стало две

Шаг 5 — проверка upsert. Если клиентов стало двое, ищите расхождение в email: пробел по краям или заглавные буквы.

Слайд 23

Журнал

Что случилось с заявкой

select received_at, result, http_status, message
from webhook_log
order by received_at desc
limit 20;

webhook_log отвечает на вопрос «куда делась заявка», не открывая код. Смоделируйте сбои скриптом send-test.sh: без ключа, с ошибкой в полях, дубль — и найдите каждый в журнале. Технические детали смотрите в Edge Functions → Logs.

Слайд 24

Фиксируем

Базовый интеграционный сценарий

#УсловиеОтветЧто в базе
1Проверка от Tilda test=test200лог test
2Корректная заявка200клиент + заявка + лог ok
3Повтор того же tranid200лог duplicate, новой заявки нет
4Нет ключа или ключ неверный401лог unauthorized
5Ошибки в полях400лог invalid + текст ошибок
6Сбой базы500лог error

Заполнять эту таблицу нужно не по памяти: пять строк из шести воспроизводятся за минуту скриптом send-test.sh.

Слайд 25

Чего нельзя делать

Пять ошибок, которые стоят дорого

ОшибкаПоследствие
service_role-ключ в T123 «для быстроты»Любой посетитель может прочитать и удалить всю базу
RLS выключенAnon-ключ даёт чтение персональных данных всех клиентов
Нет уникальности tranidДубли заявок при повторной доставке
Ключ записан в журналУтечка секрета через логи — в проекте ключ маскируется функцией redact()
Не выключен Verify JWTВсе запросы от Tilda получают 401, журнал пуст

Слайд 26

Мини-артефакт урока 6

Данные доехали до базы

  • Заявка из формы Tilda появляется в таблице requests, клиент — в clients
  • Таблица интеграционного сценария заполнена: все шесть строк проверены вживую
  • Каждый запрос виден в webhook_log, ключ в журнал не попадает

Задание: assignments/module-3.md, задание 3.2 · Итог курса: форма → webhook → прослойка → БД работает на вашем проекте

1/14
Made on
Tilda