Run your workspace
BYOK (workspace AI keys)
BYOK โ Bring Your Own Keys โ lets each workspace pay their own Cloudflare / OpenAI / OpenRouter / Qdrant bill instead of running on the platform operator's shared credentials. Strategic for operators reselling Pitchbar as a SaaS to many end customers: every customer's chat, embeddings, and vector storage land on their upstream account, not yours.
How the policy matrix works
BYOK has two switches that compose:
| Global flag | Per-user override | BYOK unlocked? |
|---|---|---|
| OFF | not set | No โ platform keys are used. |
| OFF | force-on | Yes โ this user gets BYOK access even though the platform default is off. |
| OFF | force-deny | No. |
| ON | not set | Yes โ every workspace must supply own keys. |
| ON | force-on | Yes (same as not-set when global is on). |
| ON | force-deny | No โ explicit deny always wins. |
The matrix is enforced by
App\Support\ByokResolver::isUnlockedFor($user, $workspace).
The LLM and Vector container bindings call into the resolver on
every resolution, so flipping a switch takes effect on the next
request without an Octane reload.
Enabling BYOK
Global (every workspace)
- Sign in as super_admin.
- Open
/settings/system. - Toggle "Enable BYOK globally" ON. Save.
From this point every workspace must paste their own keys before their widget can serve visitors. Workspaces with no keys hit a friendly "this workspace is not configured" bubble in chat instead of a stack trace.
Per-user override
- Sign in as super_admin.
- Open
/admin/users. - Find the user. The BYOK column has a tri-state dropdown:
Inherit global(default) โ follow the global flag.Force enableโ grant BYOK access to this user only.Force disableโ block BYOK for this user. Always wins.
Where customers paste their keys
When BYOK is unlocked for a workspace, an
AI keys entry appears in the customer's Settings
sidebar (between API tokens and the platform pages).
Click it to open /settings/byok-keys:
- Cloudflare โ account_id, API token, Vectorize index, optional chat / embed model overrides.
- OpenAI โ API key, optional chat / embed model overrides.
- OpenRouter โ API key, optional chat model override.
- Qdrant โ base URL, API key, collection name.
Each section has a separate Save button so a customer can configure providers one at a time, and a per-provider Clear button to wipe just that provider's credentials. The form never echoes secrets back into the UI โ customers re-paste on every rotation. Public fields (account_id, model names, index names, Qdrant URL) are pre-filled.
Super-admins do not see the "AI keys" entry. They
manage platform-wide credentials at
/settings/system โ
AI providers. Workspace-level BYOK is customer-only.
What's stored, where, and how it's protected
| Field | Database column | Encryption |
|---|---|---|
| All BYOK credentials | workspaces.byok_keys (JSON map) | encrypted:array via APP_KEY |
| Global flag | app_settings.byok_enabled_globally (boolean) | Plain (it's a public flag) |
| Per-user override | users.byok_enabled (nullable boolean) | Plain |
The encrypted column stores a Laravel Crypt envelope. The raw DB
column never contains plaintext credentials โ anyone reading the
column directly only gets a base64 blob. Decryption requires
APP_KEY; rotating APP_KEY renders the
column unreadable until customers re-paste.
Tenant isolation guarantees
BYOK isolates each workspace from every other. The boundary is enforced in five layers:
-
DB column encryption. Reading
workspaces.byok_keysoutside the application returns a Crypt envelope, not the plaintext token. -
Tenant resolution. Every call to
ByokResolver::keysFor(...)takes a Workspace model that comes fromCurrentWorkspace::get(). That helper resolves the workspace from the authenticated admin'sdefault_workspace_idOR the widget JWT'sagent.workspace_idโ never from request body input. There's no global lookup that could leak the wrong tenant. -
Container binding lifetime.
OpenAiClientandQdrantClientare boundscoped()inAppServiceProvider, notsingleton(). Each HTTP request rebuilds the client. Workspace A's keys never persist into worker memory for workspace B's next request, even under Octane. -
Mutation surface.
ByokKeysController::clearand::updateresolve the workspace fromCurrentWorkspace, never from a?workspace_id=param. Workspace A can't wipe or read workspace B's keys via any documented API call. -
Negative regression tests.
tests/Feature/Byok/ByokTenantIsolationTest.phppins each guarantee โ encryption at rest, A-doesn't-see-B, mutating A doesn't touch B, clearing A doesn't touch B. Removing any of the protections fails the test suite.
Resolution flow at runtime
When a visitor sends a message, the LLM container binding runs:
- Resolve
CurrentWorkspacefrom the widget JWT. - Call
ByokResolver::isUnlockedFor($user, $workspace)(visitor flow has no user, so the resolver checks only the global flag plus the workspace's keys). - If unlocked AND
workspaces.byok_keyshas the matching provider's keys, build the client with those credentials. - If unlocked but keys are missing AND the global flag is ON, throw
MissingByokKeyExceptionโ the visitor sees a friendly "workspace not configured" line, not a stack trace. - Otherwise (global OFF + no per-user override + no workspace keys) fall through to platform-wide credentials.
The same chain drives Vectorize / Qdrant binding selection, the crawler's Cloudflare Browser Rendering credentials, and the reranker.
FAQ
Do BYOK customers pay Pitchbar nothing?
They still pay you for the application itself (subscription, lifetime deal). BYOK only shifts the variable AI / vector / browser-rendering cost. Your Pitchbar billing is unrelated to the upstream LLM bill.
What happens to existing platform-keyed workspaces when I flip global ON?
Their next request misses keys and they see the "workspace not configured" bubble. The fix is one of:
- Paste keys via
/settings/byok-keysas the workspace owner. - Force-deny that specific user (super_admin โ
/admin/users) so they fall back to platform credentials. - Flip global OFF and only force-enable for the customers you want on BYOK.
Can I rotate APP_KEY?
Yes, but the existing encrypted byok_keys column entries
are sealed with the old key โ rotating renders them unreadable until
customers re-paste. Run a one-shot artisan command (or migrate
in-place) to decrypt with the old key and re-encrypt with the new.
Same caveat applies to every other encrypted cast in the
app.
Does BYOK affect the SSRF guards on the crawler?
No. The SSRF guard (private-IP blocklist + DNS rebind protection) runs regardless of which Cloudflare account is fetching the URL. BYOK only swaps the credentials; the safety net stays in place.
Where do BYOK customers see their usage?
They check it on their own provider's dashboard (Cloudflare, OpenAI, Qdrant Cloud) โ Pitchbar doesn't proxy usage metering back. Workspace-level analytics in your Pitchbar dashboard still show conversation + message counts, which is enough for them to correlate.