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

# Self-hosting

> Run your own astilba server: Postgres, environment variables, migrate and seed, and one-origin serving. Honest about what's not built yet.

astilba's server and dashboard live in the [repo](https://github.com/astilbahq/astilba)
(Apache-2.0) and are self-hostable today.

<Warning>
  Self-hosting is **Alpha**. There is **no production container image or deploy artifact yet**
  — you run it from source. There is also **no self-service sign-up UI**: the first user and
  organization are created by the seed script (dev) or Better Auth's API (see
  [First user and organization](#first-user-and-organization)).
</Warning>

## Prerequisites

* **Node.js >= 22.14** and **pnpm**.
* **PostgreSQL** — bring your own, or use the one in the repo's `docker-compose.yml` for local
  development.

## Local development

The repo's `docker-compose.yml` provisions **only Postgres** (and Adminer, a DB UI) — not the
app. Bring the database up, then run the server and dashboard from source.

<Steps>
  <Step title="Start Postgres">
    ```sh theme={null}
    pnpm dev:db          # docker compose up -d --wait — Postgres on :5432, Adminer on :8080
    ```
  </Step>

  <Step title="Configure the server">
    ```sh theme={null}
    cp apps/server/.env.example apps/server/.env
    ```

    The example's `DATABASE_URL` already matches the compose Postgres. It also sets a
    dev-only `BETTER_AUTH_SECRET` — fine locally, but generate a real one for anything else.
  </Step>

  <Step title="Migrate and seed">
    ```sh theme={null}
    pnpm --filter @astilba/server db:migrate
    pnpm --filter @astilba/server db:seed
    ```

    The seed creates a demo user, organization, project, and API token, and prints the
    credentials. It **refuses a non-local database** unless `ASTILBA_SEED_ALLOW_REMOTE=1`.
  </Step>

  <Step title="Run the server and dashboard">
    ```sh theme={null}
    pnpm dev             # runs apps/server and apps/dashboard together
    ```

    Open the dashboard and sign in with the credentials the seed printed. In dev the Vite
    dev server proxies `/api` to the server, so the two run on separate ports.
  </Step>
</Steps>

## Environment variables

The server validates its environment on start and **fails fast** with a clear message if
anything is missing or malformed.

| Variable             | Required | Default        | Purpose                                                         |
| -------------------- | -------- | -------------- | --------------------------------------------------------------- |
| `DATABASE_URL`       | yes      | —              | Postgres connection string                                      |
| `BETTER_AUTH_SECRET` | yes      | —              | Session-signing key; **must be at least 32 characters**         |
| `BETTER_AUTH_URL`    | no       | request origin | The server's public URL (cookies/redirects)                     |
| `PORT`               | no       | `3000`         | Port the Node server listens on                                 |
| `ASTILBA_STATIC_DIR` | no       | —              | Path to the built dashboard, for one-origin serving (see below) |

Generate a real secret with:

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

<Note>
  `db:migrate` and `db:seed` load `apps/server/.env`; `start` reads the environment directly
  (no `.env`). For production, either provide a `.env` with your production `DATABASE_URL`
  before migrating, or run the migrate script with the variables set in the environment.
</Note>

## Production (manual)

There's no image yet, so production is a manual, from-source deploy. The shape:

<Steps>
  <Step title="Provision Postgres and secrets">
    Point `DATABASE_URL` at your database, set a real `BETTER_AUTH_SECRET` (>= 32 chars), and
    set `BETTER_AUTH_URL` to your public URL.
  </Step>

  <Step title="Apply migrations">
    Run `db:migrate` against the production database (see the note above about `.env`).
  </Step>

  <Step title="Build the dashboard">
    ```sh theme={null}
    pnpm --filter @astilba/dashboard build   # emits apps/dashboard/dist
    ```
  </Step>

  <Step title="Serve both on one origin">
    Set `ASTILBA_STATIC_DIR` to the built dashboard and start the server:

    ```sh theme={null}
    ASTILBA_STATIC_DIR=/path/to/apps/dashboard/dist \
      pnpm --filter @astilba/server start
    ```

    The server serves the dashboard's assets and falls back to `index.html` for client-routed
    paths, while `/api/*` is handled first — so a static file can never shadow the API, and the
    dashboard and API share one origin. Run it behind a TLS-terminating reverse proxy.
  </Step>
</Steps>

<Warning>
  The server targets the web-standard runtime, and the code is written to run on Cloudflare
  Workers (env is injected per request, Postgres via Hyperdrive) — but a Workers deploy is
  **not a shipped artifact yet**.
</Warning>

## First user and organization

There is no sign-up page. To create the first account:

* **Locally**, `db:seed` does it for you and prints the credentials.
* **Otherwise**, use Better Auth's endpoints under `/api/auth/*`: register a user, create an
  organization, then **set it as the session's active organization** (Better Auth's
  set-active-organization endpoint) — or just sign in again once the membership exists. The
  active org is chosen when a session is *created*, so a session made before the org existed
  has none, and `/api/projects` / `/api/tokens` answer `No active organization` (403) until you
  set it. With an active org, mint an [API token](/guides/orgs-and-tokens) and you're ready to
  [`push`/`pull`](/guides/team-workflow).

<Tip>
  Hitting a rough edge self-hosting? It's Alpha — please open an
  [issue](https://github.com/astilbahq/astilba/issues).
</Tip>
