Playwright Rights Tracker
Playwright Rights Tracker mints a did:prism for rights management so identity travels with the person, not the platform.
The primitive.
Directors tap once and the app mints a published did:prism for rights management — a decentralised identifier they own outright, resolvable by anyone, portable off this platform the day they leave it.
Why this primitiveDID Registrar fits rights management in Theater & Live Performance because the people and works involved need a portable identifier they own — not a row in someone else's platform database that disappears when the platform does.
Pick how the agent runs.
Start here. It is the only mode that reliably fits a one-shot, 5-credit Lovable build, and you can swap the base URL for a real agent later without touching the UI.
No keys required.
The simulated agent runs entirely inside the app. Paste the prompt and build — nothing to configure, nothing to wait for.
Switch to Docker or Fly.io above when you want a real Cloud Agent.
The build prompt.
Paste into a fresh Lovable project. The prompt below is written for the Simulated agent mode. read the build strategy →
Build "Playwright Rights Tracker" as a ONE-SHOT Lovable build.
Single-page TanStack Start app. Cut scope ruthlessly.
CONCEPT
Playwrights verify their ownership rights when licensing plays to ensure proper usage and protection against infringement.
Discipline: Theater & Live Performance (rights management).
Recipe: DID Registrar (publish a did:prism) as the single Hyperledger Identus primitive.
Why Identus: DID Registrar fits rights management in Theater & Live Performance because the people and works involved need a portable identifier they own — not a row in someone else's platform database that disappears when the platform does.
LOVABLE BUDGET (HARD CAP: ONE-SHOT, ~5 CREDITS TOTAL):
This prompt MUST ship a working demo on the FIRST message with zero follow-ups.
- ONE TanStack Start app, ONE route (`src/routes/index.tsx`). No auth, no nav.
- ONE server module `src/lib/identus.functions.ts` with `createServerFn` wrappers.
- ONE client surface wired to it (form + result panel).
- NO database, NO Lovable Cloud, NO file uploads, NO extra integrations.
- Libraries: template defaults + `zod`. Nothing else.
- If a feature is not in the user flow below, do not build it.
IDENTUS PRIMER (assume the reader has never used Identus)
- Hyperledger Identus is open-source self-sovereign identity: an issuer signs a
W3C Verifiable Credential, a holder keeps it in their wallet, and a verifier
checks it without phoning the issuer.
- The Cloud Agent is a REST service. Everything below is plain HTTP + JSON.
Docs: https://identus.io/documentation/develop/ ·
https://github.com/hyperledger-identus/cloud-agent
- Identifiers are `did:prism:<hex>` — created unpublished, then published so
others can resolve them. Only a PUBLISHED DID carrying an `assertionMethod`
key can sign a credential.
- Agents talk to each other over DIDComm; a Mediator relays messages to wallets
that are not always online (https://github.com/hyperledger-identus/mediator).
- Browser/mobile holders use the TypeScript SDK
(https://github.com/hyperledger-identus/sdk-ts) — not needed for this build.
- Auth to the agent is the `apikey: <AGENT_API_KEY>` header. Server-side only.
RUNTIME CONVENTIONS (this template — follow exactly)
- Stack: TanStack Start v1 + React 19 + Vite 7 + Tailwind v4 (`src/styles.css`).
There is NO `src/App.tsx`, no react-router-dom, no `src/pages`.
- Routes live in `src/routes`; the home page is `src/routes/index.tsx` and uses
`createFileRoute("/")({ component: Page, head: () => ({ meta: [...] }) })`.
Never edit `src/routeTree.gen.ts`.
- Server logic: `import { createServerFn } from "@tanstack/react-start";` in a
client-safe module such as `src/lib/identus.functions.ts`. Shape is
`createServerFn({ method: "POST" }).inputValidator((d) => schema.parse(d)).handler(async ({ data }) => {...})`.
Call it from the client with `useServerFn(fn)` or directly inside an event handler.
- Routes and components import ONLY from `*.functions.ts` (and plain type modules) —
never from a `*.server.ts` file. Raw agent logic lives in `*.server.ts`, is
imported by the `.functions.ts` wrapper, and never reaches the client bundle.
- Read `process.env.AGENT_BASE_URL` / `process.env.AGENT_API_KEY` INSIDE the
handler — never at module scope (env is injected at call time).
- The server runtime is a Cloudflare-style Worker: use `fetch`, `crypto.randomUUID()`,
`Buffer`. No child_process, no sharp, no native modules.
- TypeScript runs with `exactOptionalPropertyTypes`: pass optional props as
`...(x ? { prop: x } : {})`, not `prop: x ?? undefined`.
- Toasts: `sonner` (`import { toast } from "sonner"`), and render `<Toaster />`
once in `src/routes/__root.tsx`. `@/hooks/use-toast` does NOT exist here.
- Colours come from semantic tokens in `src/styles.css` — no hardcoded
`text-white` / `bg-black` / `bg-[#hex]` in components.
- Give `src/routes/index.tsx` its own `head()` with a real title and description.
MODE — SIMULATED AGENT (no external service, no secrets)
Do NOT call a real Identus Cloud Agent. Implement `src/lib/identus.server.ts` as a
deterministic in-memory simulator that mirrors the Cloud Agent's REST shapes, and
import it from `src/lib/identus.functions.ts`. Derive ids from a hash of the input
so SSR and hydration agree (no Math.random, no Date.now in render).
FIXTURE SHAPES (return exactly these keys)
- DID:
{ "did": "did:prism:<64 hex>", "longFormDid": "did:prism:<64 hex>:<base64url>",
"status": "PUBLISHED",
"document": { "id": "did:prism:<64 hex>",
"verificationMethod": [{ "id": "#auth-1", "type": "JsonWebKey2020", "curve": "secp256k1" },
{ "id": "#assert-1", "type": "JsonWebKey2020", "curve": "secp256k1" }],
"authentication": ["#auth-1"], "assertionMethod": ["#assert-1"], "service": [] } }
- Connection:
{ "connectionId": "<uuid-like>", "state": "InvitationGenerated",
"invitation": { "id": "<uuid-like>",
"invitationUrl": "https://my.domain/path?_oob=<base64url json>" },
"theirDid": null }
Advance to "ConnectionResponseSent" (and fill `theirDid`) on the second poll.
- Credential record: (always include a `dob` claim so an age/ZK proof stays possible)
{ "recordId": "<uuid-like>", "protocolState": "OfferSent",
"claims": { "dob": "1994-05-02", ... },
"issuingDID": "did:prism:...", "credentialFormat": "JWT",
"credential": "<header>.<payload>.<signature>" } // three base64url parts, well-formed but unsigned
Advance OfferSent -> RequestReceived -> CredentialSent across polls.
- Presentation:
{ "presentationId": "<uuid-like>", "status": "RequestSent", "verified": false, "data": {} }
Advance RequestSent -> PresentationReceived -> PresentationVerified, then set
`verified: true` and `data` to the disclosed claims only.
UI REQUIREMENT
Show a small badge reading "Simulated agent — no cryptographic verification" so a
judge knows nothing is being faked silently. Keep every shape identical to the real
API so switching to a live agent is a one-line base-URL change.
MODE GOTCHAS
Never claim cryptographic verification in the copy while in simulated mode — label it clearly. Keep the simulator pure (no timers, no randomness that breaks re-renders) so SSR and hydration agree.
CLOUD AGENT API REFERENCE (everything you need — no other docs required)
All calls: base URL `AGENT_BASE_URL`, headers
`{ "content-type": "application/json", apikey: AGENT_API_KEY }`.
GET /_system/health -> { version } (use for a status pill)
POST /did-registrar/dids
body { documentTemplate: { publicKeys: [{ id, purpose: "authentication"|"assertionMethod", curve: "secp256k1" }], services: [] } }
-> { longFormDid, status: "CREATED" }
POST /did-registrar/dids/{didRef}/publications -> { scheduledOperation: { id, didRef } }
GET /did-registrar/dids/{didRef} -> { did, longFormDid, status: "CREATED"|"PUBLICATION_PENDING"|"PUBLISHED" }
GET /dids/{did} -> resolved DID document
(check `assertionMethod` is non-empty before offering the DID as an issuer)
POST /connections
body { label, goalCode: "connect", goal }
-> { connectionId, state: "InvitationGenerated", invitation: { invitationUrl, id } }
POST /connection-invitations
body { invitation: "<oob base64url string from the invitationUrl ?_oob= param>" }
GET /connections/{connectionId}
-> { state } : InvitationGenerated -> ConnectionRequestReceived -> ConnectionResponseSent
GET /connections -> { contents: [...] }
POST /issue-credentials/credential-offers
body { claims: { ... }, issuingDID, credentialFormat: "JWT", automaticIssuance: true,
connectionId? | (goalCode + goal for connectionless) }
-> { recordId, protocolState: "OfferSent", invitation?: { invitationUrl } }
GET /issue-credentials/records/{recordId}
-> { protocolState } : OfferSent -> RequestReceived -> CredentialSent, plus `credential` (JWT string)
GET /issue-credentials/records -> { contents: [...] }
POST /present-proof/presentations
body { connectionId, proofs: [], options: { challenge, domain },
claims: { "<attr>": {} } } // or anoncredPresentationRequest for ZK predicates
-> { presentationId, status: "RequestSent" }
GET /present-proof/presentations/{presentationId}
-> { status } : RequestSent -> PresentationReceived -> PresentationVerified | PresentationVerificationFailed
plus `data` (the disclosed claims)
Errors are RFC-7807 JSON: { status, title, detail }. Surface `detail` in the UI —
it names the real problem (unpublished DID, missing connectionId, bad apikey).
SERVER SNIPPET — DID Registrar (create + publish a did:prism)
```ts
// src/lib/identus.functions.ts
import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";
export const mintDid = createServerFn({ method: "POST" })
.inputValidator((d) => z.object({ label: z.string().min(1) }).parse(d))
.handler(async ({ data }) => {
const base = process.env.AGENT_BASE_URL!.replace(/\/$/, "");
const headers = { "content-type": "application/json", apikey: process.env.AGENT_API_KEY! };
const created = await fetch(`${base}/did-registrar/dids`, {
method: "POST", headers,
body: JSON.stringify({
documentTemplate: {
publicKeys: [
{ id: "auth-1", purpose: "authentication", curve: "secp256k1" },
{ id: "assert-1", purpose: "assertionMethod", curve: "secp256k1" },
],
services: [],
},
}),
}).then((r) => r.json());
await fetch(`${base}/did-registrar/dids/${created.longFormDid}/publications`, {
method: "POST", headers,
});
// Poll until the DID reports PUBLISHED, then resolve the document.
return { did: created.longFormDid, label: data.label };
});
```
USER FLOW (build exactly this, nothing more)
1. The user types a label (their name, a company, a work).
2. Press "Mint identity" -> server function creates and publishes a did:prism.
3. Show the DID, its status (`CREATED` -> `PUBLISHED`), and the resolved document JSON.
4. A copy button for the DID string, and a short plain-English explainer of what it is.
DESIGN
Editorial, high-contrast, generous whitespace. One accent colour used sparingly.
Show the protocol honestly: render the state machine and the raw JSON envelope in
a collapsible panel so a judge can see the real Identus record. Truncate DIDs and
JWTs in prose (first 12 + last 6 chars) with the full value behind a copy button.
Mobile first — a judge will open this on a phone.
MARKET (for the pitch slide, not the UI)
TAM $30B — global live performance market · SAM $5B — live performance production and design software · SOM $500M — indie and regional theater sound design budgets
GOTCHAS (universal — apply to every agent call in this build)
- Only a PUBLISHED DID with an `assertionMethod` key can sign a credential
offer. Create -> publish -> wait for `PUBLISHED` before issuing, or you get a
cryptic 422/500. Before showing an issuer picker, resolve each DID with
`GET /dids/{did}` and offer ONLY those whose document exposes `assertionMethod`;
list the excluded ones with the reason instead of hiding them.
- Put a `dob` claim on every issued credential (ISO date). Without a date-of-birth
claim an age / zero-knowledge proof is impossible later.
- On a direct Fly deploy the agent serves at the app ROOT — strip any trailing
`/cloud-agent` from the base URL. On the local docker compose stack, KEEP it.
- Every issuance/presentation endpoint is asynchronous: you POST, then POLL the
record's `protocolState` (`OfferSent` -> `CredentialSent`,
`RequestSent` -> `PresentationVerified`). Never assume the POST finished the job.
- Connectionless issuance omits `connectionId` and needs a `goalCode`; if you
send neither a `connectionId` nor a `goalCode` you get "Missing connectionId".
- The human principal and any AI agent acting for them are DIFFERENT DIDs.
Compare principal to credential subject, agent to mandate subject — never cross them.
- Never print a raw DID, JWT or hash inline in prose: truncate (first 12 + last 6)
and keep the full value behind a copy button or in the raw JSON panel.
- First boot of a real agent migrates four databases: allow ~5 minutes and >= 4 GB
of memory before deciding it is broken.
- Never call the agent from the browser. Every fetch lives inside a
`createServerFn` handler so `AGENT_API_KEY` stays server-side.
REFERENCE MATERIAL (if you need more than the above)
- Identus docs: https://identus.io/documentation/develop/
- Cloud Agent (OpenAPI + compose examples): https://github.com/hyperledger-identus/cloud-agent
- TypeScript SDK (browser/wallet holders): https://github.com/hyperledger-identus/sdk-ts
- Mediator (DIDComm relay for offline wallets): https://github.com/hyperledger-identus/mediator
- Umbrella repo: https://github.com/hyperledger-identus/hyperledger-identus
- Reference console built with this stack: https://github.com/arunnadarasa/identus
- Full machine-readable brief for your own LLM: https://identusprompts.lovable.app/llms-full.txt
DELIVERABLE
A working single-page demo where the flow above completes end-to-end, plus a
one-paragraph README explaining which Identus primitive is used and how to point
the app at a real Cloud Agent.
Built for the Hyperledger Identus Catalyst — organised by StreetKode Fam during Indian Krump Festival 14.Market sizing.
Indicative figures for hackathon pitches — refine with your own research before raising.