---
title: "Deploy Stack Auth | (Just Updated) Auth0 and Clerk Alternative, Admin Login and Agent API Ready"
description: "Stack Auth server: self-hosted user auth API and dashboard, admin preset"
category: "Authentication"
url: https://railway.com/deploy/stack-auth-or-just-updated-auth0-and-cle
---

# Deploy Stack Auth | (Just Updated) Auth0 and Clerk Alternative, Admin Login and Agent API Ready

Stack Auth server: self-hosted user auth API and dashboard, admin preset

**[Deploy Stack Auth | (Just Updated) Auth0 and Clerk Alternative, Admin Login and Agent API Ready on Railway](https://railway.com/template/stack-auth-or-just-updated-auth0-and-cle)**

Machine-readable deploy manifest (JSON, validated by TemplateCI): https://railway.com/deploy/stack-auth-or-just-updated-auth0-and-cle/manifest.json

- **Creator:** SuperSlowSloth
- **Category:** Authentication

## Template content

### postgres

- **Image:** postgres:17.10-alpine

### stack-auth

- **Image:** stackauth/server:latest@sha256:ef2700da17329a9bf9bf21f5c8f956005fdefc036a08cd05484bdecc6d9a8d39
- **Start command:** `/bin/sh -c '#!/bin/bash
# Stack Auth on Railway: one container runs the backend (8102) and the dashboard (8101) behind
# a small path router on $PORT, plus the minute cron the image does not run itself.
# This script is wrapped in a shell -c string by make-spec.py, so it contains no single quote.
set -e

# Railway injects PORT; the healthcheck dials it. Backend and dashboard keep their own ports.
export ROUTER_PORT="${PORT:-8080}"
export BACKEND_PORT=8102
export DASHBOARD_PORT=8101
unset PORT

# One public URL serves both the API and the dashboard (the router splits them by path).
export NEXT_PUBLIC_STACK_API_URL="$STACK_PUBLIC_URL"
export NEXT_PUBLIC_STACK_DASHBOARD_URL="$STACK_PUBLIC_URL"
export STACK_TRUSTED_PROXY=generic
export STACK_RUN_MIGRATIONS=true
# The seed script aborts on a database that is already seeded, so a redeploy
# would crash-loop. Seed only while the internal project row does not exist yet.
export STACK_RUN_SEED_SCRIPT=true
if [ "$(psql "$STACK_DATABASE_CONNECTION_STRING" -tAc "select 1 from \"Project\" where id = \$\$internal\$\$" 2>/dev/null)" = "1" ]; then
  export STACK_RUN_SEED_SCRIPT=false
fi
export STACK_SEED_INTERNAL_PROJECT_USER_EMAIL=admin@stack-auth.example.com
export STACK_SEED_INTERNAL_PROJECT_USER_PASSWORD="$ADMIN_PASSWORD"
export STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=true
export STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED=false
export STACK_CLICKHOUSE_ADMIN_USER=stackadmin
export STACK_CLICKHOUSE_ADMIN_PASSWORD="$CLICKHOUSE_PASSWORD"
export STACK_CLICKHOUSE_EXTERNAL_PASSWORD="$CLICKHOUSE_EXTERNAL_PASSWORD"

cat > /var/tmp/router.mjs <<"JS"
import http from "node:http";
const listen = Number(process.env.ROUTER_PORT);
const backend = Number(process.env.BACKEND_PORT);
const dashboard = Number(process.env.DASHBOARD_PORT);
const publicUrl = new URL(process.env.STACK_PUBLIC_URL);
const toBackend = (p) =>
  p === "/health" || (p.startsWith("/api/") && !p.startsWith("/api/development-environment"));
function target(req) {
  const p = req.url.split("?")[0];
  return toBackend(p) ? backend : dashboard;
}
function headers(req) {
  const h = { ...req.headers };
  const xff = String(req.headers["x-forwarded-for"] || "").split(",")[0].trim();
  h["x-real-ip"] = xff || req.socket.remoteAddress;
  h["x-forwarded-host"] = publicUrl.host;
  h["x-forwarded-proto"] = publicUrl.protocol.replace(":", "");
  return h;
}
const server = http.createServer((req, res) => {
  const up = http.request(
    { host: "127.0.0.1", port: target(req), method: req.method, path: req.url, headers: headers(req) },
    (r) => { res.writeHead(r.statusCode, r.headers); r.pipe(res); },
  );
  up.on("error", () => { if (!res.headersSent) res.writeHead(502); res.end("upstream not ready"); });
  req.pipe(up);
});
server.on("upgrade", (req, socket, head) => {
  const up = http.request({ host: "127.0.0.1", port: target(req), method: req.method, path: req.url, headers: headers(req) });
  up.on("upgrade", (r, s, h) => {
    socket.write("HTTP/1.1 101 Switching Protocols\r\n" + Object.entries(r.headers).map(([k, v]) => k + ": " + v).join("\r\n") + "\r\n\r\n");
    if (h.length) socket.write(h);
    s.pipe(socket); socket.pipe(s);
  });
  up.on("error", () => socket.destroy());
  up.end(head);
});
server.listen(listen, "::", () => console.log("router listening on " + listen));
JS

# Router first, so the platform healthcheck has something to dial while migrations run.
node /var/tmp/router.mjs &

# Cron: exactly one scheduler, the container itself. Starts after the backend answers.
(
  until node -e "fetch(process.argv[1]).then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" "http://127.0.0.1:$BACKEND_PORT/health"; do sleep 5; done
  while true; do
    for path in email-queue-step external-db-sync/sequencer external-db-sync/poller workflow-engine-step; do
      node -e "fetch(process.argv[1],{headers:{authorization:\"Bearer \"+process.env.CRON_SECRET}}).catch(()=>{})" "http://127.0.0.1:$BACKEND_PORT/api/latest/internal/$path" || true
    done
    sleep 60
  done
) &

exec ./entrypoint.sh'`
- **Health check:** /health
- **Public domain:** Yes

### clickhouse

- **Image:** clickhouse/clickhouse-server:25.12-alpine
- **Start command:** `/bin/sh -c 'export CLICKHOUSE_USER=stackadmin CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1; exec /entrypoint.sh'`

## Documentation

# Deploy and Host Stack Auth on Railway

**Built for an LLM agent to deploy and use.** There is no setup wizard and nothing to fill in: the server secret, the admin password and every database password are generated at deploy time, the admin user exists from the first boot, and the contract below lists every URL, variable name and the one script that proves it works.

Stack Auth is an open-source (MIT and AGPL) authentication platform: a user-management API with a ready-made dashboard, sign-in by password, magic link, OTP, passkey and OAuth, teams and permissions, plus SDKs for Next.js, React and plain REST. It is a self-hosted alternative to Auth0, Clerk and Supabase Auth. The project is also published under the name Hexclave; the same `stackauth/server` image carries both names.

## About Hosting Stack Auth

This template runs three services: the official `stackauth/server` image (pinned by digest), Postgres 17 and ClickHouse 25.12, each with a volume. The Stack Auth service serves the dashboard and the API from one public domain: a small path router in the start command sends `/api/*` and `/health` to the backend and everything else to the dashboard, so only one URL is needed. On every boot it applies the database migrations and, only the first time, seeds the internal project and the admin user. The per-minute background jobs (email queue, external database sync, workflow engine) are called by the start command itself, authenticated with `CRON_SECRET`, because Railway has no cron for a long-running service. Allow about two minutes for the first deploy to turn healthy.

The first sign-in at the dashboard is the user `admin@stack-auth.example.com` with `ADMIN_PASSWORD`. Public sign-up on the dashboard project is switched off. No mail server or Svix webhook service is attached, so emails are not sent and webhooks are not delivered until you set the `STACK_EMAIL_*` and `STACK_SVIX_*` variables.

### Agent contract

- Public URL: `https://` plus the `RAILWAY_PUBLIC_DOMAIN` of the `stack-auth` service, also published as the variable `STACK_PUBLIC_URL`; used below as `STACK_URL`. The dashboard is at `/`, the REST API at `/api/latest`.
- Private URL: `http://stack-auth.railway.internal:8102` (the backend API, for services in the same project)
- Credentials: `ADMIN_PASSWORD` (user `admin@stack-auth.example.com`) and `STACK_SERVER_SECRET`. The internal project keys are derived from `STACK_SERVER_SECRET`, so they stay the same across redeploys as long as it does.
- Auth: API clients send the headers `x-stack-project-id: internal`, `x-stack-publishable-client-key: $PK` and `x-stack-access-type: client`, plus `x-stack-access-token: $ACCESS_TOKEN` once signed in. `PK` is `base64(HMAC-SHA256(key=STACK_SERVER_SECRET, message="internal-project-publishable-client-key"))`; the script below computes it. The server key for `x-stack-access-type: server` is the same derivation with the message `internal-project-secret-server-key`, sent as `x-stack-secret-server-key`.
- Health: `GET /health` returns 200 with no credentials

```bash
# run with STACK_URL, ADMIN_PASSWORD and STACK_SERVER_SECRET set
PK=$(printf '%s' internal-project-publishable-client-key | openssl dgst -sha256 -hmac "$STACK_SERVER_SECRET" -binary | base64 -w0)
H=(-H "x-stack-project-id: internal" -H "x-stack-publishable-client-key: $PK" -H "x-stack-access-type: client" -H "content-type: application/json")
curl -s -o /dev/null -w 'health %{http_code}\n' "$STACK_URL/health"
curl -s -o /dev/null -w 'dashboard %{http_code}\n' "$STACK_URL/"
curl -s -o /dev/null -w 'wrong-password %{http_code}\n' "${H[@]}" "$STACK_URL/api/latest/auth/password/sign-in" -d '{"email":"admin@stack-auth.example.com","password":"wrong-password-1"}'
AT=$(curl -s "${H[@]}" "$STACK_URL/api/latest/auth/password/sign-in" -d "{\"email\":\"admin@stack-auth.example.com\",\"password\":\"$ADMIN_PASSWORD\"}" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')
echo "token ${#AT} chars"
curl -s "${H[@]}" -H "x-stack-access-token: $AT" "$STACK_URL/api/latest/users/me" | grep -o '"primary_email":"[^"]*"'
```

A fresh deploy prints `health 200`, `dashboard 200`, `wrong-password 400`, a token length above zero and `"primary_email":"admin@stack-auth.example.com"`. Create more projects and keys in the dashboard (`STACK_URL`), then point your app's SDK at `STACK_URL` as the API URL.

## Why Deploy Stack Auth on Railway?

Railway is a singular platform to deploy your infrastructure stack. Railway will host your infrastructure so you don't have to deal with configuration, while allowing you to vertically and horizontally scale it.

By deploying Stack Auth on Railway, you are one step closer to supporting a complete full-stack application with minimal burden. Host your servers, databases, AI agents, and more on Railway.

- **Agent-ready** - generated secrets, a contract block and one working script; no wizard before the first sign-in.
- **One URL** - dashboard and API share a domain, so a single `STACK_PUBLIC_URL` configures the SDKs.
- **Redeploy-safe** - Postgres and ClickHouse sit on volumes, the seed runs once, and keys derive from one stable secret.
- **Background jobs included** - email queue, database sync and workflow engine run without an external scheduler.
- **Pinned image** - pinned by digest, so a redeploy never changes the server version underneath you.

## Common Use Cases

- **Self-hosted user authentication** - sign-up, sign-in, sessions and teams for your own app without per-user pricing.
- **Auth0 and Clerk replacement** - keep users in your own Postgres instead of a vendor.
- **Authentication for AI agents and apps** - an agent can create projects, keys and users over the REST API.

## Dependencies for Stack Auth Hosting

- Postgres 17 (users, projects, sessions) and ClickHouse 25.12 (analytics and event data), both included in this template.

### Deployment Dependencies

- [hexclave/hexclave](https://github.com/hexclave/hexclave) - upstream project (formerly stack-auth/stack-auth)
- [Stack Auth documentation](https://docs.stack-auth.com) - SDKs, REST API and self-hosting
- [stackauth/server on Docker Hub](https://hub.docker.com/r/stackauth/server) - the image this template runs


## Similar templates

- [Keycloak](https://railway.com/deploy/mSwigX) — [2026-11-04] Keycloak with keywind theme + Apple & Discord providers
- [lua-protector](https://railway.com/deploy/lua-protector) — Test deployed my project first
- [bknd](https://railway.com/deploy/p4nTYL) — Feature-rich yet lightweight backend

Open this page in a browser: https://railway.com/deploy/stack-auth-or-just-updated-auth0-and-cle
