Skip to content

Data model

One chain — Naan → Manager → Shoulder → Ark — carries every NAAN. Even an organisation with a NAAN of its own goes through a shoulder. Skip that and the model forks per NAAN, and the first-digit convention holds for some NAANs and not others.

erDiagram
    NAAN ||--o{ MANAGER : "delegates a namespace to"
    NAAN ||--o{ SHOULDER : "contains"
    NAAN ||--o{ ARK : "is authoritative for"
    NAAN ||--o{ CLIENT : ""

    MANAGER ||--o{ SHOULDER : "holds"
    MANAGER |o--|| SHOULDER : "default mint target"
    MANAGER |o--o{ MANAGER : "is succeeded by"
    MANAGER ||--o{ CLIENT : ""

    SHOULDER ||--o{ ARK : "minted in"
    SHOULDER |o--o{ CLIENT : "pinned to (optional)"

    CLIENT ||--o{ CREDENTIAL : "holds"
    ARK ||--o{ MINT_RECEIPT : "receipt for"
    ARK ||--o{ ARK_CHANGE : "where it used to point"

    NAAN {
        string naan PK "N2: a string. 099999 is not 99999"
        string name
        bool   is_authoritative "D3: may answer 404 for an unknown name"
        string redirect "where to forward when not authoritative"
        string na_policy "persistence statement"
        string minter "where minting happens, if elsewhere"
    }

    MANAGER {
        int    id PK
        string naan FK
        string name "internal only; never published"
        int    default_shoulder_id FK "used when shoulder is omitted"
        string commitment_level "NLM permanence ratings"
        int    quota_per_day "R3: null means no limit"
        bool   active
        int    succeeded_by_id FK "successor; identifiers survive"
    }

    SHOULDER {
        int    id PK
        string shoulder "e.g. /x9"
        string naan FK
        int    manager_id FK "null: no organisation yet"
        string redirect "N2T: delegated resolution"
        string minter "N2T: delegated minting"
        string status "active / reserved / delegated / retired"
        string note
    }

    ARK {
        string ark PK "naan/name. never deleted"
        string naan FK
        int    shoulder_id FK
        string assigned_name
        string url "empty: return a description (D6)"
        string commitment "commitment to this object"
        string metadata
        string who "ERC"
        string what_title "ERC: title 列"
        string when "ERC"
        string created_by "R2: audit trail"
        string updated_by
    }

    CLIENT {
        int    id PK
        string client_id UK "public identifier; matched against OIDC azp"
        string naan FK
        int    manager_id FK
        string subject_type "machine / person"
        string authority "system / naan / manager"
        int    shoulder_id FK "pin to a single shoulder (optional)"
        string allowed_scopes "ark:mint ark:update ..."
        bool   active "deactivating kills tokens at once"
        date   expires_at "required when authority=naan"
    }

    CREDENTIAL {
        int    id PK
        int    client_pk FK
        string kind "api_key / client_secret / password"
        string prefix "lookup prefix; not a secret"
        string hashed "Argon2; the clear text is never stored"
        bool   active "revoked rows are kept"
        int    failed_attempts "brute-force guard"
        date   locked_until
    }

    MINT_RECEIPT {
        int    id PK
        string client_id "scoped to one principal"
        string request_id "F4: idempotency key"
        string ark FK
    }

    ARK_CHANGE {
        int    id PK
        string ark FK
        date   at
        string action "update / tombstone"
        string before_url "**what you want back**"
        string after_url
        string by
        string ip
    }

    AUDIT_EVENT {
        int    id PK
        date   at
        string client_id
        string authority
        string action "mint / update / succeed / depart …"
        string target
        json   detail
    }
    UNKNOWN_SUBJECT {
        int    id PK
        string subject "認可サーバ側の識別子(azp / client_id / sub)"
        string issuer
        date   first_seen
        date   last_seen
        int    seen
        string ip
    }

UNKNOWN_SUBJECT holds subjects whose token verified but who were not in the ledger. One wrong character in a client_id produces a 401, and at the moment of rejection arkhe already holds the right string — azp has passed signature verification by then. Keeping it lets an operator register without retyping, and the row disappears from the list once registered (the match is made per query, so nothing has to be cleaned up).

It has no foreign keys: which organisation it belongs to is unknowable from the token, and arkhe does not guess — which is why only NAAN-wide principals see the list.

ARK_CHANGE records where an ARK used to point, separately from the audit log. The audit log keeps only what reaches NAAN scope or wider, but minting and repointing are done by organisations, so the audit log alone loses exactly the changes that matter. Declaring NR and saying the identifier does not change means being able to show what changed, when and by whom — otherwise the promise cannot be checked from outside.

AUDIT_EVENT has no foreign keys into the rest. A record should outlive what it describes, and referential integrity would push the other way: it makes deleting the record the easy way out.

What the diagram cannot show

An ER diagram shows shape. In arkhe the design lives in the constraints.

An ARK is never deleted Deleting the row stops resolution — the identifier breaks. before_delete refuses. When a target is lost you tombstone it, or empty url so a description is returned
A shoulder is never deleted either Random assignment could hand out the same string again — the seed of an NR violation. Set status=retired
retired is one-way Reviving a retired namespace cannot rule out that something outside used the name meanwhile
Minting never becomes an update A primary key collision must fail. This was the worst defect in arklet
Reach is a registration attribute authority, manager_id, shoulder_id and allowed_scopes come from the client registration; no request or token grant widens them
People and machines are separate machine subjects cannot be named through external login; person subjects cannot hold API keys
A circular reference manager.default_shoulder_id ⇄ shoulder.manager_id. PostgreSQL wants the target to exist at CREATE TABLE, so the constraint is added afterwards with use_alter

On capacity

Child resources are never minted. Suffix passthrough covers a reference of any depth — ark:/99999/x9abc/page/3 needs no row of its own — so one record per minting is enough. Nothing else matters as much for capacity.