W3C-compliant Decentralized Identifiers (DIDs) on Cardano blockchain using transaction metadata.
New here? See the Developer Setup Guide to get running locally in minutes.
Prisma DIDs implements a lightweight DID method (did:cardano:) that:
- Uses stake addresses as DID identifiers
- Stores DID Documents on IPFS (pinned via Pinata)
- Records DID events in Cardano transaction metadata (label
199674) - Supports full lifecycle: create, update, revoke
- Provides cryptographic verification via Ed25519 signatures
Architecture: Metadata-based approach (see ADR-001) Specification: Technical Design v1.3.1
prisma-DIDs/
├── apps/
│ ├── dashboard/ # Next.js 16 dashboard
│ ├── api/ # REST API for DID resolution
│ └── indexer/ # DID event indexer
├── packages/
│ ├── sdk/ # Core Prisma DIDs SDK
│ ├── types/ # Shared TypeScript types
│ ├── crypto/ # Verifiable credentials signing
│ └── ui/ # Shared React components
└── documentation/ # Technical specs & ADRs
- Node.js 20+ and pnpm 9+
- Blockfrost API key (https://blockfrost.io)
- Pinata API credentials (https://pinata.cloud)
- Cardano wallet with preprod ADA (Eternl/Lace/Nami)
# Install dependencies
pnpm install
# Configure environment
cp .env.example .env.local
# Edit .env.local with your API keys
# Build all packages
pnpm build
# Run tests
pnpm test
# Start dashboard (dev)
cd apps/dashboard
pnpm devimport {
deriveDID,
generateDIDDocument,
buildCreatePayload,
PinataClient,
BlockfrostProvider
} from '@prisma-dids/sdk';
// 1. Derive DID from stake address
const did = deriveDID('stake_test1...');
// 2. Generate W3C DID Document
const didDoc = generateDIDDocument({
did,
publicKeyHex: '4cb5abf6...', // From wallet
baseAddress: 'addr_test1...'
});
// 3. Pin to IPFS
const pinata = new PinataClient({ apiKey, apiSecret });
const cid = await pinata.pinJSON(didDoc);
// 4. Build & sign payload (with wallet)
const payload = buildCreatePayload({ did, ipfsCid: cid });
// Sign with CIP-30 wallet.signData()...
// 5. Submit to Cardano
// See documentation for full transaction flowFrontend:
- Next.js 16 (Turbopack)
- React 19
- TypeScript 5.7
SDK:
- @noble/ed25519 v3 (WebCrypto)
- Lucid Cardano
- Blockfrost API
Infrastructure:
- Turborepo 2.6
- pnpm workspaces
- Vitest 4
- WebCrypto API for Ed25519 signatures
- No Node.js dependencies in browser bundles
- Automatic WASM library aliasing (Node ↔ Browser)
- W3C DID Core 1.0
- Ed25519 signature scheme
- IPFS content addressing (CIDv1)
- Cardano metadata standards
- Full TypeScript support
- Comprehensive test coverage
- Monorepo architecture with Turborepo
- Hot module replacement (Turbopack)
The credential schema is defined entirely in packages/schemas/src/credentials/contribution.ts using Zod. This is the only file you need to edit to change what fields a credential contains.
What to edit:
- Add or remove fields in the
ContributionCredentialSchema.extend({})block. Each field is a Zod type (e.g.z.string(),z.number().positive().optional()). - Change the contribution type options in the
ContributionTypeEnumarray at the top of the file. - Update selective disclosure in the
contributionDisclosableFieldsarray at the bottom. This controls which fields a holder can choose to reveal or hide when creating a presentation.
What you do not need to change:
- On-chain transactions only store a
vcHashand anipfsCid. The actual credential payload lives on IPFS, so the blockchain never sees or validates the schema fields. No smart contract changes needed. - The indexer only indexes on-chain event metadata (
txHash,event,issuerDid,holderDid, etc.). It never inspects the credential body stored on IPFS, so it keeps working as is regardless of schema changes. - The registry (
packages/schemas/src/registry.ts) already points to this schema and re-exports it automatically. The/schemasAPI endpoint will reflect your changes without any additional wiring.
You also need to update the issuance form. The form in apps/vc-interface/components/IssuanceForm.tsx has a hardcoded credentialFields object (around line 35) that does not read from the Zod schema. If you change the schema, you must mirror those changes in the form or it will break.
Example: adding a role field to ContributionCredential
-
In
packages/schemas/src/credentials/contribution.ts, add the field to the Zod schema:role: z.string().min(1), // required text field // or role: z.string().optional(), // optional text field
If it should support selective disclosure, add
'role'to thecontributionDisclosableFieldsarray. -
In
apps/vc-interface/components/IssuanceForm.tsx, add a matching entry to theContributionCredentialarray insidecredentialFields:{ key: 'role', label: 'Role', type: 'text', required: true, canDisclose: false, defaultDisclosed: false },
Each field needs:
key: must match the Zod field name exactlylabel: what the user sees in the formtype:'text'for strings,'number'for numbers,'select'for enumsrequired:trueif the Zod field is not.optional()options: only for'select'type, list the enum values (e.g.['code', 'design', 'other'])canDisclose:trueif the field is incontributionDisclosableFieldsdefaultDisclosed:trueif the disclosure checkbox should be pre-checked
The same pattern applies when removing or renaming fields. Delete or update the entry in both files.
All services deploy to Railway from the same GitHub repo. Each service has its own railway.toml config file.
| Service | Config File | Description |
|---|---|---|
| DIDs Indexer | railway.toml (root) |
Indexes DID events from Cardano |
| ALJ VC Indexer | railway.toml (root) |
Indexes Verifiable Credential events |
| Dids Dashboard | apps/dashboard/railway.toml |
DID management interface |
| VCs Dashboard | apps/vc-interface/railway.toml |
VC issuance & verification interface |
| DIDs - Postgres | — | Database for DIDs Indexer |
| ALJ VC - Postgres | — | Database for ALJ VC Indexer |
- In the Railway dashboard, click New Service → GitHub Repo → select this repo
- Go to the service Settings:
- Config File Path: set to
apps/dashboard/railway.tomlorapps/vc-interface/railway.toml - Leave Root Directory empty (the monorepo needs full workspace access)
- Config File Path: set to
- Under Networking, generate a public domain and set the port to 8080
- Add the required environment variables (see below)
- Push to
main— Railway auto-deploys on every push
Dids Dashboard:
| Variable | Description |
|---|---|
BLOCKFROST_PREPROD_KEY |
Server-side Blockfrost API key |
NEXT_PUBLIC_BLOCKFROST_PREPROD_KEY |
Client-side Blockfrost API key |
PINATA_JWT |
Server-side Pinata JWT for IPFS pinning (POST /api/ipfs/pin) |
NEXT_PUBLIC_DEFAULT_NETWORK |
Network (preprod or mainnet) |
INDEXER_URL_PREPROD |
DIDs Indexer URL (e.g. https://prisma-didsindexer-production.up.railway.app) |
VCs Dashboard:
| Variable | Description |
|---|---|
NEXT_PUBLIC_VC_INDEXER_ENDPOINT |
ALJ VC Indexer URL |
NEXT_PUBLIC_DID_INDEXER_ENDPOINT |
DIDs Indexer URL |
NEXT_PUBLIC_NETWORK |
Network (preprod or mainnet) |
NEXT_PUBLIC_BLOCKFROST_API_KEY |
Blockfrost API key |
PINATA_JWT |
Server-side Pinata JWT for IPFS pinning (POST /api/ipfs/pin) |
NEXT_PUBLIC_ISSUER_DIDS |
Comma-separated list of authorized issuer DIDs |
NEXT_PUBLIC_DASHBOARD_URL |
DIDs Dashboard URL for "View in DID Dashboard" links |
No code changes are required — the app supports both networks via environment variables. To deploy on mainnet:
- Get a mainnet Blockfrost API key from blockfrost.io
- Deploy new indexer instances for mainnet (new Indexer + Postgres services) with:
NETWORK=mainnetBLOCKFROST_API_KEY=<mainnet key>
- Update dashboard env vars:
NEXT_PUBLIC_DEFAULT_NETWORK=mainnetBLOCKFROST_PREPROD_KEY→ replace with mainnet key (or addBLOCKFROST_MAINNET_KEY)NEXT_PUBLIC_BLOCKFROST_PREPROD_KEY→ replace with mainnet keyINDEXER_URL_PREPROD→ addINDEXER_URL_MAINNETpointing to the mainnet indexer
- Update VCs Dashboard env vars:
NEXT_PUBLIC_NETWORK=mainnetNEXT_PUBLIC_BLOCKFROST_API_KEY=<mainnet key>NEXT_PUBLIC_ISSUER_DIDS→ use mainnet DIDs (stake1...instead ofstake_test1...)NEXT_PUBLIC_VC_INDEXER_ENDPOINT→ point to mainnet VC indexerNEXT_PUBLIC_DID_INDEXER_ENDPOINT→ point to mainnet DID indexer
- API Reference - Indexer REST API documentation
- POC Plan - Implementation roadmap
- Technical Design - Full specification
- ADR-001 - Architecture decision
# Run all tests
pnpm test
# Type checking
pnpm type-check
# Build verification
pnpm build
# SDK tests only
cd packages/sdk
pnpm test[Add your license here]