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

# Self-host

> Deploy Hexclave on your own infrastructure with full control over your authentication system.

<Danger>
  **If you self-host, YOU will be responsible for updating Hexclave and its dependencies.** Security patches, bug fixes, and new features require manual updates on your infrastructure. If you would like premium support to help with this, [contact us](mailto:team@hexclave.com).
</Danger>

Hexclave is fully open-source and can be self-hosted on your own infrastructure. The supported production path is the `stackauth/server` Docker image, which runs the Hexclave API backend and dashboard in one container.

<Info>
  If you are unsure whether you should self-host, here are some things to consider:

  * **Complexity**: Hexclave is a complex project with many interdependent services. Self-hosting requires managing these services and ensuring they work together seamlessly.
  * **Updates**: Hexclave is a rapidly evolving project with frequent feature and fix releases. Self-hosting requires you to manage updates and apply them timely.
  * **Reliability**: Self-hosting requires you to ensure the reliability of your infrastructure. Downtimes and outages can be costly to handle.
  * **Security**: Self-hosting requires ensuring the security of your infrastructure. A compromised service can affect your users.

  For most users, we recommend using [Hexclave's cloud hosted solution](https://app.hexclave.com). However, if you understand the above challenges and are comfortable managing them, follow the instructions below to self-host!
</Info>

## What You Run

In production, plan for these components:

* **Hexclave server**: The Docker image that serves the API backend and dashboard. The API is what your application SDKs call. The dashboard is where you manage projects, users, auth methods, and app settings.
* **Postgres**: Required. Stores Hexclave data and is migrated by the server image on startup unless you disable migrations.
* **Cron scheduler**: Required for production. Calls internal maintenance endpoints for email queue processing and database sync jobs.
* **Reverse proxy or load balancer**: Required for a production deployment. Terminate HTTPS and route traffic to the API and dashboard ports.
* **Email provider**: Required for production email flows such as magic links, verification, password reset, and invitations. Configure SMTP or use a provider integration from the dashboard.
* **Svix**: Required only if you use webhooks. You can use Svix Cloud or self-host Svix.
* **S3-compatible storage**: Required for features that store files or assets.
* **ClickHouse**: Required for the supported Docker deployment path. The migration script creates ClickHouse databases, tables, views, users, grants, and row policies after Postgres migrations.
* **Freestyle and OpenRouter keys**: Freestyle and a bootstrapped VM snapshot are required for custom, manual, or programmatic email sending. OpenRouter is optional and used by AI-assisted dashboard features.

For local app development, use [Local Development](/guides/going-further/local-development) or a development environment. Do not use the production Docker guide as your day-to-day local development setup.

## Deploy With Docker

### 1. Create Postgres and ClickHouse

Use a managed Postgres service for production. The server reads its database URL from `STACK_DATABASE_CONNECTION_STRING`.

Use a managed ClickHouse service or your own ClickHouse cluster. The server reads its ClickHouse URL and credentials from `STACK_CLICKHOUSE_URL`, `STACK_CLICKHOUSE_ADMIN_USER`, `STACK_CLICKHOUSE_ADMIN_PASSWORD`, and `STACK_CLICKHOUSE_EXTERNAL_PASSWORD`, and the database name from `STACK_CLICKHOUSE_DATABASE` (defaults to `default` when unset—set it to match the database you use in ClickHouse).

Self-hosting requires PostgreSQL 15 or newer.

The ClickHouse admin user must be able to create databases, tables, views, users, grants, and row policies. The migration script creates the `analytics_internal` database, views in the configured database, and a limited external user used by analytics queries.

For a quick non-production smoke test, you can run both databases locally:

```bash title="Terminal" theme={null}
docker network create hexclave

docker run -d \
  --name hexclave-postgres \
  --network hexclave \
  -e POSTGRES_USER=postgres \
  -e POSTGRES_PASSWORD=password \
  -e POSTGRES_DB=stackframe \
  -p 5432:5432 \
  postgres:latest

docker run -d \
  --name hexclave-clickhouse \
  --network hexclave \
  -e CLICKHOUSE_DB=analytics \
  -e CLICKHOUSE_USER=stackframe \
  -e CLICKHOUSE_PASSWORD=password \
  -e CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 \
  -p 8123:8123 \
  clickhouse/clickhouse-server:25.10
```

Set `STACK_CLICKHOUSE_DATABASE` to the same logical database your ClickHouse server uses (for this smoke test, `CLICKHOUSE_DB=analytics` and `STACK_CLICKHOUSE_DATABASE=analytics`). If you omit `STACK_CLICKHOUSE_DATABASE`, the backend defaults to `default` and will not match a container created only with `CLICKHOUSE_DB=analytics`.

Do not use the example passwords, open ports, or single-node database layout for production.

### 2. Create an Environment File

Start from the [server environment template](https://github.com/hexclave/hexclave/blob/dev/docker/server/.env), then fill in your production values.

At minimum, set:

```env title="hexclave.env" theme={null}
NEXT_PUBLIC_STACK_API_URL=https://auth-api.example.com
NEXT_PUBLIC_STACK_DASHBOARD_URL=https://auth.example.com

# Required for the ordinary locked-down reverse proxy used by this guide.
HEXCLAVE_TRUSTED_PROXY=generic

STACK_DATABASE_CONNECTION_STRING=postgresql://postgres:password@hexclave-postgres:5432/stackframe
STACK_SERVER_SECRET=replace-with-a-32-byte-base64url-secret
CRON_SECRET=replace-with-a-long-random-secret

STACK_CLICKHOUSE_URL=http://hexclave-clickhouse:8123
STACK_CLICKHOUSE_DATABASE=analytics
STACK_CLICKHOUSE_ADMIN_USER=stackframe
STACK_CLICKHOUSE_ADMIN_PASSWORD=password
STACK_CLICKHOUSE_EXTERNAL_PASSWORD=replace-with-a-long-random-password

STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=replace-with-a-random-value
STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY=replace-with-a-random-value
STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=replace-with-a-random-value

STACK_SEED_INTERNAL_PROJECT_USER_EMAIL=admin@example.com
STACK_SEED_INTERNAL_PROJECT_USER_PASSWORD=replace-with-a-long-random-password
STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=true
STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED=false
```

Generate `STACK_SERVER_SECRET` with a stable, high-entropy value and keep it unchanged across deploys:

```bash title="Terminal" theme={null}
openssl rand -base64 32 | tr '+/' '-_' | tr -d '='
```

Generate the three internal project key values with any stable random values, for example:

```bash title="Terminal" theme={null}
openssl rand -hex 32
```

The Docker entrypoint derives the internal project keys deterministically from `STACK_SERVER_SECRET` when they are missing, keeping them stable across restarts. You can also set the keys explicitly; keep all three values stable across deploys.

Generate `CRON_SECRET` with a stable random value too. Your scheduler uses it to authenticate internal maintenance requests:

```bash title="Terminal" theme={null}
openssl rand -hex 32
```

### 3. Run the Server

Run the Docker image with the environment file:

```bash title="Terminal" theme={null}
docker run -d \
  --name hexclave \
  --network hexclave \
  --env-file hexclave.env \
  -p 8101:8101 \
  -p 8102:8102 \
  stackauth/server:latest
```

The container starts two services:

| Service | Container port | Purpose |
| - | - | - |
| Dashboard | `8101` | Admin dashboard for Hexclave projects |
| API backend | `8102` | API used by the dashboard and your applications |

On startup, the image runs database migrations and the seed script by default. To separate migrations from application startup, run one deployment with `STACK_RUN_MIGRATIONS=true` and `STACK_RUN_SEED_SCRIPT=true`, then run steady-state application containers with:

```env theme={null}
STACK_RUN_MIGRATIONS=false
STACK_RUN_SEED_SCRIPT=false
```

Do this only after migrations and seeding have completed successfully for the current image version.

### 4. Run Cron Jobs

The Docker image does not start cron jobs for you. In production, configure exactly one scheduler for each deployment environment to call these internal endpoints with the `CRON_SECRET` bearer token:

| Endpoint | Purpose |
| - | - |
| `/api/latest/internal/email-queue-step` | Processes queued emails. |
| `/api/latest/internal/external-db-sync/sequencer` | Schedules external database sync work. |
| `/api/latest/internal/external-db-sync/poller` | Polls and advances external database sync work. |

For example, a scheduler can run these requests every minute:

```bash title="Scheduler" theme={null}
curl -fsS -H "Authorization: Bearer $CRON_SECRET" https://auth-api.example.com/api/latest/internal/email-queue-step
curl -fsS -H "Authorization: Bearer $CRON_SECRET" https://auth-api.example.com/api/latest/internal/external-db-sync/sequencer
curl -fsS -H "Authorization: Bearer $CRON_SECRET" https://auth-api.example.com/api/latest/internal/external-db-sync/poller
```

Use your deployment platform's scheduler, Kubernetes `CronJob`, systemd timer, or another reliable cron service. Configure it to treat every non-2xx response as a failed run—the `-f` option in the example does this. A request that reached the server is not necessarily a successful tick. If you run multiple application replicas, do not let every replica run its own scheduler against the same database.

### 5. Put It Behind HTTPS

Expose the dashboard and API through HTTPS with your reverse proxy or load balancer:

| Public URL | Proxies to |
| - | - |
| `https://auth.example.com` | dashboard port `8101` |
| `https://auth-api.example.com` | API backend port `8102` |

The public URLs must match `NEXT_PUBLIC_STACK_DASHBOARD_URL` and `NEXT_PUBLIC_STACK_API_URL`. The API URL must be reachable from browsers, your application servers, and the dashboard.

For a standalone API backend behind an ordinary locked-down nginx, Traefik, Caddy, or similar reverse proxy, explicitly enable generic proxy trust:

```env theme={null}
HEXCLAVE_TRUSTED_PROXY=generic
```

Use a provider-specific value instead when that provider is the backend's ingress:

* `HEXCLAVE_TRUSTED_PROXY=vercel`
* `HEXCLAVE_TRUSTED_PROXY=cloudflare`
* `HEXCLAVE_TRUSTED_PROXY=cloudrun`

The backend automatically selects `vercel` or `cloudrun` when the complete platform-provided environment identifies one of those platforms. An explicit value always takes precedence. The legacy `STACK_TRUSTED_PROXY` spelling remains supported.

Generic proxy trust cannot be selected automatically. Forwarding headers are controlled by the caller until the backend already knows that a trusted proxy is in front of it, so using those headers for auto-detection would let clients opt themselves into a trusted state. A production-like standalone backend with an HTTPS `NEXT_PUBLIC_STACK_API_URL` therefore refuses to start until generic or provider-specific trust is established. Development, tests, and plain-HTTP local deployments do not require it.

<Warning>
  Enable a trusted proxy mode only when direct access to the API origin is blocked by a private network, firewall, security group, or equivalent control. Otherwise, a client can bypass the proxy and spoof the forwarding headers used to construct OAuth and identity-provider absolute URLs.
</Warning>

For `generic` mode, configure the proxy to discard client-supplied values and overwrite all of the following on every request:

* `X-Real-IP` with the client address observed by the proxy
* `X-Forwarded-Host` with the validated public API host
* `X-Forwarded-Proto` with the public request protocol (`https`)

Appending to or preserving incoming values is not sufficient. Restrict accepted public hosts at the proxy as well.

<Info>
  Keep `NEXT_PUBLIC_STACK_API_URL` as the browser-reachable API URL. In the bundled Docker image, the entrypoint sets the dashboard's server-side API URL to the backend inside the same container, so most deployments should not set API split variables manually. If you run the API and dashboard as separate services outside the bundled image, the codebase also supports `NEXT_PUBLIC_BROWSER_STACK_API_URL` and `NEXT_PUBLIC_SERVER_STACK_API_URL` for advanced network layouts.
</Info>

### 6. Sign In to the Dashboard

Open your dashboard URL and sign in with the seeded admin user from `STACK_SEED_INTERNAL_PROJECT_USER_EMAIL` and `STACK_SEED_INTERNAL_PROJECT_USER_PASSWORD`.

After you have access, create a project for your application and follow the [setup guide](/guides/getting-started/setup). For self-hosted projects, your app must also point the SDK at your API URL:

```env title=".env.local" theme={null}
NEXT_PUBLIC_STACK_API_URL=https://auth-api.example.com
STACK_API_URL=https://auth-api.example.com
```

Keep using the project ID, publishable client key, and secret server key shown in your self-hosted dashboard. Do not mix keys from Hexclave Cloud with a self-hosted API URL.

## Service Configuration

### Email

Production auth flows need a real email provider. Configure Custom SMTP or Resend in the dashboard after first sign-in, or provide SMTP environment variables if you want default server-level email settings:

```env theme={null}
STACK_EMAIL_HOST=smtp.example.com
STACK_EMAIL_PORT=587
STACK_EMAIL_USERNAME=...
STACK_EMAIL_PASSWORD=...
STACK_EMAIL_SENDER=noreply@example.com
STACK_EMAILABLE_API_KEY=disable_email_validation
```

Set `STACK_EMAILABLE_API_KEY` to an Emailable key if you want email validation. Use `disable_email_validation` only when you intentionally want to skip validation.

The dashboard's Managed Domain email flow is an operator-managed integration. It requires additional server-side provider credentials such as `STACK_RESEND_API_KEY`, `STACK_DNSIMPLE_API_TOKEN`, and `STACK_DNSIMPLE_ACCOUNT_ID`. If you do not operate that integration, use Custom SMTP or your own Resend API key instead.

### Webhooks

If you use webhooks, configure Svix:

```env theme={null}
STACK_SVIX_API_KEY=...
STACK_SVIX_SERVER_URL=
```

Leave `STACK_SVIX_SERVER_URL` empty when using Svix Cloud. Set it when you self-host Svix. If the browser and container need different Svix URLs, also set `NEXT_PUBLIC_STACK_SVIX_SERVER_URL` to the external URL.

### S3-Compatible Storage

Configure S3-compatible storage for features that store assets:

```env theme={null}
STACK_S3_ENDPOINT=https://s3.amazonaws.com
STACK_S3_PUBLIC_ENDPOINT=https://your-public-bucket-url.example.com
STACK_S3_REGION=us-east-1
STACK_S3_ACCESS_KEY_ID=...
STACK_S3_SECRET_ACCESS_KEY=...
STACK_S3_BUCKET=stack-storage
STACK_S3_PRIVATE_BUCKET=stack-storage-private
```

### AI and Custom Code Features

Some dashboard AI features require OpenRouter:

```env theme={null}
STACK_OPENROUTER_API_KEY=...
```

Custom, manual, and programmatic email sending requires Freestyle. Before starting Hexclave for the first time, build the private Node 24 sandbox snapshot in the same Freestyle account as the API key:

```bash theme={null}
HEXCLAVE_FREESTYLE_API_KEY=... HEXCLAVE_FREESTYLE_SNAPSHOT_ID=hexclave-js-node24-v4 pnpm --filter @hexclave/backend freestyle:bootstrap-snapshot
```

The bootstrap downloads a checksum-pinned Node 24 glibc-217 build, installs it directly into a resized Freestyle BusyBox VM, and snapshots that single VM after verifying Node and npm can install a package. The captured private `hexclave-js-node24-v4` snapshot contains BusyBox, Node, npm, and the runtime support needed by the sandbox. Then configure the server:

```env theme={null}
HEXCLAVE_FREESTYLE_API_KEY=...
HEXCLAVE_FREESTYLE_SNAPSHOT_ID=hexclave-js-node24-v4
```

Choose a different `HEXCLAVE_FREESTYLE_SNAPSHOT_ID` in both commands if that slug is already in use. Existing `STACK_FREESTYLE_*` environment names remain accepted.

### Request Body Size

Direct Docker and Node deployments accept request bodies up to 4.5 MiB by default. This protects the process from buffering arbitrarily large bodies before authentication and schema validation. Built-in large deployment assets use presigned object-storage uploads instead of this request path.

If an existing integration legitimately sends larger requests, set the limit before starting the upgraded image:

```env theme={null}
HEXCLAVE_MAX_REQUEST_BODY_SIZE_BYTES=10485760
```

The value is a positive number of bytes. Choose the smallest limit that covers the integration, and enforce an equal or lower limit at the reverse proxy.

## Operations

### Upgrades

Before upgrading:

1. Back up Postgres and any configured object storage.
2. Back up ClickHouse if you depend on analytics or external database sync data.
3. Read the release notes and compare the current [server environment template](https://github.com/hexclave/hexclave/blob/dev/docker/server/.env) with your saved environment file.
4. Pull the new Docker image by immutable digest. Keep the previous digest available for rollback.
5. Run the new image once with migrations enabled.
6. Verify the database health check, dashboard sign-in, project loading, cron jobs, email sending, and any webhook flows you use.
7. Roll forward your application containers to the same image.

The server image runs migrations by default. If you run multiple replicas, use your deployment system to ensure migrations run once before scaling the new version.

<Warning>
  The Elysia server upgrade requires explicit `HEXCLAVE_TRUSTED_PROXY=generic` for ordinary reverse proxies. Older Next.js images do not understand the `generic` value, so do not add it to one shared environment while old and new replicas are serving simultaneously. For a single-container deployment, stop the old revision and start the new image with the new value. For a zero-downtime deployment, give each revision its own environment, verify the new revision, switch traffic atomically, and then drain the old revision. Provider-specific `vercel`, `cloudflare`, and `cloudrun` values work across both revisions.
</Warning>

All other configuration in this release remains backward compatible. The new image automatically detects Vercel and Cloud Run, retains legacy `STACK_*` environment variable aliases, and applies a safe request-body default. Only generic proxy trust and a larger-than-default body requirement need operator input because neither can be inferred safely.

### Health Checks

The existing health endpoint supports an optional database check:

* `GET /health` checks the backend process without depending on Postgres.
* `GET /health?db=1` also reads a real Prisma model, verifying the configured database read path and the Project schema expected by the running server revision. It returns a non-2xx response when that check fails.

The official Docker images use the existing `?db=1` mode and allow five minutes for startup because migrations and seeding complete before the services listen.

After deploy, verify the database-backed response:

```bash title="Terminal" theme={null}
curl -fsS "https://auth-api.example.com/health?db=1"
curl https://auth-api.example.com/api/v1/internal/backend-urls
```

Also test a complete sign-up or sign-in flow from your application, because redirect domains, email delivery, cron jobs, and SDK environment variables are the most common deployment issues.

### Common Issues

#### The Dashboard Cannot Reach the API

Check that `NEXT_PUBLIC_STACK_API_URL` is the public API URL and has no typo or unreachable internal hostname.

#### Redirects Fail

Add your application origin to the project's allowed domains in the dashboard. OAuth providers also need callback URLs that point at your self-hosted API URL.

#### Emails Do Not Arrive

Check the email provider configuration, sender domain verification, and any provider logs. For development-only email testing, use a development environment instead of a production self-host deployment.

#### You Cannot Access the Dashboard

If you did not seed an admin user, temporarily enable internal project sign-up and rerun the seed script by restarting a container with seeding enabled:

```env theme={null}
STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED=true
STACK_RUN_SEED_SCRIPT=true
```

After creating your admin account, disable sign-up again, restart with `STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED=false`, and prefer a seeded admin user for future deployments.
