The standard behind this demo

# DPP Platform: Technical Specification v1.0 (application level)

BSV Association / EB Labs · July 2026 · Status: normative for this app's
build; documents the implementation as built and tested.

**Scope boundary.** This specification covers the consumer application:
architecture, flows, API, storage, keys as used, modes and deployment. The
token standard's normative text, the Domain Profile definitions and the
resolver contract belong to the protocol and standards workstream
(`uora-bsv`); this document references them and never restates them. The
app's exact protocol-level emissions and the confirmations pending from
that workstream are in `PROTOCOL_COMPATIBILITY.md`. Requirements and
acceptance: `DPP PRD v1.0.md`.

## 1. System overview

One deployable unit plus libraries and external services:

| Component | Role |
|-----------|------|
| `apps/web` | Next.js App Router app: consumer UI (pending the design handover) and all backend logic as route handlers under `src/app/api/`, service code under `src/server/` |
| `packages/dpp-core` | Token standard reference implementation: codec, canonical preimages, `verifyChain` incl. SPV, blob hash binding. **Single source of truth; never reimplement any of it** |
| `packages/overlay-topics` | `tm_dpp` topic manager + `ls_dpp` lookup service as a library; run in-process in offline mode, hosted overlay in real mode (home: D-CR7) |
| MongoDB | Application store: tip pointers, encrypted owner blobs, photos (§6) |
| Treasury wallet | `@bsv/wallet-toolbox-client` against remote Wallet Storage; funds, signs and broadcasts every default-path state |
| Overlay node | Admission (`tm_dpp`) and lookup (`ls_dpp`) on mainnet |
| UORA resolver | External (`uora-bsv`); consumed through the `ResolverClient` seam (§5) once its contract is published (G10) |

Two process modes, selected at start (§7): **offline** (in-memory wallet,
in-process overlay, in-memory store; nothing external, nothing publishes)
and **real** (Wallet Storage, hosted overlay, MongoDB, SPV against
mainnet). All 17 flow tests run against the offline graph, which executes
the same service, admission and verification code.

The design round (signed off) adds a **workspace layer** above every flow:
industry → workspace → identity (§5A). In real mode this splits publishing
per workspace: **Demonstration** workspaces execute the full flows against
simulated wallet/overlay instances (nothing broadcasts; always labelled),
while **Live** workspaces publish through the treasury to mainnet. The
process-level offline mode remains the local development default; the
per-workspace mode is the hosted equivalent.

## 2. The standard, by reference

DPP Token Standard v1 = the v0 standard (archive `dpp-archived/docs/
TECH_SPEC.md` §2, field encodings and key pins §2.8 / I1-I3) plus the
CR-001 conventions: mandatory `profile` / `profile_version` stamping and
the optional `object_did`, `uora_type` and `attestation_hash` JSON
properties. Field layout, invariants, signature preimages and verification
are untouched; the version field carries `1`; no v0 token is ever
broadcast. Profile definitions are normative at the protocol layer's
`/spec` (governance D-CR5); the app carries identifiers, versions and the
regulatory one-liners only (`src/server/profiles.ts`).

## 3. Identities and keys, as used

All demo identities derive from the treasury root (G3/G6: stubbed
credentials, stated honestly). Protocols (BRC-42/43):

| Protocol | keyID | Purpose |
|----------|-------|---------|
| `[1, 'dpp identity v1']` | account id | Per-user actor identity; signs `user_signature` |
| `[1, 'dpp owner v1']` | passport id | Per-user **per-passport** owner key (field 6); owners are unlinkable across products |
| `[2, 'dpp owner data v1']` | passport id | Symmetric key (AES-256-GCM) for the owner-tier blob |
| `[1, 'dpp lock v1']` | passport id | Treasury-held locking key for the PushDrop output |
| root key directly | passport id (I3) | `server_signature`; verifiers re-derive from on-chain data plus the published service identity key |

Identity casts are **per workspace** (PRD FR-9, design handover):
textiles Maglia Sportiva / Chris / Gary; batteries Cella Energia /
Nordwind Service / Vela Storage Systems / Gary; General Atelier Fournier /
Chris / Gary; plus Presenter as an identity value. The implemented
`maker` / `collector` / `fan` set maps to the textile cast; **open
build decision:** whether cast identities derive per workspace (e.g. keyID
`workspaceId/accountId`) or per role with workspace scoping in the store.
The restricted-tier mechanism (credential vs per-verifier wrap, PRD §5.2
item 5) remains **open**: fixed when that decision lands; the owner-blob
structure must not preclude it (CR-8).

## 4. Flows (normative, as implemented and tested)

All state changes are built by `src/server/dppService.ts`, signed with
dpp-core's `completeState`, locked as 1-satoshi PushDrop outputs (basket
`dpp`), and submitted to the overlay. Fees are read back from the returned
BEEF for the presenter panel.

**4.1 Activate (FR-1).** Maker-gated (403 otherwise). Stamps
`dataCarrier`, `profile`, `profile_version`, and optionally `object_did`
into the public payload; encrypts the owner tier with the maker's blob key
and binds it via SHA-256 in field 11; genesis state (`ACTIVATE`, empty
`previous_txid`); persists passport row, photo and blob; submits;
fires the attestation hook (§5).

**4.2 Resolve (FR-2/FR-4).** `ResolverClient.lookup` by passport id or
uid; merge returned BEEF; `chainFromBeef`; `verifyChain` with the
configured chain tracker and the service identity key. Story = one entry
per state with actor labels (unknown keys render as "verified partner").
Owner path: only if the session user's derived owner key equals field 6;
the blob must hash to field 11 before decryption, otherwise the result is
"could not verify". Non-owners receive no decryptable material and no
owner fields.

**4.3 Append event (FR-3).** Owner-only (403 otherwise); ops SOLD /
RESOLD / REPAIRED / RECYCLED (400 otherwise). Event details are stamped
with the canonical `uora_type` (CR-4). The tip is spent and recreated:
`createAction` with the tip as input, PushDrop unlock under
`[1,'dpp lock v1']`, `signAction`, store tip update, submit, attestation
hook.

**4.4 Transfer (FR-6).** Owner-only. The owner blob is decrypted with the
sender's key and re-encrypted to the recipient's; the `TRANSFER` state
carries the recipient's owner key (field 6) and the new blob hash; a new
blob row is stored and the app-level owner pointer updated.

**4.5 Attest (connected path).** Verifies the client's `user_signature`
and, for genesis states, the genesis rules, then countersigns. **Open
decision:** whether attest rejects payloads without profile stamps
(CR-5); currently pass-through, matching v0.

**4.6 Admin overview.** Treasury balance, low-balance alert (<10k
satoshis), last 50 `dpp`-labelled actions. Bearer-token gated.

## 5. Resolver seam and attestation rail

`ResolverClient` (CR-1): implementation (A) wraps the overlay lookup and
is the default and demo critical path; implementation (B) is the
UORA-Query-API client, a compiled stub until the contract lands (G10),
opt-in via `RESOLVER_CLIENT=uora`. Verification claims shown to users must
stay consistent with `POST /validate` once (B) is active (FR-12).

Attestation rail (CR-6): when `ATTEST_POST=1` and `RESOLVER_URL` are set,
every state change POSTs to `{RESOLVER_URL}/attestations`,
fire-and-forget; the token chain never depends on it. Payload is a
placeholder pending the contract (see `PROTOCOL_COMPATIBILITY.md` §5).

## 5A. Workspaces, provisioning and demo control (to build)

Added by the signed-off design round (PRD FR-15 to FR-17); not yet
implemented. Normative intent:

- **Workspace model.** New `workspaces` collection: id, name, industry,
  mode (`demonstration` | `live`), brand accent (one of five curated
  token swatches), cast, createdAt. Every `passports` record gains a
  `workspaceId`; catalogue, resolve-by-uid collision spaces, casts and
  stories are workspace-scoped. Sample workspaces are seeded from the
  handover's demo content at first boot.
- **Per-workspace publishing.** Demonstration workspaces run the real
  service code against per-workspace simulated wallet/overlay instances
  (the offline graph, workspace-scoped); Live workspaces use the treasury
  wallet and hosted overlay. One process serves both; the mode is resolved
  per request from the workspace, not from the environment.
- **Provisioning.** `POST /api/workspaces` creates a workspace
  (Demonstration by default). Live creation is guarded (PRD G14: gating,
  rate limits, funding caps) and disabled until those guardrails exist.
- **Demo control.** Presenter-only, Demonstration workspaces only: an
  ordered step runner that invokes the ordinary flow endpoints in journey
  order (each step is a real state change on the workspace's record) and
  a reset that restores the workspace to its seeded state. Store-level
  reset is sufficient because demonstration records never broadcast.
- **Consumer product pages carry no workspace chrome**; scanning resolves
  by uid across workspaces exactly as before.

## 6. Storage (MongoDB)

`PassportStore` interface with `MongoPassportStore` (real) and
`MemoryPassportStore` (offline, tests). Collections and indexes:

| Collection | Content | Indexes |
|------------|---------|---------|
| `passports` | passport id, uid, name, app-level owner pointer, tip txid/vout, tip AtomicBEEF (base64) | `passportId` unique; `uid` |
| `owner_blobs` | AES-256-GCM ciphertext (base64) + SHA-256 hash per state | `(passportId, stateTxid)` unique; `(passportId, hash, createdAt)` |
| `photos` | maker-uploaded image, base64 + mime | `passportId` unique |

Privacy invariants: nothing personal on-chain, ever; the store holds
owner-tier data only encrypted; erasure = deleting the blob row (the
on-chain hash is not personal data); ciphertext must hash to field 11
before any decryption. The tip BEEF is retained only to spend the tip;
history is served by the overlay/resolver, not this store.

## 7. Modes and environment

Mode selection: `DEMO_OFFLINE=1` forces offline; `DEMO_OFFLINE=0` forces
real; unset with no `SERVER_PRIVATE_KEY` falls back to offline (the
zero-configuration path). Real mode requires `SERVER_PRIVATE_KEY`,
`WALLET_STORAGE_URL`, `ADMIN_TOKEN`, `MONGO_URL`; SPV uses WhatsOnChain on
`NETWORK` (offline reports SPV "pending"). Full variable list:
`.env.example`. The service graph is a per-process singleton
(`src/server/context.ts`), hot-reload safe via `globalThis`.

## 8. HTTP API

Session model: the demo account rides the `x-account` header (PRD §9:
sign-in is an ordinary account; production issuance out of scope). Errors
are product-language only; unexpected failures return the generic 500
message.

| Method | Path | Auth | Purpose |
|--------|------|------|---------|
| GET | `/api/accounts` | none | Demo account list |
| POST | `/api/products` | maker | Activate (FR-1); accepts `profile`, `objectDid` |
| GET | `/api/products?id=\|uid=` | optional | Resolve (FR-2/FR-4); view decided by identity |
| GET | `/api/products/photo?id=` | none | Product image |
| GET | `/api/catalog` | signed in | Registered products |
| POST | `/api/products/story` | owner | Append event (FR-3) |
| POST | `/api/products/transfer` | owner | Transfer (FR-6) |
| POST | `/api/attest` | none | Countersign a client-built state |
| GET | `/api/admin/overview` | bearer `ADMIN_TOKEN` | Treasury balance and activity |

To build for the workspace layer (§5A): `GET /api/workspaces?industry=`,
`POST /api/workspaces` (FR-16), `POST /api/demo/step` and
`POST /api/demo/reset` (FR-17, presenter-gated), and workspace scoping on
the existing product and catalogue routes.

Status codes: 400 validation, 401 no session, 403 authorisation, 404 not
found, 502 wallet/store failure, 500 otherwise.

## 9. Deployment

Vercel first: route handlers on the Node runtime; `mongodb` and
`@bsv/wallet-toolbox-client` are `serverExternalPackages`; the Mongo
client and wallet are held in the process-level singleton (acceptable
under serverless reuse; revisit connection pooling if cold-start volume
grows). Dockerisation stays open by policy: no Vercel-only APIs on
critical paths, all state in MongoDB, Next `standalone` output available
when needed. The public deployment target is the `*.bsvb.tech` link
(FR-14).

## 10. Testing

| Suite | Count | Covers |
|-------|-------|--------|
| `packages/dpp-core` | 68 | Codec, signatures, transitions, chain verification, SPV, blob binding |
| `packages/overlay-topics` | 8 | Admission policy, lookup indexing |
| `apps/web/test/flows.test.ts` | 17 | All flows incl. selective disclosure, blob integrity, CR-5/CR-2/CR-4 stamping |

All suites run offline. Before any screen is built, the v0
banned-vocabulary tripwire is ported and extended over new surfaces
(CLAUDE.md hard rule 1).

## 11. Open technical decisions

| Item | Blocked on |
|------|-----------|
| Attest profile enforcement (§4.5) | This spec round with the protocol workstream |
| Battery restricted-tier mechanism (§3) | Protocol confirmation (CR-8 path); UX resolved (G12) |
| Per-workspace identity derivation (§3) | Build decision at FR-15 implementation |
| Live-mode provisioning guardrails (§5A) | PRD G14: gating, rate limits, funding caps |
| CR-6 attestation post on serverless | Wrap in Next `after()` when the flag goes live |
| `object_did` derivation | D-CR2 |
| Resolver client (B) implementation and switch | G10 contract, D-CR1 |
| Profile JSON fetch mechanism | D-CR5 |
| Overlay deployment home | D-CR7 (infra) |