Self hosting

psh.now is MIT licensed and runs as one Bun process in front of Postgres. Two containers, three environment variables, and a domain that resolves to the host. Everything below has been run on a clean machine, in this order.

Quick start

git clone https://github.com/passy-ai/psh-now && cd psh-now
cp .env.example .env

# the two that compose refuses to start without
PUBLIC_BASE_URL=https://psh.example.com
PSH_SECRET=$(openssl rand -base64 48)

# sign-in needs one of these
MAIL_FROM="psh.now <no-reply@example.com>"
SMTP_URL=smtps://user:password@smtp.example.com:465

docker compose up -d --build

Migrations run inside the container on start, with retries, so there is no separate migrate step to forget. The app comes up on http://localhost:3000; put TLS in front of it (next section) before you pair anything, because pairing links embed PUBLIC_BASE_URL.

Environment

Three variables are required. The rest have defaults that work.

PUBLIC_BASE_URL   required   the URL people and agents will use, no trailing slash
PSH_SECRET        required   encrypts stored agent keys; 32+ random bytes
DATABASE_URL      required   compose builds it from POSTGRES_* for you

MAIL_FROM         mail       the From line, e.g. psh.now <no-reply@example.com>
SMTP_URL          mail       smtps://user:pass@smtp.host:465
RESEND_API_KEY    mail       alternative to SMTP_URL

PORT              3000       app port inside the container
DEFAULT_TIMEOUT_MS  90000    how long an ask waits for a human
PAIRING_TTL_MS    600000     how long a pairing link lives
PSH_FREE_REQUESTS 20         requests granted before a confirmed account
PSH_CLAIMED_REQUESTS 100     requests granted to a confirmed account

Rotating PSH_SECRET is not supported in place. It encrypts recoverable agent keys at rest, so changing it makes every stored key undecryptable. Back it up with the database, not separately.

Mail

Email is how a person signs in. Set MAIL_FROM plus one transport:

With neither set, the server still runs and says so at boot: no mail transport configured. In development the code prints to the console and comes back in the API response, so local sign-in needs no mail at all. In production, sign-in returns an error rather than pretending an email went out.

TLS

Two supported shapes. Caddy is the one-liner:

# Caddyfile
psh.example.com {
  reverse_proxy app:3000
}

Or hand certificates to Bun directly with TLS_CERT_FILE and TLS_KEY_FILE and skip the proxy. If you use nginx or Cloudflare in front, read Limits and proxies before you test approving.

First account

  1. Open https://psh.example.com/app.
  2. Enter your email, then the six digit code from that mailbox.
  3. Open /devices and pair a phone or the Notch app. The pairing link is built from PUBLIC_BASE_URL, so a wrong value here is the most common first-run failure.

There is no seeded admin and no default password. The first person to claim a workspace owns it, which is why a self-hosted instance should not be exposed before you claim it.

Connecting agents

Agents do not need a plugin. In any project, install the skill and run setup:

npx skills add passy-ai/psh-now-skill -g -y
curl -fsSL https://psh.example.com/install.sh | sh

Or point an agent at the instance without installing anything: give it the channel key from /channels and the prompt at /docs#sandbox. The CLI is plain shell plus curl, so it works anywhere an agent can run a command.

Upgrades and backups

git pull
docker compose up -d --build          # migrations run on start

# backup: database and the secret together, or neither is useful
docker compose exec db pg_dump -U psh.now psh.now > psh-$(date +%F).sql
printf '%s\n' "$PSH_SECRET" > psh-secret.txt

Restoring is the same two files: a fresh volume, psql < dump, and the original PSH_SECRET. A dump without the secret restores rows whose encrypted values cannot be read.

Limits and proxies

An ask blocks until a human answers, so the request is only as long as your proxy allows. Keep DEFAULT_TIMEOUT_MS below it:

The default of 90s sits under all three. If agents report timeouts at exactly the same second every time, it is the proxy, not psh.now.

What is not included

Troubleshooting

The long version, including the release checklist and the security notes, is in the repository at docs/self-hosting.md.