Skip to main content
Um post de changelog é escrito como rascunho, mirado em todo mundo ou numa audiência, e publicado por um ato próprio. Três superfícies respondem três perguntas diferentes sobre as mesmas linhas, e as diferenças entre elas são o desenho inteiro.

Publicar é um ato, não uma coluna

published_at é um momento e não uma flag: a lista é ordenada por ele, e o badge de não lidos compara a chegada de um contato contra ele. Publicar tem endpoint próprio porque é o passo que entra num documento cacheável e anuncia changelog_post.published para os seus webhooks — e um formulário enviado duas vezes não pode anunciar um release duas vezes. O UPDATE leva published_at IS NULL, então essa idempotência é do banco e não de uma checagem que alguém lembrou de escrever. Editar um post publicado é uma edição; muda o documento e não anuncia nada. Os fatos do rascunho ficam dentro do prédio: um post não publicado é um anúncio que você ainda não fez.

O documento público

Cacheável, endereçado pela chave que já está na sua página, seguro atrás de um CDN, e leva os 50 posts mais recentes. Limitado em vez de paginado: um cursor num documento cacheável multiplica as entradas de cache pelo número de posições em que um leitor pode estar, e o que uma página de releases desenha são os releases recentes. O histórico inteiro continua disponível na leitura do próprio contato, que não é cacheada. O documento público abre mão do direcionamento, e isso é deliberado. Uma publishable key fica no HTML da página, então este documento não tem leitor para resolver. Honrar segment_id aqui exigiria um identificador que quem chama fornece — e um documento que responde “aqui está o post para este endereço de e-mail” é um oráculo de quem está em qual segmento, respondível por qualquer um com uma página e uma lista de endereços. Pertencer a um segmento é a sua própria análise sobre os seus usuários; um documento cacheável não pode ser sondado atrás disso. Então só um post mirado em ninguém em particular chega a este documento. Um post direcionado é alcançável exclusivamente por uma sessão de contato. body_markdown é markdown como foi escrito. Renderizar é da página que desenha — a API nunca te devolve HTML, e você nunca deve entregar texto de banco a um renderizador de markup que confia nele.

O feed do próprio contato

Num app Next, importe do @userkit/nextjs — é o mesmo componente, e em modo proxy a leitura e o recibo de leitura são encaminhados pelos handlers, com a sessão num cookie httpOnly da sua própria origem. O <WhatsNew /> mais abaixo e o useChangelogFeed atravessam pela mesma porta. Esta é a superfície que carrega os posts direcionados, e ela fica atrás do portão de contato verificado exatamente pela razão que faz o documento público abrir mão do direcionamento. Uma sessão não verificada mas identificada é uma afirmação que qualquer um que saiba um external_id pode fazer de um navegador, e a resposta deste endpoint difere por pertencimento a segmento. Sem o portão, seria o mesmo oráculo alcançado pela outra porta: “esse external_id vê o post do beta enterprise” é “essa pessoa está no segmento do beta enterprise”. Sessões anônimas passam pelo portão, como passam em todo lugar — a linha foi criada pela mesma chamada que a está segurando, então não há ninguém para personificar. O que elas veem são os posts não direcionados, com estado de leitura.

O badge conta a partir da chegada

since é quando este contato chegou, e nada publicado antes disso conta. Um badge que abre em 47 no primeiro dia de alguém é um badge que a pessoa dispensa uma vez e nunca mais olha. O número ser pequeno o bastante para agir é a única coisa que faz dele um badge em vez de decoração. Marcar um post como lido é idempotente — ler duas vezes é ler uma — e um post que esta pessoa nunca viu responde 404 em vez de deixar um badge ser limpo por uuid.
Todo post tem um endereço, e é por ele que o post é linkado: do widget, de uma resposta de suporte, de um e-mail. O slug é gerado do título quando o post é escrito — com os acentos dobrados para a letra base, porque Exportações virando exporta-es é um endereço que ninguém lê — e é único por ambiente, pela mesma razão de todo environment_id aqui: um ensaio no test não pode tomar o endereço que o gêmeo live ia usar. Trocar o título não move a página. Essa é a promessa inteira de um permalink: os links já enviados continuam apontando para o post, e nada os reenvia. Mover o endereço é mandar slug no PATCH, de propósito, e custa o endereço antigo. Um rascunho e um post direcionado respondem 404 aqui, com a mesma frase de um slug que não existe. Distinguir os três responderia “existe um post não publicado neste endereço” para qualquer um que adivinhe uma manchete — e numa superfície pública o palpite é de graça. Um post mirado num segmento guarda o slug: ampliá-lo para todo mundo depois liga aquele endereço em vez de inventar um. O prefixo para montar essas URLs é changelog_url, em GET /v1/config/{publishable_key} — resolvido por nós, porque só este lado sabe se o domínio próprio do inquilino foi verificado. Na lista do contato, cada post carrega slug e public: só ofereça o link quando public for true, ou você está oferecendo um 404.

”Enquanto você esteve fora”

O mesmo changelog pelo outro lado. O badge espera ser clicado; este abre sozinho, para o contato que está logado, com o que ele não viu — e o render comum é nada. É a ordem das duas perguntas que garante isso. Ele lê /v1/contact/changelog/unread primeiro e, num zero, acaba ali: a lista não é pedida, nenhum diálogo é montado, e quem está em dia recebe a própria página de volta intacta. Um modal que cumprimenta alguém com “nada de novo” é um modal que a pessoa aprende a fechar antes de ler, e quem paga essa conta é o próximo anúncio de verdade. O conjunto que ele mostra é o do próprio contador, repetido do lado do cliente: não lido e publicado depois de since. A lista responde com tudo o que aquele contato pode ver, inclusive o que é anterior a ele; o contador conta a partir da chegada. Ou os dois números descrevem o mesmo conjunto, ou um deles está mentindo — e o que dá para conferir é o que está na tela. Quem chegou hoje tem conjunto vazio por construção. Ser mostrado é o que marca esses posts como lidos, senão o mesmo diálogo volta amanhã com os mesmos posts. Os recibos são escritos depois que o diálogo está na tela, um por post desenhado, e um recibo que falha custa uma reaparição — não um post. E “mostrado” quer dizer na tela mesmo: o centro da página comporta um diálogo por vez — um termo pendente vem antes deste, uma pesquisa vem depois —, e enquanto este espera a vez nada é marcado como lido. Ele aparece assim que o de cima sair, ou na próxima visita. A ordem está em Pesquisas, com a página que explica a fila. Diferente do badge, o corpo aqui é renderizado como markdown, pelo mesmo renderer da central de ajuda: ele constrói elementos e nunca HTML, então a prosa que chega pela rede não tem onde injetar. Uma nota de release lida num diálogo é prosa que alguém escreveu para ser lida; a linha de preview do badge não tinha o que ganhar com títulos e listas, e esta tem.

A segunda audiência: quem pediu

Um post pode ser mirado num segmento, ou nos votantes de um post de feedback:
Os dois são mutuamente exclusivos, e qualquer um deles mantém o post fora do documento público. A audiência é um JOIN, não uma lista de destinatários copiada, o que é o que a faz se comportar certo nas duas pontas: quem vota depois de você publicar ainda recebe, e quem retira o voto para de receber. Veja feedback e roadmap para a outra metade.

O que sai do prédio

changelog_post.published é entregável como webhook. O payload leva o título e o momento e nunca a audiênciasegment_id é o único campo que o documento público recusa, e um webhook entregando isso seria o mesmo vazamento por outra porta. Editar e excluir um post não são entregues.

Permissões

engagement:manage — owner e admin por padrão, compartilhada com checklists e pesquisas, porque operar o que seus usuários veem dentro do seu produto é um ato e não três.