15 de junho de 2026
Por trás de uma plataforma de assinatura digital: arquitetura, decisões e desafios técnicos
Plataformas de assinatura eletrônica de documentos parecem simples na superfície: alguém recebe um link, desenha uma assinatura, e um PDF "assinado" sai do outro lado. Mas entre o clique do usuário e o arquivo final certificado existe uma cadeia de decisões de engenharia que precisa equilibrar segurança, rastreabilidade jurídica, experiência do usuário e custo operacional. Este artigo descreve a arquitetura de um sistema desse tipo construído em PHP/Laravel, cobrindo as três partes mais sensíveis do fluxo: notificação por e-mail, coleta da assinatura e geração do documento final.
Stack e organização do código
O backend é construído em PHP 8.3 com Laravel 11, mas foge do "MVC padrão de tutorial" em um ponto importante: em vez de concentrar tudo em app/Models e app/Http/Controllers, o domínio de negócio vive em uma arquitetura modular própria, sob app/Modules/*. Cada módulo — SignatureRequest, Envelope, Signatory, Document, Signature, Auth, Support — carrega seus próprios Services, Repositories e Models, organizados por conceito de negócio e não por camada técnica.
Essa escolha é comum em sistemas que preveem crescimento de complexidade de domínio: um "envelope" de assinatura, um "signatário" e um "documento" são conceitos com regras próprias, ciclos de vida próprios e, eventualmente, times ou responsabilidades próprias. Modularizar por domínio evita que um único DocumentController vire um Frankenstein de 2000 linhas conforme o produto cresce. A estrutura MVC tradicional do Laravel (app/Http, app/Mail, app/Jobs) continua existindo ao lado — ela cuida da camada de entrada (rotas, requests, mailables, filas), enquanto os módulos concentram a lógica de negócio.
A persistência roda sobre MySQL, com Redis + Horizon disponíveis para processamento assíncrono em produção — peça central, como veremos, porque quase nada nesse sistema acontece de forma síncrona: notificação, geolocalização, geração de PDF e certificação digital são todos trabalhos de fila.
Desafio 1: notificar o signatário certo, na hora certa, com prova de que ele recebeu
Um dos requisitos implícitos de qualquer produto de assinatura digital é jurídico, não técnico: é preciso provar que o signatário foi notificado. Isso empurra decisões de arquitetura que não apareceriam em um sistema de e-mail transacional comum.
Isolamento por signatário. Ao criar uma solicitação de assinatura, cada signatário recebe seu próprio registro em document_signature_requests, com um token único que compõe seu link individual. Não existe link compartilhado entre signatários — decisão que evita um problema clássico desse tipo de produto: um signatário abrir o link de outro (ou reenviar por engano) e assinar em nome de quem não deveria, ou visualizar dados de um contrato que não é seu.
Assincronia como regra, não exceção. O disparo do e-mail é feito por um Job (NotifySignatory) com ShouldQueue e 3 tentativas, chamando Mail::to($signatory->email)->send(...). Isso significa que a criação da solicitação de assinatura nunca fica bloqueada esperando o SMTP responder — e que falhas transitórias de envio (timeout, rate limit do provedor) são absorvidas pelo mecanismo de retry da fila em vez de virarem erro 500 para o usuário.
Rastreamento de estado de notificação. O envio grava notified_at, e a abertura do e-mail é rastreada via email_opened_at (tipicamente por pixel de rastreamento embutido no template Blade Markdown). Isso dá ao requisitante visibilidade de "enviei, mas ele não abriu ainda" — informação valiosa tanto para UX (permitir reenvio) quanto para eventual disputa sobre se a notificação foi entregue.
Modo sequencial. Quando a assinatura precisa seguir uma ordem (comum em contratos com hierarquia — primeiro o contratado, depois o contratante, por exemplo), o próximo signatário só é notificado depois que o anterior assina, via NotifyNextSignatory. Isso transforma a fila de e-mails em uma máquina de estados orientada a eventos: cada assinatura completada dispara a notificação seguinte, em vez de todos os signatários serem notificados de uma vez.
2FA como parte do fluxo de e-mail, não um recurso à parte. Ao acessar o link, o signatário ainda passa por um código OTP enviado por e-mail antes de poder assinar. Isso adiciona uma segunda "prova de posse" da caixa de e-mail — mitigando o risco de um link vazado (encaminhado, capturado em log, etc.) ser suficiente para assinar em nome de outra pessoa.
O desafio de engenharia aqui não é o envio de e-mail em si — é orquestrar um fluxo de estado distribuído entre Jobs, com garantias de ordem (no modo sequencial) e auditabilidade de cada transição (notificado → aberto → autenticado via OTP → assinado), tudo isso sem acoplar a lógica de negócio ao transporte de e-mail.
Desafio 2: capturar uma assinatura que tenha valor probatório
A parte mais visível do produto — o desenho da assinatura — é também a que carrega o maior risco jurídico se malfeita.
Captura no cliente, dois caminhos. O usuário desenha a assinatura em um <canvas> via signature_pad.js, convertida para base64 e enviada ao backend. Para quem prefere não desenhar, existe alternativa: gerar automaticamente uma assinatura estilizada (nome em fonte cursiva) usando Intervention/Image no servidor. Ter os dois caminhos é uma decisão de UX importante — nem todo signatário tem controle fino de mouse/touch para desenhar algo que pareça uma assinatura, e forçar isso derruba conversão.
Sendo honesto sobre o nível de garantia de identidade. Um ponto que vale destacar tecnicamente: o método de autenticação registrado (authentication_method) é e-mail/link + OTP — não há biometria, nem certificado ICP-Brasil A1/A3 de terceiros, nem HSM envolvido na verificação de identidade do signatário. Isso posiciona o produto na categoria de "assinatura eletrônica simples/avançada" (no sentido da legislação brasileira, MP 2.200-2/ICP-Brasil e Lei 14.063/2020), e não "assinatura qualificada". É uma decisão de produto legítima — grande parte dos casos de uso (contratos comerciais, termos, NDAs) não exige o nível de garantia de uma assinatura qualificada — mas é uma decisão que precisa ser compensada com trilha de auditoria robusta, porque é essa trilha que sustenta a validade jurídica do documento em caso de contestação.
A trilha de auditoria é o verdadeiro produto. No momento da assinatura, o sistema coleta: IP do signatário, geolocalização por IP (via API externa api.ipgeolocation.io — cidade, latitude/longitude, ISP), user agent, URL completa da requisição, e um registro explícito de consentimento (document_signature_agreements, com IP, user agent e versão do termo aceito). Tudo persiste em document_signatures e document_signature_contexts.
Do ponto de vista de arquitetura, separar document_signatures (o evento de assinatura em si) de document_signature_contexts (o contexto forense ao redor dela) é uma boa decisão de modelagem: permite evoluir o que é capturado como contexto (novos provedores de geolocalização, novos metadados de dispositivo) sem tocar na tabela que representa a assinatura como fato jurídico. Também facilita LGPD — dados de geolocalização e IP são dados pessoais, e tê-los isolados em uma tabela própria simplifica políticas de retenção/expurgo no futuro, caso necessário.
O desafio real aqui não é técnico no sentido de "difícil de implementar" — é decidir o que capturar e como estruturar isso para que sobreviva a uma disputa judicial anos depois, sem se tornar um passivo de LGPD no processo.
Desafio 3: gerar um PDF final que seja prova, não apenas imagem
Esta é a etapa mais tecnicamente densa do sistema, e onde a maior parte do "valor de engenharia" está concentrada.
Orquestração assíncrona. Quando todos os signatários concluem, um Job (SignatureProcessDocumentHandler → DocumentSign) assume o processamento. Rodar isso em fila (não em request síncrono) é a decisão certa: manipulação de PDF, certificação criptográfica e chamadas a um servidor de timestamp externo são operações que podem levar segundos e não devem bloquear a resposta HTTP nem arriscar timeout.
Composição visual do PDF. O Job desenha as imagens de assinatura/rubrica/texto em cada campo do documento via SetaPDF-Core, insere um identificador único por página (mitigando substituição de páginas), e faz merge com um manifesto de auditoria — uma página extra anexada ao PDF contendo o histórico completo do processo de assinatura e um QR code para verificação de autenticidade. Essa página de manifesto é o que transforma um PDF assinado em um documento auto-descritivo: qualquer pessoa que receba o arquivo, sem acesso ao sistema, consegue ver quem assinou, quando, e verificar a autenticidade escaneando o QR code.
Certificação digital de verdade, não cosmética. Esta é a decisão arquitetural mais importante do sistema: o PDF é certificado com SetaPDF-Signer, implementando PAdES/CMS — um padrão real de assinatura digital de documentos, não apenas uma imagem carimbada no PDF. É uma biblioteca comercial embarcada no backend (solução própria), o que é uma escolha estrutural relevante: em vez de terceirizar a certificação para um gateway externo (modelo DocuSign/Clicksign, onde a assinatura "sai" da sua infraestrutura), a certificação acontece dentro do próprio sistema, usando um certificado da própria plataforma (não do signatário individual).
Isso tem implicações nos dois sentidos:
- Vantagem: menos dependência de terceiros, controle total do pipeline, possivelmente menor custo por documento em escala, e nenhum vazamento de dados do documento para um provedor externo de assinatura.
- Trade-off: a responsabilidade de manter a biblioteca de certificação atualizada, o certificado válido e renovado, e a conformidade com os padrões PAdES é inteiramente da equipe — não há um fornecedor de assinatura absorvendo esse risco.
O nível de certificação escolhido é "no changes allowed", que bloqueia qualquer edição futura do PDF sem invalidar a assinatura — uma garantia técnica de integridade equivalente ao que se espera de um documento assinado fisicamente e depois notarizado.
Carimbo de tempo (timestamp) via RFC 3161. O sistema usa um servidor de timestamp (FreeTSA ou o servidor da Imprensa Oficial) para provar quando a assinatura ocorreu, de forma independente do relógio do próprio servidor da aplicação. Isso é criticamente importante: sem timestamp de terceiro confiável, a data de assinatura é apenas "o que o banco de dados diz" — algo teoricamente alterável por quem tem acesso administrativo ao sistema. Com RFC 3161, a prova de tempo vem de uma autoridade externa, fechando essa brecha.
Hash e versionamento como prova de integridade. Um hash SHA-256 do documento final assinado é gerado e armazenado — permitindo que qualquer verificação futura confirme que o arquivo não foi alterado byte a byte desde a assinatura. O arquivo vai para um disco de storage dedicado via Laravel Filesystem (Disks::ARCHIVE_SIGNED_DOCUMENTS), com suporte tanto a disco local quanto S3, sendo salvo como uma nova versão do documento (DocumentStorageManager::createVersionSigned()) — preservando o histórico de versões em vez de sobrescrever o original. O registro do documento é então atualizado para status SIGNED, com hash e signed_at.
O padrão que amarra tudo
Olhando o sistema como um todo, um padrão se repete nas três frentes: cada etapa sensível gera sua própria prova, e essa prova é desacoplada do dado operacional que a originou.
- O e-mail não é só "enviado" — vira
notified_at+email_opened_at, rastreável. - A assinatura não é só "um desenho" — vira contexto forense em uma tabela própria.
- O PDF não é só "gerado" — vira um documento certificado com PAdES, timestamp RFC 3161 e hash SHA-256.
Essa disciplina de "todo evento de negócio produz um artefato de prova verificável independentemente" é o que diferencia um sistema de assinatura eletrônica sério de um que apenas parece um. E é também onde mora a maior parte da complexidade real do projeto: não em CRUD de documentos ou envio de e-mail, mas em orquestrar Jobs assíncronos, bibliotecas criptográficas de terceiros e serviços externos (geolocalização, timestamp) de forma que o resultado final resista a escrutínio jurídico.
Desafios em aberto / pontos de atenção
Vale mencionar honestamente alguns pontos que qualquer arquitetura como essa carrega como débito ou risco a monitorar:
- Dependência de serviços externos na hora crítica. A certificação depende de um servidor RFC3161 (FreeTSA, gratuito, sem SLA formal) e a geolocalização de uma API externa. Falhas nesses serviços podem travar o pipeline de assinatura — vale ter fallback ou retry robusto no Job de certificação.
- Gestão do certificado da plataforma. Como a certificação usa um certificado próprio da plataforma (não do signatário), a segurança de toda a cadeia de confiança do sistema depende de como esse certificado é armazenado, rotacionado e protegido contra vazamento.
- Nível de garantia de identidade vs. expectativa do usuário. Vale deixar claro para o cliente final que o produto oferece assinatura eletrônica simples/avançada (e-mail + OTP), não qualificada — para evitar expectativa equivocada em casos de uso que exigiriam ICP-Brasil.
- Volume em fila. Com Redis/Horizon estrategicamente posicionado para produção, o monitoramento de filas (falhas, retries, tempo de processamento do
DocumentSign) se torna operacionalmente crítico — é o gargalo natural do sistema conforme o volume de documentos cresce.
No fim, é um bom exemplo de como um problema aparentemente simples de produto ("assine este documento") esconde, na implementação séria, uma quantidade considerável de engenharia de confiabilidade, criptografia aplicada e modelagem de dados voltada para auditabilidade jurídica.