# ADR 0001: v1 control-plane boundaries and domain - Status: Accepted - Date: 2026-07-16 - Tracking: #2 ## Context Rendezvous must help two game peers discover and attempt an authenticated direct connection without becoming a game server, an identity provider, or a gameplay traffic service. The HTTP API and UDP mediator share short-lived state and must agree on authorization, endpoint freshness, and tenant scope. ## Decision V1 is one ASP.NET Core deployable with separable directory, join-authorization, endpoint-registry, NAT-mediator, and operations modules. Modules communicate through application interfaces, not through transport DTOs or one another's storage implementation. Contracts and the client SDK remain independently packageable. The service is a connection control plane. A successful join authorization only grants permission to attempt a direct connection. The game host remains the final authority for player identity, capacity, bans, admission, and gameplay. Rendezvous success is reported only after the host accepts a valid connection ticket and LiteNetLib establishes the authenticated peer connection. ## Domain glossary | Term | Definition | Lifetime and exposure | | --- | --- | --- | | `SessionListing` | Bounded public discovery data for one hosted game session. | Visible only while its lease and host presence are fresh. Never contains endpoints or credentials. | | `Lease` | Renewable capability controlling the lifetime of a listing. | Secret, host-only, expires unless renewed. | | `HostPresence` | Authenticated observation of the host's local and public UDP endpoints from its gameplay socket. | Internal, short-lived, never returned by browsing. | | `JoinAttempt` | Authorization linking one client attempt to one compatible session. | Internal and short-lived; it is not authoritative game admission. | | `PunchCapability` | Opaque, one-time credential scoped to attempt, role, tenant, and expiry. | Sent only to its intended peer; consumed at the UDP mediator. | | `ConnectionTicket` | Compact signed credential presented to the host during the direct connection. | One-time, short-lived, and scoped to the attempt and host. | IDs are opaque and tenant-scoped. They are never canonical player, entity, or world identities. ## Trust boundaries ```mermaid flowchart LR Browser["Untrusted browser/client"] -->|"HTTPS: browse/join"| Proxy["Reverse proxy"] Host["Game host"] -->|"HTTPS: register/renew"| Proxy Operator["Privileged operator"] -->|"separate authenticated route"| Proxy Proxy -->|"normalized HTTP + trusted forwarding metadata"| Service["Rendezvous service"] Host -->|"host gameplay UDP socket"| Mediator["UDP mediator module"] Browser -->|"client gameplay UDP socket"| Mediator Mediator <--> Service Service -->|"read keys; never list or log values"| Secrets["Secret provider"] Service -.->|"future authenticated state protocol"| Store["Future shared store"] Service -->|"redacted events and aggregate metrics"| Ops["Observability systems"] ``` - Public HTTP input is hostile even after TLS termination. The proxy may be trusted to terminate TLS and supply forwarding metadata only when its source address is allowlisted; forwarded headers from other sources are discarded. - Public UDP input is hostile even when structurally valid. HTTP-supplied endpoints are claims, never proof. Public response targets come only from an authenticated UDP packet's observed source. A private local candidate may be carried inside that packet only under ADR 0002's bounded same-LAN rules. - Operator routes use a separate authentication policy and network exposure. Operator access does not bypass tenant scoping, audit, or secret redaction. - The client SDK is convenience code in an untrusted process. Server decisions never rely on client-side validation or secrecy. - Game hosts are authoritative only for their own gameplay admission. A host cannot enumerate or mutate another game/environment tenant. - The secret provider is trusted with long-lived key material. The application receives only the minimum named key version it needs. - A future shared store is a distinct authenticated boundary. Moving state to it does not make stored input trusted and requires a new availability ADR. ## Connection data flow ```mermaid sequenceDiagram participant H as Game host participant R as Rendezvous HTTP participant M as Rendezvous UDP mediator participant C as Game client H->>R: Register listing (publisher authorization) R-->>H: Lease + host-presence capability H->>M: Presence from gameplay UDP socket M->>R: Store observed endpoint and freshness H->>R: Renew lease C->>R: Browse compatible visible listings C->>R: Request join attempt R-->>C: Client punch capability R-->>H: Host attempt/capability via authenticated poll or stream H->>M: Host capability from gameplay UDP socket C->>M: Client capability from gameplay UDP socket M->>M: Validate scope, freshness, expiry, and replay state M-->>H: Introduce verified client endpoints + connection ticket M-->>C: Introduce verified host endpoints + connection ticket C->>H: Direct LiteNetLib connect + ticket H->>H: Validate and consume ticket; apply game admission H-->>C: Authenticated peer connection or rejection ``` The mediator does not forward normal gameplay packets. A connection attempt that times out or is rejected returns a typed outcome to the caller. ## Consequences - Directory and mediator can ship together without erasing their module boundary. - Contracts cannot expose server storage or LiteNetLib implementation types. - Tests must cover the three-party host/service/client flow; an HTTP-only test is insufficient evidence of a successful connection. - Splitting modules into processes requires an explicit protocol, shared-state ownership, deterministic mediator routing, and a superseding ADR.