Skip to content

Publishing your app's data scope (hds-dataset.json)

If your app stores its users’ health data in Health Data Safe, a practitioner who wants to follow those users needs to know exactly which data your app collects: which items, which ones are set once (profile-like) and which are recorded repeatedly, and how often. Instead of re-creating that by hand, publish it once as a small JSON file, a data-set template. The practitioner pastes its URL into the Doctor Dashboard, gets a data set that matches your app, and receives a permanent invite link that your app can show its users.

  1. You publish hds-dataset.json at an HTTPS URL next to your app.
  2. The practitioner pastes the URL in the Doctor Dashboard (New data set → Import from an app’s data-set URL). The dashboard validates the file and lists any item that does not resolve against the HDS data model. The practitioner can adjust the texts and save.
  3. The data set stays linked to your URL. Each time the practitioner opens it, the dashboard re-checks the file and shows what changed if you published a new version. Nothing is applied until the practitioner reviews and saves.
  4. On the data set’s patients page, the practitioner creates a permanent link: one invite, without expiry, that any number of your users can accept. Your app can embed it.

The format is the hds-lib AppTemplate with publication fields. JSON Schema: healthdatasafe.github.io/hds-lib-js/schemas/appTemplate.json (pinned per release at …/hds-lib-js/v<version>/schemas/appTemplate.json).

{
"$schema": "https://healthdatasafe.github.io/hds-lib-js/schemas/appTemplate.json",
"format": "hds-dataset-template",
"formatVersion": 1,
"id": "cycle-app",
"version": "1.0.0",
"publishedAt": "2026-10-05",
"app": {
"id": "cycle-app",
"name": { "en": "Cycle App" },
"publisher": "Example Clinic",
"url": "https://example.org/cycle-app/",
"contact": "mailto:support@example.org"
},
"title": { "en": "Cycle chart" },
"description": { "en": "Daily fertility-awareness log" },
"consent": { "en": "I share my chart read-only with my practitioner." },
"chat": true,
"sections": [
{
"key": "cycle", "type": "recurring", "name": { "en": "Cycle landmarks" },
"itemKeys": ["fertility-cycles-start"],
"itemCustomizations": {
"fertility-cycles-start": {
"required": true,
"reminder": { "expectedInterval": { "min": "P21D", "max": "P35D" }, "importance": "must" }
}
}
},
{
"key": "daily", "type": "recurring", "name": { "en": "Daily observations" },
"itemKeys": ["body-vulva-bleeding", "body-vulva-mucus-inspect", "body-temperature-basal", "body-urine-hormones-lh"],
"itemCustomizations": { "body-temperature-basal": { "required": true, "repeatable": "P1D" } }
}
],
"existingStreamRefs": [
{ "streamId": "cycle-app-notes", "permissions": ["read"], "purpose": "app-private", "label": { "en": "Daily notes" } }
]
}
FieldMeaning
format, formatVersion"hds-dataset-template", 1. Once format is set, version and app are required.
versionSemver of the file’s content.
appWho publishes the data: id, name, publisher, url (https), optional contact, appVersion.
sections[]type: "permanent" for data set once (profile-like), "recurring" for repeated records. itemKeys are data-model item keys.
itemCustomizations[itemKey]How often: repeatable (once | any | unlimited | an ISO-8601 duration such as P1D), reminder (expected interval, importance), required (boolean), labels.
consentThe consent text you propose; the practitioner may edit it.
existingStreamRefs[]Your own data that is not in the data model: purpose: "app-private", permissions: ["read"], a label. The practitioner gets read access and sees the raw entries; they are never shown as form fields.
requiredBridges[]Data bridges the data set expects (e.g. bridge-mira).

Use data-model items wherever one exists. Unknown keys are reported to the practitioner and not collected; deprecated keys still work but the dashboard suggests their replacement. If your app records something the data model lacks, keep it in an app-private stream and ask for the item.

Bump version on every change:

  • major: the scope narrows or a grant changes (an item removed, moved between permanent and recurring, an existingStreamRef added, removed or with other permissions, or chat switched on or off). Users who already consented no longer match the published scope.
  • minor: additive (items or custom fields added) or a frequency change (repeatable, reminder, required).
  • patch: texts only (titles, descriptions, consent, labels).

The dashboard also fingerprints the scope, so a change published without a version bump is detected and flagged to the practitioner. In this release a change applies to new invites; users who already accepted keep the version they consented to.

  • HTTPS, publicly readable, no authentication.
  • Access-Control-Allow-Origin: * (the dashboard fetches it from the browser).
  • A short Cache-Control (minutes), and under 256 KB.
  • Convention: <your app's base URL>/hds-dataset.json; a root-hosted app may also serve /.well-known/hds-dataset.json.

GitHub Pages meets all of this as is: put the file in the folder that is published (e.g. public/ for a Vite app).

Section titled “Embedding the practitioner’s permanent link”

The practitioner gives you either form of the link shown on their data set’s patients page:

  • the patient link (https://app.hds.ngo/?action=invite&capabilityUrl=…), which opens the HDS patient app; or
  • the raw capability URL, if your app has its own invite screen and accepts CMC invites itself.

Keep it in runtime configuration (e.g. your app’s settings.json) rather than in code, so the link can be changed when the practitioner regenerates it. Regenerating invalidates the previous link; users who already accepted are not affected.

The file grants nothing. The dashboard derives every permission itself, from the item keys through the data model (level read), plus read on your app-private streams. Any other access level in the file is reduced to read. Nothing is shared until a user accepts an invite. The dashboard validates the file strictly (schema, cross-field rules, https only, size and time limits) and renders every text as plain text.

Terminal window
curl -sI https://example.org/cycle-app/hds-dataset.json # 200, access-control-allow-origin: *
import { appTemplates } from 'hds-lib';
const tpl = await appTemplates.loadTemplateFromUrl('https://example.org/cycle-app/hds-dataset.json');
const { itemKeyIssues } = appTemplates.templateToFormSpec(tpl); // after initHDSModel()

itemKeyIssues should be empty before you publish. Library reference: docs/dataset-templates.md.