Concepts

Resolution

How an address becomes a verified answer: the PNS API, the signed bundle every server returns, the verification chain a client runs, and how time frames and directions are chosen.

On this page
  1. The PNS servers
  2. GET /resolve/{address}
  3. GET /status/{address}
  4. GET /root-key and GET /health
  5. The signed bundle
  6. The verification chain
  7. Four servers, one answer
  8. Time frames
  9. Direction overrides

The PNS servers

Portal Name Services runs as 4 servers. They are the same program, each with its own copy of the signed records, and they answer identically. A client may ask any of them, and should try the next if one does not answer.

ServerOrigin
pns1https://pns1.portalmesh.net
pns2https://pns2.portalmesh.net
pns3https://pns3.portalmesh.net
pns4https://pns4.portalmesh.net
  • HTTPS only: a plain HTTP request is redirected with a 301.
  • JSON only, with CORS open to every origin for reads (GET, HEAD, OPTIONS).
  • A PNS server holds no private key. It serves signed bundles exactly as they were signed and can never alter one.
  • Any other method is refused with status 405 and { "code": "not_found", "message": "PNS answers GET requests." }.

GET /resolve/{address}

Resolves any address to its signed bundle. Everything after /resolve/ is the address, scheme and query included. It may be written as is, or percent-encoded as a whole, which is what portal.js sends:

text
GET https://pns1.portalmesh.net/resolve/portal://oasis.character
GET https://pns1.portalmesh.net/resolve/activate%3A%2F%2Foasis.character
GET https://pns1.portalmesh.net/resolve/portal://jerusalem.destination?time=30AD

The address goes through the scheme lock, so the wrong scheme is corrected, never refused. The server reads the bundle from its own copy. If it has none, it confirms in the registry’s database that the name is born, has the bundle rebuilt and republished, and answers with servedFrom: "origin".

200 OK, for GET /resolve/portal://oasis.characterjson
{
  "address": "activate://oasis.character",
  "requested": "portal://oasis.character",
  "corrected": true,
  "view": null,
  "server": "pns2",
  "servedFrom": "edge",
  "bundle": {
    "schema": "pm.bundle/1",
    "name": "oasis.character",
    "pid": "pid:01JBX3Q8Z9M4T7K2V5N6R8W0YC",
    "scheme": "activate",
    "serial": 3,
    "records": [
      {
        "name": "oasis.character",
        "type": "ID",
        "version": 1,
        "ttl": 3600,
        "data": {
          "pid": "pid:01JBX3Q8Z9M4T7K2V5N6R8W0YC",
          "rootType": "character",
          "family": "identity",
          "publicKey": {
            "kty": "OKP",
            "crv": "Ed25519",
            "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
          },
          "keyId": "pid:01JBX3Q8Z9M4T7K2V5N6R8W0YC#k1",
          "born": "2026-10-23T12:00:00.000Z"
        },
        "issued": "2026-10-23T12:00:00.000Z",
        "kid": "jubilee-root-example",
        "signature": "Jzs--LBsgsL2QoxOiko4O0GTizhqMzie8ajRXN5h-uugPJcUTgyAvpnTcadaG44AKMQRdxOoXf8Jr4C_8n5LBg"
      },
      {
        "name": "oasis.character",
        "type": "ENDPOINT",
        "version": 1,
        "ttl": 300,
        "data": {
          "url": "https://endpoint.example.com/session",
          "modes": [
            "text",
            "voice"
          ],
          "directions": [
            "bring"
          ]
        },
        "issued": "2026-10-23T12:10:00.000Z",
        "kid": "pid:01JBX3Q8Z9M4T7K2V5N6R8W0YC#k1",
        "signature": "86tNfhoNBTvMH4Ob4R-zNoM0jANQgpr1aP6HKYVgDcANvXiMFsTQ_zE6067huraKPU2cv7x4qE2c_PyzSkrEDA"
      },
      {
        "name": "oasis.character",
        "type": "STATUS",
        "version": 2,
        "ttl": 60,
        "data": {
          "state": "active",
          "since": "2026-10-23T12:30:00.000Z"
        },
        "issued": "2026-10-23T12:30:00.000Z",
        "kid": "jubilee-root-example",
        "signature": "bT-SgdCwcL6GYRkkKIBXvY0Ihxpzs-OCcqp9t2hOfQUIM4Q5426OUXRHXofZr-mTFQc1E9Ocowr9Ai97mm44Dw"
      },
      {
        "name": "oasis.character",
        "type": "PERSONA",
        "version": 1,
        "ttl": 300,
        "data": {
          "personaVersion": "2026.10.23",
          "hash": "sha256:f666f7b6e5c7b0a0c94366a7b55b902d2d3c1fb6d6d130922c70f7a274f00a24",
          "profile": "https://files.theportalmesh.com/personas/f666f7b6e5c7b0a0c94366a7b55b902d2d3c1fb6d6d130922c70f7a274f00a24.json"
        },
        "issued": "2026-10-23T12:10:00.000Z",
        "kid": "pid:01JBX3Q8Z9M4T7K2V5N6R8W0YC#k1",
        "signature": "lk49Qy02Qyh-8UsNRJOrtcgwGleY0FSEvPLAtBmKxKcP0XQiKBYMLQkwJgCVYMSnUgs3qTW4_8Bt9pqb7-cEDQ"
      },
      {
        "name": "oasis.character",
        "type": "GUARDIAN",
        "version": 1,
        "ttl": 300,
        "data": {
          "class": "worker",
          "parent": null,
          "guardian": {
            "displayName": "Example Studio"
          },
          "responsibleGuardian": {
            "displayName": "Jordan Lee"
          },
          "status": "active",
          "successorDesignated": true
        },
        "issued": "2026-10-23T12:00:00.000Z",
        "kid": "jubilee-root-example",
        "signature": "cJlKcraiPHeWU7jcMBDHS8fgq8wMI1LDbYCyn4WcWddp1MaCSuAX1qH0Zo5eHNBMRR68JpcMRjXAmMJWT5rrCw"
      },
      {
        "name": "oasis.character",
        "type": "CERT",
        "version": 1,
        "ttl": 3600,
        "data": {
          "certificateId": "crt_01JBX3Q8ZA6M2T9K4V7N1R3W5Y",
          "kind": "birth",
          "version": 1,
          "fingerprint": "sha256:8985a02b0098e873418e5de978268581dfc2c1777279d3799e9345379b33e9ae",
          "url": "https://theportalmesh.com/certificate/pid:01JBX3Q8Z9M4T7K2V5N6R8W0YC",
          "issuedAt": "2026-10-23T12:00:00.000Z",
          "personaFingerprint": "sha256:f666f7b6e5c7b0a0c94366a7b55b902d2d3c1fb6d6d130922c70f7a274f00a24",
          "dnaFingerprint": "sha256:eacb55f7bb7e1c0240780e660ade3ddea5a4fc08d36fb23f3f52f983a604e54d"
        },
        "issued": "2026-10-23T12:00:00.000Z",
        "kid": "jubilee-root-example",
        "signature": "d8jPFlYaIF_BEUkCkzufNmfb8ucTPqfjxXqn0aCvMnq7q1tIakFlRDrLRs2l5kvgfRmOC8pYmm-4tMIl7i7EAA"
      }
    ],
    "issued": "2026-10-23T12:30:01.000Z",
    "kid": "jubilee-root-example",
    "signature": "apovBt_TcpB1Nqio-bau7S9Z5yyTI2iMM0U1jL42-4I4HiJISSn1cFC252slUg7LewFA4NBaIA_AblmdpwHOAw"
  },
  "resolution": {
    "direction": {
      "requested": null,
      "granted": "bring"
    },
    "time": null
  }
}
FieldMeaning
addressThe canonical address, with the locked scheme, any view label, and sorted parameters.
requestedThe address exactly as it was received.
correctedtrue when the other scheme was typed and corrected.
viewpersona, cert, status or time when the address names a view, else null. The bundle is then the parent name’s.
serverWhich server answered: pns1 to pns4.
servedFromedge from this server’s copy, or origin when it was just rebuilt.
bundleThe signed bundle. Verify it before using anything in it.
resolution.direction{ requested, granted, notice? }, or null when the bundle has no ENDPOINT. See directions.
resolution.timeThe chosen time frame, or null when the bundle has no WORLD time frames. See time frames.

Headers: Cache-Control: public, max-age= the lowest TTL in the bundle, X-PNS-Server, and X-PNS-Serial (the bundle’s serial).

Errors are JSON { code, message } with Cache-Control: no-store:

Statuscodemessage
400invalid_address, invalid_name, unknown_namespaceThe address parser’s message, for example “https:// is not a Mesh scheme. Mesh addresses use activate://, portal:// or experience://.”
404not_found“oasis.character is not a registered portal name.” Also for a name that is registered but not yet born.
404not_foundThere is nothing at this address. (Any other path.)
500internalThis PNS server could not answer. Try another: pns1 to pns4 answer identically.

The Registry API also answers GET https://api.theportalmesh.com/v1/resolve/{address}, with a 307 redirect to the first PNS server. The query string is kept, so ?time= and ?dir= carry through.

GET /status/{address}

The live status of a name, read from the registry’s database on every call, never from a cache. A bundle can be up to a minute old at the edge; this cannot. Clients call it before every activation or other sensitive action, and portal.js refuses to open a portal unless it says active.

200 OK, for GET /status/activate://oasis.characterjson
{
  "name": "oasis.character",
  "address": "activate://oasis.character",
  "pid": "pid:01JBX3Q8Z9M4T7K2V5N6R8W0YC",
  "registered": true,
  "state": "active",
  "active": true,
  "serial": 3,
  "checkedAt": "2026-10-23T12:31:00.000Z",
  "source": "d1",
  "server": "pns2"
}
FieldMeaning
nameThe name that was looked up (the parent, for a view).
addressThe canonical address.
pidThe permanent ID, or null when the name is not registered.
registeredWhether the name is registered at all.
stateOne of the STATUS states (born, active, sleeping, suspended, dormant, dead, resting, released, retired, terminated); pending for a name that is registered but not yet born; unregistered.
activetrue only when the state is active.
serialThe name's current serial: compare it with the bundle's to see whether the bundle is current.
reasonPresent only when the name is suspended: why.
checkedAtWhen the database was read.
sourceAlways d1, the registry’s database.
serverWhich server answered.

The answer is 200 for any valid address, registered or not, with Cache-Control: no-store. It is not signed; it is a live check that complements the signed bundle, which a client has already verified.

GET /root-key and GET /health

/root-key returns the Jubilee root public key (cacheable for an hour). It is published for convenience and for tools; a client that verifies bundles should pin the key it trusts rather than fetch it from the server it is checking. portal.js pins it at build time.

200 OK, for GET /root-key (the example key)json
{
  "kid": "jubilee-root-example",
  "publicKey": {
    "kty": "OKP",
    "crv": "Ed25519",
    "x": "PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw"
  },
  "algorithm": "Ed25519"
}

/health reports this server’s synchronization: { ok, server, highestSerial, lastSync, names }, the highest serial it holds, when it last received a bundle, and how many names it holds. GET / describes the service and lists its endpoints.

The signed bundle

A name resolves to one bundle: its ID, STATUS and CERT records and every other record it has, wrapped in an envelope signed by the Jubilee root key.

FieldMeaning
schemapm.bundle/1
nameThe portal name.
pidIts permanent ID.
schemeactivate, portal or experience, the namespace’s locked scheme.
serialThe name's current version. It goes up with every change, so a higher serial is always newer.
recordsEvery record, ordered by type (ID, ENDPOINT, ALIAS, STATUS, PERSONA, GUARDIAN, CONSENT, SOURCE, WORLD, CERT, TXT), then by slot: a TXT record’s kind, a CONSENT’s experience.
issuedWhen the bundle was signed.
kidThe root key's id.
signatureEd25519 by the root key over the canonical JSON of every other field of the bundle, base64url.

A name whose state is dead, resting, released, retired, terminated keeps resolving, as a tombstone: its bundle carries only ID, STATUS and CERT, so it stays discoverable and nothing the owner signed can be activated again. A signed example is on The life of a name.

The bundle of oasis.character, signed with the example root keyjson
{
  "schema": "pm.bundle/1",
  "name": "oasis.character",
  "pid": "pid:01JBX3Q8Z9M4T7K2V5N6R8W0YC",
  "scheme": "activate",
  "serial": 3,
  "records": [
    {
      "name": "oasis.character",
      "type": "ID",
      "version": 1,
      "ttl": 3600,
      "data": {
        "pid": "pid:01JBX3Q8Z9M4T7K2V5N6R8W0YC",
        "rootType": "character",
        "family": "identity",
        "publicKey": {
          "kty": "OKP",
          "crv": "Ed25519",
          "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
        },
        "keyId": "pid:01JBX3Q8Z9M4T7K2V5N6R8W0YC#k1",
        "born": "2026-10-23T12:00:00.000Z"
      },
      "issued": "2026-10-23T12:00:00.000Z",
      "kid": "jubilee-root-example",
      "signature": "Jzs--LBsgsL2QoxOiko4O0GTizhqMzie8ajRXN5h-uugPJcUTgyAvpnTcadaG44AKMQRdxOoXf8Jr4C_8n5LBg"
    },
    {
      "name": "oasis.character",
      "type": "ENDPOINT",
      "version": 1,
      "ttl": 300,
      "data": {
        "url": "https://endpoint.example.com/session",
        "modes": [
          "text",
          "voice"
        ],
        "directions": [
          "bring"
        ]
      },
      "issued": "2026-10-23T12:10:00.000Z",
      "kid": "pid:01JBX3Q8Z9M4T7K2V5N6R8W0YC#k1",
      "signature": "86tNfhoNBTvMH4Ob4R-zNoM0jANQgpr1aP6HKYVgDcANvXiMFsTQ_zE6067huraKPU2cv7x4qE2c_PyzSkrEDA"
    },
    {
      "name": "oasis.character",
      "type": "STATUS",
      "version": 2,
      "ttl": 60,
      "data": {
        "state": "active",
        "since": "2026-10-23T12:30:00.000Z"
      },
      "issued": "2026-10-23T12:30:00.000Z",
      "kid": "jubilee-root-example",
      "signature": "bT-SgdCwcL6GYRkkKIBXvY0Ihxpzs-OCcqp9t2hOfQUIM4Q5426OUXRHXofZr-mTFQc1E9Ocowr9Ai97mm44Dw"
    },
    {
      "name": "oasis.character",
      "type": "PERSONA",
      "version": 1,
      "ttl": 300,
      "data": {
        "personaVersion": "2026.10.23",
        "hash": "sha256:f666f7b6e5c7b0a0c94366a7b55b902d2d3c1fb6d6d130922c70f7a274f00a24",
        "profile": "https://files.theportalmesh.com/personas/f666f7b6e5c7b0a0c94366a7b55b902d2d3c1fb6d6d130922c70f7a274f00a24.json"
      },
      "issued": "2026-10-23T12:10:00.000Z",
      "kid": "pid:01JBX3Q8Z9M4T7K2V5N6R8W0YC#k1",
      "signature": "lk49Qy02Qyh-8UsNRJOrtcgwGleY0FSEvPLAtBmKxKcP0XQiKBYMLQkwJgCVYMSnUgs3qTW4_8Bt9pqb7-cEDQ"
    },
    {
      "name": "oasis.character",
      "type": "GUARDIAN",
      "version": 1,
      "ttl": 300,
      "data": {
        "class": "worker",
        "parent": null,
        "guardian": {
          "displayName": "Example Studio"
        },
        "responsibleGuardian": {
          "displayName": "Jordan Lee"
        },
        "status": "active",
        "successorDesignated": true
      },
      "issued": "2026-10-23T12:00:00.000Z",
      "kid": "jubilee-root-example",
      "signature": "cJlKcraiPHeWU7jcMBDHS8fgq8wMI1LDbYCyn4WcWddp1MaCSuAX1qH0Zo5eHNBMRR68JpcMRjXAmMJWT5rrCw"
    },
    {
      "name": "oasis.character",
      "type": "CERT",
      "version": 1,
      "ttl": 3600,
      "data": {
        "certificateId": "crt_01JBX3Q8ZA6M2T9K4V7N1R3W5Y",
        "kind": "birth",
        "version": 1,
        "fingerprint": "sha256:8985a02b0098e873418e5de978268581dfc2c1777279d3799e9345379b33e9ae",
        "url": "https://theportalmesh.com/certificate/pid:01JBX3Q8Z9M4T7K2V5N6R8W0YC",
        "issuedAt": "2026-10-23T12:00:00.000Z",
        "personaFingerprint": "sha256:f666f7b6e5c7b0a0c94366a7b55b902d2d3c1fb6d6d130922c70f7a274f00a24",
        "dnaFingerprint": "sha256:eacb55f7bb7e1c0240780e660ade3ddea5a4fc08d36fb23f3f52f983a604e54d"
      },
      "issued": "2026-10-23T12:00:00.000Z",
      "kid": "jubilee-root-example",
      "signature": "d8jPFlYaIF_BEUkCkzufNmfb8ucTPqfjxXqn0aCvMnq7q1tIakFlRDrLRs2l5kvgfRmOC8pYmm-4tMIl7i7EAA"
    }
  ],
  "issued": "2026-10-23T12:30:01.000Z",
  "kid": "jubilee-root-example",
  "signature": "apovBt_TcpB1Nqio-bau7S9Z5yyTI2iMM0U1jL42-4I4HiJISSn1cFC252slUg7LewFA4NBaIA_AblmdpwHOAw"
}

The verification chain

This is the registry’s verification, used by portal.js, the public lookup page and the tests. It stops at the first failure in steps 1 to 3; from step 4 on, every problem is collected. A client that gets any problem fails closed. The messages are the registry’s own.

  1. Shape. The bundle is an object with schema: "pm.bundle/1", a name, a pid and a records list, and the name is a valid portal name.
  2. The envelope. Find the trusted root key whose id is the bundle’s kid (“The bundle is signed by an unknown key (kid).” if there is none), and verify the signature over the canonical JSON of the bundle without signature (“The bundle signature does not verify against the Jubilee root key.”).
  3. The ID record. There is exactly one. It verifies against the root key; its pid is the bundle’s; its rootType is the name’s namespace; its publicKey is a valid Ed25519 key. That key, and its keyId, are the name’s key from here on.
  4. Every record. Each one is for this name and is a type valid in this namespace. STATUS, CERT, GUARDIAN verify against the root key. Every owner record carries the ID’s keyId as its kid and verifies against the ID’s public key.
  5. Uniqueness. At most one each of STATUS, CERT, PERSONA, GUARDIAN, WORLD and ALIAS; at most one CONSENT per experience and one TXT per kind; and a STATUS must be present.
  6. Persona integrity. In a persona namespace, a PERSONA record’s hash must equal the CERT record’s personaFingerprint: the fingerprint on the current birth certificate (PNS section 9).
  7. Completeness. Every record the namespace requires is present. The verifier always reports what is missing; it counts as a problem only when the caller asks for it. portal.js refuses an incomplete portal, after first checking that the signed STATUS is active; a lookup page shows what is missing instead.

The bundle’s STATUS is part of the signed answer, but it may be up to a minute old. Before opening a portal, a client also calls /status and requires that it names the same PID and says active, and that the signed STATUS says active too.

What a failure looks like

Both of these were produced while this page was built.

What was done to the example bundleThe registry's verifier says
A relay changed the ENDPOINT url in transit.The bundle signature does not verify against the Jubilee root key.
The envelope was signed correctly, but the ENDPOINT record inside was signed with a key that is not the name's.The ENDPOINT record's signature does not verify against the name's key.

Verifying without the registry’s library

The chain for one pinned root key, in standalone JavaScript. It uses canonicalize and b64urlDecode from mesh-core.js. During the build of this page it accepted the example bundle and refused both tampered ones. It checks steps 1 to 6; add the namespace rules and the completeness check from Records and signing if you need them.

mesh-verify.jsjs
// Ed25519 over the UTF-8 bytes of a canonical JSON string. A malformed key or signature is a
// failed verification, never an exception.
async function ed25519Verify(publicKeyX, signatureB64url, canonicalText) {
  try {
    const key = await crypto.subtle.importKey("raw", b64urlDecode(publicKeyX), { name: "Ed25519" }, false, ["verify"]);
    const signature = b64urlDecode(signatureB64url);
    if (signature.length !== 64) return false;
    return await crypto.subtle.verify({ name: "Ed25519" }, key, signature, new TextEncoder().encode(canonicalText));
  } catch {
    return false;
  }
}

// What a record, a bundle or a session answer signs: every field except "signature".
function withoutSignature(signed) {
  const copy = { ...signed };
  delete copy.signature;
  return copy;
}

// A record: the canonical JSON of the envelope without its signature.
function verifyRecord(record, publicKeyX) {
  return ed25519Verify(publicKeyX, record.signature, canonicalize(withoutSignature(record)));
}

// A certificate: the canonical JSON of { body, kid }, signed by the root key named by kid.
function verifyCertificate(certificate, root) {
  if (certificate.kid !== root.kid) return Promise.resolve(false);
  return ed25519Verify(root.x, certificate.signature, canonicalize({ body: certificate.body, kid: certificate.kid }));
}

const REGISTRY_SIGNED = ["ID", "STATUS", "CERT", "GUARDIAN"];
const AT_MOST_ONE = ["STATUS", "CERT", "PERSONA", "GUARDIAN", "WORLD", "ALIAS"];
// One per slot: a CONSENT per experience (an avatar's own has none), a TXT per kind.
const SLOT_FIELD = { CONSENT: "experience", TXT: "kind" };

// A resolution bundle, checked against a root key YOU pinned: { kid, x }. Never take the root key
// from the bundle, the resolver or the page.
async function verifyBundle(bundle, root) {
  const fail = (problem) => ({ ok: false, problem });
  if (!bundle || bundle.schema !== "pm.bundle/1") return fail("Unknown bundle schema.");
  if (bundle.kid !== root.kid) return fail("The bundle is signed by an unknown key.");
  if (!(await ed25519Verify(root.x, bundle.signature, canonicalize(withoutSignature(bundle))))) {
    return fail("The bundle signature does not verify against the root key.");
  }
  const ids = bundle.records.filter((r) => r.type === "ID");
  if (ids.length !== 1) return fail("A bundle carries exactly one ID record.");
  const id = ids[0];
  if (id.kid !== root.kid || !(await verifyRecord(id, root.x))) return fail("The ID record does not verify against the root key.");
  if (id.data.pid !== bundle.pid) return fail("The ID record names a different permanent ID than the bundle.");
  if (id.data.rootType !== bundle.name.split(".").pop()) return fail("The ID record's root type does not match the namespace.");
  for (const r of bundle.records) {
    if (r.name !== bundle.name) return fail("A " + r.type + " record is for another name.");
    if (r === id) continue;
    if (REGISTRY_SIGNED.includes(r.type)) {
      if (r.kid !== root.kid || !(await verifyRecord(r, root.x))) return fail("The " + r.type + " record does not verify against the root key.");
    } else if (r.kid !== id.data.keyId || !(await verifyRecord(r, id.data.publicKey.x))) {
      return fail("The " + r.type + " record does not verify against the name's key.");
    }
  }
  const one = (type) => bundle.records.filter((r) => r.type === type);
  for (const type of AT_MOST_ONE) if (one(type).length > 1) return fail("A bundle carries at most one " + type + " record.");
  for (const [type, field] of Object.entries(SLOT_FIELD)) {
    const slots = one(type).map((r) => (r.data && typeof r.data[field] === "string" ? r.data[field] : ""));
    if (new Set(slots).size !== slots.length) return fail("A bundle carries at most one " + type + " record per slot.");
  }
  const status = one("STATUS")[0];
  const cert = one("CERT")[0];
  const persona = one("PERSONA")[0];
  if (!status) return fail("The bundle has no STATUS record.");
  if (persona && (!cert || cert.data.personaFingerprint !== persona.data.hash)) {
    return fail("The PERSONA hash does not match the persona fingerprint on the birth certificate.");
  }
  return { ok: true, state: status.data.state, keyId: id.data.keyId, nameKeyX: id.data.publicKey.x, records: bundle.records };
}
Using itjs
// Pin the root key you trust. Never take it from the bundle, the resolver or the page.
const ROOT = { kid: "jubilee-root-example", x: "PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw" }; // the example key: replace it

const res = await fetch("https://pns1.portalmesh.net/resolve/" + encodeURIComponent("activate://oasis.character"));
const answer = await res.json();
const result = await verifyBundle(answer.bundle, ROOT);
if (!result.ok) throw new Error(result.problem);

const live = await (await fetch("https://pns1.portalmesh.net/status/" + encodeURIComponent(answer.address))).json();
if (live.pid !== answer.bundle.pid || live.state !== "active" || result.state !== "active") {
  throw new Error(answer.bundle.name + " is not active.");
}

Four servers, one answer

The four servers never write to each other. The registry’s database is the only master, and one publisher keeps the servers in step:

  1. Every change is written to the database first, and the name’s serial goes up by one.
  2. The publisher builds the name’s bundle from the database, signs it once with the root key, and writes the same bundle to all four servers’ stores at once, under name:<name> and pid:<pid>.
  3. A failed write is retried with growing delays (10, 20, 40 and 80 seconds). After five failures an alert goes to Jubilee’s PNS Console.
  4. Every five minutes, every name changed in the last 24 hours is compared on every server with the database, and any server that is behind is rewritten.
  5. Every night, every name on every server is audited the same way, with a report.
  6. Revocations go first. Every change that stops a name from opening (a suspension, a lapse to dormant, death, rest, release, retirement or termination) is published directly to all four servers and is reported complete only when all four have confirmed it. Retiring a name answers 200 when it is confirmed everywhere and 202 when it is not yet.
  7. Whenever it publishes a STATUS that is neither active nor born, the publisher also tells the three Mesh servers, so they close everything the name has open at once. See How the Mesh is run.

So the servers answer identically: for a given serial, every server holds the same signed records. (The envelope’s issued time can differ on a server that was repaired later; its records cannot.) And because a client verifies every signature itself, it never has to trust which server answered. Edge copies can take about a minute to update worldwide, which is why status is always checked live.

Time frames

A destination’s WORLD record may list the time frames it offers and a default. An address asks for one with ?time=; the resolver chooses the frame and returns it as resolution.time, and portal.js passes the chosen frame’s id to the endpoint.

What a time can be

The request is trimmed, uppercased, and stripped of spaces and dots. Then it is one of:

  • now or today: the living present.
  • A year of 1 to 5 digits, optionally followed by AD, CE, BC or BCE: 30AD, 30 ad, 1000BC, 2100AD. A bare year, such as 1944, is AD.
  • AD or CE followed by a year: AD30, A.D. 30.

Year zero does not exist (1 BC is followed by 1 AD). Years are whole numbers, BC negative.

How a frame is chosen

  1. No time asked for: the default frame.
  2. The request matches a frame’s id, ignoring case (?time=today): that frame.
  3. The request is not a time the grammar understands: the default frame, and a notice.
  4. now: the frame marked now: true, if there is one.
  5. A year: the frame with exactly that year, if there is one.
  6. Otherwise the nearest frame by distance in years, where a now frame counts as the current year. A tie goes to the earlier frame, so the choice never depends on the order of the list. The answer carries a notice.

Whenever fallback is true, the client must tell the visitor, using the notice. These are the registry’s choices for the example Jerusalem WORLD on the Records page, with the current year set to 2026:

?time=OpensReasonNotice
(none)Jerusalem todaydefault
nowJerusalem todayexact
30ADJerusalem in 30 ADexact
30 adJerusalem in 30 ADexact
todayJerusalem todayexact
70ADJerusalem in 30 ADnearest, fallbackThis destination does not offer 70 AD. The nearest time it offers, Jerusalem in 30 AD, has opened.
1000BCJerusalem in 30 ADnearest, fallbackThis destination does not offer 1000 BC. The nearest time it offers, Jerusalem in 30 AD, has opened.
1944Jerusalem todaynearest, fallbackThis destination does not offer 1944 AD. The nearest time it offers, Jerusalem today, has opened.
2063Jerusalem todaynearest, fallbackThis destination does not offer 2063 AD. The nearest time it offers, Jerusalem today, has opened.
2100ADJerusalem in 2100, a depicted futureexact
tomorrowJerusalem todaydefault, fallback"tomorrow" is not a time this destination understands, so its default, Jerusalem today, has opened.

The selection is returned in this shape:

json
{
  "frame": {
    "id": "30ad",
    "label": "Jerusalem in 30 AD",
    "year": 30,
    "source": "history"
  },
  "reason": "nearest",
  "fallback": true,
  "requested": "70AD",
  "notice": "This destination does not offer 70 AD. The nearest time it offers, Jerusalem in 30 AD, has opened."
}

Direction overrides

An address may ask for a direction with ?dir=go or ?dir=bring. The resolver grants it only if the portal’s signed ENDPOINT record lists it in directions:

  1. the requested direction, if the ENDPOINT allows it;
  2. otherwise the namespace’s default direction, if the ENDPOINT allows it;
  3. otherwise the first direction the ENDPOINT lists.

The answer is resolution.direction: { "requested": "go", "granted": "bring", "notice": "This portal does not allow go; it opens as bring." } when a request was overridden, or { "requested": null, "granted": "bring" } when none was made. It is null when the bundle has no ENDPOINT.

portal.js opens the session in the direction the page asked for if the ENDPOINT allows it, otherwise in the granted direction. See portal.js.