Namespace tree web view — org-owned structure management (Slice 1.5, part 1)

Created: Fri 26 Jun 2026 16:42:25 CEST Updated: Fri 26 Jun 2026 16:42:25 CEST - Initial design from brainstorming session Document Version: 1.0 - Initial design from brainstorming session Security Classification: Internal Technical Documentation Target Audience: Frontend Engineer, API Engineer, Product (Paul) Author: Paul Wisén


Problem

Slice 1 built the namespace registry backend (tables + GraphQL mutations) so the DB is the source of truth for the issuer tree and path allocation. But there is no surface for a person to build their tree — the createNamespaceNode / namespaceTree mutations are live but unreachable from any UI. Paul needs to build a category tree (e.g. Kartonger, Påsar) so he can then generate tags for those physical objects.

This spec covers Slice 1.5 part 1: a web view in the existing Plings-Web app to build/name/browse an organization’s namespace tree. Part 2 — keygen ↔ registry generation (reserve → derive locally → confirm, which actually mints tags) is a separate follow-up spec; it is gated on a CLI-auth decision and is out of scope here.

Key decision: the organization owns the structure

The namespace tree is owned by an organization, not a user. The model is organization ↔ manufacturer (1:1 per wallet): registering an org as an issuer assigns it the next manufacturer_index. Individual users can still own sub-branches within the org’s tree (namespace_node.owner_user_id), preserving the two-tier model of ADR-0050. Manufacturer-index assignment stays Plings-governed (ADR-0050) — it is not a self-service action in this slice.

Concrete setup for Paul (one-time, performed by an admin / via MCP, not the UI)

  1. Create the dev wallet row. wallet_versions only has v1; Paul mints under wallet 3 (dev). Insert version_id=3 (version_name='v3', environment='development', is_default=false, master_key_id = the dev key ref). The registry tables FK wallet_version → wallet_versions, so this row must exist before any wallet-3 node.
  2. Register org “Pauls” as a manufacturer at wallet 3 → index 2 (Plings seed holds index 1). Sets manufacturer_registry.organization_id = the Pauls org id (9ba2023b-117f-4346-a809-839a66b8c0d4). Paul’s namespace root becomes 3.2, his nodes 3.2.x. (Org “Plings” → existing index 1 is a separate, later step.)

Wallet 3 is the dev key (disposable per ADR — production starts clean under wallet 1 at go-live). Tags minted now are for testing the full flow and may be reprinted at go-live.

Backend (Plings-API) — one small read query

Everything else reuses Slice 1. The only addition:

  • organizationManufacturer(organizationId: ID!, walletVersion: Int!): ManufacturerInfo returning { walletVersion: Int!, manufacturerIndex: Int!, name: String! } or null when the org is not registered as a manufacturer for that wallet. Reads manufacturer_registry by (organization_id, wallet_version). Auth: the caller must be a member of the org (mirrors the organization_members check used elsewhere). This lets the frontend resolve the (walletVersion, manufacturerIndex) pair that scopes every tree call.

Reused as-is from Slice 1: namespaceTree(walletVersion, manufacturerIndex), createNamespaceNode, renameNamespaceNode, retireNamespaceNode.

Frontend (Plings-Web)

Placement & routing

  • New protected route /organizations/:orgId/namespace, reached from each Organization card on the dashboard (a “Tag structure” / “Namespace” action). Scoped to that org and the dev wallet (3) (hardcoded for this slice; wallet switching is future).
  • Follows the existing routing/auth conventions (react-router, session-validated; the Apollo authLink attaches the Supabase JWT automatically — no manual auth work).

Data layer

  • A useNamespaceTree(orgId) hook: first resolves the org’s manufacturer via organizationManufacturer, then queries namespaceTree(walletVersion=3, manufacturerIndex). GraphQL documents go in src/graphql/{queries,mutations}.ts; types in src/types/graphql.ts (run codegen after the API schema change). Mutations refetchQueries the tree (mirrors useMyOrganizations).
  • If organizationManufacturer is null, the page shows an explanatory empty state (“This organization isn’t set up as a tag issuer yet — contact Plings”) rather than a register button (registration is Plings-governed; deferred to an admin surface).

UI

  • Container Card (matching OrganizationsSection), title from the org name.
  • The tree is rendered recursively from the flat namespaceTree result (each node has prefixDecimal, parentNodeId, levelIndex, name) using shadcn Collapsible/Accordion + lucide-react chevrons. Each node shows its name and prefixDecimal (e.g. 3.2.1), with per-node Add child, Rename, Retire actions (a Dialog for add/rename, an AlertDialog confirm for retire). A top-level Add root node adds directly under the manufacturer.
  • Retired nodes are visually de-emphasized (Badge “retired”), not removed.

Error/empty handling

  • Loading → Skeleton; query error → inline retry (the OrganizationsSection pattern).
  • createNamespaceNode enforces depth (≤ 6 category levels) and authz server-side; the UI surfaces the GraphQL error message in the dialog (e.g. depth-cap, not-authorised).

Testing

  • API: a resolver test for organizationManufacturer (member sees the row; non-member → null/denied; no row → null), using the existing fake-engine pattern. Full poetry run pytest green.
  • Frontend (no unit runner): npx tsc --noEmit + npm run build succeed. Manual on-device/in-app check: build a tree under “Pauls”, confirm prefixes (3.2.1 …), rename, retire, depth-cap error, empty state for an org with no manufacturer.

Out of scope / next

  • Slice 1.5 part 2 — keygen ↔ registry generation: reserve → derive locally (master key) → confirm. Needs the CLI-auth decision (paste-token / email+password / file-token). Separate spec.
  • Admin manufacturer management (self-service “register my org as an issuer”, wallet switching, org “Plings” → index 1). Deferred; tracked in ROADMAP Backlog.
  • Production wallet (1) minting — these dev (wallet 3) tags may be reprinted at go-live.

Decisions to record (ADR, when this lands)

  • Organization ↔ manufacturer mapping (the manufacturer tier of ADR-0050 = an organization; individuals own sub-branches via owner_user_id). Likely a short ADR refining 0050, written when part 1 + the setup land.