2026-10-06_user-mgmt-module_spec

Spec — Wine2You user-mgmt module (task 2)

Date: 2026-10-06 Repo: digid-co/wine2you → apps/user-mgmt/ Stack: Next.js (App Router) + TypeScript + Prisma + Tailwind Depends on: task 1 (infra/identity/compose.yaml — Postgres, ZITADEL, OpenFGA, Unkey)

1. Goal & Boundary

Deliverable: the Wine2You user-management module — the source of truth for users, user types, entities, memberships, and integrator→entity associations — plus the integrator UI to administer (ZITADEL user/role) + (OpenFGA relations) + (Unkey keys) as one combination.

In scope:

Out of scope:

2. Type & API Contracts

type UserType = 'buyer' | 'supplier' | 'integrator' | 'curator' | 'staff';
type EntityKind = 'estate' | 'shop';

interface User { id: string; email: string; types: UserType[]; }
interface Entity { id: string; kind: EntityKind; name: string; }
interface Membership { userId: string; entityId: string; role: string; }
interface IntegratorGrant { integratorUserId: string; entityId: string; canMintKeys: boolean; }
interface ApiKeyRef { id: string; unkeyKeyId: string; entityId: string; createdAt: Date; revokedAt?: Date; }

Key-mint invariant (enforced in the route, not the UI): POST /api/keys with { entityId } MUST call openfga.check({ user: 'integrator:'+session.userId, relation: 'can_mint_key', object: 'entity:'+entityId }) and only on allowed===true call Unkey. On deny → HTTP 403. Never mint without the check.

3. File Execution Matrix

Action Path
Create apps/user-mgmt/package.json, tsconfig.json, next.config.ts, tailwind.config.ts
Create apps/user-mgmt/prisma/schema.prisma
Create apps/user-mgmt/src/lib/{zitadel,openfga,unkey,prisma}.ts
Create apps/user-mgmt/src/app/api/{users,entities,members,grants,keys}/route.ts
Create apps/user-mgmt/src/app/integrator/keys/page.tsx
Create apps/user-mgmt/tests/ (openfga gate tests)
Create apps/user-mgmt/README.md, .env.example
Modify root README.md (monorepo note)

4. Verification & Testing Strategy

cd apps/user-mgmt
npm ci
npx prisma generate
npx tsc --noEmit
npm test          # openfga gate: deny-without-grant, allow-with-grant
npm run build     # Next.js production build succeeds

Constraints: no secrets in code (all via env); OpenFGA check is server-side only.