Step-by-Step: Your First Custom Template
This tutorial builds a custom certificate template from scratch and takes it all the way to a rendered on-chain certificate, using the local sandbox as the development environment. At the end you will also load-test the template with generated data and measure the issuance fees.
If you want the API reference instead of a tutorial, see Building a Custom Template.
What you will build
An event badge template: a card that shows an attendee’s badge for a conference, with the event name, a badge tier, and the attendee’s name kept off-chain via the uv_url_* privacy pattern.
Prerequisites
Step 1 — Start the sandbox
git clone https://github.com/UVerify-io/uverify-examples.git
cd uverify-examples
uv run sandbox.py startWait until the URL table appears. The UI runs at http://localhost:3000.
Step 2 — Scaffold the template
uv run sandbox.py template add EventBadgeThis runs npx @uverify/cli init under the hood, places the project in sandbox/custom-ui-templates/, and registers it in the sandbox’s additional-templates.json in one step. Verify the registration:
uv run sandbox.py templatesStep 3 — Write the template
Open the scaffolded Certificate.tsx and replace it with:
import {
Template,
type ThemeSettings,
type UVerifyCertificate,
type UVerifyCertificateExtraData,
type UVerifyMetadata,
} from '@uverify/core';
import type { JSX } from 'react';
export default class EventBadge extends Template {
public name = 'EventBadge';
public defaultUpdatePolicy = 'first' as const;
public theme: Partial<ThemeSettings> = {
background: 'bg-gradient-to-br from-violet-950 via-slate-900 to-slate-950',
};
public layoutMetadata = {
event: 'Name of the event (e.g. "Cardano Summit 2026")',
tier: 'Badge tier: attendee, speaker, or organizer',
uv_url_attendee: 'Attendee name — stored as hash on-chain, revealed via ?attendee= URL param',
issued: 'Issue date (ISO 8601)',
};
public render(
hash: string,
metadata: UVerifyMetadata,
certificate: UVerifyCertificate | undefined,
pagination: JSX.Element,
extra: UVerifyCertificateExtraData,
): JSX.Element {
if (extra.isLoading) {
return <div className="text-white/60 text-center mt-24">Loading…</div>;
}
if (extra.serverError) {
return <div className="text-red-400 text-center mt-24">Could not load certificate.</div>;
}
if (!certificate) {
return <div className="text-white/60 text-center mt-24">No certificate found for this hash.</div>;
}
const attendee = new URLSearchParams(window.location.search).get('attendee');
return (
<div className="max-w-md mx-auto mt-16 p-8 rounded-2xl bg-white/10 border border-white/20 text-white backdrop-blur-sm">
<p className="text-xs uppercase tracking-widest text-violet-300">
{String(metadata.tier ?? 'attendee')}
</p>
<h1 className="text-3xl font-bold mt-2">{String(metadata.event ?? 'Event')}</h1>
{attendee && <p className="mt-4 text-xl">{attendee}</p>}
<p className="mt-6 text-sm text-white/60">
Issued {String(metadata.issued ?? '')} · verified on Cardano
</p>
<p className="mt-2 text-xs font-mono text-white/40 break-all">TX: {certificate.transactionHash}</p>
<div className="mt-6">{pagination}</div>
</div>
);
}
}The pieces that matter:
layoutMetadatadeclares the fields the creation form shows. Only list fields the issuer actually types in. Theuv_url_prefix tells the UI to hash the value before it goes on-chain (see Building a Custom Template).defaultUpdatePolicy = 'first'locks the badge to its original submission. See Update Policies for all seven modes.extra.isLoading/extra.serverErrormust be handled before you touchcertificate, which isundefineduntil data arrives.
Step 4 — Apply and preview
uv run sandbox.py restartThe UI container rebuilds with your template included. Then:
- Open http://localhost:3000 and connect the demo wallet (it funds itself from the sandbox faucet).
- Start the creation flow and pick EventBadge from the template selector.
- The metadata form is pre-filled with your
layoutMetadatafields. Fill them in — type the attendee’s plain name intouv_url_attendee, the UI hashes it for you. - Certify any text or file and sign with the demo wallet.
After the transaction confirms, the certificate page renders your template. Append ?attendee=Jane%20Doe to the URL to reveal the name.
The template ID is the class name with the first character lowercased: EventBadge becomes uverify_template_id: "eventBadge". Certificates issued from the form set this automatically.
Step 5 — Iterate
Edit Certificate.tsx, then uv run sandbox.py restart to rebuild. Existing certificates re-render with the new code immediately, because the template lives in the UI, not on-chain. That is the core of the UVerify model: on-chain data is permanent, presentation is yours to evolve.
Step 6 — Load-test with realistic data
Create a plan file that matches your layoutMetadata (see Simulation & Fee Calculation for the format):
{
"event": { "type": "static", "value": "Cardano Summit 2026" },
"tier": { "type": "one-of", "values": ["attendee", "speaker", "organizer"] },
"issued": { "type": "one-of", "values": ["2026-06-10", "2026-06-11", "2026-06-12"] }
}Save it as sandbox/simulator/plan.event-badge.json and run:
uv run sandbox.py simulate \
--template eventBadge \
--plan sandbox/simulator/plan.event-badge.json \
--amount 200 \
--batch-size 5The simulator issues 200 badges in batches of 5 and writes sandbox/simulator/results.json with the fee of every transaction. Open a few of the generated hashes in the UI to check your template against varied data, and use the fee summary to pick a batch size before going to production.
Step 7 — Going to production
Your sandbox template folder is a normal @uverify/cli project. To ship it:
- On app.uverify.io: push the template to a Git repository and open a PR against
uverify-uiadding arepositoryentry with a pinned commit. Use the Add External Template issue template first. - Self-hosted: register it in your deployment’s
additional-templates.jsonand build with theuverify/uverify-ui:customDocker image.
Both paths are described in Building a Custom Template.