Data model

Synex lets you define your own product data model instead of forcing your products into a fixed SKU → Batch → Unit hierarchy. You draw a tree of levels, choose how each level relates to its parent, and Synex turns that into fast, addressable digital product passports. This page explains the whole model — read it once and the rest of the API falls into place.

Templates, ontologies, passports

Three resources form a pipeline from schema to data:

  • Name
    Template
    Type
    the blueprint
    Description

    A reusable schema you shape in a draft, then complete to freeze it. Templates aren't tied to any live data, so you can fork and share them. See Templates.

  • Name
    Ontology
    Type
    the live schema
    Description

    A completed template, instantiated for your team. This is the schema your real data is validated against. Creating one automatically creates its root passport. See Ontologies.

  • Name
    Passport
    Type
    the data
    Description

    A single product record at one level of an ontology — a SKU, a batch, an individual unit. Passports hold property values and a publish status. See Passports.

The lifecycle is always the same: draft a template → complete it → instantiate an ontology → create passports under the root.

Levels and depth letters

An ontology is a tree of levels. Each level is assigned a short depth letter by walking the tree breadth-first: the root is always A, its children B, C, and so on (a level key is capped at four characters). Depth letters are how you address data — you create a passport "at level B", list passports "at path B", and so on.

Every level carries its property definitions (the fields a passport at that level can hold) and a type that decides how many passports can exist there and how they're addressed.

The three element types

Every level is one of three types. The type is the single most important modeling choice — it sets the level's cardinality (how many can exist per parent) and its behavior on read and publish.

Singular — exactly one

A singular level has exactly one passport in its context. The root of every ontology is singular — think of it as the SKU or product definition. Use singular levels for one-of-a-kind sections: a spec sheet, a certificate, a controller. Creating a second passport at a singular level under the same parent fails with 409 passport_exists_at_level.

Series — an enumerated set of releases

A series level holds a set of instances where each instance is a deliberate release — a manufacturing batch, a custom order, a production run. You can have many, but each one is created on purpose and published on purpose. Series levels are treated specially at publish time (see Publish status below) and are not auto-traversed when a passport is rendered.

Many — an unbounded collection

A many level is an array: an unbounded collection of near-identical children — the individual cells in a battery pack, the cylinders in an engine block, every unit shipped. There's no uniqueness constraint, and reads page through all of them so nothing is silently truncated.

Composition rules

The types nest with a few rules that keep the tree efficient:

  • The root is always singular (the SKU). It can contain any mix of singular, series, and many children.
  • Singular and series levels can contain singular, series, or many children.
  • A many level can only contain many or singular descendants — never a series. (A collection of units can't each own their own batch.)
  • Sibling series levels are allowed as long as they represent a different separation (e.g. Batch vs. Custom Order).

Passport paths and the universal id

Every passport has two addresses:

  • A stable, prefixed idpass_00a1b2c3d4e5f607. This is a universal identifier: GET /v1/passports/:id resolves any passport at any depth, in a single lookup, without you knowing its ontology or path. It's the id encoded in the QR code.
  • A path within its ontology — the depth letter plus lineage. You list children at a path (GET /v1/ontologies/:id/passports?path=B) or resolve an exact path (GET /v1/ontologies/:id/passports/find?path=A).

Under the hood, that path is a compact sort key called the TPPMP — described next.

TPPMP: how passports are addressed

TPPMP (Typed-Prefixed Partial Materialized Path) is the encoding Synex uses for a passport's path. You rarely construct one by hand, but understanding it explains why paths look the way they do and why lookups stay fast at any depth.

The rules, given a passport's depth letter and its ancestry:

  • Name
    Fully static
    Type
    {depth}
    Description

    The passport and all its ancestors are singular. The path is just the depth letter — e.g. the root is A. There's only one, globally.

  • Name
    Static
    Type
    {depth}:{self}
    Description

    All ancestors are singular, but this level is dynamic (series/many). The path embeds only the passport's own snowflake — e.g. B:07c9….

  • Name
    Dynamic singular
    Type
    {depth}:{ancestor}
    Description

    A singular level with a series/many ancestor. The path embeds the closest dynamic ancestor's snowflake.

  • Name
    Dynamic series / many
    Type
    {depth}:{parent}/{self}
    Description

    A series or many level under a dynamic parent. The path embeds the parent's snowflake and the passport's own — e.g. C:07c9…/1a4f….

The leading letter is the typed prefix (it recovers the level). It's partial because only the closest dynamic ancestor is embedded — static singular ancestors collapse away, keeping the key short. That's the "materialized path" trick that lets Synex enumerate a level's children with a single prefix query and address billions of passports without ever scanning.

Worked example

A battery ontology with three levels — SKU (A, singular) → Batch (B, series) → Unit (C, many):

Example paths

A                       # the SKU — the root passport (fully static)
B:07c9f2a1b3d4e5f6      # a batch — series, static ancestry ({depth}:{self})
C:07c9f2a1b3d4e5f6/1a4f0b2c3d4e5f60   # a unit under that batch ({depth}:{parent}/{self})

To list every batch, Synex queries paths beginning B:. To list the units in one batch, it queries paths beginning C:07c9…/. Each is a single, fast prefix lookup — never a scan.

Publish status and inheritance

Each passport has a publish_status. You set it explicitly, or leave it to inherit from its ancestors.

  • Name
    published
    Type
    status
    Description

    Visible on the public passport page.

  • Name
    draft
    Type
    status
    Description

    Not public. The default effective status when nothing up the chain is published.

  • Name
    archived
    Type
    status
    Description

    Retired; cascades to descendants.

  • Name
    automatic
    Type
    write-only
    Description

    Sent on PATCH to clear an explicit status so the passport inherits again.

Resolution walks up the parent chain (up to 25 hops): a passport's own explicit status always wins; when it's unset, the nearest ancestor's effective status is inherited. draft and archived cascade straight through.

Reads expose both the raw publish_status (what's explicitly set, possibly null) and publish_status_resolved (the effective status after inheritance), so you can always tell why a passport is or isn't public.

Discoverability

When Synex renders a public passport, it assembles data from related levels — but not all of them. Each level has a discoverable set: its singular and many children, plus its parent. Series children are deliberately excluded, because a batch is a release you navigate to on purpose, not something to inline into its parent's page. This keeps a SKU's public passport from trying to embed thousands of batches and millions of units.

Where to go next

  • Templates — draft, complete, and fork the blueprint.
  • Ontologies — instantiate a schema and read its levels.
  • Passports — create, address, value, and publish records.
  • PassQL and Lab — query and bulk-edit passports at scale.

Was this page helpful?