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.
How it works
Section titled “How it works”- You publish
hds-dataset.jsonat an HTTPS URL next to your app. - 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.
- 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.
- 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 file
Section titled “The file”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" } } ]}| Field | Meaning |
|---|---|
format, formatVersion | "hds-dataset-template", 1. Once format is set, version and app are required. |
version | Semver of the file’s content. |
app | Who 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. |
consent | The 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.
Versioning
Section titled “Versioning”Bump version on every change:
- major: the scope narrows or a grant changes (an item removed, moved between permanent and
recurring, an
existingStreamRefadded, removed or with other permissions, orchatswitched 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.
Hosting
Section titled “Hosting”- 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).
Embedding the practitioner’s permanent link
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.
Security model
Section titled “Security model”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.
Checking your file
Section titled “Checking your file”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.