Skip to content
PMPaulo Mota
Back to the demo

How it was built

Digital HACCP, from the inside

Two tables, three functions and one rule that decides everything: whoever logs sends the reading, never the verdict.

01

The problem

Temperatures get written by hand on a sheet that lives on top of the fridge. When the inspection turns up, half of it is missing, and the other half was filled in all at once on Friday afternoon, with round numbers and the same pen. That is not a record, it is a badly made alibi. And when a fridge genuinely fails, nobody notices until the fish smells.

02

Data model

The control points are the house's HACCP plan: what gets measured, where, between which limits and on which shifts. Reference data, the same for everyone. The records belong to a room and carry the day, the shift, the reading, the verdict and the name of whoever signed. A unique index makes two records of the same point on the same shift of the same day impossible.

TableWhat it holds
demo_haccp_pointsThe house's HACCP plan: what gets measured, where, between which limits and on which shifts. Reference data, the same for every room.
demo_haccp_checksOne record per point, shift and day: the reading, the server's verdict, who signed, and if it failed, the corrective action and who signed that.
demo_haccp_deliveriesEach delivery: supplier, product, batch, use-by date, quantity and arrival temperature, with what the server found and what the person decided.
demo_roomsThe demo room that links the devices. Deleted after six hours, and the records with it.
03

The verdict is born on the server

This is the decision that makes the system worth anything. The phone sends the reading and the point, never the pass or fail. The function fetches that point's limits, compares, and writes the result. Whoever fills it in cannot say everything is fine: they can only say the number they read. A cold point arriving with no reading, or with an impossible value, is refused before anything is stored.

sql
-- O telemóvel envia a leitura e o ponto. Nunca o veredicto.
select * into v_point from demo_haccp_points where id = p_point;

if v_point.kind = 'tarefa' then
  v_ok := true;
  p_value := null;
else
  if p_value is null then raise exception 'sem leitura'; end if;
  if p_value < -40 or p_value > 200 then
    raise exception 'leitura impossivel';
  end if;
  -- O cumpre ou não cumpre nasce aqui, e só aqui
  v_ok := p_value >= v_point.min_c and p_value <= v_point.max_c;
end if;

-- Hoje no fuso do restaurante, não no do servidor
v_today := (now() at time zone 'Europe/Lisbon')::date;
04

A breach does not close itself

A reading outside the limits stays open until somebody writes what they did and signs it. The function refuses an empty action, and refuses an action on a record that passed, so the history does not fill with noise. Fixing a mistyped reading replaces it and reopens the breach, instead of leaving two versions of the same shift arguing with each other.

sql
-- Um incumprimento só fecha com uma acção escrita e assinada.
if length(btrim(coalesce(p_action, ''))) < 4 then
  raise exception 'accao vazia';
end if;

-- E não se "corrige" o que cumpre: isso seria ruído no registo
if v_row.ok then raise exception 'este registo cumpre'; end if;

-- Corrigir uma leitura mal metida substitui-a e reabre o
-- incumprimento, em vez de deixar duas versões do mesmo turno
on conflict (room_code, point_id, day, shift) do update
   set value_c = excluded.value_c, ok = excluded.ok,
       signed_by = excluded.signed_by, created_at = now(),
       corrective = null, corrected_by = null, corrected_at = null;
05

Who signed is not public

The name of whoever logged it, the note and the corrective action are people's data: they do not come out through the public key. The key that ships in the browser reads the point, the time, the reading and whether it passed, which is what the screen needs to light the alert in real time. The rest comes out through a function that requires the room code.

sql
-- Um ponto só se regista uma vez por turno e por dia.
create unique index demo_haccp_checks_once_idx
  on demo_haccp_checks (room_code, point_id, day, shift);

-- Um ponto de temperatura sem limites não é um ponto de controlo
constraint demo_haccp_limits_needed
  check (kind = 'tarefa'
         or (min_c is not null and max_c is not null
             and min_c <= max_c));

-- Quem assinou não sai pela chave pública: chega o que o ecrã
-- precisa para acender o alerta em tempo real.
grant select (id, room_code, point_id, day, shift, value_c, ok,
              corrected_at, created_at)
  on demo_haccp_checks to anon;
typescript
// O ecrã do responsável acende sozinho quando a equipa
// regista uma leitura fora dos limites, do outro lado da casa.
const channel = supabase
  .channel(`demo-haccp-${room}`)
  .on("postgres_changes",
      { event: "*", schema: "public", table: "demo_haccp_checks",
        filter: `room_code=eq.${room}` },
      () => void load(room))
  .subscribe();

// Incumprimentos por resolver: fora dos limites e sem acção escrita
const openFails = checks.filter((c) => !c.ok && !c.corrected_at);
06

Verifying is not the same as correcting

The cycle does not close on the corrective action. It closes when somebody else checks the action was actually done and signs. The function refuses to verify what has no action yet, and a table constraint enforces the same even if someone writes straight to the database. If the reading is corrected, both the action and the verification fall away: they refer to a number that is no longer there. In this demo any visitor signs both parts; in a restaurant, verifying belongs to the manager, not to whoever logged it.

sql
-- Não se verifica o que ainda não foi corrigido.
-- Na função, para dar um erro legível:
if v_row.corrected_at is null then
  raise exception 'ainda sem accao correctiva';
end if;

-- E na tabela, para valer mesmo que alguém escreva direto:
alter table demo_haccp_checks
  add constraint demo_haccp_verify_order
  check (verified_at is null or corrected_at is not null);

-- Uma leitura nova apaga a acção e a verificação: referem-se
-- a um número que já não está lá.
on conflict (room_code, point_id, day, shift) do update
   set value_c = excluded.value_c, ok = excluded.ok,
       corrective = null, corrected_by = null, corrected_at = null,
       verified_by = null, verified_at = null;
07

Traceability starts at the delivery door

Supplier, product, batch, use-by date and temperature on arrival. This is what lets a kitchen answer when a supplier recalls a batch. The receiving limits live on the server: chilled up to 4 degrees, frozen up to -18. The server says whether it was in range; accepting or rejecting stays the decision of whoever is at the door, and is recorded as such. Accepting an out-of-range delivery is a legitimate call in some cases, and it gets written down as a decision rather than hidden. Rejecting without writing why is refused by a constraint.

sql
-- Os limites de recepção são do servidor, não do telemóvel.
v_max := case p_kind when 'refrigerado' then 4.0
                     when 'congelado' then -15.0
                     else null end;

v_temp_ok := p_temp <= v_max;
v_date_ok := p_expiry >= v_today;

-- Repare no que NÃO acontece aqui: o servidor não decide aceitar.
-- Diz se estava dentro; aceitar é de quem está na doca, e fica
-- registado como decisão dessa pessoa.

-- Recusar sem dizer porquê não é recusar, é esconder:
constraint demo_delivery_reason_needed
  check (accepted
         or (reject_reason is not null
             and length(btrim(reject_reason)) >= 4));
08

What this software does not do

It does not invent critical limits or validate a HACCP plan. The control points, the limits and the frequencies are configuration, written from the house's own plan, which is the responsibility of someone qualified. The software documents, measures against what is configured, and keeps the proof. Telling a client that software makes them compliant would be a lie, and a client who has been through an inspection spots it immediately.

09

What I would do differently for a real client

Probes logging automatically in the fridges, so the number does not depend on somebody remembering. A phone alert when a point goes out of range after hours. A photo attached to the goods-received record. A monthly PDF export to hand the inspector without printing anything by hand. And real authentication: here any visitor signs with whatever name they like because it is a demo; in a kitchen, each person logs in with their own code.