Wine2You — Identity, Authorization & API-Key Architecture (deployed)

Wine2You — Identity, Authorization & API-Key Architecture

Status: ACCEPTED & DEPLOYED (Kris, 2026-10-06). Scope: how machines/services/users communicate under Wine2You's own infrastructure. Databunker is out of scope here (separate age-verification module).

1. Domain scope (what the platform must express)

Concept Definition
User One identity (human).
User type(s) A user may hold multiple types simultaneously: buyer, supplier, integrator (+ later: curator, staff). Types are not exclusive.
Entity A trading unit — an Estate (supplier) or a Shop (a curated selection, optionally with storefront).
Membership Multiple users join one entity. A user may belong to several entities.
Integrator A user type that creates/manages API credentials for the entities they are associated with — enabling feeds of inventory, prices, shipping, etc.
API key A machine credential bound to exactly one entity — keys do NOT span entities (Kris, 2026-10-06).

2. Architecture — deployed shape (3 containers + Unkey Cloud)

┌──────────────────────────────────────────────────────────────────────────────┐
│  Wine2You services (Docker on contabo1, tailnet 100.64.0.8)                    │
│                                                                                │
│  ┌────────────────────────┐   ┌──────────────────┐     ┌──────────────┐       │
│  │ (a) user-mgmt module   │──▶│ (b) ZITADEL      │     │ (c) Unkey    │       │
│  │  OUR CODE (Next.js+TS) │   │  identity/OIDC   │     │  CLOUD       │       │
│  │  - owns domain model   │   │  :18080          │     │  api.unkey.  │       │
│  │  - integrator UI       │   │  auth.tail.      │     │  dev         │       │
│  │  - key-mgmt UI         │   │  digid.co        │     │  (free tier) │       │
│  │  - orchestrates b/c/d  │   └──────────────────┘     └──────────────┘       │
│  └───────────┬────────────┘                                  ▲                │
│              │                                               │ mint checks    │
│              ▼                                               │                │
│  ┌────────────────────────┐   :18081/18082                 │                │
│  │ (d) OpenFGA            │◀────────────────────────────────┘                │
│  │  ReBAC relations       │   "may integrator X mint key for entity E?"       │
│  │  user↔type, user↔      │                                                  │
│  │  entity, integrator↔   │   openfga.tail.digid.co                          │
│  │  entity, key↔entity    │                                                  │
│  └────────────────────────┘                                                  │
│                                                                                │
│  Postgres 16 (:15432, internal) — ZITADEL + OpenFGA schemas                    │
└──────────────────────────────────────────────────────────────────────────────┘
   Tailnet-only (WireGuard). Public path: Cloudflare Tower/edge → api.wine2you.shop
   (Access service tokens for M2M; OIDC for users). No inbound port on contabo1.

   Core app (Medusa v2 + Next.js + Meilisearch) lives on the Oracle free box
   (wine2you-dev-zrh), separate host — see blast-radius rule (§5).

Deployment facts (live 2026-10-06)

Component Where Address Status
user-mgmt (a) not yet deployed (task 3) — coded, tests pass
ZITADEL (b) contabo1 /opt/compose/w2y-identity 100.64.0.8:18080 / auth.tail.digid.co:18080 UP
OpenFGA (d) contabo1 /opt/compose/w2y-identity 100.64.0.8:18081 (http) :18082 (grpc) / openfga.tail.digid.co UP
Postgres contabo1 /opt/compose/w2y-identity 100.64.0.8:15432 (internal) UP
Unkey (c) Unkey Cloud https://api.unkey.com (key in 1P Digid/Unkey) live (SaaS)
Core app Oracle wine2you-dev-zrh dev.wine2you.shop reserved

(a) user-mgmt module — ours, the source of truth

Owns the domain model and the Wine2You UI for integrators:

(b) ZITADEL — authentication (who you are)

(c) Unkey — API-key lifecycle (what a machine presents) — CLOUD

(d) OpenFGA — fine-grained authorization (what a user may do to what)

Division of responsibility (one line each)

Layer Owns Answers
user-mgmt module domain model, UI, orchestration "what IS the state?"
ZITADEL credentials, login, service accounts "who are you?"
OpenFGA relationship tuples "may you do X to Y?"
Unkey key material + lifecycle "is this key valid/scoped?"

3. Core flows

A. User onboarding — user signs up → module registers in ZITADEL (org) → module writes user#type + user#member entity tuples to OpenFGA → token carries type claims.

B. Integrator mints a key for an entity —

  1. Integrator opens the key UI (module).
  2. Module → OpenFGA Check(integrator:i, can_mint_key, entity:E).
  3. Allowed → module → Unkey createKey(scopes, entity:E).
  4. Module stores key#owner entity:E in OpenFGA + shows the plaintext key once.
  5. Any service verifying the key → Unkey → entity scope resolved.

C. Machine feed (inventory/prices/shipping) — integrator's system calls api.wine2you.shop/… with its entity-scoped key → Cloudflare Access + Unkey verify → module/API acts within that entity only.

D. Entity membership — user invites users to an Estate/Shop → module writes membership tuples → OpenFGA governs what members can read/write.

4. Exposure & security

5. Hosting split (approved 2026-10-06) — blast-radius rule

Host Runs Why
Oracle free box (wine2you-dev-zrh, 2 OCPU/12 GB) Wine2You core app (Medusa v2, Next.js, Meilisearch) Project app owns its own host
contabo1 (8 vCPU/23 GB) ZITADEL + OpenFGA + user-mgmt (identity = shared service) Shared service, blast-radius isolation
Unkey Cloud (free tier) Self-host min 8 cores/16 GB/K8s — doesn't fit

Blast-radius principle: identity/auth is expensive to rebuild and shared across projects → runs on the shared-services host (contabo1). Project apps own their own hosts.

6. Why Unkey is Cloud (not self-hosted)

Unkey's self-hosted deployment requires 8 cores / 16 GB RAM / 100 GB disk + Kubernetes 1.28+. Neither the Oracle free box (2/12) nor hetzner1 (7.6 GB) fits; contabo1 could but at a large share of its capacity for a component that Unkey runs well as SaaS. The Cloud free tier covers the pre-launch need; the module talks to it over HTTPS with UNKEY_ROOT_KEY from 1Password.

7. Build order (status)

  1. CF Tunnel + tailnet-only admin + Docker host — done.
  2. Postgres container — done (contabo1, :15432).
  3. ZITADEL container — done (contabo1 :18080, live, migrations applied).
  4. OpenFGA container + model — done (contabo1 :18081/:18082, live; model = task 3).
  5. Unkey — done (Cloud free tier, key in 1P).
  6. user-mgmt module — code done (apps/user-mgmt/, 9/9 tests); wiring to live stack = task 3.
  7. Medusa/Next.js storefront + Meilisearch — on Oracle box (pending).

8. Open questions / carry-forward

Related