Firebase Dynamic Links dejó de funcionar el 25 de agosto de 2025. Si tu app abría pantallas concretas desde un correo, un SMS o una campaña con enlaces page.link, esos enlaces ya no resuelven, y el reemplazo no es un botón que activas: tienes que montar App Links en Android y Universal Links en iOS por tu cuenta, y para una parte del problema (saber a qué contenido venía un usuario que aún no tenía la app) vas a necesitar un tercero. Esta guía es el mapa que seguimos cuando migramos una app desde Dynamic Links.

Qué murió y qué te dejó a medias

Dynamic Links hacía dos cosas en un solo paquete. La primera, abrir una pantalla concreta cuando el usuario ya tenía la app instalada. La segunda, y la que duele perder, era el deferred deep linking: si el usuario no tenía la app, Dynamic Links lo mandaba a la store, y después de instalar y abrir, la app sabía a qué contenido quería llegar. Ese segundo comportamiento no se resuelve con estándares del sistema operativo, y volvemos a él más abajo.

El resto del trabajo, abrir https://tudominio.com/producto/42 directo en la pantalla del producto, sí lo cubren App Links y Universal Links, que son gratis y no dependen de ningún proveedor. La contrapartida es que la configuración vive en tu dominio y en los ficheros de la app, y falla de formas silenciosas.

Tres cosas distintas que llamas igual

Antes del código conviene separar tres mecanismos que se mezclan en la palabra "deep link":

  • Custom URL scheme (miapp://producto/42): lo más viejo y lo más frágil. Cualquier app puede registrar el mismo scheme, así que otra app puede secuestrar tus enlaces. Vale para navegación interna o pruebas, no para enlaces que mandas a un usuario.
  • App Links (Android): un enlace https:// normal que Android abre en tu app en lugar del navegador, siempre que verifiques la propiedad del dominio con un fichero assetlinks.json.
  • Universal Links (iOS): el equivalente de Apple, con un fichero apple-app-site-association y el permiso Associated Domains.

App Links y Universal Links usan URLs https de verdad. Si la app no está instalada, el enlace abre la web sin errores. Esa degradación limpia es la razón por la que son el estándar, y el scheme propio queda para casos internos.

Para una app nueva, la combinación que recomendamos es go_router para el enrutado y app_links para recibir el enlace que abrió la app. go_router ya integra el deep linking del sistema, así que en muchos casos no tocas app_links directo; lo añades cuando necesitas leer el enlace inicial o reaccionar a enlaces en caliente con lógica propia.

Android: assetlinks.json y autoVerify

Publica el fichero en https://tudominio.com/.well-known/assetlinks.json, servido por HTTPS y sin redirecciones:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.tudominio.app",
    "sha256_cert_fingerprints": ["AB:CD:EF:..."]
  }
}]

El sha256_cert_fingerprints es la huella del certificado con el que firmas. Si firmas con Play App Signing, la huella que va aquí es la de Google, no la de tu keystore de subida; confundirlas es el motivo número uno de que los enlaces abran el navegador en producción aunque funcionen en debug.

En el AndroidManifest.xml, el intent-filter con autoVerify:

<intent-filter android:autoVerify="true">
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="https" android:host="tudominio.com" />
</intent-filter>

iOS: AASA y Associated Domains

El fichero apple-app-site-association va en https://tudominio.com/.well-known/, sin extensión y servido como application/json:

{
  "applinks": {
    "details": [{
      "appIDs": ["TEAMID.com.tudominio.app"],
      "components": [{ "/": "/producto/*" }]
    }]
  }
}

En Xcode añades la capability Associated Domains con la entrada applinks:tudominio.com. El TEAMID es tu Team ID de Apple Developer; un Team ID mal copiado da el mismo síntoma que en Android: el enlace abre Safari y no la app.

El pegamento en Dart

Con go_router, defines las rutas que corresponden a los paths de tus enlaces:

final router = GoRouter(
  routes: [
    GoRoute(
      path: '/producto/:id',
      builder: (context, state) =>
          ProductoScreen(id: state.pathParameters['id']!),
    ),
  ],
);

Si necesitas el enlace que arrancó la app en frío, o escuchar enlaces mientras está abierta, app_links te lo da:

final appLinks = AppLinks();
final initialUri = await appLinks.getInitialLink();
appLinks.uriLinkStream.listen((uri) {
  router.go(uri.path);
});

En Android hay que activar el flag flutter_deeplinking_enabled en el manifest para que go_router gestione los enlaces; sin él, el enlace llega a la app pero no navega. Es un detalle de una línea que cuesta una tarde encontrar.

El hueco que queda: deferred deep linking

App Links y Universal Links solo funcionan si la app ya está instalada. Cuando el usuario no la tiene, el enlace abre la web, y la web puede mandarlo a la store, pero después de instalar, la app arranca en blanco: no hay forma estándar de que sepa a qué producto o promoción venía. Ese es justo el trozo de Dynamic Links que Google apagó y no sustituyó.

No hay solución nativa buena. Las opciones reales:

  • Un proveedor de atribución (Branch, Adjust, AppsFlyer, Airbridge o Singular, entre los que Google recomienda). Resuelven el deferred deep linking y además te dan medición de campañas. Cuestan dinero y meten un SDK más, con todo lo que eso implica en permisos y privacidad.
  • Reconstruir el contexto sin atribución: pasar un código en la propia URL de la store, o pedir al usuario que pegue un código al abrir. Vale para casos concretos y no arrastra un SDK, pero la experiencia es peor y no cubre atribución de marketing.
  • Asumir que no lo necesitas. Muchos equipos montaron Dynamic Links por defecto y el deferred deep linking apenas se usaba. Si tus enlaces van a usuarios que ya tienen la app (recuperación de sesión, enlaces internos, notificaciones), no necesitas nada de esto.

La pregunta que le hacemos a un cliente antes de recomendar un proveedor de pago es corta: ¿mides captación por campaña y necesitas atribuir instalaciones a un enlace concreto? Si la respuesta es sí, un Branch o un AppsFlyer se paga solo. Si es no, meter uno es cargar la app con un SDK de tracking para un problema que no tienes.

Antes de subir: verificación

La configuración de deep links falla en producción de formas que no ves en tu máquina. Repasa esto con el build real, junto con el resto de tu checklist de publicación en las tiendas:

  1. assetlinks.json y apple-app-site-association accesibles por HTTPS, sin redirección, con el content-type correcto y sin caché agresiva de la CDN (una AASA cacheada vieja es un clásico).
  2. Huella SHA-256 de Play App Signing en assetlinks.json, no la de tu keystore local.
  3. Team ID correcto en la AASA.
  4. autoVerify en el intent-filter y flutter_deeplinking_enabled activo.
  5. Probado desde fuera de la app: un enlace en Notas o en un correo, no escrito en la barra del navegador, que a veces se comporta distinto.
  6. Probado tras reinstalar, no solo en hot restart.

Qué haríamos nosotros

Si vienes de Dynamic Links y tus enlaces son para usuarios que ya tienen la app, monta App Links y Universal Links con go_router y app_links y cierra el tema: es gratis y no depende de nadie. Si tu negocio vive de campañas de captación y necesitas saber de qué anuncio salió cada instalación, añade un proveedor de atribución desde el principio y trátalo como lo que es, infraestructura de marketing, no una librería técnica que metes sin pensar en el coste de privacidad. Lo que no tiene sentido es replicar todo Dynamic Links por si acaso: la mayoría de apps que lo tenían usaban una fracción de lo que ofrecía.

Si estás en esa migración y no tienes claro qué parte necesitas de verdad, es justo el tipo de decisión que ayudamos a tomar en un discovery corto antes de tocar código.