Publish at.sovrn.mail.service lexicon (goat, after webmail eval ac83db8)
openGoal
Publish the locked at.sovrn.mail.service record lexicon so any PDS can resolve and validate it on putRecord (explicit validate:true writes).
Blocked until after webmail client evaluation (ac83db8): if the webmail fork needs extra endpoint fields (e.g. mailEndpoints.jmap/imap/smtp inventory), finalize those schema changes first. Lexicon evolution is append-only (new optional fields only; breaking change = new NSID), so do not publish until the record shape is final.
Context
- Canonical file:
lexicons/at/sovrn/mail/service.json(lexicon:1,id: at.sovrn.mail.service,defs.main.type: record,key: literal:self, requiredappview, webmailURIs, optionalmailEndpoints{jmap,imap,smtp}). - NSID authority:
at.sovrn.mail.service→ name=service, authority=at.sovrn.mail→ domainmail.sovrn.at. Resolver looks up_lexicon.mail.sovrn.atTXT only (no hierarchy walk)._lexicon.sovrn.atalone does NOT cover it. - Authority account:
did:plc:54ba6vxrdbzi6nexczpk5xzb(alsoKnownAs: at://sovrn.at, PDShttps://selfhosted.social). Verified viaplc.directory. - DNS today (checked 2026-09-16): both
_lexicon.sovrn.atand_lexicon.mail.sovrn.atNXDOMAIN — nothing published yet. - Production write path:
internal/authbroker/servicerecord.go:111-122(serviceRecordBody) viainternal/authbroker/authbroker.go:188(com.atproto.repo.putRecord, currently novalidateflag → optimistic validation).
Steps (goat + app password)
1. Freeze and lint canonical lexicon
cat lexicons/at/sovrn/mail/service.json
jj diff -- lexicons/at/sovrn/mail/service.json # must be clean; v1 locked, do not edit
goat lex lint ./lexicons/at/sovrn/mail/service.json
Confirm no external $refs (only string/format:uri + nested object) — single-record publish suffices. Do NOT publish account.json, defs.json, etc. in this issue.
2. Publish DNS authority TXT
In the sovrn.at zone:
_lexicon.mail.sovrn.at. 300 IN TXT "did=did:plc:54ba6vxrdbzi6nexczpk5xzb"
Optional (future at.sovrn.* group lexicons):
_lexicon.sovrn.at. 300 IN TXT "did=did:plc:54ba6vxrdbzi6nexczpk5xzb"
Verify:
dig +short TXT _lexicon.mail.sovrn.at
curl -s 'https://dns.google/resolve?name=_lexicon.mail.sovrn.at&type=TXT' | jq .
Must return "did=did:plc:54ba6vxrdbzi6nexczpk5xzb" before proceeding (allow 5-10 min, TTL 300).
3. Publish schema record with goat (app password, never main password)
export PDS_URL='https://selfhosted.social'
goat account login --pds "$PDS_URL" # handle sovrn.at + app-password
goat account whoami # must be did:plc:54ba6vxrdbzi6nexczpk5xzb
goat lex publish ./lexicons/at/sovrn/mail/service.json
# expected: 🟢 at.sovrn.mail.service
goat lex pull at.sovrn.mail.service
diff ./lexicons/at/sovrn/mail/service.json ./lexicons/at/sovrn/mail/service.json && echo IDENTICAL
This writes com.atproto.lexicon.schema with rkey: at.sovrn.mail.service and record: {$type: com.atproto.lexicon.schema, lexicon:1, id, defs, description}. Do not add revision or extra fields.
4. Verify resolution (DNS → DID → PDS → record)
curl -s 'https://selfhosted.social/xrpc/com.atproto.repo.getRecord?repo=did:plc:54ba6vxrdbzi6nexczpk5xzb&collection=com.atproto.lexicon.schema&rkey=at.sovrn.mail.service' | jq '{uri,cid,value:.value.id}'
curl -s 'https://selfhosted.social/xrpc/com.atproto.lexicon.resolveLexicon?nsid=at.sovrn.mail.service' | jq .
Expect uri: at://did:plc:54ba6vxrdbzi6nexczpk5xzb/com.atproto.lexicon.schema/at.sovrn.mail.service. AuthorityNotPublished = DNS TXT wrong/missing. Also check https://pds.ls/at://did:plc:54ba6vxrdbzi6nexczpk5xzb/com.atproto.lexicon.schema/at.sovrn.mail.service renders.
5. Prove PDS validation on write (throwaway test account only)
# valid write
curl -s -X POST "$PDS/xrpc/com.atproto.repo.putRecord" \
-H "Authorization: Bearer $TEST_JWT" -H 'Content-Type: application/json' \
-d '{"repo":"<TEST-DID>","collection":"at.sovrn.mail.service","rkey":"self","validate":true,"record":{"$type":"at.sovrn.mail.service","appview":"https://mx1.eu.sovrn.at","webmail":"https://mx1.eu.sovrn.at/webmail"}}' | jq .
# expect validationStatus: valid
# invalid write (missing appview) must fail
curl -s -X POST "$PDS/xrpc/com.atproto.repo.putRecord" \
-H "Authorization: Bearer $TEST_JWT" -H 'Content-Type: application/json' \
-d '{"repo":"<TEST-DID>","collection":"at.sovrn.mail.service","rkey":"self","validate":true,"record":{"$type":"at.sovrn.mail.service","webmail":"https://mx1.eu.sovrn.at/webmail"}}' | jq .
# expect 400 LexiconViolation / missing appview
Clean up throwaway test records afterwards; leave real user records untouched.
6. Follow-ups (separate)
- Consider adding
"validate": truetoserviceRecordBody(internal/authbroker/servicerecord.go:111) so first-login writes fail loudly (existingErrPDSUnavailableretry page) instead of optimistic writes. File as separate bug; out of scope here. - Update
docs/09-open-questions.mdQ3 row once published (date + CID + AT-URI).
References
- https://atproto.com/guides/publishing-lexicons
- https://atproto.com/specs/lexicon#lexicon-publication-and-resolution
- Blocked by:
ac83db8(webmail evaluation may changemailEndpointsinventory)