Skip to content

Specification

Portolan Specification — Core

The keywords MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 [RFC 2119] [RFC 8174] when, and only when, they appear in all capitals, as shown here.

This document defines the requirements that apply to every Portolan catalog, regardless of data format. Format-specific requirements live in formats.md.

Introduction

The goal of Portolan is to make it easier and cheaper for data providers to publish their data, and to make that data more accessible to both humans and agents. If Portolan is successful then all the world's spatial data will be much easier to query and utilize in decisions that affect all of our lives.

The center of Portolan is the SpatioTemporal Asset Catalog specification - every Portolan implementation is a STAC catalog, and can be used with any STAC tooling. But where STAC can be used with any data format and can be implemented as an API or files on cloud storage, Portolan requires well-formed cloud-native formats, stored as files on browser-accessible online services. All Portolan implementaions are static catalogs - there is no 'Portolan API' like the STAC API, as many of the key benefits of Portolan, like scalability and lower cost, are achieved by the full embrace of cloud-native geospatial.

Portolan further requires that all catalogs follow the best practices to guide AI agents to make use of them. Today that means adding an agents.md file to every catalog and collection and building up a set of 'skills' that guide agents to make use of the cloud-native formats. In the future this will likely evolve as the general best practices for agents continue to advance.

The specification aims to standardize the minimum requirements for a 'great' catalog, and to provide guidance to go beyond that. But really the hope is that everyone goes well beyond the minimum and tries to make each catalog better than before. The aspiration of the authors of the specification is to build a real community of collaborators who are all working together to build great data catalogs and share tools and best practices so all can benefit.

Core Structure

A Portolan catalog is a directory of STAC metadata and cloud-native geospatial data. It MUST be a valid STAC catalog following STAC 1.1.0, with a root catalog.json always required, even for a single collection.

A catalog is a tree of catalogs, collections, items, and assets: catalogs and collections are internal nodes, items are leaves, and assets (data files) MUST sit at the collection or item level. Catalogs organize only and MAY nest into sub-catalogs; collections MUST be one level deep, containing only items or assets, never nested collections. Every catalog and sub-catalog MUST contain a catalog.json, an AGENTS.md, and a README.md; every collection MUST contain a collection.json, an AGENTS.md, and a README.md.

A catalog has two entry points: catalog.json for machines (required at the root) and README.md for humans. It is fully navigable; every catalog, collection, and item links to the rest, so any object is reachable from any other (see Links).

project/
├── catalog.json
├── AGENTS.md
├── README.md
└── {collection_id}/
    ├── collection.json
    ├── AGENTS.md
    ├── README.md
    └── {item_id}/
        └── data.parquet

Conformance and Versioning

A Portolan object declares conformance through the Portolan STAC extension, whose versioned schema URI carries the specification version the object was authored against.

Every catalog and collection MUST declare the versioned Portolan schema URI (e.g. https://schemas.portolan-sdi.org/portolan/v0.2.0/schema.json) in its stac_extensions array. The schema URI is the single signal of specification version; no separate version property is defined. Declaration happens at the catalog and collection level only, since assets cannot carry stac_extensions, and items inherit conformance from their collection.

The specification version MUST NOT be conflated with the dataset version. Dataset versioning, where used, MUST use the STAC version extension.

All objects within a catalog SHOULD declare the same Portolan schema URI. A validator MUST flag any catalog or collection whose declared URI differs from the root catalog's; such a mismatch is a warning, not an error, and a mixed-version catalog remains valid.

Declaring the Portolan extension is a claim of conformance, not proof of it. An object conforms to this specification only by passing the Portolan validator. Validation runs in separable passes:

  • Structural validation against the STAC 1.1.0 schemas.
  • Portolan metadata validation, covering every requirement checkable from metadata alone.
  • Portolan data validation, covering requirements that require reading asset bytes, such as GeoParquet spatial ordering, row-group statistics, and embedded COG statistics.

The metadata passes MUST be executable without reading asset data; the data pass MAY be run independently.

Portolan schemas are published at schemas.portolan-sdi.org. versions.json is a tooling artifact and is NOT REQUIRED in a catalog for v0.1.

Consistent with the reuse-first approach, Portolan datasets use established STAC extensions wherever they apply rather than re-encoding the same information in portolan. Which extensions are required versus recommended, the exact schema URIs and versions to pin, and their usage are defined normatively by the Portolan STAC Profile (see stac/), not restated here.

Catalogs

The canonical entrypoint is a STAC catalog.json in the catalog root, which MUST be valid. Catalogs MAY use nested sub-catalogs for organization but MUST NOT use nested collections.

Collections

Each collection lives in a subdirectory named with its collection ID. For a nested collection the ID is a POSIX path (e.g. environment/air-quality) and the directory sits under one or more intermediate catalog directories (see Nested Catalogs, Flat Collections). A collection directory MUST contain a collection.json and holds one subdirectory per item. Collection IDs SHOULD contain only lowercase letters, numbers, hyphens, and underscores, start with a letter, and be unique within the catalog.

Single-File Collections

When a collection holds a single data file (e.g. one GeoParquet file), that data MUST be represented as a collection-level asset — no item directory or item JSON is needed (see Vector). Alongside the collection.json and the .parquet, such a collection may optionally carry a .pmtiles, a thumbnail.png, and a styles/ directory.

Raster Collections

A collection holding multiple raster scenes MUST model each scene as an item carrying its COG as an item-level asset; scene COGs MUST NOT be listed as collection-level assets. Per-scene items are what let each scene carry its own footprint and acquisition time, which a flat asset list cannot express. A collection holding a single COG follows the single-file rule above: it MUST expose that COG as a collection-level asset with no item directory (see Raster).

Nested Catalogs, Flat Collections

Portolan organizes hierarchy with nested catalogs, not nested collections: intermediate levels are catalogs (catalog.json) and collections are always leaves. A collection MUST NOT contain a child collection. This keeps collections flat for STAC API compatibility while still allowing thematic organization above them. A nested collection's ID is its POSIX path from the catalog root (e.g. environment/air-quality); portolan add writes a catalog.json at each intermediate level and links parent to child down to the leaf collection.json.

In directory terms, the root holds the root catalog.json, a level above a collection holds an intermediate (thematic) catalog.json, a data directory holds the leaf collection.json, and a subdirectory within a collection may hold a catalog.json that organizes many items. Deep nesting is allowed, with every level above the leaf a catalog; a catalog may also appear below a collection to organize its items (for example, a raster collection grouping items by year). A catalog or collection with twenty or more children SHOULD organize them into subcatalogs, thematic or otherwise, so users can browse the data.

environment/
├── catalog.json              ← intermediate catalog
├── air-quality/
│   ├── collection.json       ← leaf collection (data)
│   └── pm25.parquet
└── water-quality/
    ├── collection.json       ← leaf collection (data)
    └── turbidity.parquet

A collection.json with a child collection subdirectory beneath it is invalid.

Items

Most collections represent their data as one or more STAC items, each with its own metadata and assets. Portolan adds nothing to the STAC 1.1 Item beyond core: an item MUST be a valid STAC item, carrying an id, a geometry and bbox (or null geometry for non-spatial data), a datetime or a start_datetime/end_datetime interval, its assets, and the structural links defined under Links. The datetime clause restates STAC 1.1 core validity and is enforced by structural validation; the Portolan-level guidance to carry an explicit datetime lives under Temporal Metadata as a SHOULD. Item-level detail that other extensions already cover — file, raster, vector — MUST use those extensions rather than being re-specified here.

Items are not always required. A single-file collection has none: its data is a collection-level asset (see Single-File Collections). A partitioned collection MAY represent each partition as an item, but this is not required — see Partitioned Collections for when it is worthwhile. An ordinary collection with multiple distinct data files represents each as an item.

Assets

Every asset MUST include an href, the URI to the data, with relative or absolute both allowed for now. Where an asset href is absolute, it MUST use https rather than s3, since browsers cannot fetch s3 URLs directly. An asset SHOULD also provide s3 or other cloud-native URLs through the alternate extension, so tools that prefer direct bucket access can use them. Portolan additionally requires a type, meaning the media type, and at least one role on every asset. STAC leaves both optional, but a validator and a browser cannot do their jobs without them, so Portolan makes them mandatory. The title and description fields are optional but recommended.

The required media type for each core format is:

Format Media type
GeoParquet and plain Parquet application/vnd.apache.parquet
COG image/tiff; application=geotiff; profile=cloud-optimized
PMTiles application/vnd.pmtiles
COPC application/vnd.laszip+copc
Thumbnail image/png, image/jpeg, or image/webp

Roles describe what each asset is for. Portolan uses the standard STAC role names wherever they fit and requires at least one per asset: data for the primary GeoParquet, COG, or Parquet; visual for the PMTiles rendering derivative; thumbnail for the preview image; metadata for sidecar metadata, with iso-19115 used specifically for an ISO 19115 file. Multiple roles on one asset are fine — use all that apply. The style role, used for MapLibre style files, is a Portolan-defined role rather than a standard STAC one (see Visualization Styles).

The source role identifies an upstream original from which a cloud-native asset in this catalog was derived. For example, a publisher that converts an upstream Shapefile or GeoPackage to GeoParquet may include the original file as a source asset. PORTO-FMT-002 says that a mirror SHOULD include the original when it is directly downloadable rather than available only through an API. This means linking to the upstream file; the publisher does not need to retain, rehost, or redistribute it. A source asset is exempt from the format requirements because it represents the upstream original and may use a non-cloud-native format. A catalog that provides metadata for data that is already available upstream in a cloud-native format does not need a source asset; its data asset points directly to the upstream data. collection-mirror identifies the STAC-GeoParquet copy of a collection's items, as specified under Item mirror in formats.md.

Portolan reuses existing extensions rather than restating their fields: file for file:values, table for schema and columns, raster and vector for band and layer detail, license for per-asset license, and scientific for citation or DOI.

Assets SHOULD carry file:size and file:checksum from the file extension. Both are cheap to compute for bytes the publisher uploads, and they let a client budget a download and verify what it received. Neither is required, because a catalog that describes data it does not host cannot always produce them, and a fabricated value is worse than an absent one.

A file:checksum MUST use multihash encoding, not a raw sha256 string. Any file:size and file:checksum an asset carries MUST match the bytes its href resolves to. A stale value is a conformance failure, so a publisher that cannot keep one current should omit it.

Primary-vs-alternate. A non-cloud-native representation MAY be included as an alternate asset as long as an equivalent cloud-native primary asset exists (e.g. a GeoJSON alongside the required GeoParquet). The cloud-native asset is the primary; the rest are alternates.

Every catalog, collection, and item MUST include the structural links that make the tree navigable: a catalog or collection MUST include root and parent links (except the root catalog, which has no parent) and a child or item link for every object it contains; an item MUST include root, parent, and collection links. Every structural link MUST carry a type of application/json (or application/geo+json for links to items).

Structural links can be relative or absolute; Portolan takes no position. Relative links keep a catalog portable, so the same tree of files validates locally, in staging, and in production without a rewrite. Absolute links name their location outright, which some hosts and clients handle more reliably. The STAC best practices on the use of links walk through the choice, and Git-Backed Catalogs covers how it plays out for a catalog maintained in git.

A catalog served over the internet from a single fixed URL SHOULD carry an absolute self link on its root catalog. The link records the canonical location, so someone holding a downloaded copy can see where it came from, and a tool can resolve the catalog's relative references into URLs from the metadata alone. STAC requires that a self link be absolute.

Every link in a catalog MUST resolve. A validator MUST resolve each relative link against the catalog's own file tree, whether on a local filesystem or on object storage, and confirm the referenced file is present and is the correct object, rather than merely checking that the href is present or well-formed. A link that fails to resolve, or resolves to the wrong object, is a conformance failure.

A validator resolves an absolute structural link through the base that the root self link names. It strips that base from the href. It then resolves the remainder against the file tree, as it does for a relative href.

A catalog that carries absolute structural links without a root self link gives a validator no base. The validator reports nothing for those links, because it cannot place them. An href that does not start with the base gets the same treatment. A root catalog that links a child in a different bucket therefore gets no check on that link.

The requirement to resolve still holds. A publisher who wants the check keeps a root self link.

Provenance (via) links are covered under Source Provenance.

Alternate-Language Trees

A catalog MAY publish one STAC object tree for each language. The root catalog links to each translated root with an alternate link, as defined by the STAC Language extension.

A translated tree represents the same catalog in another language. It is not part of the containment tree. A catalog MUST NOT link to a translated root with child or item.

Each language tree has its own root catalog. That root has no parent link. Every root link in the tree points to that root catalog. Equivalent objects keep the same ID across language trees.

A validator MUST treat a translated root as the start of a separate language tree. It MUST NOT report missing containment links or duplicate IDs between language trees.

Publishers choose which languages and objects to translate. Multilingual Catalogs gives maintenance guidance.

Human-Readable Titles

STAC Browser and other clients render child and item link titles directly; without them a client must fetch every child just to display its name. Portolan therefore requires human-readable titles throughout: every catalog.json and collection.json MUST have a non-empty title and description; titles MUST be human-readable, so a raw slug (snake_case) or a technical namespace prefix (ns:LayerName) is not acceptable; and every child and item link MUST include a title. A validator checks readability heuristically; it MUST flag a title that fails the check, and because heuristics misfire, the finding is a warning, not an error.

Bounding Boxes and Spatial Extent

Bounding boxes carry the spatial footprint that drives extent unions and map-UI browsing, and garbage coordinates poison the catalog-level extent and break viewers. So every bbox — catalog extent, collection extent, and item — MUST:

  • contain no NaN or infinite values (including 3D elevation coordinates);
  • contain no sentinel "effectively infinite" values (e.g. ±1.79e308);
  • use only WGS84 coordinates in range (longitude -180 to 180, latitude -90 to 90) with south ≤ north; and
  • in a 3D bbox, order the vertical axis with minimum elevation ≤ maximum elevation.

STAC requires extent.spatial.bbox for collections; for tabular (non-geospatial) collections it represents the area of interest the data pertains to, not a geometric footprint (see Tabular).

Temporal Metadata

Items SHOULD carry an explicit datetime (or a start_datetime / end_datetime interval) describing when the data applies.

Data Storage

Cloud-native formats depend on clients fetching only the bytes they need, so Portolan catalogs assume data is hosted in cloud object storage reachable over HTTP range requests (S3-compatible or otherwise).

The requirements in this section apply to servers hosting the catalog's own cloud-native assets: the copies of the data that the publisher has uploaded and controls. They do not apply to upstream sources hosted by third parties.

A server hosting the catalog's cloud-native assets MUST support range requests: honor the Range header, return 206 Partial Content, and advertise Accept-Ranges: bytes; HEAD requests MUST return an accurate Content-Length. The server MUST support HTTP/1.1 or greater; HTTP/2 or /3 is RECOMMENDED. Endpoints that compress or transform responses in ways that break range semantics are not conformant.

To let browser clients read data directly, a server hosting the catalog's cloud-native assets MUST also enable CORS on all metadata and asset files: Access-Control-Allow-Origin: * (or an equivalent read-permitting policy), allowed methods including GET and HEAD, allowed request headers including Range, If-Match, If-Modified-Since, If-None-Match, and If-Unmodified-Since (via Access-Control-Allow-Headers), and exposed response headers including Content-Type, Content-Length, Content-Range, Accept-Ranges, and ETag (via Access-Control-Expose-Headers).

The conditional-request headers let a browser revalidate a cached range instead of refetching it, which keeps tile and range readers cheap on repeat views. Provider-specific headers such as x-amz-* and x-goog-* fall outside this requirement, since reading a public catalog takes no signed request. Portolan leaves cloud-specific configuration to separate guidance.

Catalogs also reference bytes that the publisher does not host, including upstream sources that a mirror complements and original files carried over from an upstream source. Because those servers are operated by third parties and are outside the publisher's control, the requirements above do not apply to them.

A validator MUST probe the servers hosting the catalog's own cloud-native assets and MUST NOT require upstream servers to satisfy these requirements. An upstream server that does not support range requests or provide the required CORS headers limits the capabilities available to clients using that upstream copy, but does not make the catalog non-conformant.

Providers

Every collection MUST identify the parties responsible for its data through the STAC providers field. The list MUST include at least one provider with the producer role, the organization that originally captured or created the data, and exactly one provider with the host role, listed as the last element. The host is the organization responsible for operating and maintaining this copy of the catalog and its data, not the underlying cloud vendor whose storage it happens to sit on; a catalog on S3 maintained by a city GIS office lists the office as host, not AWS. Providers with the processor and licensor roles SHOULD be included where they apply. A single organization MAY hold multiple roles; a self-published dataset will typically list one provider as both producer and host. Catalogs MAY also declare providers, but the collection-level declaration is authoritative.

The host provider MUST include contact information: a url pointing to a page where the maintainer can be reached, or an email field with a maintainer address. email is not a core STAC field but is a valid extension of the provider object, and the Portolan schema enforces that at least one of the two is present. This is the maintainer-contact requirement for every Portolan catalog; a catalog whose host provides neither is non-conformant. A collection MAY additionally use the contacts extension for richer contact information, but doing so does not replace the url-or-email requirement on the host provider.

Source Provenance

Often a Portolan catalog is a mirror that complements an official source with cloud-native formats rather than being the source itself, and users need to trace back to the original. Every Portolan catalog is therefore one of two kinds:

  • Official — published by the originating authority for the data. The catalog is a canonical home for the data, not a copy of one.
  • Mirror — a cloud-native copy that complements a source maintained elsewhere. The mirror exists to add open formats, better access, or discoverability; the authoritative copy lives at the source.

Which kind a catalog is, is derived from its providers, not declared through any dedicated property. A catalog is official when its producer and host are the same organization; it is a mirror when they differ. An organization hosting data it did not produce is a mirror under this definition even when it is the primary distributor; the via link then simply records where the data originated.

A mirror MUST include a via link (type text/html) pointing to the original source. When that source publishes its own STAC catalog, the mirror MUST also include a canonical link pointing to the source's STAC, so consumers and agents can follow the chain to the authoritative metadata, not just a landing page. Whether the source publishes a STAC catalog cannot be determined from the mirror's metadata alone, so a validator MAY surface a mirror without a canonical link as informational, never as a failure. The via and canonical links MAY both be present and point at different targets (a human page and a STAC root). An official catalog carries no via or canonical link to an upstream source, because it is the source.

A mirror MUST record when it was last synced from its source by setting the top-level updated field (RFC 3339) at each synced catalog and collection to the time of the sync. A sync is a metadata update, so this reuses the core STAC field with no Portolan-specific addition; a consumer judges the freshness of a copy by comparing updated against the source. Sync cadence and richer source-freshness metadata are deferred to a future version.

License

Every collection MUST declare a license in its collection.json. The value MUST be an SPDX license identifier (e.g. CC-BY-4.0, Apache-2.0), or the STAC value other when no SPDX identifier fits, in which case the collection MUST include a license link (rel: license) pointing to the license text. The deprecated STAC 1.1 value proprietary MUST NOT be used. A collection whose data is genuinely restricted still declares its license explicitly rather than omitting it.

AGENTS.md

Every catalog and collection MUST include an AGENTS.md in Markdown, referenced in the STAC links array (rel: "agents", type: text/markdown). It is a link, not an asset — it describes the data, it is not the data. Beyond existing and being linked, its content is open-ended; the aim is to help agents use the data well, covering access patterns (base URLs, S3 paths, code examples), useful aggregations and queries, data-quality notes, related collections, and schema or coordinate-system conventions — anything non-obvious. See best-practices for guidance on what makes a good AGENTS.md.

README.md

Every catalog and collection MUST have a README.md in Markdown, referenced in the STAC links array (rel: "describedby", type: text/markdown). Like AGENTS.md it is a link, not an asset — it describes the data, it is not the data. describedby is the IANA-registered relation for a resource carrying a description of the linked resource, and is already common in STAC. The README.md MUST contain at minimum a title, description, license, and data provenance.

A registry lists many catalogs side by side. A logo makes each one recognizable before the reader has read a word. The root catalog MAY publish a logo as a link with rel: "icon".

STAC defines no logo relation. icon is registered with IANA, and stac-js and STAC Browser already read it. STAC uses preview for a preview of the data itself, so preview is not the relation for a logo. A collection publishes its data preview as a thumbnail asset, described under Visualization.

An icon link MUST declare a type, and that type MUST be one of image/apng, image/avif, image/gif, image/jpeg, image/png, image/svg+xml, or image/webp. A client drops an icon whose media type it does not recognize, so an undeclared type renders nowhere. stac-js rejects image/svg+xml. STAC Browser therefore does not show an SVG logo.

An icon link SHOULD carry a title. A page that renders the logo needs the title as the image's accessible label. Only the publisher knows what the logo shows.

The href SHOULD be relative, which keeps the catalog portable for the reason Links gives. A catalog stores the image under _assets/ beside the root catalog.json. An absolute href to an external host stays valid, and a publisher who serves one logo across several catalogs will use one.

Metadata

Collections SHOULD include machine-readable metadata (e.g. Apache Ossie) when they have many coded or categorical variables or complex classification schemes, and SHOULD include column descriptions, which will likely become a requirement as tooling matures.

Visualization

Every geospatial collection MUST provide a way to render its data with zero infrastructure, either by:

  • rendering from source — the data asset is small and simple enough for clients to draw directly (e.g. a small GeoParquet, or a COG that is already display-ready, whether through an embedded color table or as a continuous raster colorized at draw time via the render extension and the required min/max statistics); or
  • a visualization derivative — an additional cloud-native product optimized for rendering, published as a collection-level asset with roles: ["visual"]; PMTiles is the recommended vector format today.

A consumer decides by inspection: use a visual asset if present, otherwise render from source. Non-geospatial collections are exempt. Whether an asset is small and simple enough to render from source is not decidable from metadata, so a validator cannot enforce this requirement directly; it MAY advise, as informational, when a large vector collection lacks a visual derivative. Collections MUST include a thumbnail generated from default styling and MUST provide visualization styles as standalone STAC assets appropriate to the render path, except where the render path is self-rendering (a display-ready COG or a small GeoParquet drawn directly), which needs no separate style file.

Visualization Styles

When a collection provides a render path it MUST provide at least one style telling clients how to draw it, in a format appropriate to that render path. Style files are STAC assets: each style MUST be registered as a collection-level asset carrying the style role, alongside the data and thumbnail. A client or agent discovers a collection's styles by filtering assets on that role, so no separate manifest is needed and this specification defines none. Because STAC assets are an unordered JSON object, and because STAC states that asset keys carry no meaning a client is expected to understand, the default style is identified by a second role rather than by position or key: when a collection provides more than one style, exactly one style asset MUST carry both style and default in its roles. STAC encourages multiple roles per asset, so this needs no extension. Asset keys stay free-form — style-<variant> keys such as style-labeled are the convention — and every style carries a human-readable name in its title. Where a collection provides exactly one style, that style is the default and needs no marker, though carrying the default role there too is encouraged so a client can find the default the same way everywhere.

The concrete style format is defined per data format in formats.md: for vector (PMTiles) it is a MapLibre GL style file, while raster styling is still under discussion (see specs/incubating/raster-styling.md).