URN grammar¶
The grammar lives in gitlab.com/phpboyscout/go/cloudevents/urn, a package that depends on nothing
but go/errors. Anything can parse an identifier without taking
the codec, and go/schema cites this package rather than
restating the format — two copies with two test suites drift while both stay green.
Schema identifiers¶
urn:phpboyscout:schema:orders.created:3
└─┬─┘ └────┬────┘ └─┬──┘ └─────┬──────┘ ┴
urn estate namespace name version
This is what a CloudEvent's dataschema attribute carries.
s, err := urn.ParseSchema("urn:phpboyscout:schema:orders.created:3")
// s.Name == "orders.created", s.Version == 3
// s.String() returns the input, exactly
Name. Dot-separated segments. Each segment starts with a lower-case letter and continues with lower-case letters, digits, hyphens or underscores. The grammar is narrow because a schema name appears in log lines, in metric labels and in directory paths, and every character class allowed here is one somebody downstream has to escape.
Version. A positive integer, written without a leading zero — 3, never 03, v3.1.0 or
2026-08-28. It is part of identity because the CloudEvents specification requires an incompatible
change to get a different URI. It carries no compatibility meaning: semantic versioning would
imply a promise nothing checks. See go/schema spec 0001 D8 and D9 — D9 settles the form, D8 the
consequence.
It must be written in canonical form: 3, never 03 or +3. Two spellings of one version would be
two identifiers for one schema, and the immutability guarantee is stated in terms of the
identifier.
Scope identifiers¶
s, err := urn.ParseScope("urn:phpboyscout:scope:continuity:01JQ8Z3F7K2M4N6P8R0T2V4W6X")
// s.Kind == "continuity", s.ID == "01JQ8Z3F7K2M4N6P8R0T2V4W6X"
ADVISORY, AND NEVER THE AUTHORITY
A scope is a claim in a message, not a boundary. It is for routing, filtering and
legibility. Enforcement is NATS accounts with subject-scoped credentials, and a consumer that
filters on scope has isolated nothing.
The attribute is called scope rather than tenant partly for this reason: tenant sounds
like isolation, scope sounds like routing, and whoever misuses it will not have read this
page.
Kind. A lower-case identifier: account, continuity, installation. The kind is explicit
because there is no single boundary to name. In scoutdm, commerce is scoped by account, play by
continuity, and reference content by neither — it is grant-controlled, because a book is legitimately
readable by accounts with nothing to do with each other. A single unqualified attribute would assert
that every service has one boundary and that all of them mean the same thing by it.
Identifier. ASCII letters, digits, hyphens and underscores. Deliberately narrow, so a display name cannot be put in one without the grammar noticing.
Errors¶
| Sentinel | When |
|---|---|
urn.ErrNotAURN |
Not a urn:phpboyscout identifier, or in a different namespace |
urn.ErrMalformed |
Ours, but the wrong number of colon-separated parts |
urn.ErrInvalidName |
Schema name breaks the grammar |
urn.ErrInvalidVersion |
Version is not a positive integer without a leading zero |
urn.ErrInvalidKind |
Scope kind breaks the grammar |
urn.ErrInvalidID |
Scope identifier breaks the grammar |
ErrNotAURN is separate from the shape errors on purpose. It is the one a caller usually handles
rather than reports: a dataschema pointing at a wiki page today and a registry URN tomorrow is a
migration in progress, not a defect.
Why a URN rather than an HTTPS URL¶
A dataschema outlives the infrastructure that served it. Once a message carries one it is in logs
and object stores for years, long after a registry has been renamed, moved or replaced — and every
one of those historical messages would then point at a dead host.
A URN separates identity from location, which is what the specification's own wording asks for ("Identifies the schema"). Resolution becomes configuration: embedded, cached, or over the network.
The cost is that it is not dereferenceable. Somebody outside the estate holding one of our messages cannot fetch the schema without knowing our resolver. That is acceptable for internal traffic; it would not be if we published events externally. See Why a URN and not a URL.