DataSchema
The DataSchema is the contract that drives three product surfaces at runtime — browse cards, search & filters, detail page. It lives in the project-schemas collection and validates the data blob on every records row.
A schema is “good” when scanning a card answers what is this and why should I care? in a glance, search returns relevant matches without opaque-ID noise, and the detail page surfaces what the card couldn’t fit.
AnnotatedField
type AnnotatedField = {
key: string
label: string
type: 'string' | 'number' | 'boolean' | 'date' | 'url' | 'image' | 'enum' | 'array'
searchable: boolean
facetable: boolean
role?: Role | null
cardVisibility?: 'primary' | 'secondary' | 'detail-only' | null
enumValues?: { value: string }[]
required: boolean
min?: number
max?: number
}type—string | number | boolean | date | url | image | enum | array. There is nolocation/geotype — geo is handled by the auto-pair onlat/lngcolumns. There is no multi-value-enum — compound values like"Mon, Tue"need preprocessing in the source CSV.searchable— included in the Orama full-text index.facetable— exposed as a sidebar facet on/explore.
Roles
role is the universal vocabulary that decides where a field renders. The role taxonomy is shared across every card style and detail template; the card style picks which zones it exposes.
Universal slots
| Role | Renders as |
|---|---|
title | Card and detail header (required identifier) |
subtitle | Inline under the title (e.g. business type, industry, neighbourhood) |
summary | Card body snippet (2–3 lines) |
image | Hero photo (also accepts video / Lottie) |
logo | Square brand mark |
rating | Numeric 0–5 trust signal |
ratingCount | Review count next to the rating |
verified | Boolean trust badge |
category | Primary tag (multi-value via array) |
factPrimary | Top-tier fact (salary, price, funding) |
factSecondary | Second-tier fact (hours, distance) |
actionPrimary | Main URL action (Apply, Visit) |
actionSecondary | Phone, directions |
timestamp | Drives freshness chips, feed sort, expiry |
pricingTier | Structured pricing data (tier preview chip + matrix on detail) |
awardBadge | Trophy / leader / certification |
Kit-specific slots
Operators opt into a kit at setup based on the archetype (local biz / asset gallery / API marketplace / AI generation / …). Kits enable additional roles like license, format, contentRating, leadScore, signalPill, sloChip, rateLimit, recipe, parentRecord, version. Kits are bundles, not new component trees — they just toggle which optional slots/sections render.
cardVisibility
Solves the “we have 30 fields, what shows on the card?” problem.
| Value | Meaning |
|---|---|
primary | Must show on card; ordered first |
secondary | Show on card if budget allows |
detail-only | Only on detail page |
null | Auto-rank via heuristic (default) |
Per-card-style budgets (hardcoded):
| Style | Max badges | Max meta |
|---|---|---|
media | 3 | 2 |
logo | 4 | 3 |
minimal | 2 | 1 |
| Detail page | unlimited | unlimited |
Auto-inference at import
When an operator runs shipmore schema infer, the platform proposes types, roles, searchable, and facetable from key names + value distributions:
| Heuristic | Inferred role |
|---|---|
name / title / company field name | title |
Number with rating / score in key, range 0–5 | rating |
Number with count / reviews / views in key | ratingCount |
| URL field, first one | actionPrimary |
| URL field, additional | actionSecondary |
| Image field, square aspect | logo |
| Image field, landscape | image |
Boolean with verified / claimed semantic | verified |
| Enum / array, low cardinality, facetable | category |
| Currency-like number, not 0–5 range | factPrimary |
Auto-inference is best-effort. The agent and operator iterate via shipmore schema field set/drop and re-run schema apply.
Separation of concerns
| Concern | Collection | Owns |
|---|---|---|
| Validation contract | project-schemas | Shape of records.data. Pure JSON Schema. |
| Source projection | import-mappings | Source column renames, lat/lng extraction, ignores. |
| Rows | records | The data. data validated against the DataSchema at write time. |
This split lets you re-run an import after a column rename without touching the schema, and update the schema without re-importing.