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.
| 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. |
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.
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;"
.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.
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.
php artisan migrate --force
php artisan db:seed --class=PlanSeeder --force
PlanSeeder creates four plan rows the billing system
reads — free, standard, pro,
and custom (enterprise / contact-sales placeholder).
It is idempotent — re-running it won't duplicate plans. Edit
pricing, conversation caps, and Stripe price IDs in the admin at
/admin/plans after seeding.
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.
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.
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.
# /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.
# /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.
# /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.
sudo systemctl daemon-reload
sudo systemctl enable --now pitchbar-app pitchbar-horizon pitchbar-reverb
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.
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
Pitchbar can deploy a Cloudflare Worker that hits your install's
/api/v1/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.
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.
Open Settings → System as the super_admin. Paste:
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.
{APP_URL}/admin and confirm the platform dashboard renders without red banners./admin/queue-health — crawl + index queues should be draining, no failed jobs.| 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. |
.env key — required, optional, and platform-overridable.