# LLM Gateway: an OpenAI-shaped front door with AGPL core and a commercial ee/ directory

> This is a TypeScript monorepo that fronts several model providers behind one OpenAI-compatible endpoint, with Postgres, Redis and a second persistent Redis for responses. The deployment path is documented carefully, down to a warning about bind mounts and a secret generated with openssl, and the licensing is described three different ways across the repository.

**theopenco/llmgateway** — Route, manage, and analyze your LLM requests across multiple providers with a unified API interface.

- Repository: https://github.com/theopenco/llmgateway
- Website: https://llmgateway.io
- Stars: 1,671 · Forks: 192
- Language: TypeScript
- License: NOASSERTION
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/theopenco-llmgateway

## The documented docker run passes a secret under a different name than it exports

The self-hosted path starts by generating two secrets:

```bash
export LLM_GATEWAY_SECRET="$(openssl rand -base64 32 | tr -d '\n')"
export GATEWAY_API_KEY_HASH_SECRET="$(openssl rand -base64 32 | tr -d '\n')"
./scripts/run-unified-container.sh
```

The one-off docker run alternative builds the same two values and passes them under names that do not match the exports:

```bash
docker volume create llmgateway_postgres
docker volume create llmgateway_redis

docker run -d \
  --name llmgateway \
  --restart unless-stopped \
  -p 3002:3002 \
  -p 3003:3003 \
  -p 3005:3005 \
  -p 3006:3006 \
  -p 3007:3007 \
  -p 4001:4001 \
  -p 4002:4002 \
  -e AUTH_SECRET="$LLM_GATEWAY_SECRET" \
  -e GATEWAY_API_KEY_HASH_SECRET="$GATEWAY_API_KEY_HASH_SECRET" \
  ghcr.io/theopenco/llmgateway-unified:latest
```

LLM_GATEWAY_SECRET goes out as AUTH_SECRET, while the API key hash secret keeps its own name. Both are worth generating per deployment, since a predictable value in either place means predictable tokens. The seven published ports correspond to the dashboard, playground, docs, admin and two other web ports in the low 3000s plus the gateway and API in the 4000s, and none of the low 3000s is mapped back to 80.

## Postgres ships with the password pw and Redis runs with protected-mode off

The development compose file is written for worktrees rather than for a deployment, and the defaults say so:

```yaml
name: llmgateway${STACK_SUFFIX:-}
```

The comment above it explains that every host-facing name and port is overridable so several worktrees can run isolated stacks side by side, and that STACK_SUFFIX has to carry its own separator. Below that, Postgres runs as postgres:17-alpine with POSTGRES_USER=postgres, POSTGRES_PASSWORD=pw and POSTGRES_DB=db, and Redis starts with:

```yaml
  redis:
    image: redis:8
    command: ["redis-server", "--protected-mode", "no", "--bind", "0.0.0.0"]
```

protected-mode no with a bind to every interface and a published port is a local development convenience. So is log_statement=all on the database, which writes every statement to the log. Both are fine on a laptop and wrong on a shared host.

The .env.example file expects something stricter, with POSTGRES_PASSWORD=change_this_secure_password and REDIS_PASSWORD=change_this_redis_password. The compose file does not read those two variables for the services it defines, which is the mismatch to watch when adapting this stack.

## The setup script deletes your database volumes before it starts

One command sets up the development environment:

```bash
pnpm i && pnpm run setup
```

The README says this installs dependencies, starts the Docker services, syncs the schema and seeds initial data. What the script actually does is longer than that:

```
pnpm build:core && docker compose down -v && docker compose up -d && pnpm wait-for-services && pnpm push-test && pnpm push-dev && pnpm seed
```

`docker compose down -v` is the part to notice. The v flag removes named volumes, so this is a reset, not a start. Anyone running setup against a stack that holds data loses it, and the README's description of the step does not mention that.

The seed path also defaults to a fixed database when nothing is set: push-test expands TEST_DATABASE_URL, then DATABASE_URL, then falls back to postgres://postgres:pw@localhost:5432/test. That default matches the compose credentials, which is why the two files agree with each other and both use pw.

There is a smaller oddity in the same manifest. prepare-codex moves ~/.nvm to ~/.nvm-bak, switches the PATH to asdf shims, and starts local Postgres and Redis with sudo before running pnpm sync. A script that rewrites a developer's home directory to prepare an agent session is worth reading before running it.

## Three different descriptions of the same licence

The repository describes its licensing three ways, and they do not line up.

The README states a dual licence: core functionality under AGPLv3, with commercial features in the ee/ directory requiring an Enterprise license, and multi-organization administration requiring a white-label license. Enterprise features are listed as advanced billing and subscription management, extended data retention described as unlimited versus 30 days, custom provider key configurations, team and organization management, and priority support. A contact address is given for licensing.

The root package.json says "license": "SEE LICENSE IN LICENSE" and private true. A LICENSE file exists at the root and ee/LICENSE exists inside the enterprise directory.

The repository metadata field for the licence reads NOASSERTION rather than naming AGPLv3, so automated tooling reading the API will not learn the terms. If your compliance process reads licence fields rather than README text, this repository gives it nothing.

The practical boundary is the directory. Everything under apps/ and packages/ is the AGPL core, ee/ is the part that needs a separate agreement, and the README is explicit that commercial features live in the second.

## Setting one STORAGE_REDIS variable changes how the others resolve

There are two Redis instances, and the second one exists because of what it stores. The compose comment is explicit: storage Redis holds Responses API state with 30 day retention plus the gateway response cache, and it uses AOF persistence because it holds a source of truth rather than just a cache. It runs with --appendonly yes on its own port and a named volume, while the first Redis keeps the default persistence.

The resolution rule is documented as prose in .env.example, and it is the kind of detail that bites:

```
# With no STORAGE_REDIS_* var set, the main REDIS_* connection is reused
# (single-Redis setups keep working). Once any STORAGE_REDIS_* var is set,
# the others default to localhost/6379/no-password rather than inheriting
# from REDIS_*.
```

So adding STORAGE_REDIS_PASSWORD without setting STORAGE_REDIS_HOST silently points the host at localhost, where in a container nothing is listening. The migration path for an existing single-Redis setup is deliberately frictionless, and the price is that one variable is enough to change the meaning of the others.

STORAGE_REDIS_PORT is set to 6479 in the example, which is why it does not collide with the main instance on 6379.

## One published port has no name in the environment example

The docker run block publishes seven ports: 3002, 3003, 3005, 3006, 3007, 4001 and 4002. The .env.example file names six of them under service port mappings: GATEWAY_PORT=4001, API_PORT=4002, UI_PORT=3002, PLAYGROUND_PORT=3003, DOCS_PORT=3005 and ADMIN_PORT=3006.

Port 3007 is the odd one out. It is published by the documented command and absent from the configuration reference, so the service on it is undocumented in both places. The README's folder structure explains five of the web surfaces: a Next.js dashboard at apps/ui, a consumer chat app called Lounge at apps/playground, a Dev Plans and coding tools landing and dashboard at apps/code, a self-serve provider portal called Airside at apps/airside, and a documentation site at apps/docs. An internal admin dashboard sits at ee/admin. Six front ends, two backends in apps/api and apps/gateway, three shared packages, and one port left over.

That count is worth pausing on. What is described as an API gateway is, in this repository, one package among nine applications, most of which are Next.js front ends.

## The build has a core path that excludes every front end

The root manifest is a turbo-driven pnpm workspace over apps/* and packages/*. Most scripts are thin turbo fan-outs, and three of them describe the build in layers:

```
"build": "turbo run build 2>&1"
"build:ci": "turbo run build --concurrency=1 2>&1"
"build:core": "turbo run build --filter=!ui --filter=!docs --filter=!playground --filter=!admin --filter=!code --filter=!airside"
```

build:core excludes six packages by name. The three scripts that matter for understanding the project's shape are setup, which calls build:core, so a fresh environment compiles the backend and skips every web surface, and build:ci, which drops turbo's concurrency to one so a CI machine builds in order.

The redirect to stderr on both build commands is a small tell: turbo writes its summary to stderr, so the redirect exists to keep CI logs readable.

Around the apps sit the tools of a busy TypeScript repo: eslint.config.mjs, playwright.config.ts, vitest.config.mts and a vitest/ directory, a patches/ directory, an http/ directory that looks like a request collection, sql/, infra/ and legal/. Three more files are unusual enough to name: AGENTS.md and CLAUDE.md for agent instructions, .mcp.json for a model context protocol server, and terragon-setup.sh, which suggests the infrastructure is managed through an external tool rather than by hand.

## Conclusion

Adopt LLM Gateway when you want provider switching, token accounting and response caching without writing that layer yourself, and when AGPLv3 on the core is acceptable for your distribution model. Do not adopt it expecting a single permissive licence, since the core is AGPLv3 and the ee/ directory needs a separate enterprise or white-label agreement, and the repository describes its own licensing three different ways. Before deploying, reconcile the three, set real database and Redis credentials because the compose file ships POSTGRES_PASSWORD=pw and Redis with protected-mode off, and read the storage Redis note, because setting one STORAGE_REDIS_ variable silently changes how the others resolve.

## FAQ

### What licence does LLM Gateway use?

The README describes a dual licence: the core is AGPLv3, while commercial features in the ee/ directory need an Enterprise license and multi-organization administration needs a white-label license. The root package.json says SEE LICENSE IN LICENSE, and the repository's licence metadata field reads NOASSERTION.

### How do I deploy LLM Gateway with Docker?

Generate LLM_GATEWAY_SECRET and GATEWAY_API_KEY_HASH_SECRET with openssl rand and run scripts/run-unified-container.sh, or use the documented docker run against ghcr.io/theopenco/llmgateway-unified:latest with named volumes for Postgres and Redis. The README warns against bind-mounting a host directory to /var/lib/postgresql/data because initialization sets permissions there.

### What databases does LLM Gateway need?

Two. Postgres holds the application data, and a second Redis instance holds Responses API state with 30 day retention plus the gateway response cache, which is why it runs with appendonly persistence. If no STORAGE_REDIS_ variable is set, the main Redis connection is reused.

### Does the LLM Gateway setup command delete my local data?

Yes. pnpm run setup runs docker compose down -v before starting services, and the v flag removes named volumes. It also builds only the core packages, excluding the six web applications, then pushes the schema and seeds.

### Is the LLM Gateway API compatible with OpenAI?

The README describes a unified interface compatible with the OpenAI API format, with the example request going to a /v1/chat/completions path. Providers named are OpenAI, Anthropic and Google Vertex AI.

## Sources

- [Issues](https://github.com/theopenco/llmgateway/issues)
- [Project website](https://llmgateway.io)
- [README](https://github.com/theopenco/llmgateway/blob/main/README.md)
- [Releases](https://github.com/theopenco/llmgateway/releases)
- [theopenco/llmgateway on GitHub](https://github.com/theopenco/llmgateway)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/theopenco-llmgateway
