Saltar para o conteúdo
PMPaulo Mota
Voltar à demo

Como foi feito

KDS, por dentro

O modelo de dados, as políticas de segurança, o que corre em tempo real e as decisões que tomei, incluindo as que mudaria.

01

O problema

Numa sala cheia, o pedido passa de cabeça para papel e de papel para a cozinha. Perde-se tempo, perdem-se pratos e ninguém sabe há quanto tempo a mesa 7 está à espera. O sistema resolve três coisas: o pedido chega sem intermediários, a cozinha vê o tempo decorrido, e o cliente sabe em que estado está.

02

Modelo de dados

Seis tabelas. O catálogo é partilhado e só de leitura; os pedidos pertencem a uma sala, que é o que liga o telemóvel ao ecrã da cozinha na mesma demonstração. Num cliente real, a sala é o restaurante.

TabelaPara que serve
demo_roomsA sala partilhada entre dispositivos. Apaga-se sozinha ao fim de 6 horas.
demo_menu_categoriesEntradas, pratos, sobremesas, bebidas. Só leitura.
demo_menu_itemsPratos com preço e descrição em português e inglês.
demo_venue_tablesAs mesas da sala, com lugares.
demo_ordersUm pedido: mesa, estado, nota, total e carimbos de tempo.
demo_order_itemsAs linhas do pedido, com o nome e o preço fixados no momento.
03

Segurança ao nível da linha

O Postgres decide o que cada pedido pode fazer, não o código do site. A chave que vai no browser é publishable: é pública de propósito e só consegue o que as políticas deixarem.

sql
-- Catálogo: só leitura
create policy demo_menu_items_read on public.demo_menu_items
  for select to anon, authenticated using (true);

-- Salas: criar, com o formato validado no próprio Postgres
create policy demo_rooms_insert on public.demo_rooms
  for insert to anon, authenticated
  with check (code ~ '^[A-Z0-9]{4,8}$');

-- Pedidos: criar e mudar de estado. Nunca apagar,
-- e só enquanto a sala ainda é recente.
create policy demo_orders_update on public.demo_orders
  for update to anon, authenticated
  using (created_at > now() - interval '6 hours')
  with check (created_at > now() - interval '6 hours');
04

A armadilha do returning

Um insert com returning precisa também de política de leitura, porque a linha devolvida é uma leitura. A tabela de contactos não tem política de select, por isso o formulário escreve sem pedir nada de volta. Descobri isto a testar com o papel anon, não a ler documentação.

sql
-- Contactos: escrever sim, ler não.
-- Sem política de select, ninguém lê as leads com a chave pública.
alter table public.leads enable row level security;

create policy leads_insert on public.leads
  for insert to anon, authenticated
  with check (true);
05

Uma transacção, não duas

A primeira versão inseria o pedido e depois as linhas. O evento de tempo real chegava entre as duas e a cozinha via um talão vazio durante um instante. Agora tudo entra numa função só, e o total é calculado no servidor a partir dos preços reais da ementa, não do que o browser diz.

sql
-- O pedido e as suas linhas na mesma transacção
insert into public.demo_orders (room_code, table_id, note, total_cents)
values (p_room, p_table, nullif(btrim(coalesce(p_note, '')), ''), 0)
returning id into v_order_id;

insert into public.demo_order_items
  (order_id, menu_item_id, name_snapshot, qty, unit_price_cents)
select v_order_id, m.id,
       case when line->>'locale' = 'en' then m.name_en else m.name_pt end,
       least(greatest((line->>'qty')::smallint, 1::smallint), 20::smallint),
       m.price_cents                    -- o preço vem da ementa, não do browser
from jsonb_array_elements(p_items) as line
join public.demo_menu_items m on m.id = (line->>'id')::integer;
06

Tempo real, e a rede da cozinha

O ecrã de cozinha subscreve as alterações da sua sala e actualiza sem recarregar. Por trás, há uma leitura a cada oito segundos. O tempo real é o que torna isto instantâneo; o intervalo é o que impede o ecrã de ficar vazio numa cozinha cuja rede bloqueia websockets, o que acontece mais do que se pensa.

typescript
const channel = supabase
  .channel(`demo-orders-${room}`)
  .on("postgres_changes",
      { event: "*", schema: "public", table: "demo_orders",
        filter: `room_code=eq.${room}` },
      () => void loadOrders(room))
  .subscribe((status) => {
    if (status === "SUBSCRIBED") void loadOrders(room);
  });

// Rede de segurança: se a cozinha bloquear websockets, o ecrã
// continua a encher, só que de oito em oito segundos.
const poll = window.setInterval(() => void loadOrders(room), 8000);
07

O que muda num cliente real

As políticas desta demo são permissivas de propósito: é uma demonstração pública com dados inventados e qualquer pessoa tem de conseguir experimentar sem se registar. Num restaurante a pagar, cada linha passa a estar ligada ao restaurante e às contas da equipa, e ninguém vê os pedidos de outra casa.

08

O que faria de outra forma

Se o volume subisse, o histórico de pedidos sairia da mesma tabela dos pedidos activos, porque o ecrã de cozinha só quer o dia de hoje. E acrescentaria uma fila local no telemóvel do empregado, para o pedido não se perder quando o wi-fi falha entre a esplanada e a cozinha.