Reference
Schema
The source of truth is schemas/entity.schema.json (JSON Schema draft-07). Everyentities/{id}/entity.json is validated against it on every PR. Nothing malformed reaches main.
Design decisions
Categories are tags, not folders
Every entity lives in one flat entities/{id}/ directory, and categories is an array. This is the single most important structural choice in the schema.
A folder hierarchy would force a false primary category. Paytm is a UPI app, a wallet, a BNPL provider, a payments bank, a wealthtech platform and a payment gateway — genuinely, all at once. Filing it under one of those makes the other five queries fail to find it, and picking which one is "primary" is arbitrary.
Tags also make the taxonomy additive: a new category is a new enum value, not a repository reshuffle.
id is permanent
The id is the folder name, the npm export key, and part of every CDN URL. Once published it must never change, because a rename breaks every hotlinked image in the wild.
This is why rebrands are modelled with status: 'rebranded' and acquired_byrather than by renaming the folder.
Logo values may be null
logos.full is a required key, but its value may be null. An entity with verified metadata and no artwork yet is a genuinely useful record — it carries the IFSC prefix, the UPI handles, the category tags. Requiring artwork before accepting an entity would either block those contributions or push contributors toward submitting bad logos.null means "not sourced yet" and is an honest, valid state.
Nullable identifier fields
fip_id, fiu_id, ifsc_prefix and upi_handlesaccept null as well as a value. The distinction is deliberate: absentmeans nobody has looked into it; null means someone checked and could not verify it. In a dataset where a wrong FIP ID silently breaks an account aggregator consent flow, that difference is worth encoding.
Validation
Some invariants span files, so scripts/validate.mjs enforces them separately:
| Check | Severity |
|---|---|
Folder name matches the id field | error |
id values are unique | error |
acquired_by resolves to an entity in the dataset | error |
| Declared logo files exist on disk and are non-empty | error |
SVGs have a viewBox, no raster embeds, no <script>, under 50KB | error |
The category enum matches schemas/categories.json | error |
SVGs use hex rather than rgb() | warning |
| Mono variants use a single fill colour | warning |
Account aggregators carry an aa_id | warning |
Errors fail CI. Warnings never do — they flag things worth a human's attention without blocking a contribution that is otherwise fine.
Generated output
scripts/generate-index.mjs builds three files. All are generated — never hand-edit them.
| File | Contents |
|---|---|
dist/index.json | Full records with resolved CDN URLs, plus category_counts |
dist/index-lite.json | id, name, short_name, categories, brand_color |
dist/categories.json | The taxonomy with live counts |
Adding a category
- Add the id to the
categoriesenum inentity.schema.json - Add the full definition to
schemas/categories.json - Add the union member to
packages/Finmarks/src/types.ts - Run
pnpm validate— it fails if the schema and the taxonomy have drifted apart