Concepts
Records and signing
Every PNS record is a signed JSON envelope. This page defines the envelope, the canonical JSON every signature is made over, the keys and key ids, who signs which record, and the data of every record type.
On this page
The envelope
Every PNS record is a JSON object with the same eight fields (PNS section 7). Any other top-level field is refused. This is the signed ENDPOINT record of the example name oasis.character:
{
"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"
}| Field | Type | Rule |
|---|---|---|
name | string | The full portal name the record belongs to. It must be the name it is published for. |
type | string | One of the 11 record types: ID, ENDPOINT, ALIAS, STATUS, PERSONA, GUARDIAN, CONSENT, SOURCE, WORLD, CERT, TXT. |
version | integer | From 1. Each new record for a slot must carry a higher version than any the slot has held, even one since removed, so an old record can never be replayed. |
ttl | integer, seconds | From 30 to 86,400. See TTL below. |
data | object | The record's content, defined per type below. At most 16,384 bytes of canonical JSON; large content goes in a package that a record links to. |
issued | string | An ISO 8601 time in UTC: YYYY-MM-DDTHH:MM:SS, an optional fraction, then Z. The registry writes milliseconds, from new Date().toISOString(). |
kid | string | The id of the signing key: the root key’s id for ID, STATUS, CERT or GUARDIAN; the name’s current key id, <pid>#k<n>, for everything else. |
signature | string | The Ed25519 signature, base64url without padding (64 bytes, 86 characters). |
A slot is one record type for one name; TXT records have one slot per kind, and CONSENT records one per experience, as CONSENT:<experience>. Publishing a record replaces the one in its slot. An owner can also remove a record they made; the removal is published like any other change, and an active name can never remove a record it needs. A removed slot keeps its highest version as a floor, which the Registry API reports as versionFloors.
What is signed
The signature covers the canonical form of every field except signature. To sign a record:
- Take the envelope without
signature:name,type,version,ttl,data,issued,kid. - Write it as canonical JSON (below) and encode the text as UTF-8.
- Sign those bytes with Ed25519, using the key named by
kid. - Encode the 64-byte signature as base64url without padding and add it as
signature.
These are the exact bytes the example ENDPOINT record signs:
{"data":{"directions":["bring"],"modes":["text","voice"],"url":"https://endpoint.example.com/session"},"issued":"2026-10-23T12:10:00.000Z","kid":"pid:01JBX3Q8Z9M4T7K2V5N6R8W0YC#k1","name":"oasis.character","ttl":300,"type":"ENDPOINT","version":1}To verify, do the same and check the signature against the public key: the Jubilee root key for ID, STATUS, CERT or GUARDIAN, and the name’s key from its ID record for every other type. Certificates and bundles use the same method; see Certificates and Resolution.
Canonical JSON
Every signature on the Mesh is made over canonical JSON, so two parties that agree on a value always agree on the bytes. The registry’s implementation is exactly this:
- Objects: keys sorted by UTF-16 code units (JavaScript’s default sort), written as
{"key":value,...}with no whitespace. A key whose value isundefinedis left out. Only plain objects are allowed: a Date, a Map or a class instance is refused. - Arrays: in their own order, no whitespace. An
undefinedelement is refused. - Strings and numbers: exactly as
JSON.stringifywrites them.NaNand the infinities are refused. - Literals:
true,false,null. - Anything else (a BigInt, a function, a symbol) is refused.
- The text is then encoded as UTF-8.
For every value the registry produces this output is identical to RFC 8785, the JSON Canonicalization Scheme: ECMAScript number serialization, ECMAScript string escaping, and keys sorted by UTF-16 code units. Anything JSON cannot represent without ambiguity is refused rather than coerced, because a signer that quietly turned undefined into null and a verifier that dropped it would disagree about the bytes.
{
version: 1,
type: "ENDPOINT",
name: "oasis.character",
data: { url: "https://endpoint.example.com/session", modes: ["text", "voice"], protocol: undefined },
ttl: 300,
note: "Café ☕",
ratio: 1.50,
big: 1e21,
Zebra: true,
éclair: null,
}{"Zebra":true,"big":1e+21,"data":{"modes":["text","voice"],"url":"https://endpoint.example.com/session"},"name":"oasis.character","note":"Café ☕","ratio":1.5,"ttl":300,"type":"ENDPOINT","version":1,"éclair":null}Note the sorted keys (capital letters before lowercase, é last), protocol: undefined left out, 1.50 written as 1.5, and 1e21 as 1e+21. Keys sort as strings, so "10" comes before "9":
{ b: 1, a: 2, B: 3, "é": 4, Z: 5, "10": 6, "9": 7 }
{"10":6,"9":7,"B":3,"Z":5,"a":2,"b":1,"é":4}What the registry refuses, with its own error messages:
| Call | Error |
|---|---|
canonicalize({ n: NaN }) | non-finite number at $.n |
canonicalize({ list: [1, undefined] }) | undefined at $.list[1] |
canonicalize({ when: new Date(0) }) | non-plain object at $.when |
canonicalize({ count: 10n }) | unsupported bigint at $.count |
The same rules as standalone JavaScript, for verifiers that do not use the registry’s library. This code ran during the build of this page and produced the same bytes as the registry:
// Canonical JSON: keys sorted by UTF-16 code units, no whitespace, strings and numbers exactly
// as JSON.stringify writes them (RFC 8785 compatible for every value the registry produces).
// Anything JSON cannot represent without ambiguity is refused, never coerced.
function canonicalize(value) {
if (value === null) return "null";
switch (typeof value) {
case "boolean":
return value ? "true" : "false";
case "number":
if (!Number.isFinite(value)) throw new Error("non-finite number");
return JSON.stringify(value);
case "string":
return JSON.stringify(value);
case "object": {
if (Array.isArray(value)) {
return "[" + value.map((item) => {
if (item === undefined) throw new Error("undefined inside an array");
return canonicalize(item);
}).join(",") + "]";
}
const proto = Object.getPrototypeOf(value);
if (proto !== Object.prototype && proto !== null) throw new Error("not a plain object");
const keys = Object.keys(value).filter((k) => value[k] !== undefined).sort();
return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalize(value[k])).join(",") + "}";
}
default:
throw new Error("unsupported " + typeof value);
}
}
// base64url without padding. The decoder also accepts standard base64, padded or not.
function b64urlEncode(bytes) {
let binary = "";
for (const b of bytes) binary += String.fromCharCode(b);
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function b64urlDecode(text) {
const b64 = text.replace(/\s+/g, "").replace(/-/g, "+").replace(/_/g, "/").replace(/=+$/, "");
const binary = atob(b64 + "=".repeat((4 - (b64.length % 4)) % 4));
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
}Ed25519 keys
Every signature on the Mesh is Ed25519 (RFC 8032), made and checked through the Web Crypto API, the same code in Workers, Node and browsers. portal.js falls back to a JavaScript implementation in browsers without native Ed25519.
- A public key is a JWK:
{ "kty": "OKP", "crv": "Ed25519", "x": "<32 bytes, base64url>" }. - A signature is 64 bytes, written in base64url without padding. Readers also accept standard base64, padded or not.
- A malformed key or signature is a failed verification, never an error that might be mistaken for success.
There are two kinds of key:
- The Jubilee root key signs certificates, the ID, STATUS, CERT, GUARDIAN records, and every resolution bundle. Its public half is served at
/root-keyon every PNS server, and portal.js pins it at build time. - A name’s own key is generated on the registrant’s device at birth. Only the public key is sent to the registry, which writes it into the ID record and the birth certificate. The private key never reaches Jubilee, so the registry can only accept owner records that already verify against the key on the certificate.
The examples on these pages use the RFC 8032 test keys: test 2 as an example root key (jubilee-root-example) and test 1 as the name’s key. They are public and must never be used for anything real.
Key ids
| Key | kid | Example |
|---|---|---|
| The Jubilee root key | A configured name | jubilee-root-2026-10 |
| A name's key, version n | <pid>#k<n> | pid:01JBX3Q8Z9M4T7K2V5N6R8W0YC#k1 |
- A name’s first key, at birth, is
#k1. Each key rotation adds one, and so do a transfer and a recovery, because the new owner, or the owner who lost the key, brings a new key. - A rotation is made by the signed-in owner, or an admin on an organization’s team, with two-factor authentication, and it must be signed by the current key (pm.key-rotation/1). An owner who has lost the key recovers the name instead, with the same verification it was registered with (Recovery).
- The ID record carries the current key id as
keyId. An owner record must carry exactly that kid; a record signed with an older key no longer verifies. - When an active name rotates its key, the rotation must include re-signed copies of every owner record, so the Mesh never serves a record that no longer verifies. A name that is born but not yet active may rotate alone; its old records are dropped.
Who signs which record
| Record | Signed by | Made |
|---|---|---|
| ID | The registry, with the Jubilee root key | At birth, and again on every key change: a rotation, an accepted transfer, or a completed recovery. |
| ENDPOINT | The owner, with the name's own key | Uploaded by the owner. |
| ALIAS | The owner, with the name's own key | Uploaded by the owner. |
| STATUS | The registry, with the Jubilee root key | At birth (born), on activation, sleeping and waking, suspension, restoration, a missed renewal (dormant), death, rest and release, retirement and termination, and when an active name is transferred or recovered. |
| PERSONA | The owner, with the name's own key | Uploaded by the owner. |
| GUARDIAN | The registry, with the Jubilee root key | For a .character, at birth and on every change of the people behind it: a designation, a pause, a succession. Display names only, never contact details. |
| CONSENT | The owner, with the name's own key | Uploaded by the owner: an avatar's own permission, or a Guardian's approval of one role in one experience. |
| SOURCE | The owner, with the name's own key | Uploaded by the owner. |
| WORLD | The owner, with the name's own key | Uploaded by the owner. |
| CERT | The registry, with the Jubilee root key | At birth, on every re-certification, and when a death certificate is issued. |
| TXT | The owner, with the name's own key | Uploaded by the owner, one record per kind. |
The registry makes ID, STATUS, CERT, GUARDIAN itself and refuses them as uploads. The owner uploads ENDPOINT, ALIAS, PERSONA, CONSENT, SOURCE, WORLD, TXT through PUT /v1/names/:pid/records, and each one is accepted only if its envelope is valid for the name, its kid is the name’s current key id, its signature verifies against that key, and its version is newer than the stored one.
Where each record may appear
A record type outside its namespaces is refused on upload and flagged by every verifier.
| Record | Valid in |
|---|---|
| ID | Every namespace |
| ENDPOINT | Every namespace |
| ALIAS | Every namespace |
| STATUS | Every namespace |
| PERSONA | .character, .avatar, .figure |
| GUARDIAN | .character |
| CONSENT | .character, .avatar, .figure |
| SOURCE | .figure, .realm, .destination, .history, .event, .game, .movie, .music, .book |
| WORLD | .realm, .destination |
| CERT | Every namespace |
| TXT | Every namespace |
Record data, type by type
Each record type defines the shape of data. The registry checks every rule below before it accepts a record, and every example on this page passes those checks.
ID (registry)
| Field | Type | Rule |
|---|---|---|
pid | string | The permanent ID. Must equal the bundle's pid. |
rootType | string | The namespace without its dot. Must equal the name's namespace. |
family | string | identity, destination or experience. A record or certificate signed before October 25, 2026 may say environment or place, the two families that became Destinations. |
publicKey | JWK | The name's current Ed25519 public key. |
keyId | string | The current key id, <pid>#k<n>. Owner records must carry it as their kid. |
born | string | When the name was born, ISO 8601 UTC. |
{
"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"
}STATUS (registry)
| Field | Type | Rule |
|---|---|---|
state | string | One of born, active, sleeping, suspended, dormant, dead, resting, released, retired or terminated. |
since | string | When this state began, ISO 8601 UTC. |
reason | string, optional | awaiting-activation on a born name that is not yet active; the reason given, on a suspended name. |
{
"state": "active",
"since": "2026-10-23T12:30:00.000Z"
}| Registry state | Published STATUS | Means |
|---|---|---|
| pending, paid, expired | Never published | Not born yet, or an unpaid hold that ran out. |
| born | born, reason awaiting-activation | Born, with its certificate, but not activated yet. |
| active | active | The only state in which a portal opens. |
| sleeping | sleeping | A character whose owner's model key failed or ran out of credit. It shows a sleeping screen instead of an error, and wakes when the key works again. |
| suspended | suspended, with the reason | Stopped by Jubilee, for example while a report is reviewed. Can be restored. |
| dormant | dormant | A missed renewal. Held for a year; paying the renewal revives it at once. |
| dead | dead, a tombstone | A year dormant: a death certificate is issued, and the PID and DNA fingerprint are retired forever. |
| resting | resting, a tombstone | The address rests a year, so no one can pose as the former name. |
| released | released, a tombstone | The address is open to anyone again, as a new name with a new PID. |
| retired | retired, a tombstone | Retired by its owner, with a death certificate. Final. |
| terminated | terminated, a tombstone | Terminated by Jubilee, with a death certificate. Final. |
A tombstone (dead, resting, released, retired, terminated) resolves with its ID, STATUS and CERT only, so it stays discoverable and nothing its owner signed can be activated again. See The life of a name.
CERT (registry)
| Field | Type | Rule |
|---|---|---|
certificateId | string | The current birth certificate, crt_ and a ULID. |
kind | string | Always birth: the CERT record points at the current birth certificate. |
version | integer | That certificate's version, from 1. |
fingerprint | string | sha256:<64 hex> of the whole signed certificate. See Certificates. |
url | string | https address of the certificate page. |
personaFingerprint | string, optional | sha256:<64 hex>, for persona namespaces: the fingerprint bound into the certificate. |
dnaFingerprint | string, optional | sha256:<64 hex>, for a .character: its Character DNA fingerprint, signed into the certificate. See Characters. |
issuedAt | string | When that certificate was issued. |
death | object, optional | Once the name has died, or been retired or terminated: { certificateId, fingerprint, issuedAt } of its death certificate. |
{
"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"
}GUARDIAN (registry)
Every .character publishes a GUARDIAN record: who answers for it right now. The registry writes it at birth and issues a new version whenever the people behind the character change. It names people by display name only; contact details are never published, and a record that carries one is refused.
| Field | Type | Rule |
|---|---|---|
class | string | family (registered by a person) or worker (registered by an organization). |
parent | object or null | A Family character’s Parent: { role, shownAs, displayName }. The role follows the Character DNA: parent for a humanoid, owner for a pet, sponsor for an alien, guardian for any other creature. null for a Worker. |
guardian | object or null | A Worker character’s organization, its Guardian: { displayName }. null for a Family character. |
responsibleGuardian | object | { displayName }: the one living, verified person answerable for the character now. |
status | string | active; paused when the character has no living Guardian (it does not run until someone steps in); or succeeded when a successor has taken over. |
successorDesignated | boolean | Whether the registrant has filed a guardianship designation naming a successor. |
{
"class": "worker",
"parent": null,
"guardian": {
"displayName": "Example Studio"
},
"responsibleGuardian": {
"displayName": "Jordan Lee"
},
"status": "active",
"successorDesignated": true
}{
"class": "family",
"parent": {
"role": "parent",
"shownAs": "mother",
"displayName": "Ruth Brown"
},
"guardian": null,
"responsibleGuardian": {
"displayName": "Ruth Brown"
},
"status": "active",
"successorDesignated": false
}Both passed the registry’s validateRecordData while this page was built. The same record with an email address as the Responsible Guardian’s name was refused: “GUARDIAN never carries contact details.” The Character Server checks GUARDIAN before every activation, and a paused character does not run.
ENDPOINT
| Field | Type | Rule |
|---|---|---|
url | string | Where sessions open: an https:// or wss:// address, at most 2,048 characters. portal.js opens sessions with an HTTPS POST, so an endpoint for web pages uses https://. |
modes | array | One or more of text, voice, vr, hologram. |
directions | array | One or more of go, bring: the directions the portal allows. |
protocol | string, optional | Free text, for example pm.session/1. Not checked. |
permittedActions | array | .agent only, and required there: a non-empty list of the actions the agent may take, each up to 200 characters. |
{
"url": "https://endpoint.example.com/session",
"modes": [
"text",
"voice"
],
"directions": [
"bring"
]
}{
"url": "https://agent.example.com/session",
"modes": [
"text"
],
"directions": [
"bring"
],
"protocol": "pm.session/1",
"permittedActions": [
"read invoices",
"send payment reminders"
]
}ALIAS
| Field | Type | Rule |
|---|---|---|
target | string | A valid portal name, not the name itself. It must be registered and born when the record is uploaded, and must not be retired or terminated when the name is activated. |
time | string, optional | A time frame in the registry’s grammar, such as 1944 or 30AD. See time frames. |
ALIAS may not cross root types (section 9 of the standard). An experience (.history, .event, .game, .movie, .music, .book) must point to the place it happens in, a .destination or a .realm, or to a name in its own namespace: experiences point, they do not duplicate. A name in any other namespace may point only to a name in its own namespace.
{
"target": "normandy.destination",
"time": "1944"
}PERSONA
| Field | Type | Rule |
|---|---|---|
personaVersion | string | The persona's own version, up to 64 characters. |
hash | string | sha256:<64 hex>: the persona fingerprint. It must equal the fingerprint on the current birth certificate. |
profile | string | An https address where the persona's public profile can be read. |
{
"personaVersion": "2026.10.23",
"hash": "sha256:f666f7b6e5c7b0a0c94366a7b55b902d2d3c1fb6d6d130922c70f7a274f00a24",
"profile": "https://files.theportalmesh.com/personas/f666f7b6e5c7b0a0c94366a7b55b902d2d3c1fb6d6d130922c70f7a274f00a24.json"
}See Personas for what the hash commits to and how a new persona version is certified.
CONSENT
There are two kinds of CONSENT, told apart by whether the data names an experience.
An avatar’s own. The represented person’s permission, resting on their identity verification. Required for every .avatar; it keeps the plain slot CONSENT.
| Field | Type | Rule |
|---|---|---|
verificationId | string | The id of the owner's approved identity verification, up to 64 characters. The registry checks that it exists, belongs to the owner, and passed. |
statement | string | The represented person's statement of permission. |
grantedAt | string | When permission was given, ISO 8601 UTC. |
scope | array, optional | What the permission covers. Free text, not checked. |
{
"verificationId": "ver_01JBX3Q8ZC5N7R2T4V6W8Y0A1B",
"statement": "I permit this avatar to represent me.",
"grantedAt": "2026-10-23T12:05:00.000Z",
"scope": [
"voice",
"likeness"
]
}A character’s role in an experience. An experience may use a character only with the signed permission of its Parent, Owner, Guardian or Sponsor (PNS section 10.8). The Guardian sees the role and the script and approves; the approval is a CONSENT record signed with the character’s own key, one per experience, in the slot CONSENT:<experience>. The Character Server will not activate the character inside that experience without it.
| Field | Type | Rule |
|---|---|---|
experience | string | The experience’s name, written normalized and without a scheme, in one of .history, .event, .game, .movie, .music, .book. |
role | string | The character's role in it, up to 200 characters. |
scriptHash | string | sha256:<64 hex> of the published script the Guardian approved. A new script needs a new approval. |
grantedAt | string | When the Guardian approved, ISO 8601 UTC. |
{
"name": "oasis.character",
"type": "CONSENT",
"version": 1,
"ttl": 300,
"data": {
"experience": "redseacrossing.history",
"role": "A guide who welcomes visitors on the shore",
"scriptHash": "sha256:26df9a5b7eaded97d893acd8ddf88ec9d0e026ec16ca75b3809f4468f05fa345",
"grantedAt": "2026-10-23T12:20:00.000Z"
},
"issued": "2026-10-23T12:20:00.000Z",
"kid": "pid:01JBX3Q8Z9M4T7K2V5N6R8W0YC#k1",
"signature": "EY-mZMhIqht2XlU_5gjLr9r5Fon6eW5zZqJGu6979I61INgZ7lPZmv_Np0YqL89INnCrRY-PtDn6wmtY9LpQBA"
}It verified against the name’s key while this page was built. These were refused, with the registry’s own words:
| Sent | Refused with |
|---|---|
| A character's CONSENT with no experience | A character's CONSENT is an approval for one experience: it needs experience, role and scriptHash. |
| An experience that is a place | CONSENT experience must be an experience:// name, such as redseacrossing.history. |
| No script hash | CONSENT scriptHash must be the sha256 of the published script. |
Withdrawing consent is removing the record, or replacing it for another script, through PUT /v1/names/:pid/records with "remove": [{ "type": "CONSENT", "kind": "redseacrossing.history" }]. The registry tells the Character Server at once, and the character leaves the experience immediately.
SOURCE
| Field | Type | Rule |
|---|---|---|
grounding | string | history, scripture, imaginative. |
citations | array | Each { title, url?, ref? } with a title up to 500 characters. At least one, unless the grounding is imaginative. |
documented | array, optional | For a .figure, required: what is documented. |
reconstruction | string, optional | For a .figure, required: what is reconstruction. |
note | string, optional | Free text. |
{
"grounding": "history",
"citations": [
{
"title": "Farewell Address",
"ref": "Published September 19, 1796"
}
],
"documented": [
"Served as the first President of the United States, from 1789 to 1797."
],
"reconstruction": "Voice, manner and conversational phrasing are reconstructed from his letters and contemporary accounts."
}{
"grounding": "scripture",
"citations": [
{
"title": "Revelation 21 and 22",
"ref": "Revelation 21:1 to 22:5"
}
],
"note": "A depiction, not a claim to show the actual place."
}For realms and destinations, publishing a SOURCE record opens a review by Jubilee, and the name cannot be activated until it is approved.
WORLD
| Field | Type | Rule |
|---|---|---|
scenes | array | At least one scene, { id, title }. |
modes | array | One or more of text, voice, vr, hologram. |
capabilities | object, optional | The capability profile. Free form. |
anchor | object, optional | An optional physical anchor, { lat, lon } in degrees. Approximate by design. |
timeFrames | array, optional | The frames offered: each { id, label, year?, now?, source? }. An id (up to 64 characters, unique) and a label are required; a frame is either now: true or has a whole-number year (BC negative); every frame that is not now must carry its own source: history, scripture or imaginative. |
defaultTimeFrame | string | Required with timeFrames: the id of the frame that opens when no time is asked for. |
links | array, optional | Time portals: { from, to } pairs of frame ids a visitor may jump between. |
{
"scenes": [
{
"id": "old-city",
"title": "The Old City"
},
{
"id": "mount-of-olives",
"title": "The Mount of Olives"
}
],
"modes": [
"text",
"voice",
"vr"
],
"anchor": {
"lat": 31.78,
"lon": 35.23
},
"timeFrames": [
{
"id": "30ad",
"label": "Jerusalem in 30 AD",
"year": 30,
"source": "history"
},
{
"id": "today",
"label": "Jerusalem today",
"now": true
},
{
"id": "2100ad",
"label": "Jerusalem in 2100, a depicted future",
"year": 2100,
"source": "imaginative"
}
],
"defaultTimeFrame": "today",
"links": [
{
"from": "30ad",
"to": "today"
},
{
"from": "today",
"to": "2100ad"
}
]
}TXT
Free-form verification and metadata. data may carry any fields; an optional kind (a short word, up to 64 characters) names what it is, and each kind has its own slot. The kinds below have rules the registry checks; other kinds are accepted as free form.
| kind | Required by | Fields |
|---|---|---|
rights | Movies, music and books | basis: owns or licenses; statement. |
rating | Every experience namespace | rating, a label of up to 32 characters such as PG-13, and minAge (0 to 21), which is what a door reads. In an experience namespace it also needs intensity, the default intensity (gentle, standard or full), and exit: true, the always-available exit. |
package | Movies, music and books | url (https) and an optional sha256 (sha256:<64 hex>). |
cast | History, events and games | members: a non-empty list. |
script | History, events and games | text (up to 16,000 characters) or a url (https). |
{
"kind": "rights",
"basis": "licenses",
"statement": "Example Pictures licenses this film for distribution on the Mesh."
}{
"kind": "rating",
"rating": "PG",
"minAge": 8,
"intensity": "standard",
"exit": true
}{
"kind": "package",
"url": "https://files.theportalmesh.com/packages/example-movie.zip",
"sha256": "sha256:07531963baeda377d96af8e056cb8d13b9f6e20eba0349410c0026df1b9c6053"
}{
"kind": "rating",
"rating": "PG-13",
"minAge": 13,
"intensity": "standard",
"exit": true
}{
"kind": "cast",
"members": [
{
"role": "Narrator"
},
{
"name": "eisenhower.figure",
"role": "Supreme Allied Commander"
}
]
}{
"kind": "script",
"url": "https://example.com/dday/script.json"
}TTL
A record’s ttl is how many seconds a cache may keep it. Every record must have a TTL from 30 to 86,400 seconds (one day). The registry signs its own records with these defaults, and they are the recommended values for owner records:
| Record | Default TTL, seconds |
|---|---|
| ID | 3,600 |
| ENDPOINT | 300 |
| ALIAS | 300 |
| STATUS | 60 |
| PERSONA | 300 |
| GUARDIAN | 300 |
| CONSENT | 300 |
| SOURCE | 3,600 |
| WORLD | 300 |
| CERT | 3,600 |
| TXT | 300 |
A PNS server sets Cache-Control: public, max-age= to the lowest TTL in the bundle it serves, so a bundle with a STATUS record is cached for at most a minute. Clients still check status live before opening a portal.
Signing a record yourself
The registry website signs records in your browser: it opens your key file with your passphrase, signs, and uploads. The key is held in memory only. To do the same in your own tooling, open the key file (the pm.name-key/1 format), then sign with the standalone code below, which uses canonicalize and b64urlEncode from mesh-core.js. During the build of this page it produced the same signature as the registry for the example ENDPOINT record.
// The DER prefix of an Ed25519 PKCS#8 private key (RFC 8410); the 32-byte seed follows it.
const PKCS8_PREFIX = [0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20];
function signingKeyFromSeed(seed) {
if (seed.length !== 32) throw new Error("an Ed25519 seed is 32 bytes");
const pkcs8 = new Uint8Array(48);
pkcs8.set(PKCS8_PREFIX, 0);
pkcs8.set(seed, 16);
return crypto.subtle.importKey("pkcs8", pkcs8, { name: "Ed25519" }, false, ["sign"]);
}
// unsigned = { name, type, version, ttl, data, issued, kid }; kid is "<pid>#k<n>", the name's
// CURRENT key id. Returns the envelope with its base64url signature.
async function signRecord(unsigned, seed) {
const key = await signingKeyFromSeed(seed);
const bytes = new TextEncoder().encode(canonicalize(unsigned));
const signature = new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, key, bytes));
return { ...unsigned, signature: b64urlEncode(signature) };
}// seed: the 32-byte Ed25519 seed from your key file (openKeyFile on the Registry API page).
const record = await signRecord(
{
name: "oasis.character",
type: "ENDPOINT",
version: 1, // the first ENDPOINT; later ones go above the stored version and any floor
ttl: 300,
data: { url: "https://endpoint.example.com/session", modes: ["text", "voice"], directions: ["bring"] },
issued: new Date().toISOString(),
kid: "pid:01JBX3Q8Z9M4T7K2V5N6R8W0YC#k1",
},
seed,
);The upload is the body of PUT /v1/names/:pid/records. The registry made ID, STATUS, CERT and GUARDIAN at birth; the owner sends the rest:
{
"records": [
{
"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": "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"
}
]
}