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.
When an ontology is instantiated, Synex regenerates every property id. Always
read property ids from the ontology (GET /v1/ontologies/:id → levels),
never from the template you started with.
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 id —
pass_00a1b2c3d4e5f607. This is a universal identifier:GET /v1/passports/:idresolves 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
PATCHto 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.
The series release gate. A series passport with fully-static ancestry never
inherits published — each series instance (each batch, each run) is treated as
its own release decision and stays draft until you publish it explicitly. This is
why publishing a SKU doesn't accidentally publish every batch under it.
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.