# Self-hosting (https://www.lurq.run/docs/self-hosting)



Everything here runs against your **own lurq Postgres index** and needs a
`DATABASE_URL`. It's for operators running the hosted service and for
self-hosters, **not** for end users, who only run `lurq setup`
([Quick start](/quickstart)).

## The two planes [#the-two-planes]

<Callout type="warn">
  The commands that **build** an index are not in the published npm package.

  `lurqrun` ships as a read-only oracle: it can query an index, extract API
  surfaces, scan a codebase, and serve MCP. Everything that ingests, crawls,
  mines, runs a sandbox, issues keys, or migrates the schema lives on a separate
  **operator** binary that is deliberately excluded from the published files, so
  ingestion code and its heavy dependencies never land on a user's machine.

  To run the operator plane, clone the repo:

  ```bash
  git clone https://github.com/jadenryu/lurq && cd lurq
  npm install
  npm run operator -- <command>          # or: npx tsx src/bin/operator.ts <command>
  ```

  A built checkout can also run `node dist-operator/bin/operator.js <command>`,
  which is what the production cron jobs use.
</Callout>

## Configuration [#configuration]

Only `DATABASE_URL` is required; everything else degrades gracefully.

| Variable                                                                       | Required                     | Purpose                                                                                 |
| ------------------------------------------------------------------------------ | ---------------------------- | --------------------------------------------------------------------------------------- |
| `DATABASE_URL`                                                                 | yes                          | Postgres connection (pgvector-enabled)                                                  |
| `GITHUB_TOKEN`                                                                 | recommended                  | GitHub signals (stars, cadence, issues, archived)                                       |
| `EMBEDDING_PROVIDER` / `EMBEDDING_API_KEY` / `EMBEDDING_BASE_URL`              | no                           | Any OpenAI-compatible `/v1/embeddings`; falls back to a local embedder                  |
| `SUMMARY_PROVIDER` / `SUMMARY_API_KEY` / `SUMMARY_BASE_URL`                    | no                           | Any OpenAI-compatible chat endpoint for usage guides; falls back to the npm description |
| `LURQ_SYNC_CONCURRENCY`                                                        | no                           | Parallel ingest workers (default 5)                                                     |
| `LURQ_SYNC_REFRESH_CAP`                                                        | no                           | Stalest non-seed packages refreshed per sync (default 400)                              |
| `PORT`                                                                         | no                           | Port for `serve-http` (defaults to 8080)                                                |
| `LURQ_RATE_LIMIT_MAX` / `LURQ_IP_RATE_LIMIT_MAX` / `LURQ_RATE_LIMIT_WINDOW_MS` | no                           | Per-key / per-IP rate limits for `serve-http`                                           |
| `REDIS_URL`                                                                    | recommended for real traffic | Response cache **and** the shared rate-limit store                                      |
| `LURQ_METRICS_TOKEN`                                                           | no                           | Bearer token guarding `/metrics`; unset disables the endpoint                           |
| `LURQ_ISSUER_SECRET`                                                           | no                           | Shared secret for the dashboard-authenticated routes                                    |
| `E2B_API_KEY` / `E2B_TEMPLATE`                                                 | no                           | VM-isolated sandbox for verifying untrusted packages                                    |

<Callout type="info">
  Without `REDIS_URL` the response cache is a transparent pass-through and every request recomputes
  its search on the database. Fine for one box; set it before serving real traffic, since it also
  backs the rate limiter across instances.
</Callout>

## Set up the index [#set-up-the-index]

```bash
npm run operator -- db migrate   # pgvector extension + schema + curated seed list
npm run operator -- sync         # compute scores from public APIs (~2 min)
```

`sync` is idempotent and tolerant of single-source outages. Run it daily to keep
the index fresh.

Optional, to grow coverage beyond the seed list without curation:

```bash
npm run operator -- discover --cap 25   # crawl, merit-gate, ingest survivors
npm run operator -- worker              # the autonomous loop (Ctrl-C stops cleanly)
npm run operator -- rescore             # re-derive scores after a weight change
```

## Serve [#serve]

These two **are** in the published package:

```bash
lurq serve-http   # HTTP MCP server + API-key auth (helmet, rate limits)
lurq serve        # stdio MCP server against your own DB
```

`serve-http` fronts the central DB, so `DATABASE_URL` lives only on the service
host. Self-hosters can instead wire an agent to a local stdio server with
`lurq install-skill --local`.

Point your own clients at it with `lurq setup --url https://lurq.internal/mcp`;
the endpoint is stored alongside the key, so a later bare `lurq setup` stays on
your server.

## API keys [#api-keys]

For operators issuing keys to end users (operator plane, needs `DATABASE_URL`):

```bash
npm run operator -- keys create --label "acme"      # issue a key (shown once)
npm run operator -- keys list                       # prefix, tier, last-used, status
npm run operator -- keys rotate lurq_live_ab12cd    # replace, then revoke the old one
npm run operator -- keys revoke lurq_live_ab12cd    # revoke by prefix or id
```

<Callout type="warn">
  Keys are shown **once** at creation and stored only as hashes. Interactively, the key is wiped
  from your terminal (screen and scroll-back) after you confirm you've copied it. Capture it when
  it's printed — it can't be recovered, only rotated or revoked and reissued.
</Callout>

`rotate` creates the replacement **before** revoking the original, so a failure
part-way through leaves you with a working key rather than none.
