Datas na base de dados: guarde sempre em UTC
É uma das decisões mais baratas de tomar no início e mais caras de corrigir depois: em que fuso horário é que as datas ficam guardadas.
A resposta é sempre UTC, e converte-se para o fuso de quem lê no último momento possível — na altura de mostrar.
1. Porque é que o fuso local não serve
O servidor muda de máquina e o fuso muda com ele. As datas antigas passam a mentir.
Há dias com 23 horas e dias com 25, por causa da mudança da hora. Em outubro, a hora entre as 01:00 e as 02:00 acontece duas vezes: uma data guardada nesse intervalo é ambígua e não há forma de a desfazer.
Duas pessoas em fusos diferentes ordenam a mesma lista de forma diferente.
2. Em Postgres: timestamptz, não timestamp
-- Guarda um instante. Converte para UTC ao escrever.
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
-- Guarda números num relógio, sem dizer de onde. Evitar.
criado_em TIMESTAMP NOT NULLO nome engana: o timestamptz não guarda o fuso. Guarda um instante — normaliza para UTC ao escrever e converte para o fuso da sessão ao ler. É precisamente o que se quer.
3. Quando o que interessa é o dia
Nem tudo é um instante. Uma data de nascimento, ou o dia em que um artigo foi publicado, não têm hora nem fuso — se as guardar como instante, alguém do outro lado do Atlântico vê o dia anterior.
publicado_em DATE NOT NULL4. A conversão é na fronteira
// Do lado do servidor: sempre o instante
const criadoEm = new Date(); // internamente é UTC
// À saída, para quem lê
new Intl.DateTimeFormat('pt-PT', {
dateStyle: 'short',
timeStyle: 'short',
timeZone: 'Europe/Lisbon',
}).format(criadoEm);A regra em três palavras: guardar em UTC, mostrar em local. E nunca, nunca guardar uma data já formatada como texto — é o erro que obriga a adivinhar mais tarde o que "03/04/2009" queria dizer.