Как делать12 октября 2026 г.7 мин чтения
Вайбкодинг: от прототипа к продуктуШаг 4 из 7

Доступ к данным в AI-собранном MVP: роли, ключи на клиенте и RLS

Коротко

В MVP на базе как сервисе (Supabase и похожие платформы) браузер обращается к базе напрямую с публичным ключом, и это нормально: ключ виден любому посетителю и сам по себе ничего не защищает. Защищает Row Level Security — политики в PostgreSQL, которые для каждой строки решают, может ли текущий пользователь её прочитать или изменить. Таблица без RLS при доступном API открыта всем, у кого есть публичный ключ, а служебный ключ обходит RLS и должен жить только на сервере.

Пошаговый урокУровень: СреднийВайбкодинг: от прототипа к продуктуПрактика: ~3–6 ч

После материала вы сможете

  • объяснить, почему публичный ключ в браузере — норма, а таблица без RLS при нём — открытые данные
  • воспроизвести утечку на стенде PostgreSQL 17 + PostgREST: прочитать и изменить чужие записи с публичным ключом
  • включить ENABLE ROW LEVEL SECURITY и написать политики USING и WITH CHECK для чтения, вставки, изменения и удаления своих строк
  • проверить доступ запросами от имени двух пользователей и анонима и найти таблицы без RLS одним запросом

Проверено на

PostgreSQL 17 (17.11, postgres:17-alpine)PostgREST 16.4Node.js 24 LTS (24.18)curl 8.21

Сверено с документацией: 12 октября 2026 г.

Содержание
  1. 01Как утекают данные в MVP с базой как сервисом
  2. 02Стенд: PostgreSQL 17 и PostgREST в Docker
  3. 03Воспроизводим утечку: публичный ключ читает и меняет чужие записи
  4. 04Включаем RLS и пишем политики
  5. 05Проверка от имени двух пользователей и анонима
  6. 06Служебный ключ — только на сервере
  7. 07Ловушки: UPDATE без SELECT, представления, метаданные пользователя
  8. 08Практика

Как утекают данные в MVP с базой как сервисом

Платформы вроде Supabase строят API прямо из схемы базы: Supabase прямо пишет, что его REST API работает на PostgREST — тонком слое над PostgreSQL. Фронтенд обращается к этому API сам, без своего бэкенда, и отправляет в каждом запросе публичный ключ. Ключ лежит в JavaScript-бандле, его видно во вкладке «Сеть» браузера, и так задумано: документация Supabase прямо относит публичный (publishable, раньше — anon) ключ к коду, который уходит пользователям.

Что тогда защищает данные? Не секретность ключа, а роль, которую он даёт, и политики Row Level Security (RLS) в базе. Без входа запрос выполняется от роли anon, после входа — от роли authenticated, а идентификатор пользователя берётся из подписанного токена (JWT). Если на таблице нет RLS, а у роли есть права, любой посетитель с публичным ключом читает и меняет все строки.

  1. 01Браузер с публичным ключом
  2. 02Data API (PostgREST)
  3. 03Роль anon или authenticated
  4. 04Политики RLS
  5. 05Таблица
Ключ выбирает роль, роль и политики решают, какие строки видны

Как таблица остаётся без RLS? Чаще всего буднично. Агент или разработчик создаёт её SQL-запросом: по документации Supabase у таблиц, созданных в панели управления, RLS включён по умолчанию, а у созданных в SQL-редакторе или другими инструментами его нужно включить явно. При этом таблицы в схеме public по умолчанию получают права SELECT, INSERT, UPDATE и DELETE для ролей anon и authenticated (Supabase объявил переход к явной выдаче прав, но на момент проверки описывает именно такое поведение). Это частый пропуск при быстрой сборке, а не признак плохого кода: агент сделал ровно то, что просили, — таблицу.

Стенд: PostgreSQL 17 и PostgREST в Docker

Чтобы не трогать настоящий проект, соберём локальный стенд с тем же устройством: PostgreSQL 17 и PostgREST — тот же движок Data API. Роли названы как в Supabase, но это упрощённая эмуляция, а не сама платформа: схема auth, выдача ключей и вход пользователей там устроены сложнее.

sql/01-platform.sql
-- Local stand that mimics a "database as a service" project (Supabase-style roles).
-- Not Supabase itself: a simplified emulation for the lesson.

-- API roles: the client key maps to anon / authenticated, the server key to service_role
create role anon nologin;
create role authenticated nologin;
create role service_role nologin bypassrls;
-- PostgREST connects as authenticator and switches to the role from the JWT
create role authenticator login password 'local-test-only' noinherit;
grant anon, authenticated, service_role to authenticator;

grant usage on schema public to anon, authenticated, service_role;

-- Default grants: every new table in public is reachable by the API roles
alter default privileges in schema public
  grant select, insert, update, delete on tables to anon, authenticated, service_role;

-- auth.uid(): the user id from the JWT claims of the current request
create schema auth;
grant usage on schema auth to anon, authenticated, service_role;
create function auth.uid() returns uuid
language sql stable
as $$
  select nullif(current_setting('request.jwt.claims', true)::jsonb ->> 'sub', '')::uuid
$$;
Роли API, права по умолчанию и функция auth.uid(), которая читает идентификатор пользователя из claims текущего запроса.

PostgREST подключается к базе ролью authenticator, проверяет подпись JWT и для каждого запроса выполняет SET LOCAL ROLE с ролью из токена; без токена — роль из настройки db-anon-role. Claims токена доступны в SQL через current_setting('request.jwt.claims', true) — на этом построена auth.uid(). Таблица — как её создал бы агент по задаче «сделай записи клиентов на услуги»:

sql/02-app.sql
-- The table as it was created from the SQL editor: no RLS
create table public.bookings (
  id bigint generated always as identity primary key,
  user_id uuid not null default auth.uid(),
  client_name text not null,
  phone text not null,
  service text not null,
  starts_at timestamptz not null
);

-- Demo data: fictitious users and placeholder phone numbers
insert into public.bookings (user_id, client_name, phone, service, starts_at) values
  ('11111111-1111-4111-8111-111111111111', 'Alice', '+7 700 000 0001', 'haircut',  '2026-10-20 10:00+05'),
  ('11111111-1111-4111-8111-111111111111', 'Alice', '+7 700 000 0001', 'manicure', '2026-10-21 12:00+05'),
  ('22222222-2222-4222-8222-222222222222', 'Bob',   '+7 700 000 0002', 'massage',  '2026-10-20 15:00+05');
Таблица без RLS. Пользователи, имена и телефоны — вымышленные заглушки.

Токены для стенда подписываем сами тем же секретом, что знает PostgREST. Токен anon здесь играет роль публичного ключа из бандла, service — служебного ключа.

make-jwt.mjs
// Signs a test JWT (HS256) for the local stand: node make-jwt.mjs <anon|alice|bob|service>
// The secret is a local placeholder; real projects never keep it in code.
import { createHmac } from "node:crypto";

const SECRET = process.env.JWT_SECRET ?? "local-test-secret-not-for-production-0123456789";
const USERS = {
  anon: { role: "anon" },
  alice: { role: "authenticated", sub: "11111111-1111-4111-8111-111111111111" },
  bob: { role: "authenticated", sub: "22222222-2222-4222-8222-222222222222" },
  service: { role: "service_role" },
};

const claims = USERS[process.argv[2]];
if (!claims) throw new Error("usage: node make-jwt.mjs anon|alice|bob|service");

const b64url = (data) => Buffer.from(JSON.stringify(data)).toString("base64url");
const body = `${b64url({ alg: "HS256", typ: "JWT" })}.${b64url({ ...claims, exp: 2000000000 })}`;
const signature = createHmac("sha256", SECRET).update(body).digest("base64url");
console.log(`${body}.${signature}`);
Только для локального стенда: секрет — заглушка, срок действия токена условный.
run-lab.sh
NET=kb-g-rls-net PG=kb-g-rls-pg API=kb-g-rls-api
JWT_SECRET="local-test-secret-not-for-production-0123456789"
docker network create $NET >/dev/null
docker run -d --rm --name $PG --network $NET -e POSTGRES_PASSWORD=test postgres:17-alpine >/dev/null
until docker exec $PG pg_isready -U postgres -q 2>/dev/null; do sleep 1; done; sleep 2
psql() { docker exec -i $PG psql -U postgres -X -q -v ON_ERROR_STOP=1 "$@"; }
psql -f - < sql/01-platform.sql && psql -f - < sql/02-app.sql
docker run -d --rm --name $API --network $NET -p 3901:3000 \
  -e PGRST_DB_URI="postgres://authenticator:local-test-only@$PG:5432/postgres" \
  -e PGRST_DB_SCHEMAS=public -e PGRST_DB_ANON_ROLE=anon -e PGRST_JWT_SECRET="$JWT_SECRET" \
  postgrest/postgrest:v16.4 >/dev/null
Фрагмент скрипта стенда, не самостоятельный запуск. Целиком скрипт ещё ждёт готовности контейнеров, сохраняет вывод и удаляет контейнеры и сеть после прогона.

Воспроизводим утечку: публичный ключ читает и меняет чужие записи

Берём публичный ключ — тот, что любой посетитель найдёт в бандле, — и просим у API все записи. Затем меняем чужую запись.

terminal
$ API=http://localhost:3901
$ ANON_KEY=$(node make-jwt.mjs anon)
$ curl -s "$API/bookings?select=id,client_name,phone,service" -H "Authorization: Bearer $ANON_KEY"; echo
[{"id":1,"client_name":"Alice","phone":"+7 700 000 0001","service":"haircut"}, 
 {"id":2,"client_name":"Alice","phone":"+7 700 000 0001","service":"manicure"}, 
 {"id":3,"client_name":"Bob","phone":"+7 700 000 0002","service":"massage"}]
$ curl -s -X PATCH "$API/bookings?id=eq.3" -H "Authorization: Bearer $ANON_KEY" -H "Content-Type: application/json" -H "Prefer: return=representation" -d '{"service":"cancelled"}'; echo
[{"id":3,"user_id":"22222222-2222-4222-8222-222222222222","client_name":"Bob","phone":"+7 700 000 0002","service":"cancelled","starts_at":"2026-10-20T10:00:00+00:00"}]
PostgreSQL 17.11 и PostgREST 16.4 в Docker, curl 8.21, Node.js 24 (реальный вывод).

Аноним получил имена и телефоны всех клиентов и отменил запись Боба. Входить в приложение не понадобилось: хватило публичного ключа и адреса API, а их знает браузер каждого посетителя. Такой пропуск легко найти и у себя — одним запросом к системному каталогу:

sql/05-no-rls.sql
-- Tables in the public schema without RLS
select c.relname as table_without_rls
from pg_class c
where c.relnamespace = 'public'::regnamespace
  and c.relkind in ('r', 'p')
  and not c.relrowsecurity;
terminal
$ docker exec -i kb-g-rls-pg psql -U postgres -X -q -f - < sql/05-no-rls.sql
 table_without_rls 
-------------------
 bookings
(1 row)
PostgreSQL 17.11 и PostgREST 16.4 в Docker, curl 8.21, Node.js 24 (реальный вывод). До включения RLS.

Включаем RLS и пишем политики

Правило для записей простое: пользователь видит и меняет только свои строки — те, где user_id совпадает с его идентификатором из токена.

sql/03-rls.sql
-- 1. Turn on RLS: from now on, no policy = no rows
alter table public.bookings enable row level security;

-- 2. One policy per operation, always with an explicit role
create policy "bookings_select_own" on public.bookings
  for select to authenticated
  using ((select auth.uid()) = user_id);

create policy "bookings_insert_own" on public.bookings
  for insert to authenticated
  with check ((select auth.uid()) = user_id);

create policy "bookings_update_own" on public.bookings
  for update to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);

create policy "bookings_delete_own" on public.bookings
  for delete to authenticated
  using ((select auth.uid()) = user_id);
Политики для роли authenticated. Для anon политик нет — значит, анониму не видно ничего.

SELECT

Условие
USING
Что делает база
Отбрасывает строки, для которых условие ложно; ошибки нет

INSERT

Условие
WITH CHECK
Что делает база
Проверяет новую строку; если условие ложно — ошибка, вся команда отменяется

UPDATE

Условие
USING и WITH CHECK
Что делает база
USING выбирает, какие строки можно менять, WITH CHECK — какой строка может стать после изменения

DELETE

Условие
USING
Что делает база
Удаляет только строки, прошедшие условие; чужие молча пропускает
  • После ENABLE ROW LEVEL SECURITY действует запрет по умолчанию: по документации PostgreSQL, если политики нет, не видно и нельзя изменить ни одной строки.
  • У каждой политики указана роль (to authenticated). Без to политика по умолчанию применяется к PUBLIC, то есть и к анониму; Supabase советует всегда называть роль явно.
  • (select auth.uid()) вместо auth.uid() — совет из документации Supabase: обёртка в select позволяет планировщику вычислить значение один раз на запрос, а не на каждую строку.
  • Несколько разрешающих политик на одну операцию объединяются через OR: лишняя широкая политика открывает всё, что закрыли остальные.

Проверка от имени двух пользователей и анонима

Политики, которые не проверены запросами, — это намерение, а не защита. Повторяем запросы через API от имени анонима, Алисы и Боба.

terminal
$ API=http://localhost:3901
$ ANON_KEY=$(node make-jwt.mjs anon); ALICE_KEY=$(node make-jwt.mjs alice); BOB_KEY=$(node make-jwt.mjs bob)
$ curl -s "$API/bookings?select=id,client_name" -H "Authorization: Bearer $ANON_KEY"; echo
[]
$ curl -s "$API/bookings?select=id,client_name" -H "Authorization: Bearer $ALICE_KEY"; echo
[{"id":1,"client_name":"Alice"}, 
 {"id":2,"client_name":"Alice"}]
$ curl -s "$API/bookings?select=id,client_name" -H "Authorization: Bearer $BOB_KEY"; echo
[{"id":3,"client_name":"Bob"}]
$ curl -s -X PATCH "$API/bookings?id=eq.3" -H "Authorization: Bearer $ALICE_KEY" -H "Content-Type: application/json" -H "Prefer: return=representation" -d '{"service":"cancelled"}'; echo
[]
$ curl -s -w ' %{http_code}\n' -X POST "$API/bookings" -H "Authorization: Bearer $ALICE_KEY" -H "Content-Type: application/json" -d '{"user_id":"22222222-2222-4222-8222-222222222222","client_name":"Bob","phone":"+7 700 000 0002","service":"fake","starts_at":"2026-10-22T10:00:00+05:00"}'
{"code":"42501","details":null,"hint":null,"message":"new row violates row-level security policy for table \"bookings\""} 403
$ curl -s -w ' %{http_code}\n' -X POST "$API/bookings" -H "Authorization: Bearer $ALICE_KEY" -H "Content-Type: application/json" -H "Prefer: return=representation" -d '{"client_name":"Alice","phone":"+7 700 000 0001","service":"pedicure","starts_at":"2026-10-22T10:00:00+05:00"}'
[{"id":5,"user_id":"11111111-1111-4111-8111-111111111111","client_name":"Alice","phone":"+7 700 000 0001","service":"pedicure","starts_at":"2026-10-22T05:00:00+00:00"}] 201
PostgreSQL 17.11 и PostgREST 16.4 в Docker, curl 8.21, Node.js 24 (реальный вывод). Номер 4 ушёл на отклонённую вставку: значения последовательности не возвращаются при откате.
  1. Аноним получает пустой список

    RLS включён, политик для anon нет — действует запрет по умолчанию. Ответ [], а не ошибка: база просто не показывает строки.

  2. Алиса и Боб видят только свои записи

    Один и тот же запрос возвращает разные строки: условие USING подставляет идентификатор из токена.

  3. Чужая запись не меняется

    PATCH записи Боба от имени Алисы вернул []: строка не прошла USING и молча не попала под изменение.

  4. Подделать владельца нельзя

    Вставка с чужим user_id получила ошибку 42501 и HTTP 403 — сработал WITH CHECK. Без user_id в запросе подставилось значение по умолчанию auth.uid(), и запись создана.

Те же проверки удобно повторять прямо в базе — например, в тестах миграций. Делаем то, что делает PostgREST на каждый запрос: переключаем роль и кладём claims в настройки транзакции.

terminal
$ docker exec -i kb-g-rls-pg psql -U postgres -X -a -f - < sql/10-as-users.sql
-- What PostgREST does for every request: switch role and put JWT claims into the transaction
\set alice '{"role":"authenticated","sub":"11111111-1111-4111-8111-111111111111"}'
\set bob '{"role":"authenticated","sub":"22222222-2222-4222-8222-222222222222"}'
-- Bob: sees only his own booking
begin;
BEGIN
set local role authenticated;
SET
select set_config('request.jwt.claims', :'bob', true) is not null as claims_set;
 claims_set 
------------
 t
(1 row)

select auth.uid() as uid, current_user;
                 uid                  | current_user  
--------------------------------------+---------------
 22222222-2222-4222-8222-222222222222 | authenticated
(1 row)

select id, client_name, service from public.bookings order by id;
 id | client_name | service 
----+-------------+---------
  3 | Bob         | massage
(1 row)

rollback;
ROLLBACK
-- Anonymous visitor: RLS is on, no policy for anon, so no rows
begin;
BEGIN
set local role anon;
SET
select count(*) as visible_to_anon from public.bookings;
 visible_to_anon 
-----------------
               0
(1 row)

rollback;
ROLLBACK
-- Alice: someone else's row is silently skipped, someone else's user_id is an error
begin;
BEGIN
set local role authenticated;
SET
select set_config('request.jwt.claims', :'alice', true) is not null as claims_set;
 claims_set 
------------
 t
(1 row)

select id, client_name, service from public.bookings order by id;
 id | client_name | service  
----+-------------+----------
  1 | Alice       | haircut
  2 | Alice       | manicure
  5 | Alice       | pedicure
(3 rows)

update public.bookings set service = 'cancelled' where id = 3;
UPDATE 0
insert into public.bookings (user_id, client_name, phone, service, starts_at)
  values ('22222222-2222-4222-8222-222222222222', 'Bob', '+7 700 000 0002', 'fake', now());
psql:<stdin>:26: ERROR:  new row violates row-level security policy for table "bookings"
rollback;
ROLLBACK
PostgreSQL 17.11 и PostgREST 16.4 в Docker, curl 8.21, Node.js 24 (реальный вывод). set_config(…, true) действует до конца транзакции, как у PostgREST.

Служебный ключ — только на сервере

У служебной роли атрибут BYPASSRLS: по документации PostgreSQL такие роли и суперпользователи всегда обходят политики. Тот же запрос со служебным ключом возвращает всё:

terminal
$ API=http://localhost:3901
$ SERVICE_KEY=$(node make-jwt.mjs service)
$ curl -s "$API/bookings?select=id,client_name" -H "Authorization: Bearer $SERVICE_KEY"; echo
[{"id":1,"client_name":"Alice"}, 
 {"id":2,"client_name":"Alice"}, 
 {"id":3,"client_name":"Bob"}, 
 {"id":5,"client_name":"Alice"}]
PostgreSQL 17.11 и PostgREST 16.4 в Docker, curl 8.21, Node.js 24 (реальный вывод).

Supabase формулирует правило однозначно: секретный ключ нельзя использовать в браузере и показывать пользователям, он для серверного кода — сервера, функций, фоновых задач. Если этот ключ попал в бандл, RLS больше ничего не защищает: ключ нужно отозвать и выпустить новый.

  • Служебный ключ читается только в серверном коде и не имеет публичного префикса вроде NEXT_PUBLIC_ или VITE_
  • В клиентских компонентах, мобильном приложении и логах нет служебного ключа и строки подключения к базе
  • Серверный код, который работает со служебным ключом, сам проверяет права пользователя — RLS за него этого не сделает
  • Ключ, который хоть раз попал в репозиторий или бандл, отозван и заменён

Как хранить ключи и что делать при утечке, подробно разобрано в уроке «Секреты и переменные окружения».

Ловушки: UPDATE без SELECT, представления, метаданные пользователя

Две ловушки проверим на стенде; изменения откатываются в конце каждой транзакции.

terminal
$ docker exec -i kb-g-rls-pg psql -U postgres -X -a -f - < sql/20-traps.sql
\set alice '{"role":"authenticated","sub":"11111111-1111-4111-8111-111111111111"}'
-- Trap 1: an UPDATE policy without a SELECT policy updates nothing
begin;
BEGIN
drop policy "bookings_select_own" on public.bookings;
DROP POLICY
set local role authenticated;
SET
select set_config('request.jwt.claims', :'alice', true) is not null as claims_set;
 claims_set 
------------
 t
(1 row)

update public.bookings set service = 'pedicure' where id = 1;
UPDATE 0
rollback;
ROLLBACK
-- Trap 2: a view created by the owner ignores the table's RLS
begin;
BEGIN
create view public.bookings_calendar as select starts_at, client_name, phone from public.bookings;
CREATE VIEW
set local role anon;
SET
select count(*) as rows_via_view from public.bookings_calendar;
 rows_via_view 
---------------
             4
(1 row)

reset role;
RESET
alter view public.bookings_calendar set (security_invoker = true);
ALTER VIEW
set local role anon;
SET
select count(*) as rows_via_invoker_view from public.bookings_calendar;
 rows_via_invoker_view 
-----------------------
                     0
(1 row)

rollback;
ROLLBACK
PostgreSQL 17.11 и PostgREST 16.4 в Docker, curl 8.21, Node.js 24 (реальный вывод).
  • UPDATE без политики SELECT ничего не меняет: команде нужно прочитать строки по условию WHERE, а прочитать их нельзя. Документация PostgreSQL требует для такого UPDATE и права, и политики на SELECT; Supabase предупреждает, что без политики SELECT обновление «не работает как ожидается».
  • Представление (view), созданное владельцем таблицы, по умолчанию проверяет права от имени владельца, и RLS не срабатывает: аноним увидел все четыре записи. С security_invoker = true (PostgreSQL 15 и новее) представление подчиняется политикам того, кто делает запрос. Права по умолчанию касаются и представлений, поэтому новое представление сразу доступно через API.
  • Метаданные пользователя, которые он может менять сам (в Supabase — raw_user_meta_data, обновляется через supabase.auth.update()), нельзя использовать в политиках как признак роли: пользователь назначит себе любую.
  • Владелец таблицы и роли с BYPASSRLS обходят политики, если не включён FORCE ROW LEVEL SECURITY. Подробно эти случаи и изоляция арендаторов разобраны в уроке о multi-tenant SaaS.

Второй слой защиты описан в документации Supabase: отозвать у ролей API права по умолчанию, чтобы новая таблица или функция не становилась доступной через API, пока права не выданы явно. RLS при этом остаётся обязательным на каждой таблице и представлении, к которым API обращается.

Практика

Как проверено6 · пройденосверено 12 октября 2026 г.
  • запущено, работает

    Стенд: sql/01-platform.sql, sql/02-app.sql, make-jwt.mjs, run-lab.sh (PostgreSQL 17 + PostgREST, роли anon/authenticated/service_role, auth.uid() из request.jwt.claims)

    bash run-lab.sh в Git Bash: docker run --rm postgres:17-alpine (PostgreSQL 17.11) и postgrest/postgrest:v16.4 в сети kb-g-rls-net, имена kb-g-rls-pg / kb-g-rls-api; контейнеры и сеть удаляются после прогона; токены подписаны node:crypto (Node.js 24.18), запросы — curl 8.21; вывод в out/*.txt, лог PostgREST — out/postgrest.log

  • запущено, работает

    Утечка без RLS: чтение всех записей и PATCH чужой записи с anon-токеном; поиск таблиц без RLS (sql/05-no-rls.sql)

    out/1-leak.txt, out/2-find.txt (до RLS — bookings), out/2-find-after.txt (после — 0 строк)

  • запущено, работает

    Политики sql/03-rls.sql и проверка через API от имени anon, Alice, Bob; service_role

    out/3-after.txt: anon — [], Alice — 2 строки, Bob — 1, PATCH чужой — [], POST с чужим user_id — 42501/403, своя вставка — 201; out/5-service.txt: service_role видит все строки

  • запущено, работает

    Проверка в psql от имени пользователей (sql/10-as-users.sql): set local role + set_config('request.jwt.claims', …, true)

    out/4-psql.txt: Bob — 1 строка, anon — 0, Alice — UPDATE 0 для чужой строки и ERROR new row violates row-level security policy при вставке с чужим user_id

  • запущено, работает

    Ловушки (sql/20-traps.sql): UPDATE без SELECT-политики, представление без security_invoker

    out/6-traps.txt: UPDATE 0 без SELECT-политики; anon через представление видит 4 строки, с security_invoker = true — 0; всё в транзакциях с ROLLBACK

  • запущено, работает

    Упражнения: DELETE чужой записи (7-ex-repeat), таблица reviews (sql/30-reviews.sql + 31-reviews-check.sql)

    out/7-ex-repeat.txt: DELETE от Bob — [], запись Alice на месте; out/8-ex-modify.txt: anon читает отзыв, UPDATE/DELETE от Bob — 0, INSERT от anon — ошибка политики

«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

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

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

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