Skip to main content

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

MemberRequiredWhat it is
redirect_urisyes1–10 https URLs, each on this exact domain.
namenoDisplay name, up to 64 characters. Self-asserted.
client_didonly for credentialsYour app's own protocol DID.
identity_chainnoYour 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 no client_did is 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 — a did:dfos is 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:

DocumentQuestion it answers
/.well-known/dfos-app.jsonWhat app is this domain running, and where may it be redirected?
/.well-known/dfos-didWhich 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_uris entry is on a different host, has a port, or isn't https.
  • An unknown member is present, or a required one is missing.
  • identity_chain is present without client_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.