Skip to content

Authentication

The Researcher Profiles specification does not require authentication. A server MAY serve profiles publicly with no credentials. This is the norm for static file hosts (S3, R2, GitHub Pages).

A server MAY require authentication on any endpoint. When it does, it MUST use bearer tokens:

Authorization: Bearer <token>

An unauthenticated request to a protected endpoint MUST receive 401 with {"detail": "invalid or missing bearer token"}.

A request for a profile or artifact above the caller’s access level returns 404, indistinguishable from a nonexistent resource.


A server MAY implement viewer tiers to control which artifacts a caller can see. Three tiers are defined:

TierSees
publicPublic artifacts only
limitedPublic + limited artifacts
privateAll artifacts

An unauthenticated caller defaults to the public tier. A server that does not implement tiers treats all callers as public.

When viewer tiers are active, responses SHOULD include an X-RP-Viewer-Tier header indicating the caller’s effective tier.

A tier is not a property of a credential. A server that implements per-part access decides what a credential reads from its parts table, not from a tier.


How a server mints, stores and names its credentials is the server’s business. A server that mints them SHOULD show a credential’s plaintext exactly once, store only a hash of it, and reject an unknown, revoked or expired credential with the same response in all three cases, so probing learns nothing.

A server MAY give a credential scopes: named whole-server powers such as reading profiles, matching across profiles, or uploading. What a credential may read and write of one profile is not a scope; it is the credential’s parts table. A request that needs a scope the credential lacks is refused with insufficient_scope.

A server that lets people share parts of their profile with other software SHOULD decide every read and write by an app or key per part, per audience.

An audience is the Public or one credential (an app’s, or a key acting for a person). A part is one section of the profile document or one kind of file (a manifest role). Each audience holds one level per part: none, read, or write. write includes read. A part a table does not list is none. Who sets each table, and how, is up to the server; the profile’s owner SHOULD be the one who decides.

Sections, and the document fields each governs:

PartFields
summarysummary
expertiseexpertise (the label list), and the personality/expertise.md file
focusfield, subfields, interests, not_interests, research_interests
methodsmethodological_commitments
soulthe personality/SOUL.md narrative (role soul)
clinicaltherapeutic_areas
site_capabilitiessite_capabilities
regulatory_experienceregulatory_experience
contactemail
backgroundname, affiliation, job_title, training, career, same_as, identifier, affiliation_id, career_stage

Files, by manifest role: works (also the document fields research_outputs and paper_stats), paper_summary, paper_abstract, paper_fulltext, grants, cv, interview, web, trials, citations, and other_files (every role not named here and not infrastructure). Infrastructure roles (profile, context, agent_entry_point, html, embedding_index, embedding_index_sqlite, embedding_chunks) belong to no part.

Every part is writable by an app or key holding write on it.

An app or key never reads less than the Public: its effective reads on a profile are its table’s read and write parts, united with what the Public reads there. A part or artifact the caller may not read returns 404; a profile the caller may read nothing of returns 404 too.

An app or key that reads some parts but not all is served at viewer tier public with a copy of the document rewritten for it: the parts it may read are declared public, the rest private. Those declarations describe that caller’s view, not the open web’s.

A write by an app or key MUST hold write on every part it touches:

  • a metadata patch, on the part of each field it names;
  • a SOUL write, on soul; a works edit, on works;
  • a whole-profile upload, on every part the upload changes (see the push rule).

A write missing any of these MUST be refused with insufficient_access and MUST change nothing.

A server MAY let a credential replace a whole profile at once, without the part-by-part comparison. That power covers what no part covers, including what the Public reads, so a server SHOULD keep it off unless the owner turns it on.

A server MUST NOT let any table or switch delegate the following. They are decisions belonging to the person the profile describes.

ActWhy
Publish or unpublishPublishing is a decision by the person the profile describes.
Change what the public seesOnly the owner decides what the public may read.
Delete the profileOnly the owner may delete a profile.
Grant accessOnly the owner may give access, including minting another key.

“Change what the public sees” has one exception: a caller allowed to replace whole profiles writes the whole document, including what it tells the Public. No edit endpoint may change it, whatever the caller holds.

A server SHOULD tell a credential which of these acts it cannot be given. A request to perform one of these acts, or to change a metadata field no part covers, MUST return 403 with detail.error equal to "not_delegable" and a hint.

Both bodies below are carried in the detail member of the response: {"detail": {...}}. A client MUST NOT retry on either. It should report what is missing and stop.

A write by an app or key that lacks write on a part it touches MUST return 403 with:

FieldTypeDescription
errorstringThe literal "insufficient_access"
requiredlist[string]Every part the write touches, sorted
missinglist[string]The parts in required the caller may not write, sorted
needs_replacelist[string]Uploads only. Changes no part covers (for example what the public sees, a field such as provenance_note, an infrastructure file), as short phrases. Only replacing the whole profile allows them
hintstringOne or two sentences naming what is missing and who can allow it
{
"detail": {
"error": "insufficient_access",
"required": ["paper_summary", "summary"],
"missing": ["summary"],
"needs_replace": [],
"hint": "This needs Write on summary. The profile's owner can allow it."
}
}

A request whose key is valid but lacks a required scope MUST return 403 with:

FieldTypeDescription
errorstringThe literal "insufficient_scope"
requiredlist[string]Scopes this request needs
grantedlist[string]Scopes the key holds
missinglist[string]required minus granted
hintstringOne sentence naming the fix
{
"detail": {
"error": "insufficient_scope",
"required": ["match"],
"granted": ["read"],
"missing": ["match"],
"hint": "This credential lacks scope 'match'. Ask the server's operator to add it."
}
}