Skip to content

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

urn:phpboyscout:scope:continuity:01JQ8Z3F7K2M4N6P8R0T2V4W6X
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.