The registry uses three SQLite tables. A person is stored once; their NIN and BVN, if they have them, live in separate tables linked back to that person. This mirrors how the real documents work — a NIN and a BVN both describe the same underlying person, but one doesn’t require the other.

persons

The shared biodata record. Every enrolled person has exactly one row here, regardless of which documents they’ve been issued.

nin_records

bvn_records

Tiered access to the photo field

GET /api/nin/{nin} and GET /api/bvn/{bvn} return every column shown above, including photo_path/photo_source, in one response. For callers that need to model a real registry’s tiered access — validate a number/name match first, only fetch the enrollment photo after ownership has been proven some other way (e.g. a live face comparison) — two additional endpoints are available per document type:
  • GET /api/nin/{nin}/validate / GET /api/bvn/{bvn}/validate — every field above except photo_path and photo_source.
  • GET /api/nin/{nin}/photo / GET /api/bvn/{bvn}/photo — only photo_path and photo_source.
The original unsplit endpoints are unchanged and remain available for callers that don’t need this separation (e.g. the dashboard client).

NIN/BVN number format

Both are randomly generated 11-digit strings that don’t start with 0, matching the real-world format. Uniqueness is checked against the local database only — there’s no relationship to real NIMC or NIBSS numbering.
The full schema, including constraints, is in server/src/db/schema.sql.