Get started
Install Pitchbar (self-host)
This guide walks you from a fresh server to a running Pitchbar install with one admin account, one published agent, and the widget answering on a test page. Expect 20–40 minutes if your server is already provisioned with PHP, Node, a database, and Redis.
1. Server requirements
| Component | Minimum | Notes |
|---|---|---|
| PHP | 8.3+ | 8.4 recommended. Extensions: bcmath, curl, fileinfo, gd, intl, mbstring, openssl, pdo_pgsql (or pdo_mysql), tokenizer, xml, zip. |
| Composer | 2.6+ | Used to install PHP dependencies. |
| Node.js | 20+ | Used to build the admin SPA and the visitor widget. |
| Database | PostgreSQL 14+ or MySQL 8.0+ | Postgres is the primary target. |
| Redis | 7+ | Cache, sessions, queue, hot-path retrieval cache. |
| RAM / CPU | 2 vCPU / 2 GB | One app + one worker process. Scale up if you'll run them on the same box. |
| Disk | 10 GB+ | Application + log volume. Vector store sits in Cloudflare / Qdrant, not on disk. |
| TLS | HTTPS | The widget requires an HTTPS origin to load on customer sites. Use Caddy / Nginx / Cloudflare in front of FrankenPHP. |
| SMTP | any provider | Postmark / Resend / SES / your own SMTP. Required for password reset, lead notifications, billing receipts. |
External accounts you'll need
- At least one LLM provider — Cloudflare Workers AI is the cheapest and what we recommend (chat + embeddings + vector DB + browser crawler all on one bill). OpenAI works as a drop-in. OpenRouter works too and exposes a free Llama 3.3 model.
- A vector store — Cloudflare Vectorize (preferred, shares the Cloudflare account) or a self-hosted Qdrant instance.
- Optional: Stripe for billing customers, and Sentry / Honeycomb for error / trace reporting.
2. Get the code onto the server
Upload the source bundle you downloaded (CodeCanyon zip), or clone your private repo, into the document root. Everything in this guide assumes you're inside the project directory.
cd /var/www/pitchbar # or wherever you unpacked the zip
composer install --no-dev --optimize-autoloader
cp .env.example .env
php artisan key:generate
php artisan key:generate writes a fresh
APP_KEY to .env. Back this value
up the moment you generate it — every encrypted column
in app_settings (Stripe / Cloudflare / OpenAI keys you'll
paste in step 7) is sealed with this key. Losing it means losing
those secrets.
3. Configure the database connection
Edit .env and fill the database block. The defaults
point at a local Docker Postgres; swap to your actual host.
DB_CONNECTION=pgsql # or mysql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=pitchbar
DB_USERNAME=pitchbar
DB_PASSWORD=…strong-password…
Create the database first if it doesn't exist:
createdb -U postgres pitchbar
# or, MySQL:
mysql -uroot -p -e "CREATE DATABASE pitchbar CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
4. Fill the rest of .env
Every key you can flip via the admin UI later — Stripe, PayPal,
Razorpay, Cloudflare, OpenAI, OpenRouter, mail, branding —
can be left blank in .env and pasted in the
web admin instead. The keys below are the ones the app needs at
boot, before you can open the admin.
| Variable | Value |
|---|---|
APP_URL | The public HTTPS URL you'll serve the app from, e.g. https://app.example.com. Used to build widget snippets, OAuth callbacks, and signed URLs. |
APP_NAME | Display name shown in the title bar and emails. |
APP_ENV | production. |
APP_DEBUG | false. |
REDIS_HOST / REDIS_PORT / REDIS_PASSWORD | Redis connection. |
SESSION_DRIVER / CACHE_STORE / QUEUE_CONNECTION | All redis in production. |
WIDGET_JWT_SECRET | The HS256 signing secret for visitor session JWTs. Generate openssl rand -hex 32 and paste the result. Do not leave at the default. |
BROADCAST_CONNECTION | reverb if you want the realtime inbox + live-chat handoff. Set to null to disable. |
REVERB_APP_ID / REVERB_APP_KEY / REVERB_APP_SECRET | Random tokens identifying the Reverb app. Generate fresh strings. |
REVERB_HOST | Public hostname for the WebSocket process — same domain as APP_URL if you reverse-proxy WS on the same host. |
REVERB_SCHEME | wss in production. |
MAIL_FROM_ADDRESS / MAIL_FROM_NAME | Sender identity for outgoing email. Required. |
Full reference for every variable lives at Environment variables.
LLM provider keys (you can also paste these in the admin later)
Set at least one of these so a freshly created agent can answer. The auto-binder picks Cloudflare → OpenRouter → OpenAI, in that order, based on which keys are present.
# Cloudflare Workers AI (preferred)
CLOUDFLARE_ACCOUNT_ID=
CLOUDFLARE_API_TOKEN=
CLOUDFLARE_VECTORIZE_INDEX=pitchbar-chunks
# or OpenAI
OPENAI_API_KEY=
# or OpenRouter (free Llama 3.3 model available)
OPENROUTER_API_KEY=
LLM_PROVIDER=openrouter # required to opt into OpenRouter
VectorizeClient::ensureCollection
is idempotent, so re-running install steps is safe.
5. Run the migrations and seed the plans
php artisan migrate --force
php artisan db:seed --class=PlanSeeder --force
PlanSeeder creates the Free / Pro plan rows the
billing system reads. It is idempotent — re-running it won't
duplicate plans.
Skip UserSeeder in production. It
creates the demo accounts admin@mail.com /
customer@mail.com with the public password
password — fine for local dev, an open door on a
public deployment.
6. Build the frontend bundles
npm ci
npm run build # admin Inertia SPA → public/build/
npm run build:widget # visitor widget → public/widget/widget.js
php artisan storage:link # symlinks public/storage → storage/app/public
php artisan optimize # caches routes, config, views
Both build outputs are committed alongside source in our deploy artifact (the CodeCanyon zip includes them pre-built), but re-running on the server guarantees the bundle matches the PHP version of the code you uploaded.
7. Serve the app and run the workers
Pitchbar runs on Laravel Octane + FrankenPHP for the HTTP server, Horizon for the queue, and Reverb for the WebSocket realtime channel. All three need to be supervised processes; here's the minimum shape for a single-host install using systemd.
App server
# /etc/systemd/system/pitchbar-app.service
[Unit]
Description=Pitchbar Octane (FrankenPHP)
After=network.target redis.service postgresql.service
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/pitchbar
ExecStart=/usr/bin/php artisan octane:start --server=frankenphp --host=0.0.0.0 --port=8000 --workers=4
Restart=always
[Install]
WantedBy=multi-user.target
Put your TLS terminator (Caddy, Nginx, Cloudflare proxy) in front,
pointed at 127.0.0.1:8000. The reverse proxy is what
serves https://app.example.com to the public; the
Octane process only binds to localhost.
Queue worker
# /etc/systemd/system/pitchbar-horizon.service
[Unit]
Description=Pitchbar Horizon queue worker
After=network.target redis.service
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/pitchbar
ExecStart=/usr/bin/php artisan horizon
Restart=always
[Install]
WantedBy=multi-user.target
Horizon supervises crawl / index / default queues by default.
Check the queue health from the platform admin at
/admin/queue-health.
WebSocket process
# /etc/systemd/system/pitchbar-reverb.service
[Unit]
Description=Pitchbar Reverb WebSocket server
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/pitchbar
ExecStart=/usr/bin/php artisan reverb:start --host=0.0.0.0 --port=8080
Restart=always
[Install]
WantedBy=multi-user.target
Reverse-proxy wss://realtime.example.com (or the same
domain on a different path) to 127.0.0.1:8080. Skip
this process if you set BROADCAST_CONNECTION=null —
you'll lose the live inbox and human takeover features.
Enable and start everything
sudo systemctl daemon-reload
sudo systemctl enable --now pitchbar-app pitchbar-horizon pitchbar-reverb
8. Wire up the cron scheduler
Several jobs run on a schedule — refresh stale crawls, sync OAuth sources, release stale "needs human" conversations, suggest curated answers from gaps. Pick one of the two options below.
Option A — host cron (simplest)
Add a single line to root's crontab (or the user that
owns the project files):
* * * * * cd /var/www/pitchbar && php artisan schedule:run >> /dev/null 2>&1
Option B — Cloudflare Cron Worker
Pitchbar can deploy a Cloudflare Worker that hits your install's
/_internal/queue/tick endpoint every minute, so you
don't need a host cron at all. Useful for serverless deploys where
no process can run periodically.
After you've pasted your Cloudflare account ID and API token into
Settings → System (next step), open
Settings → System → Cron worker and click
Deploy. Status is reported at
/settings/system/cron-worker/status.
9. Create your first admin
Sign up the normal way at {APP_URL}/register. Pitchbar
auto-creates the first workspace for you. Then promote your account
to super_admin from the command line so you can
reach the platform admin and paste system keys.
php artisan pitchbar:make-admin you@example.com
Log out and back in; /admin and Settings →
System are now visible in the sidebar.
10. Drop in system keys via the admin (recommended)
Open Settings → System as the super_admin. Paste:
- Cloudflare account ID + API token + Vectorize index name + AI Gateway URL (optional).
- OpenAI key (optional fallback).
- OpenRouter key (optional, free Llama 3.3 model).
- Stripe publishable + secret + webhook signing secret (optional, for billing).
- PayPal / Razorpay if you want extra payment gateways.
- Mail SMTP / API credentials.
- Branding — replace "Powered by Pitchbar" with your own label, footer logo, and link target.
Each section has a Test button that talks to the
upstream API with the key you just pasted —
Test mail sends a real email,
Test LLM calls a small chat completion,
Test Stripe hits the Stripe API root, etc. Use these
before saving so you catch a wrong key immediately instead of at
the first customer signup.
APP_KEY rotation requires a manual migration. The
encrypted columns in app_settings are sealed with
the value of APP_KEY at the time you pasted them;
rotating without re-encrypting renders them unreadable and
you'll have to paste every key again.
11. Smoke test the install
- Visit
{APP_URL}/adminand confirm the platform dashboard renders without red banners. - From Settings → System, click each Test button (mail, LLM, Stripe). Each should report success.
- From your workspace, run the Quickstart: create an agent, add one knowledge source from your own site, watch it flip to indexed, publish the agent, paste the embed snippet on a test page.
- Open your test page in an incognito window. Ask the agent a question that should be answered from the page you indexed. You should see streaming tokens and a citation appear within ~1 second.
- Check
/admin/queue-health— crawl + index queues should be draining, no failed jobs.
Troubleshooting
| Symptom | Fix |
|---|---|
Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest |
You skipped npm run build or pulled changes to resources/js/ without rebuilding. Run npm run build + npm run build:widget. |
| Agent answers "I don't have enough information" even with sources indexed | Likely a confidence threshold mismatch. Cloudflare's bge-base-en-v1.5 peaks at 0.55–0.65, OpenAI peaks higher. Open the agent's Advanced tab and lower confidence_threshold to 0.5 for Cloudflare-backed installs. New agents get this default automatically. |
| Widget script loads but never opens on customer sites | Check Agent → Settings → Allowed origins. Each entry is strict-matched against the page's Origin header; an empty list means deny-everywhere. See Allowed origins. |
Queue not draining; /admin/queue-health shows growing depth |
Horizon process isn't running or isn't subscribed to the right connection. sudo systemctl status pitchbar-horizon → check it's active (running); QUEUE_CONNECTION in .env should be redis. |
| Live inbox doesn't update in real time | Reverb process isn't running or the WS reverse-proxy isn't wired. Check REVERB_HOST + REVERB_PORT match what your reverse proxy forwards; in the browser console, you should see a successful wss://… upgrade. |
"Crawl failed: …" on every source |
Probably no LLM provider configured. Settings → System → Cloudflare + click Test LLM. The error column on the source is sanitized for customers; super_admins see the raw upstream message on the source detail page. |
Failed to load PostCSS config during npm run build |
You probably ran npm install --production. The build needs the dev dependencies — re-run npm ci with the default flags. |
What's next?
.env key — required, optional, and platform-overridable.