> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.hoop.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Architecture

> Understand how Hoop's components fit together and how traffic flows through the system.

export const GatewayVisualization = () => {
  const STREAMS = [{
    protocol: 'PostgreSQL',
    raw: 'sarah.chen@acme.io',
    masked: '[EMAIL]'
  }, {
    protocol: 'kubectl',
    raw: 'delete deploy/prod',
    masked: '\u26D4 blocked',
    blocked: true
  }, {
    protocol: 'MySQL',
    raw: 'SSN 284-19-7653',
    masked: '[REDACTED]'
  }, {
    protocol: 'SSH',
    raw: 'cat /etc/shadow',
    masked: '\u2713 logged'
  }, {
    protocol: 'gRPC',
    raw: 'card 4532-7891',
    masked: '[PCI]'
  }];
  const PHASE_TIMING = {
    enter: 0,
    travel: 900,
    cross: 1800,
    exit: 2600,
    fade: 3400
  };
  const CYCLE_MS = 4200;
  const STAGGER = 600;
  function Lane({stream, index, cycle}) {
    const [phase, setPhase] = useState('idle');
    useEffect(() => {
      const base = index * STAGGER;
      setPhase('idle');
      const timers = [setTimeout(() => setPhase('enter'), base + PHASE_TIMING.enter), setTimeout(() => setPhase('travel'), base + PHASE_TIMING.travel), setTimeout(() => setPhase('cross'), base + PHASE_TIMING.cross), setTimeout(() => setPhase('exit'), base + PHASE_TIMING.exit), setTimeout(() => setPhase('fade'), base + PHASE_TIMING.fade)];
      return () => timers.forEach(clearTimeout);
    }, [cycle, index]);
    const showRaw = phase === 'enter' || phase === 'travel';
    const showFlash = phase === 'cross';
    const showMasked = phase === 'exit' || phase === 'fade';
    const rawLeft = phase === 'enter' ? '2%' : '32%';
    const maskedLeft = phase === 'exit' ? '56%' : '82%';
    return <div style={{
      display: 'flex',
      alignItems: 'center',
      height: 52,
      position: 'relative'
    }}>
        {}
        <div style={{
      width: 88,
      flexShrink: 0,
      fontFamily: 'var(--mono)',
      fontSize: 12,
      fontWeight: 500,
      color: phase !== 'idle' ? 'rgba(var(--sand-100-rgb),0.55)' : 'rgba(var(--sand-100-rgb),0.2)',
      textAlign: 'right',
      paddingRight: 20,
      transition: 'color 0.4s'
    }}>
          {stream.protocol}
        </div>

        {}
        <div style={{
      flex: 1,
      height: 1,
      background: 'rgba(var(--sand-100-rgb),0.06)',
      position: 'relative'
    }}>
          {}
          {phase !== 'idle' && phase !== 'fade' && <div style={{
      position: 'absolute',
      top: 0,
      left: 0,
      right: 0,
      height: 1,
      background: 'rgba(var(--warm-gold-rgb),0.15)',
      transition: 'opacity 0.4s'
    }} />}

          {}
          {showRaw && <div style={{
      position: 'absolute',
      top: -15,
      left: rawLeft,
      transition: 'left 0.9s cubic-bezier(0.25, 0.1, 0.25, 1), opacity 0.3s',
      opacity: phase === 'travel' ? 1 : 0.7
    }}>
              <span style={{
      fontFamily: 'var(--mono)',
      fontSize: 13,
      fontWeight: 500,
      color: 'rgba(var(--sand-100-rgb),0.8)',
      whiteSpace: 'nowrap',
      background: 'rgba(var(--sand-100-rgb),0.06)',
      padding: '5px 14px',
      borderRadius: 6,
      border: '1px solid rgba(var(--sand-100-rgb),0.10)',
      display: 'inline-block'
    }}>
                {stream.raw}
              </span>
            </div>}

          {}
          {showFlash && <div style={{
      position: 'absolute',
      top: -20,
      left: '50%',
      transform: 'translateX(-50%)',
      width: 40,
      height: 40,
      borderRadius: '50%',
      background: stream.blocked ? 'rgba(var(--error-rgb),0.2)' : 'rgba(var(--warm-gold-rgb),0.15)',
      animation: 'gw-pulse 0.7s ease-out forwards'
    }} />}

          {}
          {showMasked && <div style={{
      position: 'absolute',
      top: -15,
      left: maskedLeft,
      transition: 'left 0.8s cubic-bezier(0.25, 0.1, 0.25, 1), opacity 0.5s',
      opacity: phase === 'fade' ? 0 : 1
    }}>
              <span style={{
      fontFamily: 'var(--mono)',
      fontSize: 13,
      fontWeight: 700,
      color: stream.blocked ? 'rgba(var(--error-rgb),0.9)' : 'var(--warm-gold)',
      whiteSpace: 'nowrap',
      background: stream.blocked ? 'rgba(var(--error-rgb),0.10)' : 'rgba(var(--warm-gold-rgb),0.10)',
      padding: '5px 14px',
      borderRadius: 6,
      border: stream.blocked ? '1px solid rgba(var(--error-rgb),0.20)' : '1px solid rgba(var(--warm-gold-rgb),0.18)',
      display: 'inline-block'
    }}>
                {stream.masked}
              </span>
            </div>}
        </div>
      </div>;
  }
  const [cycle, setCycle] = useState(0);
  const mounted = useRef(true);
  useEffect(() => {
    mounted.current = true;
    const interval = setInterval(() => {
      if (mounted.current) setCycle(c => c + 1);
    }, CYCLE_MS);
    return () => {
      mounted.current = false;
      clearInterval(interval);
    };
  }, []);
  return <div style={{
    height: 420,
    position: 'relative',
    overflow: 'hidden'
  }}>
      {}
      <div style={{
    position: 'absolute',
    inset: 0,
    backgroundImage: 'radial-gradient(rgba(var(--sand-100-rgb),0.05) 1px, transparent 1px)',
    backgroundSize: '28px 28px',
    pointerEvents: 'none'
  }} />

      {}
      <div style={{
    position: 'absolute',
    top: 32,
    bottom: 32,
    left: '50%',
    width: 1,
    background: 'linear-gradient(180deg, transparent 0%, rgba(var(--warm-gold-rgb),0.3) 20%, rgba(var(--warm-gold-rgb),0.3) 80%, transparent 100%)',
    transform: 'translateX(-50%)'
  }} />

      {}
      <div style={{
    display: 'flex',
    alignItems: 'center',
    padding: '0 0 0 88px',
    height: 44,
    position: 'relative'
  }}>
        {}
        <div style={{
    position: 'absolute',
    left: 108,
    top: 14,
    fontFamily: 'var(--sans)',
    fontSize: 11,
    fontWeight: 600,
    textTransform: 'uppercase',
    letterSpacing: '0.08em',
    color: 'rgba(var(--sand-100-rgb),0.25)'
  }}>
          Raw
        </div>

        {}
        <div style={{
    position: 'absolute',
    left: '50%',
    top: 8,
    transform: 'translateX(-50%)',
    display: 'flex',
    alignItems: 'center',
    gap: 8,
    background: 'rgba(var(--warm-gold-rgb),0.08)',
    border: '1px solid rgba(var(--warm-gold-rgb),0.15)',
    padding: '5px 14px',
    borderRadius: 99
  }}>
          <div style={{
    width: 7,
    height: 7,
    borderRadius: '50%',
    background: 'var(--warm-gold)',
    boxShadow: '0 0 10px rgba(var(--warm-gold-rgb),0.5)',
    animation: 'gw-breathe 2s ease-in-out infinite'
  }} />
          <span style={{
    fontFamily: 'var(--mono)',
    fontSize: 11,
    fontWeight: 600,
    color: 'var(--warm-gold)',
    textTransform: 'uppercase',
    letterSpacing: '0.08em'
  }}>
            Gateway
          </span>
        </div>

        {}
        <div style={{
    position: 'absolute',
    right: 16,
    top: 14,
    fontFamily: 'var(--sans)',
    fontSize: 11,
    fontWeight: 600,
    textTransform: 'uppercase',
    letterSpacing: '0.08em',
    color: 'rgba(var(--sand-100-rgb),0.25)'
  }}>
          Protected
        </div>
      </div>

      {}
      <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: 8,
    paddingTop: 12
  }}>
        {STREAMS.map((s, i) => <Lane key={`${s.protocol}-${cycle}`} stream={s} index={i} cycle={cycle} />)}
      </div>

      {}
      <div style={{
    position: 'absolute',
    bottom: 16,
    left: 88,
    right: 0,
    display: 'flex',
    justifyContent: 'center',
    gap: 40,
    borderTop: '1px solid rgba(var(--sand-100-rgb),0.06)',
    paddingTop: 14,
    marginRight: 16
  }}>
        {[{
    n: '5',
    label: 'protocols'
  }, {
    n: '<5ms',
    label: 'added latency'
  }, {
    n: '100%',
    label: 'inspected'
  }].map(s => <div key={s.label} style={{
    display: 'flex',
    alignItems: 'baseline',
    gap: 7
  }}>
            <span style={{
    fontFamily: 'var(--display)',
    fontSize: 18,
    fontWeight: 700,
    color: 'rgba(var(--sand-100-rgb),0.55)',
    letterSpacing: '-0.02em'
  }}>{s.n}</span>
            <span style={{
    fontFamily: 'var(--sans)',
    fontSize: 11,
    color: 'rgba(var(--sand-100-rgb),0.25)'
  }}>{s.label}</span>
          </div>)}
      </div>

      <style>{`
        @keyframes gw-pulse {
          0% { transform: translateX(-50%) scale(0.5); opacity: 1; }
          100% { transform: translateX(-50%) scale(2.5); opacity: 0; }
        }
        @keyframes gw-breathe {
          0%, 100% { opacity: 0.6; transform: scale(1); }
          50% { opacity: 1; transform: scale(1.4); }
        }
      `}</style>
    </div>;
};

Hoop is a secure access gateway that sits between your users and your infrastructure. It proxies connections through a lightweight agent running inside your network — no inbound firewall rules required, no VPN needed, and nothing in your infrastructure exposed to the internet.

<div style={{height: 350, overflow: 'hidden', borderRadius: 14}}>
  <GatewayVisualization />
</div>

## Components

### Gateway

The gateway is the central hub of the system. It handles all user-facing traffic — authentication, authorization, session management, and policy enforcement — and coordinates with agents over a persistent gRPC connection.

The gateway runs as a single binary and exposes:

* **Web UI** — browser-based interface for managing resource roles, running queries, reviewing sessions, and approving access requests
* **REST API** — the same API the web UI uses; available for programmatic access and automation
* **gRPC server** — the internal transport layer used by agents and native clients to stream data
* **Protocol proxies** — native protocol listeners (PostgreSQL, SSH, RDP, HTTP) that allow standard tools to connect without any Hoop-specific client

The gateway requires a **PostgreSQL database** for storing resource roles, sessions, users, and audit data.

### Agent

The agent runs inside your network, close to the resources you want to expose. It establishes an outbound gRPC connection to the gateway — meaning your infrastructure never needs to accept inbound connections.

Agents run in two modes:

* **Standard mode** — a persistent long-lived process, managed as a system service. Best for databases, internal APIs, container platforms, and jump hosts.
* **Embedded mode** — runs alongside an application process, sharing its runtime context. Best for ad-hoc tasks, REPL consoles, and single-resource connections.

A single agent can serve multiple resource roles. See the [Agents](/concepts/agents) page for deployment details.

### Clients

Users connect to resources through:

* **Web UI** — browser-based access with a built-in code editor and terminal
* **Native clients** — standard tools like `psql`, `mysql`, `kubectl`, or `ssh` pointed at a local proxy port on the gateway
* **HTTP proxy** — browser or curl access to internal HTTP/HTTPS services via a time-limited session token

***

## Ports

| Port    | Protocol         | Purpose                                      |
| ------- | ---------------- | -------------------------------------------- |
| `8009`  | HTTP / WebSocket | REST API, Web UI, and browser-based sessions |
| `8010`  | gRPC             | Agent and native client communication        |
| `15432` | PostgreSQL wire  | Native `psql` / PostgreSQL client access     |
| `12222` | SSH              | Native SSH client access                     |
| `13389` | RDP              | Remote desktop access                        |
| `18888` | HTTP / WebSocket | HTTP Proxy client access                     |

Agents only make **outbound** connections to port `8010`. No inbound ports need to be opened on the agent side.

***

## Session Flow

When a user connects to a resource, the following happens:

1. **Authentication** — the user authenticates via your identity provider (OIDC, SAML, or local auth). The gateway verifies the token and loads the user's identity and group membership.

2. **Authorization** — the gateway checks whether the user has access to the requested resource role, based on access control rules and the resource role's configured access mode

3. **Policy evaluation** — before the session begins, the gateway runs the request through an ordered plugin chain:
   * **Reviews** — if JIT approval is required, the session is held until approved
   * **Audit** — the request is logged
   * **Data masking** — DLP rules are applied to inputs and outputs
   * **RBAC** — role-based rules are enforced
   * **Webhooks / Runbook hooks** — external systems are notified

4. **Agent dispatch** — the gateway forwards the session to the agent responsible for that resource role. If the agent is offline, the connection fails.

5. **Execution** — the agent executes the operation against the target resource using native protocols (SQL, SSH, RDP, HTTP, etc.) and streams results back through the gateway.

6. **Response processing** — the gateway applies output-side plugins (data masking, audit logging) before returning data to the client.

***

## Network Architecture

The diagram below illustrates deployment scenarios including Kubernetes and networks with or without inbound connectivity.

<Frame>
  <img src="https://mintcdn.com/hoopdev/ba5h38UclvRtaLDL/images/learn/architecture-diagram-2.png?fit=max&auto=format&n=ba5h38UclvRtaLDL&q=85&s=25fe02a42184186886827586f385471d" alt="Hoop Networking Architecture" style={{width: '100%', height: 'auto', maxWidth: '900px'}} width="2122" height="1504" data-path="images/learn/architecture-diagram-2.png" />
</Frame>

***

## Deployment Topologies

### Single Node (Docker Compose)

Suitable for evaluation and smaller deployments. All components — gateway, default agent, PostgreSQL, and optional DLP services — run as containers on a single host.

See [Docker Compose deployment](/setup/deployment/docker-compose).

### Kubernetes

The recommended approach for production. The gateway runs as a pod (or multiple replicas), agents are deployed as separate pods close to your resources, and PostgreSQL is provided by an external managed service (e.g. RDS).

Optional components — data masking (Microsoft Presidio) and protocol proxies — can be enabled independently.

See [Kubernetes deployment](/setup/deployment/kubernetes).

***

## Authentication

The gateway integrates with your identity provider for user authentication. Supported methods:

* **OIDC / OAuth 2.0** — Okta, Azure AD, Google, Auth0, and any OIDC-compliant provider
* **SAML 2.0** — enterprise SSO
* **Local** — built-in username/password, suitable for self-hosted setups without an external IDP

Agents authenticate to the gateway using a signed token (`HOOP_KEY`) generated at registration time. Service accounts can be created for programmatic API access.

See [Identity Providers](/setup/configuration/idp/get-started) for configuration details.
