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

# Self-host the dashboard and API

> Run the web application, the API, and the retention worker yourself — locally or on a platform.

You do not need a server to scan. This guide is for running the hosted half: the
dashboard, the `/api/v1` JSON API, the share viewer, and the retention worker.

## Prerequisites

|            |                                      |
| ---------- | ------------------------------------ |
| Node.js    | `>=20.12` (CI pins `22.11.0`)        |
| pnpm       | `9.15.4`                             |
| PostgreSQL | 14 or newer                          |
| Docker     | optional — only for container builds |

## Local setup

<Steps>
  <Step title="Install and create databases">
    ```bash theme={null}
    pnpm install
    createdb myrqen_dev
    createdb myrqen_test
    ```
  </Step>

  <Step title="Configure">
    ```bash theme={null}
    cp .env.example .env
    ```

    Two values must be set before anything starts:

    ```bash theme={null}
    DATABASE_URL=postgresql://<your-user>@127.0.0.1:5432/myrqen_dev
    REPORT_ENCRYPTION_KEY=<32 bytes, hex or base64>
    ```

    Generate a key:

    ```bash theme={null}
    node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
    ```

    Configuration is read from the process environment; a repository-root `.env` is loaded
    as a fallback **in development only**. In production a missing variable fails fast with
    an actionable message rather than silently resolving to whatever is on disk.
  </Step>

  <Step title="Build and migrate">
    ```bash theme={null}
    pnpm build
    pnpm db:migrate
    ```

    Migrations are generated SQL, not a runtime schema diff.
  </Step>

  <Step title="Run">
    ```bash theme={null}
    pnpm --filter @myrqen/web dev        # http://localhost:3000
    pnpm --filter @myrqen/worker dev     # retention and reconciliation
    ```

    The worker also has a single-pass mode, which is what you want when watching retention
    behaviour rather than waiting for an interval:

    ```bash theme={null}
    pnpm --filter @myrqen/worker dev:once
    ```
  </Step>

  <Step title="Point the CLI at it">
    ```bash theme={null}
    myrqen link --url http://localhost:3000
    ```
  </Step>
</Steps>

## Required configuration

| Variable                | Notes                                                                                                                                                                                                        |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `DATABASE_URL`          | PostgreSQL connection string. Required.                                                                                                                                                                      |
| `REPORT_ENCRYPTION_KEY` | 32 bytes, hex or base64. Report bodies are encrypted with it before reaching object storage. Required.                                                                                                       |
| `APP_BASE_URL`          | The public origin. **Required in production** — share links and session cookie security are both derived from it, so a localhost fallback would hand out links nobody can open and cookies without `Secure`. |

Everything else has a default. The full list, with what each one changes, is in
[Environment variables](/reference/environment-variables).

## Object storage

```bash theme={null}
OBJECT_STORAGE_DRIVER=filesystem              # default, no Docker needed
OBJECT_STORAGE_LOCAL_ROOT=.data/object-store
```

For a deployment, use S3-compatible storage:

```bash theme={null}
OBJECT_STORAGE_DRIVER=s3
OBJECT_STORAGE_ENDPOINT=https://…
OBJECT_STORAGE_BUCKET=myrqen-reports
OBJECT_STORAGE_ACCESS_KEY=YOUR_ACCESS_KEY
OBJECT_STORAGE_SECRET_KEY=YOUR_SECRET_KEY
OBJECT_STORAGE_REGION=eu-west-1               # default
OBJECT_STORAGE_PATH_STYLE=true
```

`s3` requires the endpoint, bucket, access key, and secret key together.

<Warning>
  The `filesystem` driver is **refused under `NODE_ENV=production`**, because report bodies
  would live on one container's disk — lost on the next deploy and invisible to other
  replicas. Set `OBJECT_STORAGE_ALLOW_EPHEMERAL=true` only for a throwaway production-mode
  run such as the end-to-end suite.
</Warning>

## Behind a proxy

```bash theme={null}
TRUSTED_PROXY_HOPS=1
```

Login and device-link throttles read the client address that many entries in from the
**right** of `X-Forwarded-For`. Leave it unset when nothing proxies the app.

The reason is worth understanding: the leftmost `X-Forwarded-For` entry is whatever the
caller typed, so keying a throttle on it lets an attacker mint a fresh bucket per request
and the throttle stops existing. Only entries your own proxies added can be trusted.
Railway and most single-proxy platforms need `1`.

If a trusted edge sets a real-client-address header, name it:

```bash theme={null}
TRUSTED_CLIENT_IP_HEADER=cf-connecting-ip
```

## Multiple replicas

```bash theme={null}
REDIS_URL=redis://…
EVENT_BUS_DRIVER=postgres
```

Without `REDIS_URL`, rate limits are process-local counters — fine for a single process,
but N replicas allow N times each limit. `EVENT_BUS_DRIVER=postgres` uses
`LISTEN`/`NOTIFY` and works across instances; `memory` is single-process only.

## Platform administrator

```bash theme={null}
PLATFORM_ADMIN_EMAIL=admin@example.com
```

Exactly the configured address is promoted, and only when that address signs up. Nobody is
promoted implicitly, and a deployment with this unset has no administrator at all.

<Note>
  The repository's `README.md` and `docs/LOCAL-WALKTHROUGH.md` state that the first account
  in a fresh deployment becomes the platform administrator. The implementation in
  `apps/web/lib/auth-service.ts` does not do that — only `PLATFORM_ADMIN_EMAIL` grants the
  role. Trust the code.
</Note>

## Health and readiness

| Endpoint             | Purpose                                                                                                                                                                 |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /api/v1/health` | **Liveness.** Dependency-free and cheap: is this process still able to serve a request. Gate container restarts on this.                                                |
| `GET /api/v1/ready`  | **Readiness.** Probes the database, event bus, object storage, and encryption keyring individually. Any failure makes the whole response `503`. Gate *traffic* on this. |

The split is deliberate. A database outage must not put every replica into a restart loop,
and "the process is up" is not a usable signal on its own — a replica that cannot reach
Postgres serves 500s for every real operation while its marketing pages still return 200.

Both endpoints answer on **any** hostname, because redirecting a health check would make a
monitor report the redirect target's health instead.

## Container builds

Two Dockerfiles at the repository root, both building and running as non-root:

| File                | Service                      |
| ------------------- | ---------------------------- |
| `Dockerfile.web`    | Dashboard, API, share viewer |
| `Dockerfile.worker` | Retention and reconciliation |

`railway.web.json` and `railway.worker.json` carry deploy configuration: the Dockerfile
paths, a pre-deploy migration command, `/api/v1/health` as the health check, region
`europe-west4`, and drain and overlap windows so a deploy does not cut live streams.

`scripts/railway-provision.sh` creates the project, adds PostgreSQL and Redis, generates
`REPORT_ENCRYPTION_KEY`, and prints the short list of values it deliberately will not
invent. It is idempotent and refuses to change a project it did not create.

## Canonical hostnames

`apps/web/proxy.ts` handles the three hostnames the product is *intended* to answer on.
None of them is deployed today, so this matters only once you put a deployment behind real
DNS — and if you self-host under your own domain, edit `packages/brand/src/index.ts`, which
is the single source of these strings.

| Hostname        | Role                                                                                                                                                                              |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `myrqen.cc`     | The canonical origin. Share links, cookies, and redirects use it.                                                                                                                 |
| `www.myrqen.cc` | Exists because browsers add the prefix on their own. Redirected.                                                                                                                  |
| `api.myrqen.cc` | Exists so the API can be moved or fronted differently without every installed CLI needing new configuration. Serves `/api/*` only. It is also the CLI's shipped default endpoint. |

Non-API paths are redirected to the canonical host with a **308**, which preserves the
method and body — a 302 would silently turn a POST into a GET. API paths are deliberately
*not* redirected off the API host.

## Payments (optional)

```bash theme={null}
POLAR_ACCESS_TOKEN=YOUR_POLAR_TOKEN
POLAR_ENVIRONMENT=sandbox
POLAR_WEBHOOK_SECRET=polar_whs_YOUR_SECRET
POLAR_PRODUCTS={"pro":"prod_…","team":"prod_…"}
```

All four are required **together** or the application refuses to start: a half-configured
payment path takes money and grants nothing. Left unset, the deployment sells nothing — the
pricing page still renders and says checkout is unavailable.

A sandbox token cannot charge anyone. Point the provider's webhook at
`POST /api/v1/billing/webhook`; the raw body is verified byte-for-byte before anything is
parsed, the delivery id is claimed before the plan changes so a retry cannot apply an
upgrade twice, and the plan is resolved from the product id through your own configuration
rather than from the request.

## Verification suites

```bash theme={null}
pnpm lint          # secrets, brand centralization, schema drift, stray debug logging
pnpm build
pnpm typecheck
pnpm test          # unit and database integration tests
pnpm test:deploy
pnpm test:e2e      # the full acceptance journey against a real server and database
pnpm benchmark     # security quality against the owned fixtures
pnpm test:all      # all of the above, in order
```

Database tests and `test:e2e` use `TEST_DATABASE_URL`, default
`postgresql://localhost:5432/myrqen_test`.

## Demonstration data

```bash theme={null}
pnpm demo:seed                       # one demo workspace, reports, findings, share links
pnpm demo:purge --list               # which seeding runs this database holds
pnpm demo:purge --seed-id demo_1a2b  # remove exactly that run
```

Nothing runs these for you — no import, no lifecycle script, no deploy step. `demo:seed`
refuses a non-loopback or production database unless `MYRQEN_DEMO_ALLOW_REMOTE=1` is set on
that command line, and the refusal says so. Every row it writes carries `is_demo` and the
run's `demo_seed_id`, and `demo:purge` deletes nothing else. The demonstration accounts
cannot sign in; the workspace is meant to be viewed through the share links the seeder
prints.

## Signed releases

```bash theme={null}
node scripts/generate-release-key.mjs release-2026-08    # offline, once

MYRQEN_RELEASE_PRIVATE_KEY_FILE=release-2026-08.private.pem \
  pnpm release:sign --version 0.2.0 \
    --artifact darwin:arm64:dist/myrqen-darwin-arm64.tgz
```

Serve the signed manifest by pointing `UPDATE_MANIFEST_PATH` at it; clients fetch it from
`GET /api/v1/updates/manifest`. Pin the public keys clients must verify against in
`UPDATE_PUBLIC_KEYS` as `{"keyId":"-----BEGIN PUBLIC KEY-----…"}`.

Clients verify the ed25519 signature against the pinned key, check the artifact digest and
size, and refuse downgrades, stale manifests, and revoked versions **before** anything is
written to disk. The server never signs on demand and never returns executable content.
