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.
// @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.
| Slot | Allowed types | Extra members | What a schema may allow |
|---|---|---|---|
| 0 | ref | None. | equality |
| 1 | enum | members: 1 to 8 names; enum_labels: a label for each. | disclosure, equality, set-membership |
| 2 | date, datetime or uint | For uint, max_digits: 1 to 19, default 19. | disclosure, equality, range |
| 3 | string, bool or enum | For 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:
| Type | Value | Example |
|---|---|---|
enum | One of the member names, as a string. | "advanced" |
date | YYYY-MM-DD, a real calendar date. | "2026-09-01" |
datetime | YYYY-MM-DDTHH:MM:SS, optional fraction, then Z or ±HH:MM. | "2026-09-01T14:30:00Z" |
uint | A whole number as decimal text, no leading zeros, at most max_digits digits and below 2^64. | "42" |
string | Printable ASCII starting SANDBOX-, at most max_length characters including the prefix. | "SANDBOX-0001" |
bool | A 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:
| Type | A verifier may |
|---|---|
ref | Ask for nothing. |
enum | Ask for disclosure, equality with a member, or membership of a set of members. |
date, datetime | Ask for a range at least 30 days wide. Not disclosure or equality. |
uint | Ask for a range at least 10 wide, or wider if the TridentGold team sets a larger width at approval. Not disclosure or equality. |
string | Ask for disclosure. Equality only if the TridentGold team allows it at approval. |
bool | Ask 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.
| Field | Rules |
|---|---|
display.name | The 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.description | At most 280 characters. |
display.card_attributes | Optional. At most two attributes that are not hidden, shown on the credential card. |
label | Required on every attribute, including slot 0. At most 48 characters. |
description | Optional, per attribute. At most 160 characters. |
hidden | Optional. true keeps the value off the card and detail screens. |
enum_labels | Required 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 version1; - a numeric
schema_idfrom the range 100000 to 199999; - the schema document's
$id,titleanddescription, from your display fields; - the credential format details: the
ZKNC-1suite, the sandbox issuer (17), the issuance mode, the uniqueness rules, one slot per attribute, and enum codes1to8inmembersorder; - 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 onuintvalues.
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:
// @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.
| State | Meaning | Can you create invitations? |
|---|---|---|
pending | Waiting for the TridentGold team. | No |
approved | A person on the team reviewed and approved this exact document. | Only once it is published; see below. |
published | The team published it to the sandbox trust list. | Yes |
rejected | Not approved. Its name is not reused; submit a new schema. | No |
withdrawn | Removed by the TridentGold team. | No |
Approval and publication are separate, manual steps by the TridentGold team:
- 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. - 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
- Creating invitations: a Next.js route handler that creates invitations for your published schema.