# Serving assetlinks.json from the wrong path

> A dev.to report finds a Next.js Trusted Web Activity serving assetlinks.json from /api/.well-known, a path Android does not read, and traces the fallback.

On 2026-10-06 the dev.to account daniel_pertu published a build report about
CogniPrep, which the report describes as a Next.js app for practising employer
assessments, and the subject of the report is that the file proving the app owns
its domain is served from the wrong URL. The report states that
`cogniprep.app/.well-known/assetlinks.json` returns 404 and that
`cogniprep.app/api/.well-known/assetlinks.json` returns 200 with
`content-type: application/json` and the real contents, so the JSON itself is
correct and only its address is wrong
([the report](https://dev.to/daniel_pertu/our-android-verification-file-is-served-from-api-and-android-only-ever-looks-in-well-known-467p)).

## What the report found

The report identifies the missing root-path file as a configuration that would
cause verification to fail, and also examines the app's manifest and how the
Android build is declared to mobile browsers. Each finding is taken
in turn below, in the terms the report and the platform documentation it concerns
actually use
([the report](https://dev.to/daniel_pertu/our-android-verification-file-is-served-from-api-and-android-only-ever-looks-in-well-known-467p)).

### The one path Android fetches

The report states that CogniPrep ships as a website first and that there is an
Android wrapper built as a Trusted Web Activity, which it describes as Chrome's
way of putting a web app in the Play Store without a WebView of your own. It
states that a Trusted Web Activity runs the site fullscreen with no address bar,
but only if it can prove that the Android package and the domain belong to the
same owner, and that the proof is Digital Asset Links: one JSON file, fetched
over HTTPS, from exactly one path, which it gives as
`https://<your-domain>/.well-known/assetlinks.json`
([the report](https://dev.to/daniel_pertu/our-android-verification-file-is-served-from-api-and-android-only-ever-looks-in-well-known-467p)).

Google's own Digital Asset Links documentation states the same location in
stricter terms. It describes the protocol and API as enabling an app or website
to make public, verifiable statements about other apps or websites, and says
statements are contained in a statement list that is cleartext and publicly
accessible, in a location that is controlled by the principal and difficult to
spoof or tamper with. In its usage example the website publishes a statement
list at `https://www.example.com/.well-known/assetlinks.json`, and the page
states that this is the official name and location for a statement list on a
site, and that statement lists in any other location, or with any other name,
are not valid for that site. The example statement grants the Android app
permission to open links on the site, with the relation
`delegate_permission/common.handle_all_urls` and a target naming the
`android_app` namespace, a `package_name`, and `sha256_cert_fingerprints`, which
the page describes as the SHA256 fingerprints of the app's signing certificate
([Google, Digital Asset Links getting started](https://developers.google.com/digital-asset-links/v1/getting-started)).

The route handler the report prints matches that shape. It returns a literal with
`relation: ['delegate_permission/common.handle_all_urls']` and a target giving
`namespace: 'android_app'`, `package_name: 'app.cogniprep.twa'`, and a
`sha256_cert_fingerprints` array, and the report's own verdict on it is right
relation, right namespace, right package, a real signing certificate
fingerprint, served from the wrong URL
([the report](https://dev.to/daniel_pertu/our-android-verification-file-is-served-from-api-and-android-only-ever-looks-in-well-known-467p)).

### Why the file ended up under /api

The report states that the file lives at
`app/api/.well-known/assetlinks.json/route.ts`, that App Router maps directories
to URL segments, and that it is perfectly happy with directory names containing
dots, so `.well-known` is a segment, `assetlinks.json` is a segment, and the
resulting route is `/api/.well-known/assetlinks.json`. Its conclusion is that
nothing is broken and Next.js did exactly what the folder structure asked for
([the report](https://dev.to/daniel_pertu/our-android-verification-file-is-served-from-api-and-android-only-ever-looks-in-well-known-467p)).

The report locates the mistake in the `api/` at the front of that path. It states
that every other server endpoint in the app lives under `app/api/`, because that
is where endpoints go, and that a file returning JSON from a `GET` handler reads
like an endpoint but is not one: it is a static well-known resource that happens
to be generated by code, and the thing consuming it is not the frontend. The
report states that Android's verification service has no configuration, no base
path, and no override, and that it fetches `/.well-known/assetlinks.json` or it
fails
([the report](https://dev.to/daniel_pertu/our-android-verification-file-is-served-from-api-and-android-only-ever-looks-in-well-known-467p)).

The fixes are each described as one change. The first is to move the
folder to `app/.well-known/assetlinks.json/route.ts`, on the grounds that the
route path is the folder path. The second is to leave the handler where it is and
add a `rewrites()` entry in `next.config.mjs` mapping
`/.well-known/assetlinks.json` to `/api/.well-known/assetlinks.json`. The report
prefers the move, and its stated reason is that a rewrite is a second place that
has to stay true and that there is nothing API-shaped about the file
([the report](https://dev.to/daniel_pertu/our-android-verification-file-is-served-from-api-and-android-only-ever-looks-in-well-known-467p)).

### Why nothing in the build complained

The report states that a missing `assetlinks.json` does not throw, does not log,
and does not 500, that the web app is completely unaffected, and that every
browser visitor gets the same site they always did. It states that the only
symptom lives on an Android device, where a Trusted Web Activity that fails
verification does not crash but degrades, opening the site in a Custom Tab with
the URL bar visible, so the app still works and stops looking like an app
([the report](https://dev.to/daniel_pertu/our-android-verification-file-is-served-from-api-and-android-only-ever-looks-in-well-known-467p)).

Chrome's Trusted Web Activity quick start documents that fallback as the designed
behaviour rather than a quirk of this app. It states that a Trusted Web Activity
can launch a full screen browser tab without any browser UI, that the capability
is restricted to websites you own, and that ownership is proven by setting up
Digital Asset Links. It states that when a Trusted Web Activity is launched the
browser verifies the Digital Asset Links, and that if verification fails the
browser falls back to displaying the website as a Custom Tab, which it elsewhere
describes as launching the website as a Custom Tab with browser UI at the top of
the page. The same page instructs that after creating the `assetlinks.json` file
it be uploaded to the website at `.well-known/assetlinks.json` relative to the
root, and names using the wrong signature as a common cause of verification
failing
([Chrome, Trusted Web Activity quick start](https://developer.chrome.com/docs/android/trusted-web-activity/quick-start)).

The report adds that the app's own crawl rules hide the working copy. It states
that `app/robots.ts` emits `Disallow: /api/` and `Disallow: /*.json`, so the one
URL that does serve the file is excluded twice over, by prefix and by extension,
and that there are 740 `Disallow` lines in the emitted file. It states that
Android does not read robots.txt, so this does not cause the failure, but that no
crawl of the site will surface the working copy or notice the missing one. It
also identifies a stale rule, `Disallow: /manifest.ts`, which it says blocks
nothing at all, because `app/manifest.ts` is a Next.js file convention and the
URL it produces is `/manifest.webmanifest`, and a rule written against the source
filename cannot match a request path
([the report](https://dev.to/daniel_pertu/our-android-verification-file-is-served-from-api-and-android-only-ever-looks-in-well-known-467p)).

### More findings in the manifest

The report prints the app's icon array, which declares `/logo-light.png` at
`512x512` and `image/png` twice, once with `"purpose": "any"` and once with
`"purpose": "maskable"`. It states that `any` is drawn as-is, that `maskable` is
drawn into whatever shape the launcher wants and needs roughly 20 percent of
padding on every side to survive the crop, and that reusing one square logo for
both means the maskable path is cropping artwork that was never laid out for it.
Its verdict is that listing it is not wrong, but that it is a claim about the
image rather than a property of the manifest, and that nothing validates it
([the report](https://dev.to/daniel_pertu/our-android-verification-file-is-served-from-api-and-android-only-ever-looks-in-well-known-467p)).

MDN documents the member that claim is made through. It describes `purpose` as a
case-sensitive keyword string that specifies one or more contexts in which the
icon can be used by the browser or operating system, states that browsers use
these values as hints to determine where and how an icon is displayed, and
defines `maskable` as indicating that the icon is designed with icon masks and
safe zone in mind, such that any part of the image outside the safe zone can be
ignored and masked away. It defines `any` as indicating that the icon can be used
in any context, and states that this is the default value
([MDN, icons](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/icons)).

The second manifest finding is the name. The report states that the manifest says
`CogniPrep - Arctic Shores Practice` and describes "all 14 Arctic Shores
psychometric games", that this was true when it was written, and that the app now
covers dozens of assessment providers. It states that the installed app's home
screen label is a string that nothing renders in the website, so looking at the
site will never show that it drifted. It also states that there is no
`related_applications` entry, so a mobile browser visiting the site has no way to
know the Android build exists
([the report](https://dev.to/daniel_pertu/our-android-verification-file-is-served-from-api-and-android-only-ever-looks-in-well-known-467p)).

MDN describes `related_applications` as the manifest member used to specify one
or more applications that are related to a web application, which may be
platform-specific applications or Progressive Web Apps, and states that it
enables the use of web APIs like `Navigator.getInstalledRelatedApps()` to check
whether a platform-specific version of the web app, or the web app itself, is
already installed on the device. MDN lists `play` among the example `platform`
values and marks the member experimental
([MDN, related_applications](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/related_applications)).

## Why it matters for PWA developers

The report names a shape shared by its findings: a value that is
only ever consumed by something outside the browser. It states that the
verification file is read by Android, that the manifest `name` is read by a
launcher, and that the `maskable` purpose is honoured by an icon mask the
developer does not control, and that none of them appear on a page, none fail a
type check, and none are exercised by a test that renders the app. Its stated
consequence is that a route handler returning a 200 with correct JSON feels
finished, and that the only way to find out it is finished at the wrong address
is to fetch the address the consumer actually uses, for which it gives
`curl https://yourdomain.com/.well-known/assetlinks.json`
([the report](https://dev.to/daniel_pertu/our-android-verification-file-is-served-from-api-and-android-only-ever-looks-in-well-known-467p)).

The platform documentation makes the same point from the consumer's side. Google
states that the statement list derives its value from being in a location
controlled by the principal: the protocol does not authenticate the principal
making the statement, but the statement is located in a specific location
strongly associated with the principal and under the principal's control. A path
the principal chose for its own convenience is not that location, and Google's
page states outright that a statement list in any other location, or with any
other name, is not valid for the site
([Google, Digital Asset Links getting started](https://developers.google.com/digital-asset-links/v1/getting-started)).

Chrome's quick start supplies the diagnostic that distinguishes the two ways a
Trusted Web Activity can fail. It states that a Trusted Web Activity will try to
adhere to the user's default choice of browser, that if the default browser
supports Trusted Web Activities it will be launched, that failing that any
installed browser supporting them will be chosen, and that the default behaviour
is to fall back to a Custom Tabs mode. For that reason it instructs developers
debugging Trusted Web Activities to confirm which browser is in use, and prints
`adb logcat -v brief | grep -e TWAProviderPicker` as the way to check
([Chrome, Trusted Web Activity quick start](https://developer.chrome.com/docs/android/trusted-web-activity/quick-start)).

## Related entries on this site

- [Capabilities](/reference/capabilities/): follow this cross-reference
  alongside the report's discussion of Trusted Web Activity integration.
- [Storage](/reference/storage/): follow this cross-reference alongside the
  report's discussion of a well-known file at a fixed origin-relative path.
- [Performance](/reference/performance/): the report states its 404 is served as
  a full HTML page with a stylesheet and about a dozen script tags; follow this
  cross-reference alongside that finding.

## Open and unverified

- Whether the missing `/.well-known/assetlinks.json` is the only reason
  CogniPrep's Trusted Web Activity fails verification, rather than one of
  several, is not confirmed. Chrome's quick start names the wrong signing
  signature as a common cause of the same failure.
- Whether the Android package `app.cogniprep.twa` is signed with the certificate
  whose fingerprint the route handler returns is not confirmed; the report prints
  the fingerprint truncated.
- Whether moving the route handler to `app/.well-known/assetlinks.json/route.ts`
  makes verification succeed on any particular browser and Android version is not
  stated by the report.
- Whether the roughly 20 percent of padding on every side that the report
  attributes to the maskable path corresponds to the safe zone any specific
  launcher applies is not confirmed.
- Whether a `related_applications` entry would change what a mobile browser shows
  for this site is not confirmed; MDN marks the member experimental.
- Whether the 740 `Disallow` lines the report counts in its emitted robots.txt
  affect anything other than crawling is not addressed.

## Sources

- [Our Android verification file is served from /api, and Android only ever looks in /.well-known](https://dev.to/daniel_pertu/our-android-verification-file-is-served-from-api-and-android-only-ever-looks-in-well-known-467p) (dev.to)
- [Digital Asset Links: Getting Started](https://developers.google.com/digital-asset-links/v1/getting-started) (developers.google.com)
- [Trusted Web Activity quick start guide](https://developer.chrome.com/docs/android/trusted-web-activity/quick-start) (developer.chrome.com)
- [icons](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/icons) (developer.mozilla.org)
- [related_applications](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/related_applications) (developer.mozilla.org)