Skip to content
Registry StackDocsDevelopment (unreleased)

Issue registered-parent evidence from OpenCRVS

For the assertion provider

View as Markdown

Complete Assert a role-bound relationship before starting this tutorial. You will apply that pattern to the public OpenCRVS Farajaland demo and answer two questions from the same registered birth. First verify, “Is this candidate registered as a parent of this child?” Then return the recorded mother and father national IDs for a caller authorized to receive them.

Outcome
Verified relationship and parent-identifier assertions from a synthetic OpenCRVS birth.
Time
About 30 minutes
Level
Institution source with synthetic data
Prerequisites
The completed role-bound relationship tutorialThe Evidence Gateway toolsetAccess to the public OpenCRVS democurl and an editor

This tutorial uses synthetic data from a public demonstration system. Do not substitute a real person. The demo maintainers can reset its data, accounts, and integrations.

The tutorial uses this provider-published synthetic registration:

FieldValueUse
ChildJosh HoegerFind the registration in the web application
Child national ID3617402568Select the registered birth through the Events API
Tracking IDYADU1NCross-check the registration in the web application
Registration numberCJWULMP0B6Z5Cross-check the registration in the web application

The registered birth contains an authenticated national ID for Josh’s mother. It reports that the father’s details are unavailable. You will copy the mother’s national ID from the web application and use it as the candidate-parent selector.

The relationship question has two distinct subject roles:

  • child, selected by the child’s national ID
  • candidate-parent, selected by the candidate’s national ID

The source searches for one registered birth by child.nid and reads the bounded mother.nid and father.nid fields when present. One question compares those values with a candidate and returns only a boolean. A second question returns the parent identifiers in a closed, role-labeled value. The two questions use separate purposes and disclosure rules.

Open the Farajaland registration application and sign in with the provider-published Registrar account:

Username: k.mweene
Password: test
Two-factor code: 000000

Search for 3617402568, YADU1N, or CJWULMP0B6Z5. Open Josh Hoeger’s registered birth and note the mother’s national ID. Keep it outside the tracked project. You can also use this account to create another synthetic registration.

The human account cannot authorize the Events API. Create a separate OAuth client for the tutorial instead of sharing an existing client ID or secret.

In your own OpenCRVS deployment, sign in with a National System Admin account. OpenCRVS publishes this account for the Farajaland demo:

Username: j.campbell
Password: test
Two-factor code: 000000

In Configuration, open Integrations, select Create client, and choose Record Search. Give the client a name that identifies your tutorial run. Copy the client ID and secret when OpenCRVS displays them. The secret is shown once.

The client can search records. It cannot create, update, or delete registrations. OpenCRVS audits its searches and applies a daily request limit. Review the OpenCRVS guidance for creating a client and Record Search clients before using this pattern outside the demo.

Do not put the OAuth credentials, access token, or a live response in source control. The private credentials used to validate this page are not part of the tutorial or repository.

Create a working directory and download the reviewed tutorial subset of the OpenCRVS Events API:

Terminal window
mkdir opencrvs-parent-relationship
cd opencrvs-parent-relationship
curl -fsSLo opencrvs-events.openapi.yaml \
https://docs.registrystack.org/examples/evidence/opencrvs-events-search.openapi.yaml

OpenCRVS does not publish this bounded subset at a stable URL. Registry Stack maintains it for the tutorial and records its upstream review revision in the file. Review it against the target deployment before production use.

Create an editable Evidence Gateway project and disposable local keys:

Terminal window
evidencectl new registered-parent \
--openapi opencrvs-events.openapi.yaml \
--profile local
cd registered-parent

The command retains the OpenAPI document and creates empty authoring directories. It does not invent the source policy, relationship semantics, or question.

Select only the fields needed to establish cardinality, validate the birth, and compare its registered parents:

Terminal window
evidencectl source suggest \
--project . \
--source-id opencrvs-birth-parents \
--operation 'POST /events/search' \
--select /total \
--select '/results/*/type' \
--select '/results/*/status' \
--select '/results/*/declaration/child.nid' \
--select '/results/*/declaration/mother.nid' \
--select '/results/*/declaration/mother.verified' \
--select '/results/*/declaration/father.nid' \
--select '/results/*/declaration/father.verified'

The command writes one source, two scripts, and three schemas. They remain editable and cannot run until you supply the OpenCRVS-specific decisions.

Create selectors/opencrvs-national-id-v1.yaml:

maximumAggregateBytes: 128
fields:
national_id:
type: string
minimumBytes: 1
maximumBytes: 128

The same bounded field can identify the child and candidate parent, but their roles remain distinct. A caller cannot omit a role or substitute another selector field.

Replace sources/opencrvs-birth-parents.yaml with:

transport: http-json
baseUrl: https://events.farajaland-integration.opencrvs.dev
posture: record-transformed
authentication:
kind: oauth2-client-credentials
tokenEndpoint: https://auth.farajaland-integration.opencrvs.dev/token
clientIdRef: secret:file/opencrvs-client-id
clientSecretRef: secret:file/opencrvs-client-secret
scope: recordsearch
credentialPlacement: form-body
maximumCacheSeconds: 300
assumedLifetimeSeconds: 600
request:
method: POST
path: /events/search
fixedHeaders:
- name: Accept
value: application/json
selectorInputs:
- role: child
alternatives:
- profile: opencrvs-national-id-v1
fields: [national_id]
prepareScript: adapters/opencrvs-birth-parents-prepare.rhai
adapterParameters:
childSelectorRole: child
eventType: birth
registeredStatus: REGISTERED
childNationalIdField: child.nid
motherNationalIdField: mother.nid
motherVerificationField: mother.verified
fatherNationalIdField: father.nid
fatherVerificationField: father.verified
authenticatedValue: authenticated
resultLimit: 2
resultOffset: 0
adapterParametersSchema: schemas/opencrvs-birth-parents-parameters.schema.yaml
preparationLimits:
query: forbidden
jsonBody: required
maximumJsonDepth: 12
maximumCollectionItems: 32
maximumStringBytes: 512
maximumNormalizedBytes: 8192
projection:
- /total
- /results/*/type
- /results/*/status
- /results/*/declaration/child.nid
- /results/*/declaration/mother.nid
- /results/*/declaration/mother.verified
- /results/*/declaration/father.nid
- /results/*/declaration/father.verified
redirects: deny
timeoutMilliseconds: 10000
maximumResponseBytes: 262144
concurrencyLimit: 8
responseSchema: schemas/opencrvs-birth-parents-response.schema.yaml
extractScript: adapters/opencrvs-birth-parents-extract.rhai
factSchema: schemas/opencrvs-birth-parents-facts.schema.yaml

record-transformed states that OpenCRVS returns a bounded birth record and Evidence Gateway reduces it before disclosure. The fixed event type and status prevent the caller from turning this source into a general search proxy. A two-result ceiling lets extraction distinguish one match from an ambiguous national ID without paging through the registry.

The token endpoint accepts credentials in the form body. OpenCRVS omits an expiry from this demo’s token response, so the source assumes 600 seconds and clamps caching to 300 seconds.

Read the two values without echoing them and write owner-only files without a trailing newline:

Terminal window
umask 077
printf 'OpenCRVS client ID: ' >&2
IFS= read -rs OPENCRVS_CLIENT_ID
printf '\n' >&2
printf '%s' "$OPENCRVS_CLIENT_ID" > secrets/opencrvs-client-id
unset OPENCRVS_CLIENT_ID
printf 'OpenCRVS client secret: ' >&2
IFS= read -rs OPENCRVS_CLIENT_SECRET
printf '\n' >&2
printf '%s' "$OPENCRVS_CLIENT_SECRET" > secrets/opencrvs-client-secret
unset OPENCRVS_CLIENT_SECRET

Paste each value copied from OpenCRVS at its prompt and press Enter. The generated .gitignore excludes the secrets directory.

Evidence Gateway resolves these logical secret references beneath that directory. It does not read the credentials from source YAML, requests, command arguments, or logs.

Replace adapters/opencrvs-birth-parents-prepare.rhai with:

fn prepare(selectors, context) {
let parameters = context["parameters"];
let child = selectors[parameters["childSelectorRole"]];
let data = #{};
data[parameters["childNationalIdField"]] = #{
type: "exact",
term: child["values"]["national_id"]
};
#{
query: [],
body: #{
query: #{
type: "and",
clauses: [#{
eventType: parameters["eventType"],
status: #{
type: "exact",
term: parameters["registeredStatus"]
},
data: data
}]
},
limit: parameters["resultLimit"],
offset: parameters["resultOffset"]
}
}
}

The script can read the authorized child selector and fixed parameters. It cannot read the candidate parent, credentials, caller identity, purpose, or signing keys.

Replace schemas/opencrvs-birth-parents-parameters.schema.yaml with:

type: object
additionalProperties: false
required:
- childSelectorRole
- eventType
- registeredStatus
- childNationalIdField
- motherNationalIdField
- motherVerificationField
- fatherNationalIdField
- fatherVerificationField
- authenticatedValue
- resultLimit
- resultOffset
properties:
childSelectorRole: {const: child}
eventType: {const: birth}
registeredStatus: {const: REGISTERED}
childNationalIdField: {const: child.nid}
motherNationalIdField: {const: mother.nid}
motherVerificationField: {const: mother.verified}
fatherNationalIdField: {const: father.nid}
fatherVerificationField: {const: father.verified}
authenticatedValue: {const: authenticated}
resultLimit: {const: 2}
resultOffset: {const: 0}

The schema closes every parameter around its reviewed value. A later edit cannot silently change the event, status, search field, parent fields, verification value, or page size.

The generated response schema closes the projected response around total, at most two results, and the selected declaration fields. Replace adapters/opencrvs-birth-parents-extract.rhai with:

fn extract(source_response, context) {
let parameters = context["parameters"];
let total = source_response["total"];
let results = source_response["results"];
if results.len > parameters["resultLimit"] {
throw("source_protocol_error");
}
if total == 0 {
if results.len != 0 { throw("source_protocol_error"); }
return #{outcome: "no_match"};
}
if total > 1 {
if results.len < 2 { throw("source_protocol_error"); }
return #{outcome: "ambiguous"};
}
if results.len != 1 { throw("source_protocol_error"); }
let result = results[0];
if result["type"] != parameters["eventType"] ||
result["status"] != parameters["registeredStatus"] ||
!result.contains("declaration") {
throw("source_protocol_error");
}
let declaration = result["declaration"];
let child_field = parameters["childNationalIdField"];
if !declaration.contains(child_field) {
throw("source_protocol_error");
}
let parents = [];
let mother_field = parameters["motherNationalIdField"];
if declaration.contains(mother_field) {
let verification = parameters["motherVerificationField"];
if !declaration.contains(verification) ||
declaration[verification] != parameters["authenticatedValue"] {
throw("source_protocol_error");
}
parents.push(#{
role: "mother",
national_id: declaration[mother_field]
});
}
let father_field = parameters["fatherNationalIdField"];
if declaration.contains(father_field) {
let verification = parameters["fatherVerificationField"];
if !declaration.contains(verification) ||
declaration[verification] != parameters["authenticatedValue"] {
throw("source_protocol_error");
}
let father_id = declaration[father_field];
if parents.len == 1 && parents[0]["national_id"] == father_id {
throw("source_protocol_error");
}
parents.push(#{role: "father", national_id: father_id});
}
if parents.len == 0 || parents.len > 2 {
throw("source_protocol_error");
}
#{
outcome: "match",
facts: #{
child_national_id: declaration[child_field],
registered_parents: parents
}
}
}

The script refuses inconsistent cardinality, incomplete births, and parent identifiers that the record does not mark as authenticated. An absent parent identifier is not converted into an invented parent. The source must establish at least one authenticated parent before the question can run.

Replace schemas/opencrvs-birth-parents-facts.schema.yaml with:

type: object
additionalProperties: false
required: [child_national_id, registered_parents]
properties:
child_national_id:
type: string
minLength: 1
maxLength: 128
registered_parents:
type: array
minItems: 1
maxItems: 2
uniqueItems: true
items:
type: object
additionalProperties: false
required: [role, national_id]
properties:
role:
type: string
enum: [mother, father]
national_id:
type: string
minLength: 1
maxLength: 128

These facts are the complete input to both questions. Names, dates, addresses, and other birth fields cannot reach either derivation.

Create questions/registered-parent.yaml:

id: registered-parent
question: Is the candidate registered as a parent of the child?
purpose: relationship-check
subjects:
- role: child
selector: national_id
profile: opencrvs-national-id-v1
derivation: true
- role: candidate-parent
selector: national_id
profile: opencrvs-national-id-v1
derivation: true
source:
ref: opencrvs-birth-parents
answers:
- concept: relationship_confirmed
type: boolean
derivation: derivations/registered-parent.rhai
disclosure:
allow: [relationship_confirmed]

The question declares the complete role set. derivation: true gives the script the child needed to verify the returned birth and the candidate needed for the comparison. The source search itself uses only the child. The question governs the one allowed purpose, answer, and disclosure. Sharing a selector profile does not make the two roles interchangeable.

Create derivations/registered-parent.rhai:

fn answer(facts, selectors, context) {
if facts.child_national_id != selectors.child.values.national_id {
throw("derivation_input_error");
}
let candidate = selectors["candidate-parent"]["values"]["national_id"];
let confirmed = false;
for parent in facts.registered_parents {
if parent["national_id"] == candidate {
confirmed = true;
}
}
#{relationship_confirmed: confirmed}
}

The first check binds the returned birth to the requested child. The comparison checks the candidate against at most two authenticated parent identifiers, then returns only the declared boolean.

Create schemas/registered-parent-national-ids.schema.yaml:

$id: urn:registrystack:evidence:local:schema:registered-parent-national-ids:v1
type: object
additionalProperties: false
required: [parents]
properties:
parents:
type: array
minItems: 1
maxItems: 2
uniqueItems: true
items:
type: object
additionalProperties: false
required: [role, national_id]
properties:
role:
type: string
enum: [mother, father]
national_id:
type: string
minLength: 1
maxLength: 128

This closed schema preserves each parent’s role and rejects extra fields. The bounded array holds one or two role-labeled identifiers. The extraction script refuses to produce facts unless the source establishes at least one authenticated parent identifier.

Create questions/registered-parent-identifiers.yaml:

id: registered-parent-identifiers
question: Which national IDs are recorded for the child's parents?
purpose: family-case-record
subject:
role: child
selector: national_id
profile: opencrvs-national-id-v1
derivation: true
source:
ref: opencrvs-birth-parents
answers:
- concept: registered_parent_national_ids
type: reviewed-structured-value
schema: schemas/registered-parent-national-ids.schema.yaml
maximumSerializedBytes: 512
derivation: derivations/registered-parent-identifiers.rhai
disclosure:
allow: [registered_parent_national_ids]

This question has no candidate-parent subject. The caller asks for the registered identifiers and must be authorized for the separate family-case-record purpose. In production, grant this question only to relying parties with a justified need for the identifiers. The boolean question remains available to callers that need only a relationship decision.

Create derivations/registered-parent-identifiers.rhai:

fn answer(facts, selectors, context) {
if facts.child_national_id != selectors.child.values.national_id {
throw("derivation_input_error");
}
#{
registered_parent_national_ids: #{
form: "reviewed-structured-value",
schema: "urn:registrystack:evidence:local:schema:registered-parent-national-ids:v1",
fields: #{parents: facts.registered_parents}
}
}
}

Evidence Gateway validates the returned object against the reviewed schema before signing it. The derivation cannot add names, dates, addresses, or any other birth-registration field.

Compile the editable artifacts into a private local generation and start Evidence Gateway with Registry Mint:

Terminal window
evidencectl dev --detach
Evidence Gateway ready at http://127.0.0.1:8080
Mint ready at http://127.0.0.1:8081

dev runs the real Evidence Gateway configuration check before either service becomes ready. Unresolved draft markers, invalid schemas, inconsistent roles, or missing credentials stop the start.

Read the mother’s national ID without leaving its value in shell history:

Terminal window
printf 'Mother national ID: ' >&2
IFS= read -rs OPENCRVS_MOTHER_NID
printf '\n' >&2

Prepare a request using Josh’s national ID and that candidate-parent value:

Terminal window
evidencectl request prepare registered-parent \
--purpose relationship-check \
--subject child:national_id=3617402568 \
--subject "candidate-parent:national_id=$OPENCRVS_MOTHER_NID" \
--name opencrvs-parent
unset OPENCRVS_MOTHER_NID

Use this only with the public synthetic record. In a real deployment, selectors need an input path that does not expose identifiers in shell history or process arguments.

Preparation obtains short-lived local authorization and records verification expectations before a response exists. It does not contact OpenCRVS.

Send the request across the Evidence Gateway HTTP boundary:

Terminal window
curl -fsS \
--config .evidence/requests/opencrvs-parent/authorization.curl \
--request POST \
--url http://127.0.0.1:8080/v1/evidence \
--header 'Content-Type: application/json' \
--header 'Accept: application/jose+json' \
--data-binary @.evidence/requests/opencrvs-parent/request.json \
--output opencrvs-parent.jws.json

Evidence Gateway exchanges its client credentials for an OpenCRVS token, performs the fixed child search, projects the bounded parent fields, derives the relationship, and returns a signed flattened JWS. The caller cannot alter the event type, status, projection, or parent verification rule.

Verify the response against the expectations created before the request:

Terminal window
evidencectl verify opencrvs-parent.jws.json \
--context .evidence/requests/opencrvs-parent/verification.json \
--output opencrvs-parent.verified.json
VERIFIED

Inspect the verified payload:

Terminal window
python3 -m json.tool opencrvs-parent.verified.json

Using the mother recorded on Josh’s birth produces this supported value:

{
"providesValueFor": "urn:registrystack:evidence:local:concept:registered-parent:relationship_confirmed",
"value": true
}

The assertion contains pseudonymous child and candidate-parent bindings. It does not contain either national ID, the tracking ID, registration number, names, birth date, parent list, or exact source response. The civil-registration record stayed behind the source boundary.

Prepare the second question. This request needs only the child because the answer returns the recorded parent identifiers instead of comparing one with a candidate:

Terminal window
evidencectl request prepare registered-parent-identifiers \
--purpose family-case-record \
--subject child:national_id=3617402568 \
--name opencrvs-parent-identifiers

Send the request across the same HTTP boundary:

Terminal window
curl -fsS \
--config .evidence/requests/opencrvs-parent-identifiers/authorization.curl \
--request POST \
--url http://127.0.0.1:8080/v1/evidence \
--header 'Content-Type: application/json' \
--header 'Accept: application/jose+json' \
--data-binary @.evidence/requests/opencrvs-parent-identifiers/request.json \
--output opencrvs-parent-identifiers.jws.json

Verify the signature and the expectations recorded for this question before reading its value:

Terminal window
evidencectl verify opencrvs-parent-identifiers.jws.json \
--context .evidence/requests/opencrvs-parent-identifiers/verification.json \
--output opencrvs-parent-identifiers.verified.json
VERIFIED

Inspect the verified payload:

Terminal window
python3 -m json.tool opencrvs-parent-identifiers.verified.json

Josh’s public registration has an authenticated mother identifier but reports the father’s details as unavailable. Its supported value therefore contains the mother field only:

{
"providesValueFor": "urn:registrystack:evidence:local:concept:registered-parent-identifiers:registered_parent_national_ids",
"value": {
"form": "reviewed-structured-value",
"schema": "urn:registrystack:evidence:local:schema:registered-parent-national-ids:v1",
"fields": {
"parents": [
{
"role": "mother",
"national_id": "<mother-national-id>"
}
]
}
}
}

For a synthetic registration with two authenticated parent identifiers, parents contains a second item with role father. You can create such a registration in the demo and prepare the same question with that child’s national ID.

This assertion intentionally discloses identifiers. Store and transmit the verified result as sensitive civil-registration data. Callers that need only a relationship decision must use the boolean question instead.

Stop the services, verify the audit chain, and remove the sealed local generation:

Terminal window
evidencectl dev stop
evidencectl audit show --last-operation
evidencectl dev clean

The audit identifies each authorized question, purpose, requester pseudonym, decision, and disclosed concept. It does not record selectors, the OpenCRVS token, source response, boolean, or returned national IDs.

Keep the project, Record Search client, and two local credential files if you are continuing to the birth-certificate tutorial. Otherwise, delete the client in OpenCRVS and remove the credential files when you finish.