The dfos-app.json document
To use Sign in with DFOS, serve a small JSON document at
https://yourdomain/.well-known/dfos-app.json. That is the whole of it. DFOS
fetches it live when someone reaches the consent screen; a successful fetch says
"this domain asserts this name, these redirect targets, and — if it has one —
this DID, right now." Nothing is filed anywhere, and no cached verdict outlives
a short TTL.
{
"name": "Field Notes Reader",
"client_did": "did:dfos:r7z9c4kfhne2t38va6d9kn2ch7f4b6a",
"redirect_uris": ["https://yourdomain/callback"],
"identity_chain": ["<genesis JWS>", "<next op JWS>"]
}
The document is strict: an unrecognized member is a refusal, not a
forward-compatible extension. A typo in redirect_uris must never quietly widen
into "no allowlist at all," so nothing here is skipped over.
Members
| Member | Required | What it is |
|---|---|---|
redirect_uris | yes | 1–10 https URLs, each on this exact domain. |
name | no | Display name, up to 64 characters. Self-asserted. |
client_did | only for credentials | Your app's own protocol DID. |
identity_chain | no | Your app identity's full signed operation log, genesis first. |
name
Optional, and not shown on the consent card. Your domain is your identity
here — it is what the redirect allowlist, the challenge binding, and the consent
heading all key on. A name is something you assert about yourself and nobody has
checked, so the consent screen does not render it at all: under the domain it
links to the protocol explorer's page for your app's identity instead, which is a
third party's answer to the question a name was trying to answer. That link needs
your identity to be proven — an origin binding, or a credential issued to it
— because a client_did your document merely asserts is another unchecked claim,
and one that could name somebody else's chain.
A document that asserts no name is not a degraded document — it declined to assert something nobody could have verified, and the consent screen looks the same either way.
Serve one anyway if it helps elsewhere: it is validated server-side like every other member, checked for the characters that could make one app's name render as another's, and it stays on the resolved app document for other readers of that document. An empty string is a malformed value rather than a way to spell "no name" — leave the member out instead.
redirect_uris
Every entry must be https, must be on the very domain serving the document,
and must carry no port and no userinfo. A redirect target naming a foreign host
is the open-redirect trap the allowlist exists to close, so it is refused rather
than warned about. Up to 10 entries, each up to 2048 characters.
The redirect_uri your app sends to the consent screen must be one of these,
and the domain in your challenge must match it too.
client_did
Your app's own protocol DID — an identity you create with the dfos CLI or the
protocol library. Set up § 2 is the
recipe for minting one, and
§ 3 writes the DID into this file
for you rather than leaving you to copy it across.
It is optional when your requested scope set is identity alone, because
that scope issues nothing to your app: the DID is display detail on the consent
screen, never a key anything is encrypted to. Requiring it would lock out every
fork-and-deploy app that has no DFOS identity of its own, and buy no
cryptographic property.
It is required as soon as the set contains any scope that issues a credential — a credential is addressed to someone, so there is no issuing one without knowing who. See Scopes and credentials.
identity_chain
Optionally, publish your app identity's own signed operation log: an array of JWS strings, genesis first, in order, up to 100 operations. A real app chain is a genesis plus a handful of key rotations; the cap is generous headroom and a hard bound on the work an unauthenticated document can ask a fetcher to do. The carriage format is normative in SIWD § The app description document.
Two rules bind it:
- A chain requires
client_did. A chain is a claim about a DID — without one there is nobody for it to prove. A document with a chain and noclient_didis malformed and refused outright, rather than having the chain quietly ignored. - The chain must derive
client_did. The DID implied by your genesis operation has to be the DID you published. A chain that folds into a different identity is refused. Derivation is a property of the identifier rather than a lookup — adid:dfosis the fingerprint of its own genesis operation, so a fetcher recomputes it instead of trusting it. The rules are in the DFOS DID method specification.
Do not assemble the array by hand.
dfos identity well-known --patch
writes client_did and the genesis-first identity_chain into this file while
preserving name and redirect_uris, so the two can never drift from the chain
you actually hold. Re-run it after anything that signs a new operation.
Serving a chain is optional. Everything in Sign in with DFOS works without one.
Publishing your chain gives it a home
When your document carries a valid identity_chain, DFOS ingests it the first
time it encounters it — and then re-serves it from
relay.dfos.com alongside DFOS's own identities.
Your app's DID resolves at the same public relay as everyone else's, so anyone
verifying a signature your app made can look it up in the ordinary way, at the
ordinary place.
Two things are worth knowing about how this works:
- It's encounter-triggered and permissionless. There is no submission endpoint and no precondition of any kind to satisfy first. DFOS notices your chain because someone used your app, and it re-checks your published document periodically after that, so a key rotation you publish is picked up on its own within a few hours.
- Your document stays the source of truth. DFOS holds a copy of what you published; it never signs on your behalf and never mints keys for your identity. Take your chain down and the copy goes stale, then away.
If the DID your chain derives is already an identity DFOS holds keys for, the ingest is refused rather than merged. You cannot claim someone else's identity by publishing a document that names it.
The normative version of all of this — what a consumer may do with a chain it encounters, and the rules that make first encounter safe — is SIWD § Chain residence.
The other well-known document
/.well-known/dfos-app.json is your app description — it is what makes your
app usable for sign-in. There is a second DFOS document under the same namespace
that answers a different question entirely:
| Document | Question it answers |
|---|---|
/.well-known/dfos-app.json | What app is this domain running, and where may it be redirected? |
/.well-known/dfos-did | Which DFOS identity does this domain attest? |
The second is the domain attestation half of an
origin binding: plain text, containing exactly the DID and
nothing else. A _dfos.<domain> TXT record carrying did=did:dfos:<id> does the
same job, and either one suffices.
A valid app description is itself an HTTPS fallback for it — if this
document carries a client_did naming the identity being verified, you may not
need a dfos-did file at all. The exact scope of that fallback, and why it
covers absence and never contradiction, is
Origin binding § The app description is an HTTPS fallback.
The two are separate systems that happen to share a namespace, and they can hold
different verdicts at the same time. A domain can serve a perfectly valid app
description and still have a broken binding, or the reverse.
When a fetch fails
Every failure reaches your user as the same refusal, naming your domain and
saying it does not serve a valid dfos-app.json. Which way it failed —
unreachable, malformed, or blocked — is deliberately not distinguished on the
wire. The usual causes:
- The document is not served over
https, or the URL redirects. A redirecting well-known is a refusal, not a hop to follow: the document must be served by the domain it speaks for. - A
redirect_urisentry is on a different host, has a port, or isn'thttps. - An unknown member is present, or a required one is missing.
identity_chainis present withoutclient_did, or doesn't derive it.- The response is slow (there's a five-second budget for the whole exchange) or larger than 256KB.
Troubleshooting puts these in diagnosis order alongside the rest of the flow's failures, and points at the explorer lookup that shows you what DFOS sees.