piyaz
Self-hosting

Run locally

Run your own Piyaz server on your machine, then point the plugin at it.

The hosted app at app.piyaz.ai is the recommended way to use Piyaz. Running your own instance is for people who want their data on their own machine and are comfortable with Docker and Postgres.

Self-hosting is free under AGPL-3.0. You run the Piyaz server yourself and register an MCP server in your editor that points at it. The plugins ship pointing at the hosted app, so this is one command or one config entry after install.

Prerequisites

  • Bun v1.0+
  • Docker for PostgreSQL
  • Linux, macOS, or Windows with WSL2

Setup

Clone and install

git clone git@github.com:FrkAk/piyaz.git
cd piyaz
bun install --production
cp .env.local.example .env.local

Fill in .env.local

.env.local.example is numbered and self-documenting. Work through it top to bottom: it sets three Postgres roles (each with its own password and connection URL) and the auth secret. Generate secrets with openssl rand as the file describes:

openssl rand -hex 32     # role passwords
openssl rand -base64 32  # auth secret

The three roles plus row-level security scope access inside Postgres. Do not point every URL at one superuser.

The example ships TRUSTED_PROXY_HEADER=none, which is correct while nothing sits in front of the app. If you put it behind a reverse proxy, see Running behind a proxy before you expose it.

Bring up the database

bun run db:setup

This starts Postgres in Docker, bootstraps the auth and row-level-security objects, and applies the schema as versioned migrations, all in one command.

Postgres publishes 5432 on loopback only (127.0.0.1:5432). The superuser password defaults to a well-known value, read on first init only and fine while the port stays loopback-bound; to override it, set POSTGRES_PASSWORD in .env.local before the first db:setup.

Build and start

bun run build
bun run start

Open http://localhost:3000, sign up, and you are in.

Connect your agent

Install the Piyaz plugin as you would for the hosted app. It ships one MCP server, piyaz, pointing at https://app.piyaz.ai/api/mcp. Your own instance is a second server that you register and name yourself.

Register a separate server instead of editing the config the plugin ships. Claude Code and Codex keep that file in a version-scoped cache directory that the next plugin update replaces; on Antigravity it is a copy you would have to re-edit after every update.

Work out your endpoint

You own the URL, so the docs cannot guess it. Your MCP endpoint is your Piyaz origin plus /api/mcp:

Where Piyaz runsEndpoint
The machine you code onhttp://localhost:3000/api/mcp
The same machine, different porthttp://localhost:8080/api/mcp
A server or VPS you ownhttps://piyaz.example.com/api/mcp

Serve anything that is not on localhost over HTTPS. The OAuth handshake runs against the same origin as the MCP endpoint, so a plain-HTTP remote instance sends tokens in the clear.

On Windows with Piyaz running inside WSL2, an editor on the Windows side still reaches it at http://localhost:3000. WSL2 forwards listening ports to the Windows host by default.

Register the server

Substitute your own endpoint for http://localhost:3000/api/mcp in every example below. The Claude Code and Codex commands behave the same on macOS, Windows, and Linux.

claude mcp add -s user --transport http piyaz-self-hosted http://localhost:3000/api/mcp

-s user registers the server for every project on your machine. Claude Code stores it in ~/.claude.json. Drop the flag to scope it to the current project instead.

Then sign in:

claude mcp login piyaz-self-hosted

Requires Claude Code v2.1.186 or later. On older versions, run /mcp inside a session and authenticate there.

codex mcp add piyaz-self-hosted --url http://localhost:3000/api/mcp

Codex writes an [mcp_servers.piyaz-self-hosted] entry to ~/.codex/config.toml (%USERPROFILE%\.codex\config.toml on Windows, or $CODEX_HOME/config.toml if you set that variable).

Then sign in:

codex mcp login piyaz-self-hosted

Edit ~/.cursor/mcp.json, or %USERPROFILE%\.cursor\mcp.json on Windows. Use .cursor/mcp.json inside a project to scope the server to that project.

Add piyaz-self-hosted alongside whatever the file already holds rather than replacing it. If you installed Piyaz through the deeplink, the hosted piyaz entry lives in this same file:

{
  "mcpServers": {
    "piyaz": { "url": "https://app.piyaz.ai/api/mcp" },
    "piyaz-self-hosted": { "url": "http://localhost:3000/api/mcp" }
  }
}

Restart Cursor. It runs OAuth on the first tool call against the new server.

Edit ~/.gemini/config/mcp_config.json, or %USERPROFILE%\.gemini\config\mcp_config.json on Windows. Use .agents/mcp_config.json at your workspace root to scope the server to that workspace.

Merge the new entry into the existing mcpServers object rather than replacing the file:

{
  "mcpServers": {
    "piyaz-self-hosted": { "serverUrl": "http://localhost:3000/api/mcp" }
  }
}

Antigravity requires serverUrl for remote servers. It does not accept url or httpUrl.

Restart Antigravity, run /mcp (CLI) or open the MCP manager (IDE), and authenticate against piyaz-self-hosted.

Verify

Ask your agent:

List my projects.

The answer comes from your instance. An empty list is expected before you create anything.

The bundled piyaz server stays pointed at the hosted app. Leave it signed out, or disable it in your editor's MCP settings so the two never mix.

Running behind a proxy

Piyaz identifies a caller by IP address to throttle sign-in, password reset and email verification, and to record the IP on legal acceptance. Anyone can send an X-Forwarded-For header, so that address is only trustworthy when something in front of the app overwrites it. TRUSTED_PROXY_HEADER names the single header yours sets, and nothing else is ever read.

Set it to the header your proxy writes on inbound requests. nginx and Caddy write x-forwarded-for or x-real-ip depending on configuration; a CDN writes its own. Naming a header your proxy does not set is worse than naming none, because the address then comes straight from the caller.

TRUSTED_PROXY_HEADER=x-forwarded-for

Add TRUSTED_PROXIES only when there are two or more hops in front of the app, listing exactly the proxies you run as IPs or CIDR ranges, no more and no less. With a single proxy the address resolves to the hop it observed and the list is unnecessary. Behind two or more hops without it, every caller resolves to the same upstream address and shares one throttling budget. Too broad is the opposite failure: an entry that covers addresses where clients live lets a caller prepend a hop and choose its own identity. If a range really must cover everything, cover both address families (0.0.0.0/0,::/0); an IPv4-only blanket stops the walk at a prepended IPv6 hop and attributes the caller's chosen address. Entries that do not parse as an IP address or CIDR range refuse to start the app, so a typo surfaces at boot instead of silently disabling chain attribution.

TRUSTED_PROXIES=10.0.0.1,192.168.1.0/24

TRUSTED_PROXY_HEADER is required in production: bun run start refuses to launch without it. Use none if nothing fronts the app. Callers are then indistinguishable, so the budgets above become instance-wide instead of per caller: the whole deployment shares one sign-in budget, no IP is recorded on legal acceptance, and one busy caller can throttle everyone. The budgets are widened to stay usable, but naming a real header is better.

Upgrading

Your database holds real data, so schema changes apply as versioned migrations, never db:push (it force-syncs and can drop columns). Pull the latest code, apply migrations, re-apply the row-level-security objects, then rebuild:

git pull
bun install --production
bun run db:migrate
bun run db:rls
bun run build
bun run start

db:rls re-applies the canonical grants, SECURITY DEFINER functions, and RLS policies from docker/*.sql. It is idempotent (safe to run when nothing changed) and required whenever a release adds an RLS-protected table: the migration creates the table and enables RLS, but its grant and policy live in the hand-written docker/*.sql files that db:migrate does not apply.

Upgrading from a pre-migrations install: instances first set up before versioned migrations existed have an empty migration journal, so db:migrate tries to recreate existing tables and fails. Run bun run db:baseline once to mark the current schema as the baseline, then run bun run db:migrate. Running db:baseline on a newer install is a safe no-op.

TRUSTED_PROXY_HEADER is now required. bun run start exits with an error until it is set; the build itself does not need it. Add it to .env.local: name the header your reverse proxy sets, or none if nothing fronts the app. See Running behind a proxy.

Postgres now binds to loopback only. A container created before this change keeps its old mapping, which publishes 5432 on every interface and slips past host firewalls such as ufw (Docker writes its own iptables rules). Run docker compose up -d --force-recreate once to pick up the 127.0.0.1:5432 bind.

Last updated

On this page