stack/docs-mintlify/guides/other/self-host.mdx
BilalG1 57ff5d3ce9
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migration compat / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
feat(hexclave): PR 2 — visible rebrand (Hexclave brand goes public) (#1481)
## Summary

**Stacked on [#1475](https://github.com/hexclave/stack-auth/pull/1475)**
(`cl/hexclave-pr1`, the invisible compatibility layer). Diff vs that
base = the actual PR 2 code.

This is **PR 2 of the Stack Auth → Hexclave rebrand: the visible flip**.
Old wire identifiers (cookies, request/response headers, Bearer prefix,
JWT issuers, MCP tool name) keep working indefinitely via PR 1's
dual-accept. This PR flips every user-visible surface — package names
taught in docs, SDK class names in code examples, dashboard setup
snippets, page titles, error messages, email content, CLI binary,
default base URLs, GitHub repo slug, contributor guidance — to the
Hexclave brand.

See [`RENAME-TO-HEXCLAVE.md`](./RENAME-TO-HEXCLAVE.md) → *"PR 2: Rebrand
to Hexclave (visible)"* for the full per-work-area spec.

## What's implemented (per the plan's PR 2 scope)

- **SDK base URLs** flipped: `defaultBaseUrl` and
`defaultAnalyticsBaseUrl` in
[common.ts](packages/template/src/lib/stack-app/apps/implementations/common.ts:127)
→ `https://api.hexclave.com` / `https://r.hexclave.com`. PR 1's
[`getHardcodedFallbackUrls`](packages/stack-shared/src/utils/urls.tsx:199)
table now keys on the Hexclave domain.

- **Domain inventory sweep** (16 subdomains from the plan): every
`api/app/docs/discord/demo/mcp/skill/feedback/test/preview/r/api2/api.staging/idp-jwk-audience/built-with.stack-auth.com`
reference in production code, docs-mintlify, examples, READMEs, and
contributor guidance flipped to `*.hexclave.com`. Carve-outs: PR 1's
intentional JWT issuer dual-accept table in
[tokens.tsx](apps/backend/src/lib/tokens.tsx), the legacy `./docs/`
folder, the `unified-docs-widget` allowlist (deliberately accepts both
during DNS transition), and `url-targets.ts` hosted-component default
(baked into existing customer deploys).

- **`@deprecated` JSDoc** on every `Stack*` public export
([packages/template/src/lib/stack-app/index.ts](packages/template/src/lib/stack-app/index.ts)
+ [packages/template/src/index.ts](packages/template/src/index.ts)) —
`StackClientApp`, `StackServerApp`, `StackAdminApp` + every
constructor/options/JSON type, `StackHandler`, `StackProvider`,
`StackTheme`, `useStackApp`, `defineStackConfig`, `StackConfig`.
Hexclave\* aliases are now canonical.

- **Runtime `console.warn`**
([packages/template/src/internal/deprecation-warning.ts](packages/template/src/internal/deprecation-warning.ts))
— once-per-process when the SDK is loaded from a `@stackframe/*`
artifact. Detection uses the existing
`STACK_COMPILE_TIME_CLIENT_PACKAGE_VERSION_SENTINEL` (rewritten at build
time to e.g. `js @stackframe/stack@2.8.92` or `js
@hexclave/next@1.0.0`); `@hexclave/*` mirror artifacts short-circuit the
warning.

- **Tier 3 data migration**: new idempotent SQL migration
[`20260523000000_rename_internal_project_to_hexclave`](apps/backend/prisma/migrations/20260523000000_rename_internal_project_to_hexclave/migration.sql)
— updates the internal Project `displayName` 'Stack Dashboard' →
'Hexclave Dashboard' and `description` only if both still hold the
pre-rebrand defaults. Operator-renamed projects untouched, missing row
no-ops, re-runs are no-ops. [`seed.ts`](apps/backend/prisma/seed.ts:87)
default flipped. `getSharedEmailConfig("Stack Auth")` → `("Hexclave")`.

- **Tier 4 brand strings** (mechanical sweep, ~340 files):
- Page + OpenAPI titles (Hexclave API / Dashboard / REST API / Webhooks
API / Documentation). OpenAPI `info.description` documents
`X-Hexclave-*` headers as canonical with compat note on `X-Stack-*`.
- `HexclaveAssertionError` message text
([errors.tsx:71](packages/stack-shared/src/utils/errors.tsx:71)) — "an
error in Stack." → "an error in Hexclave."
- Known-error message templates
([known-errors.tsx](packages/stack-shared/src/known-errors.tsx)) flipped
to lead with `x-hexclave-*` + the new `docs.hexclave.com` URL; legacy
`x-stack-*` mentioned as compat aliases. **25 e2e test files updated in
lockstep**.
- Email content: failed-emails-digest body, sendTestEmail recipient (now
`sent-with-hexclave.com`), test-email-recipient default.
  - `CHANGELOG.md` title → "Hexclave Changelog".
- `AGENTS.md` env var convention: new vars prefix `HEXCLAVE_` /
`NEXT_PUBLIC_HEXCLAVE_` for Category A/B; legacy `STACK_*` explicitly
noted as accepted via PR 1's dual-read.

- **CLI / init wizard**:
- Every dashboard setup snippet, init-stack template, and docs-mintlify
page teaches `npx @hexclave/cli@latest init` (was
`@stackframe/stack-cli`).
[setup-page.tsx](apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx)
+
[link-existing-onboarding](apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/link-existing-onboarding.tsx).
- [init-stack](packages/init-stack/src/index.ts:634)
`STACK_*_INSTALL_PACKAGE_NAME_OVERRIDE` defaults flipped to
`@hexclave/*`.
- Generated `stack/client.ts` / `stack/server.ts` import from
`@hexclave/next` and reference `HexclaveClientApp` /
`HexclaveServerApp`.
- Internal `StackAuthKeys` dashboard component renamed to
`HexclaveKeys`.

- **docs-mintlify rewrite** (legacy `./docs/` intentionally untouched
per scoping decision):
- **78 MDX files swept**.
`@stackframe/{react,stack,js,tanstack-start,...}` →
`@hexclave/{react,stack,js,...}` in install snippets and code blocks;
`Stack*` SDK class names → `Hexclave*` in all code examples; 'Stack
Auth' brand phrase → 'Hexclave'.
- `openapi/{server,admin,client,webhooks}.json` titles → 'Hexclave REST
API' / 'Hexclave Webhooks API'.

- **Generators flipped before regeneration**:
-
[`packages/stack-shared/src/helpers/init-prompt.ts`](packages/stack-shared/src/helpers/init-prompt.ts),
[`/ai/prompts.ts`](packages/stack-shared/src/ai/prompts.ts),
[`apps/backend/src/lib/ai/prompts.ts`](apps/backend/src/lib/ai/prompts.ts),
[`apps/backend/src/lib/ai/tools/create-email-{template,draft}.ts`](apps/backend/src/lib/ai/tools/create-email-template.ts),
[`apps/skills/src/app/route.ts`](apps/skills/src/app/route.ts) (taught
MCP tool → `ask_hexclave` with compat note; CLI binary teach →
`hexclave`),
[`docs-mintlify/snippets/home-prompt-island.jsx`](docs-mintlify/snippets/home-prompt-island.jsx),
[`packages/template/README.md`](packages/template/README.md) +
integrations/convex/component/README.md.
  - `generate-sdks` propagated changes to `packages/{react,stack,js}`.

- **OpenAPI dual-documentation**:
[`apps/backend/src/app/api/latest/route.ts`](apps/backend/src/app/api/latest/route.ts)
now lists `X-Hexclave-*` headers as primary documented schemas with
`X-Stack-*` duplicates marked `.optional()` (both accepted at runtime by
PR 1's normalize-at-proxy shim).

- **`@stackframe/emails` virtual module**: dual-aliased to
`@hexclave/emails` at the bundler boundary
([email-rendering.tsx:89](apps/backend/src/lib/email-rendering.tsx:89)).
Stored email templates continue to import from either name; new
AI-generated templates and the system prompt teach `@hexclave/emails`.

- **Tier 2 mirror-publish wiring** (new this PR, lays the groundwork for
`@hexclave/*` first publish):
-
[`scripts/rewrite-packages-to-hexclave.ts`](scripts/rewrite-packages-to-hexclave.ts)
— rewrites 9 publishable `@stackframe/*` → `@hexclave/*` `package.json`
files (reads `HEXCLAVE_VERSION` env or `--version=` flag), pins
cross-deps to the shared `@hexclave` version, registers `hexclave` bin
alongside `stack` for `@hexclave/cli`.
-
[`.github/workflows/npm-publish.yaml`](.github/workflows/npm-publish.yaml)
appended with rewrite-then-republish step. `pnpm publish` skips
already-on-npm versions so reruns are safe.

- **Sender email domain**: `noreply@stackframe.co` →
`noreply@sent-with-hexclave.com` (the dedicated transactional-sender
domain split per the plan, to isolate bulk deliverability from
`hexclave.com` reputation); `security@` / `team@stack-auth.com` inbound
mailboxes → `@hexclave.com`.

- **Self-host docs**: docker network / container names in the bash
examples flipped from `stack-auth` to `hexclave` (`hexclave-postgres`,
`hexclave-clickhouse`, `hexclave.env`). The docker image tag
`stackauth/server:latest` stays per the plan's locked decision.

- **GitHub repo slug**: `hexclave/stack-auth` → `hexclave/hexclave` in
every `package.json` `repository` field, README link, CHANGELOG
raw-asset URL.

## Carve-outs (deliberately untouched)

-
**[`apps/backend/src/lib/tokens.tsx`](apps/backend/src/lib/tokens.tsx)**
JWT issuer dual-accept table — PR 1 intentional infrastructure, kept
indefinitely.
- **Legacy `./docs/` folder** — per scoping decision (only
`docs-mintlify/` rewritten).
- **`unified-docs-widget` hostname allowlist** — accepts both
`.hexclave.com` (canonical) and `.stack-auth.com` (transition window)
for DNS rollout.
- **`url-targets.ts`** hosted-domain default
`.built-with-stack-auth.com` — wire identifier baked into existing
customer deploys; indefinite read-fallback.
- **Binary visual assets** (logos, favicons, OG images, README
screenshots) — out of scope for this PR. Need design work; tracked
separately.

## Verification

- **`pnpm typecheck`** on
`packages/{template,stack-shared,react,stack,js}` + `apps/dashboard`:
**all green**. The remaining backend / e-commerce-demo typecheck errors
are pre-existing (Prisma codegen output +
`./generated/api-versions.json` not present in fresh worktrees without
`pnpm run codegen-prisma` + a live DB) and unrelated to this diff.
- **`pnpm lint`** on the same 6 packages: all green.
- **Final grep** for residual `Stack Auth` / `stack-auth.com` /
`@stackframe/stack-cli@latest` references: zero outside the intentional
carve-outs above.
- **25 e2e test files updated in lockstep** with the known-error message
changes (asserted strings flipped to match the new x-hexclave-* +
compat-note messages).

## Deploy blockers (ops sequencing before this rebrand goes live)

This PR is code-complete, but the rebrand's visible surfaces (SDK
default URLs, dashboard links, npm READMEs, REST error messages, runtime
deprecation warning) all point at `*.hexclave.com` / `@hexclave/*`
resources that don't exist yet. None of these are fixable from a PR —
they're ops/registrar/npm work that has to be sequenced before merging
this to a release tag.

Suggested ordering, hardest blockers first:

### Tier 1 — required before customer-facing deploy (everything below
this line *will visibly break customers on day 1* if skipped)

1. **DNS + TLS for `api.hexclave.com` + `api1./api2.hexclave.com`** →
must point at the same backend that serves `api.stack-auth.com` (or a
backend that mirrors PR 1's dual-accept). The SDK's new `defaultBaseUrl`
is `https://api.hexclave.com`; every customer that relied on the old
default and upgrades to a post-PR2 SDK build sends API requests here.
Until this resolves, every default-config customer's API call NXDOMAINs.
2. **DNS for `app.hexclave.com`** → the dashboard. Referenced in the
SDK's default-error messages ("Please create a project on the Hexclave
dashboard at https://app.hexclave.com"), the init-stack flow's
`wizard-congrats` redirect, and the OAuth dashboard handoff.
3. **DNS for `docs.hexclave.com`** + Mintlify deploy → the SDK runtime
deprecation warning (`https://docs.hexclave.com/migration`), every
README, every "Learn more" link in the dashboard, and every REST API
error body (`/api/overview#authentication`) points here. The MDX is in
this PR; the docs build target needs DNS.
4. **DNS for `mcp.hexclave.com`** → the MCP server endpoint that every
taught agent integration (`claude mcp add ...`, `cursor`, `codex`,
`vscode`) registers. Until this resolves, every `npx
@hexclave/cli@latest init` MCP-registration step fails.
5. **Reserve the `@hexclave` npm scope + set repo variable
`HEXCLAVE_VERSION`** → the mirror-publish step in
`.github/workflows/npm-publish.yaml` is gated on this variable. Without
it, the entire taught onboarding command `npx @hexclave/cli@latest init`
404s from the npm registry, *and* every README that says "install
`@hexclave/next`" leads to install failure. Pick the initial version
intentionally (`1.0.0` or aligned to `@stackframe/stack`); don't accept
a silent default.

### Tier 2 — required before announcing the rebrand publicly (lookalike
or low-traffic surfaces, but visibly broken)

6. **DNS for `r.hexclave.com`** → the analytics beacon
`defaultAnalyticsBaseUrl`. Silent failure if missing (analytics drops),
but should land alongside Tier 1.
7. **Register `sent-with-hexclave.com` + full email auth (SPF / DKIM /
DMARC)** → the new default sender domain for shared-sender transactional
emails. Without it the dashboard "send test email" path emits bounces,
and shared-sender flows (`getSharedEmailConfig("Hexclave")`) deliver to
spam at best.
8. **MX + SPF / DMARC for `hexclave.com`** → `team@hexclave.com` and
`security@hexclave.com` mailboxes. The security disclosure mailbox is
referenced in [`.github/SECURITY.md`](.github/SECURITY.md);
`team@hexclave.com` is the actual recipient of internal feedback emails
sent at runtime by
[`apps/backend/src/lib/internal-feedback-emails.tsx`](apps/backend/src/lib/internal-feedback-emails.tsx).
Today, every runtime feedback email bounces.
9. **DNS for `skill.hexclave.com`** → the canonical AI-agent skill fetch
URL (the agent bootstrap pivot). Without it, the entire "agent downloads
`SKILL.md` from a known URL" flow taught in
[`packages/stack-shared/src/helpers/init-prompt.ts`](packages/stack-shared/src/helpers/init-prompt.ts)
fails.
10. **Create `github.com/hexclave/hexclave` as a public repo** (even as
a redirect to `hexclave/stack-auth`) **OR** rewrite every `package.json`
`"repository"` field + dashboard footer "view on GitHub" link to point
at `hexclave/stack-auth` (which already exists). Currently every npm
package page's "Repository" link is dead, and the dashboard's GitHub
button + dev-tool repo link are dead.

### Tier 3 — broken but low-visibility / low-traffic

11. **DNS for `discord.hexclave.com`** → Discord invite redirect, used
in every README's chip and the dashboard footer.
12. **DNS for `demo.hexclave.com`** → " Demo" badge in every npm
package README. Broken-image badge on the package page.
13. **DNS + TLS for `built-with-hexclave.com`** → optional
hosted-handler domain (the default reverted to
`.built-with-stack-auth.com` in this PR's carve-outs, so this only
matters for projects that manually flip).

## Other follow-ups (not deploy-blocking)

- **E2E snapshot regen across the full suite** for the dual-emitted
`x-hexclave-*` response headers (PR 1 follow-up; `vitest -u` in CI
absorbs).
- **Binary visual assets** — logos, favicons, OG images, README
screenshots; need design pass.
- **Backend OpenAPI fumadocs regen** in CI flow — the JSON files in
`docs-mintlify/openapi/` are committed but regen runs in CI. Verify the
workflow that does this still works against the post-PR2 source.
- **Backend typecheck infra debt** — needs `codegen-prisma` +
`codegen-route-info` to clear; pre-existing, unaffected by this PR.

## Test plan

- [ ] CI runs full e2e suite (with `vitest -u` to absorb residual
snapshot deltas, then committed back).
- [ ] Spot-check: new `@hexclave/cli init` (once published) generates
`hexclave.config.ts` and works against a fresh project.
- [ ] Spot-check: existing customer with `@stackframe/stack` import sees
the once-per-process `console.warn` recommending `@hexclave/next` on SDK
init.
- [ ] Manual: dashboard setup page renders the `npx @hexclave/cli@latest
init` snippet and the `x-hexclave-publishable-client-key` API header in
the curl example.
- [ ] Manual: a fresh `pnpm run prisma migrate` against a clean DB sets
the internal project displayName to 'Hexclave Dashboard'.

---------

Co-authored-by: Konstantin Wohlwend <n2d4xc@gmail.com>
2026-05-26 19:18:20 -07:00

313 lines
14 KiB
Plaintext

---
title: Self-host
description: 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 is 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 the [Local Emulator](/guides/going-further/local-emulator). 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).
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"
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"
NEXT_PUBLIC_STACK_API_URL=https://auth-api.example.com
NEXT_PUBLIC_STACK_DASHBOARD_URL=https://auth.example.com
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_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=replace-with-a-random-value
STACK_SEED_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"
openssl rand -base64 32 | tr '+/' '-_' | tr -d '='
```
Generate the three `STACK_SEED_INTERNAL_PROJECT_*_KEY` values with any stable random values, for example:
```bash title="Terminal"
openssl rand -hex 32
```
The Docker entrypoint can generate the internal project keys if they are missing, but setting stable values yourself avoids rotating the dashboard project's keys on every fresh container start.
Generate `CRON_SECRET` with a stable random value too. Your scheduler uses it to authenticate internal maintenance requests:
```bash title="Terminal"
openssl rand -hex 32
```
### 3. Run the Server
Run the Docker image with the environment file:
```bash title="Terminal"
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
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"
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. 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.
<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"
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
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
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
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
STACK_OPENROUTER_API_KEY=...
```
Custom, manual, and programmatic email sending requires Freestyle:
```env
STACK_FREESTYLE_API_KEY=...
```
## 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. Pull the new Docker image.
4. Run the new image once with migrations enabled.
5. Verify dashboard sign-in, project loading, cron jobs, email sending, and any webhook flows you use.
6. 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.
### Health Checks
After deploy, verify:
```bash title="Terminal"
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 the [Local Emulator](/guides/going-further/local-emulator) 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
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.