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 how it is traversed on read.

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 runs

A series level holds a set of instances where each instance is a deliberate, separate run — a manufacturing batch, a custom order, a production run. You can have many, but each one is created on purpose. Series levels are not auto-traversed when a passport is rendered (see Discoverability).

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).

Property types

Every property definition has a type, which decides the values it accepts and how PassQL can filter it:

TypeHolds
short_text, long_textText — up to 255 and 7,000 characters per language. Translatable.
tagsA list of short strings. Translatable.
fileA reference to an uploaded file. Translatable — one file per language.
imageA reference to an uploaded image.
number, measurementA number; a measurement's unit is part of its definition.
rangeA numeric span.
booleantrue or false.
dateA calendar day, stored as YYYY-MM-DD.
compositeA value computed from other properties. Not writable.
categoryA divider that groups the properties after it. Holds no value.

There is no chart type. A chart is a section of the ontology's composition that plots ordinary numeric property values, which can come from any level of the passport.

Passport paths and the universal id

Every passport has two addresses:

  • A stable, prefixed id — pass_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
    released
    Type
    status
    Description

    Live: what the public passport page serves and what a regulator may read. Explicit only — never inherited.

  • Name
    draft
    Type
    status
    Description

    Not released. The default effective status when nothing up the chain says otherwise.

  • 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 with an explicit status decides: draft and archived cascade down from it, but released does not — a passport with no status of its own under a released ancestor is draft. With nothing set anywhere, the status is draft.

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 released.

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 run 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.

Reporting granularity

The EU registry knows a product at up to three tiers: the model, the batch, and the individual item. An ontology maps its own levels onto them:

  • A level's granularity — model, batch, item, or null for a level that is internal only — says which tier its passports report as. Tiers nest strictly (an item inside a batch inside a model), and only a series or many level can open batch or item.
  • Where several levels share a tier, the one marked is_core is the level whose passports the registry is given as that tier's ID.
  • A property's data_element_id names the registry field its values are reported as. A property without one isn't reported.

A full passport read resolves this upward: its granularity block names the passport it reports as for each tier. Releasing a passport at the item tier's core level is what the team is billed for. Levels, cores, and mappings are defined on the ontology; the passport side is under Full reads.

Translated values

Text-bearing properties — short_text, long_text, tags, and file — hold one value per language, in any of the 24 official EU languages. Every ontology has a default language, the one its values are authored in: a value written without a language lands in it, and a read that asks for none returns it.

Reads pick a language with ?lang=, fall back to the default language when a translation is missing — and say so with is_fallback — and return every translation with ?lang=all. Writes merge per language, so editing the German text never touches the French. In PassQL, lang: and @ choose which translation a query matches. The full rules are under Translated values.

Where to go next

  • Templates — draft, complete, and fork the blueprint.
  • Ontologies — instantiate a schema and read its levels.
  • Passports — create, address, value, and release records.
  • Compositions — lay out the public passport, charts included.
  • PassQL and Lab — query and bulk-edit passports at scale.

Was this page helpful?