Categories + tags
How catalog navigation works — a closed 10-bucket category enum (primary nav) plus optional free-form tags (search/filter only).
The head-unit's mini-app store needs to render predictable buckets, not free-form chaos. So the manifest has two distinct fields:
| Field | Authority | Cardinality | Purpose |
|---|---|---|---|
category | Closed enum (10 slugs) | exactly 1 per app | Primary navigation — drives the chip rail |
tags[] | Free-form | 0–8 per app | Search / refinement (no UI surface in v1) |
A category is mandatory at publish time. Tags are optional — schema accepts them now so future search can lean on them, but the v1 head-unit chip rail filters on category alone.
The 10 categories
| Slug | What goes here |
|---|---|
navigation | Maps, traffic, parking finder |
media | Music, podcasts, audiobooks |
vehicle | Car status, diagnostics, climate, EV-charge planner |
productivity | Calendar, notes, todos |
communication | Messaging, voice, call |
entertainment | Games, trivia |
services | Fuel, charging, food, payments |
lifestyle | Weather, news, sports |
developer | Admin tools, diagnostics, internal |
other | Fallback — only if nothing above fits |
Pick the closest match. Reaching for other regularly is a signal
the enum needs a new slug — open an issue.
Why a closed enum
Free-form categories cause a predictable UX failure: 30 apps that all call themselves "productivity" plus 3 that say "Productivity" plus 5 that say "tools" plus 2 that say "utility". The store ends up with chip-rail soup, fuzzy search becomes load-bearing, and "can't find apps" becomes a support ticket.
Closing the enum forces the conversation up-front. Adding a new
category becomes a SDK + backend lockstep PR (the slug list lives
in category-slugs.json, vendored across both repos with a CI
drift check) — intentional friction so the list doesn't sprawl.
How the chip rail consumes this
The head-unit fetches GET /api/v1/mini-apps/categories once on
store-tab open (5-minute ETag cache) and renders a chip per slug,
plus an "All" chip at the head. Slugs aren't hard-coded into
the Flutter binary — adding a category is a backend deploy, not
a Flutter release.
Filtering happens client-side over the already-fetched catalog
until the production catalog hits 200 apps; past that threshold
the chip-tap re-fetches with ?category=<slug> (server-side
filter, JSONB extract). See the SDK plan
for the migration trigger.
Tags
Optional, free-form, lowercase + hyphens, ≤ 24 chars, ≤ 8 entries:
{
"category": "media",
"tags": ["offline", "lossless", "kid-safe"]
}Use them when a single category undersells the app (a "navigation" app that's also "ev-routing-aware" — the category puts it in the right chip; the tag surfaces it in a future search).
No tag-filter UI in v1. Schema accepts them so a future search feature can lean on them, but the head-unit chip rail filters by category alone today.
Editor's picks / "Featured" / "New this week"
Out of scope for v1. Editorial sections are a separate concern from
self-declared categorization — they need a curation surface (admin
UI), an app_sections table, and ranking signals. When the store
grows enough to warrant human curation, that ships as its own
effort. For v1, the store renders categorical sections ("Music
apps", "Navigation apps") which are just ?category=... filters
under the hood.
Related
- Manifest reference — schema
for
categoryandtagsplus everything else. - App icons recipe — the other half of storefront metadata.
- Theme categories — the parallel closed enum for themes (11 slugs).