Skip to main content

Defining a schema

A schema defines a credential: its name, its four fields (called attributes), their types, what a verifier may ask about each, and how the TridentGold app displays them. You write a draft with the parts you control, submit it with the issuer SDK, and the Tenant Gateway completes the rest.

This page uses the issuer SDK, @human.tech/tridentgold-zknc-mvp-issuer. Creating invitations covers installing it.

A complete draft​

This draft defines a synthetic training record. The rest of this page explains each part.

scripts/training-record.mjs
// @ts-check
/** @type {import("@human.tech/tridentgold-zknc-mvp-issuer").SchemaDraft} */
export const trainingRecord = {
name: "training-record",
display: {
name: "Sandbox Training Record",
description: "Synthetic training record for sandbox testing.",
card_attributes: ["level", "completed_on"]
},
attributes: [
{ name: "subject_ref", type: "ref", label: "Subject reference", hidden: true },
{
name: "level",
type: "enum",
members: ["basic", "advanced"],
enum_labels: { basic: "Basic", advanced: "Advanced" },
label: "Level"
},
{ name: "completed_on", type: "date", label: "Completed on" },
{ name: "certificate_no", type: "string", max_length: 64, label: "Certificate number" }
]
};

name is the schema's short name, of lowercase letters, digits and -, unique within your organization. Its maximum length is 48 characters or 77 minus the length of your tenant slug, whichever is smaller, so that the full identifier fits in 100 bytes: a slug of up to 29 characters allows 48, and a 32-character slug allows 45. A longer name is refused with schema_name_invalid at /name. A name is never reused, even for a schema that was rejected or withdrawn.

The four attributes​

Every sandbox schema has exactly four attributes, in this order. Each position (a slot) allows particular types. The template matches the one proof circuit the sandbox runs, so a schema that fits it needs no new cryptography.

SlotAllowed typesExtra membersWhat a schema may allow
0refNone.equality
1enummembers: 1 to 8 names; enum_labels: a label for each.disclosure, equality, set-membership
2date, datetime or uintFor uint, max_digits: 1 to 19, default 19.disclosure, equality, range
3string, bool or enumFor string, max_length: 9 to 434, default 64. For enum, as slot 1.disclosure, equality

Attribute names are 1 to 48 characters of lowercase letters, digits and _, unique within the schema. An enum has 1 to 8 distinct members, each named the same way.

Slot 0 is a subject reference that the gateway generates. Its value is a random synthetic identifier, urn:tridentgold:sandbox:subject:<uuid>, created for each invitation. You never supply it and it is never disclosed. It still needs a label; mark it hidden.

Values by type. When you create an invitation, you supply each of the other three values:

TypeValueExample
enumOne of the member names, as a string."advanced"
dateYYYY-MM-DD, a real calendar date."2026-09-01"
datetimeYYYY-MM-DDTHH:MM:SS, optional fraction, then Z or ±HH:MM."2026-09-01T14:30:00Z"
uintA whole number as decimal text, no leading zeros, at most max_digits digits and below 2^64."42"
stringPrintable ASCII starting SANDBOX-, at most max_length characters including the prefix."SANDBOX-0001"
boolA JSON boolean.true

What a verifier can ask for​

An attribute's optional predicates member lists what a verifier may ask about it: a non-empty subset of its slot's operations. Leave it out to allow all of them. disclosure reveals the value; the others prove something about it without revealing it.

The sandbox verifier allows a request only when your schema, the slot and the verifier's own limits all allow it. Its limits are:

TypeA verifier may
refAsk for nothing.
enumAsk for disclosure, equality with a member, or membership of a set of members.
date, datetimeAsk for a range at least 30 days wide. Not disclosure or equality.
uintAsk for a range at least 10 wide, or wider if the TridentGold team sets a larger width at approval. Not disclosure or equality.
stringAsk for disclosure. Equality only if the TridentGold team allows it at approval.
boolAsk for equality, only if the TridentGold team allows it at approval. Not disclosure.

So a date in slot 2 can be proved to fall within a range, but not revealed. If a verifier needs to read a value, use an enum or a string.

Display fields​

The display member and each attribute's label, description, hidden and enum_labels become the schema document's x-zknc-display block. The app uses it on the offer, card, detail and consent screens.

FieldRules
display.nameThe credential's name. It must contain the word Sandbox, capitalized, as a whole word. At most 64 characters. It may name the credential it models, for example "Sandbox Barbados Driver's Licence".
display.descriptionAt most 280 characters.
display.card_attributesOptional. At most two attributes that are not hidden, shown on the credential card.
labelRequired on every attribute, including slot 0. At most 48 characters.
descriptionOptional, per attribute. At most 160 characters.
hiddenOptional. true keeps the value off the card and detail screens.
enum_labelsRequired for enums: a label for every member, at most 48 characters each.

Every piece of text uses one script (credential names use the Latin script), contains only letters, marks, numbers, punctuation, symbols and ordinary spaces, has no leading or trailing space, and has no line breaks or control characters.

hidden does not hide a value from a verifier. The consent screen shows every disclosure and every proof a verifier requests, on every attribute, hidden ones included.

The schema has no field for your organization's name as an issuer; see About this sandbox.

Terms that are flagged. Words such as "official", "government", "ministry", "police", "national", "passport", "verified" or "TridentGold" are not refused. The gateway returns them as reserved_term warnings, and the TridentGold team sees them flagged when it reviews the schema.

What the gateway fills in​

You cannot set any of these; the gateway writes them into the schema document:

  • the identifier sandbox.tridentgold.t.<tenant slug>.<name> (at most 100 bytes, which is why the name limit depends on your slug) and schema version 1;
  • a numeric schema_id from the range 100000 to 199999;
  • the schema document's $id, title and description, from your display fields;
  • the credential format details: the ZKNC-1 suite, the sandbox issuer (17), the issuance mode, the uniqueness rules, one slot per attribute, and enum codes 1 to 8 in members order;
  • the subject reference profile for slot 0, unique to your schema;
  • the synthetic-data checks: the SANDBOX- prefix and length limit on strings, and the digit limit on uint values.

The completed document is identified by its manifest hash, the SHA-256 of its canonical JSON form. Approval covers exactly that document, display fields included, so nothing in it can change after approval. To change a schema, submit a new one with a new name.

Submit the schema​

This script submits the draft above. Run it once from your server environment, with the gateway URL and API key your contact gave you in .env.local:

scripts/submit-schema.mjs
// @ts-check
import { createZkncTenantIssuer } from "@human.tech/tridentgold-zknc-mvp-issuer";
import { trainingRecord } from "./training-record.mjs";

const baseUrl = process.env.ZKNC_TENANT_GATEWAY_URL;
const apiKey = process.env.ZKNC_TENANT_API_KEY;
if (!baseUrl || !apiKey) {
throw new Error("Set ZKNC_TENANT_GATEWAY_URL and ZKNC_TENANT_API_KEY.");
}

const issuer = createZkncTenantIssuer({ baseUrl, apiKey });
const submitted = await issuer.submitSchema(trainingRecord);

console.log("schema_id:", submitted.schemaId);
console.log("identifier:", submitted.identifier);
console.log("state:", submitted.state);
for (const warning of submitted.warnings) {
console.log("warning:", warning.code, warning.path, warning.detail ?? "");
}
node --env-file=.env.local scripts/submit-schema.mjs

Record the schema_id: you create invitations with it.

Before sending, submitSchema() completes the document and checks it with the same rules the gateway applies. A draft that breaks a rule throws ZkncSchemaInvalidError without anything being sent; its findings list each rule and where in your draft it failed, for example { path: "/display/name", code: "display_name_sandbox" }. To run that check without a gateway or key, call validateSchemaDraft(draft), which returns { code, errors, warnings }. It does not know your tenant slug, so it checks the name against 48 characters only; the gateway applies the slug-based limit. Nor can it tell whether the name is already taken.

A draft with an unknown or missing member, a wrong JSON type or a value outside the rules is refused with schema_request_invalid, and path names the first offending member as a JSON pointer.

Your organization can have a limited number of schemas waiting for approval at a time (five by default). Beyond that, submitSchema() throws ZkncTenantRateLimitedError with the code schema_pending_cap until one is decided.

Approval and publication​

After you submit, a schema moves through these states. listSchemas() returns every schema of yours with its state; getSchema(schemaId) returns one with its completed document.

StateMeaningCan you create invitations?
pendingWaiting for the TridentGold team.No
approvedA person on the team reviewed and approved this exact document.Only once it is published; see below.
publishedThe team published it to the sandbox trust list.Yes
rejectedNot approved. Its name is not reused; submit a new schema.No
withdrawnRemoved by the TridentGold team.No

Approval and publication are separate, manual steps by the TridentGold team:

  1. A person on the team reviews a preview of how the app will display your schema, with any reserved terms flagged, and approves its exact manifest hash. Your schema becomes approved.
  2. The team adds it to the sandbox's signed trust list and publishes it. The sandbox issuer, the verifier and the app can use it after their next refresh of the trust list.

There is no fixed turnaround for either step. Your contact tells you when a schema is published.

Once the team has published a schema, they mark it published in the gateway, so listSchemas() and getSchema(schemaId) report it. Marking happens after publication, by hand, so a schema can show approved for a short while after it is already usable. An invitation for an approved schema that is not published yet is refused with ZkncTenantRefusedError and the code trust_schema_unauthorized. That refusal means "not published yet", not a problem with your request. The codes trust_registry_unknown and trust_registry_suite_mismatch are different: they mean the published configuration for the schema is wrong. Report them to the TridentGold team rather than waiting.

Withdrawal. The TridentGold team can withdraw a schema. The gateway then refuses new invitations for it, and once the next trust list is published, the app and the verifier stop accepting credentials under it within about a day; see About this sandbox.

Next​