DEV Community

Cristian Dornelles
Cristian Dornelles

Posted on

App Links e Universal Links: Deep Links em Produção (Parte 5)

Header

Custom schemes funcionam bem para desenvolvimento, mas para produção você precisa de HTTPS. É aqui que entram App Links no Android e Universal Links no iOS — e onde a maioria dos problemas reais aparece.

Este é o quinto conteúdo de uma série completa sobre Deep Links no Flutter. Se você ainda não viu os posts anteriores: Post 1 | Post 2 | Post 3 | Post 4.


O problema com custom schemes

Até aqui usamos fitconnect:// para testes. Funciona, mas em produção esse scheme tem limitações sérias:

fitconnect://fitconnect.app/signup?referralCode=TRAINER1234567890123
Enter fullscreen mode Exit fullscreen mode
  • Qualquer app pode registrar o mesmo scheme — sem nenhuma verificação.
  • Se o usuário não tem o app instalado, o sistema exibe um erro genérico.
  • Não há como garantir que o link chegue ao app correto.
https://deeplinkslab.dev/signup?referralCode=TRAINER1234567890123
Enter fullscreen mode Exit fullscreen mode
  • ✅ Verificação server-side: o sistema operacional confirma com o servidor antes de abrir o app.
  • ✅ Se o app não estiver instalado, o link abre no navegador como fallback natural.
  • ✅ Profissional e seguro.

A diferença não é só cosmética. É uma questão de confiança — tanto do sistema quanto do usuário.


Do domínio fictício ao domínio real

Até aqui, usamos fitconnect.app como domínio fictício para focar na implementação.

Mas agora entramos na parte onde deep links deixam de ser apenas código — e passam a depender de infraestrutura real.

App Links e Universal Links só funcionam com um domínio válido, acessível publicamente e com verificação bidirecional.

Para isso, vamos usar um domínio real dedicado a testes:

deeplinkslab.dev

A partir deste ponto, tudo que implementarmos será o mesmo que você precisa em produção.

Você não precisa usar este domínio — pode (e deveria) usar o seu próprio. Um domínio .dev custa em torno de R$ 60–80 por ano e já vem com HTTPS obrigatório, o que elimina uma configuração a menos. Qualquer registrador funciona: Registro.br, Namecheap, Cloudflare Registrar ou Squarespace Domains.

Se quiser um guia específico — do registro do domínio até o deploy no Cloudflare Pages com os arquivos de verificação prontos — me avisa nos comentários. Se tiver demanda, faço um post dedicado só sobre isso.


Android: configurando o assetlinks.json

O assetlinks.json é o arquivo que o Android baixa do seu servidor para confirmar: "esse app tem permissão para tratar links desse domínio?". Sem ele, o android:autoVerify="true" que adicionamos no Post 2 não tem como concluir a verificação — e o link pode abrir um seletor em vez do app.

Obtendo o SHA256 do certificado

O arquivo assetlinks.json precisa conter o SHA256 do certificado com que o app foi assinado. Para o keystore de debug:

keytool -list -v \
  -keystore ~/.android/debug.keystore \
  -alias androiddebugkey \
  -storepass android \
  -keypass android | grep "SHA256:"
Enter fullscreen mode Exit fullscreen mode

Para o keystore de release, substitua o caminho e as credenciais pelo seu keystore de produção. Debug e release geram SHA256 diferentes — o arquivo de produção precisa conter o fingerprint do certificado de release. Durante o desenvolvimento, o fingerprint de debug é suficiente para testar localmente.

Se você usa o Google Play App Signing, o SHA256 correto para produção está em: Play Console → Configuração → Integridade do app → Certificado de assinatura do app.

O arquivo assetlinks.json

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.fitconnect.app",
      "sha256_cert_fingerprints": [
        "SEU_SHA256"
      ]
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

A relação delegate_permission/common.handle_all_urls é o valor padrão e obrigatório — informa ao Android que o app tem permissão para tratar qualquer URL do domínio declarado.

Deploy

O arquivo deve estar acessível publicamente em:

https://deeplinkslab.dev/.well-known/assetlinks.json
Enter fullscreen mode Exit fullscreen mode

Dois requisitos que costumam causar problemas em produção:

  • O servidor precisa responder com Content-Type: application/json.
  • O arquivo deve ser acessível via HTTPS sem redirecionamentos — o Android não segue redirects ao verificar o arquivo.

Se qualquer um desses requisitos falhar, o Android simplesmente ignora o arquivo — e o link passa a abrir no navegador sem nenhum erro visível.


iOS: configurando o apple-app-site-association

No iOS, o equivalente é o apple-app-site-association (sem extensão). O iOS pode manter esse arquivo em cache.

Se algo não funcionar durante o desenvolvimento, reinstalar o app geralmente força uma nova verificação.

O arquivo apple-app-site-association

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAM_ID.com.fitconnect.app",
        "paths": ["/signup", "/signup/*"]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

O campo apps deve ser um array vazio — é uma exigência da Apple, mesmo que pareça redundante. O appID combina o Team ID com o bundle ID. O array paths lista os caminhos que o app está autorizado a tratar; /signup/* cobre rotas com subpaths ou query strings.

Obtendo o Team ID

  1. Acesse developer.apple.com/account.
  2. Vá em Membership → o Team ID aparece na lista de informações da conta.

Deploy

https://deeplinkslab.dev/.well-known/apple-app-site-association
Enter fullscreen mode Exit fullscreen mode

O arquivo não deve ter extensão. Sirva-o com Content-Type: application/json e sem redirecionamentos — os mesmos requisitos do Android. O iOS 14+ usa uma CDN da Apple para cachear o arquivo, por isso mudanças podem demorar a ser refletidas; reinstalar o app força a re-verificação durante o desenvolvimento.


O que é essa verificação bidirecional?

A lógica é a mesma nas duas plataformas: o sistema operacional só abre o app se as duas pontas se reconhecerem.

O app declara, no AndroidManifest.xml e no Runner.entitlements, quais domínios ele se propõe a tratar. O servidor, por sua vez, confirma no assetlinks.json e no apple-app-site-association quais aplicativos têm permissão para tratar URLs do domínio.

Se qualquer lado estiver faltando ou inconsistente — fingerprint errado, arquivo inacessível, caminho não listado — o link abre no navegador como fallback, sem mensagem de erro para o usuário. Isso protege contra um vetor real: sem verificação bidirecional, um aplicativo malicioso poderia registrar o mesmo scheme ou domínio e interceptar links destinados ao seu app.


Validando antes de fazer deploy

Android: Google Digital Asset Links

A ferramenta oficial do Google valida o assetlinks.json sem precisar instalar o app:

https://developers.google.com/digital-asset-links/tools/generator
Enter fullscreen mode Exit fullscreen mode

Informe o domínio e o package name — a ferramenta mostra se o arquivo está acessível, se o JSON é válido e se o fingerprint está correto.

Também é possível verificar via adb em um device com o app instalado. No Android 12+, o fluxo recomendado é resetar o estado, forçar a reverificação e só então consultar o resultado:

# Resetar o estado de verificação
adb shell pm set-app-links --package com.fitconnect.app 0 all

# Forçar reverificação
adb shell pm verify-app-links --re-verify com.fitconnect.app

# Consultar o resultado
adb shell pm get-app-links com.fitconnect.app
Enter fullscreen mode Exit fullscreen mode

O status verified confirma que a verificação foi concluída com sucesso. Pular o reset pode fazer o comando retornar um estado antigo em cache — não necessariamente o resultado da configuração atual.

iOS: inspecionando o arquivo diretamente

curl https://deeplinkslab.dev/.well-known/apple-app-site-association
Enter fullscreen mode Exit fullscreen mode

Se o curl retornar o JSON correto, o arquivo está acessível. Para validar o comportamento completo, a Apple recomenda testar em um device físico — o simulador não reproduz a verificação de Associated Domains com fidelidade.


Checklist de produção

Antes de fazer deploy, confirme cada item:

Android:

  • [ ] SHA256 obtido para o keystore de debug (testes) e release (produção)
  • [ ] assetlinks.json com package name e fingerprint corretos
  • [ ] Arquivo acessível em https://deeplinkslab.dev/.well-known/assetlinks.json
  • [ ] Servidor respondendo com Content-Type: application/json
  • [ ] Sem redirecionamentos no caminho /.well-known/assetlinks.json
  • [ ] Validado com a ferramenta Google Digital Asset Links

iOS:

  • [ ] Team ID obtido no Apple Developer Portal
  • [ ] apple-app-site-association com appID e paths corretos
  • [ ] Arquivo acessível em https://deeplinkslab.dev/.well-known/apple-app-site-association
  • [ ] Associated Domains configurado no Xcode (applinks:deeplinkslab.dev)
  • [ ] Testado em device físico (não apenas simulador)

O que construímos até aqui

Ao final desta etapa, você já tem:

  • O assetlinks.json configurado com o SHA256 correto para Android.
  • O apple-app-site-association com Team ID e paths autorizados para iOS.
  • Clareza sobre como funciona a verificação bidirecional e por que ela existe.
  • Ferramentas e comandos para validar os dois arquivos antes de ir a produção.

Este é o quinto de 9 posts da série. Com Android, iOS e Flutter integrados e a verificação de domínio no lugar, o fluxo básico de deep links está completo — mas ainda só funciona quando o usuário já tem o app instalado. No próximo post, resolvemos o caso mais difícil: o que acontece quando o usuário clica no link e ainda não tem o app.

No próximo post: Deferred Deep Links — como salvar o contexto do link e recuperar após a instalação.


Código completo disponível no repositório: FitConnect no GitHub

Este artigo é uma crosspost do Medium — leia lá também e deixa um clap se curtiu.

Já subiu App Links em produção? Qual foi o erro mais difícil de debugar? Conta nos comentários.

Top comments (0)