D Diagent docs

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 flagPer-user overrideBYOK unlocked?
OFFnot setNo โ€” platform keys are used.
OFFforce-onYes โ€” this user gets BYOK access even though the platform default is off.
OFFforce-denyNo.
ONnot setYes โ€” every workspace must supply own keys.
ONforce-onYes (same as not-set when global is on).
ONforce-denyNo โ€” 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)

  1. Sign in as super_admin.
  2. Open /settings/system.
  3. 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

  1. Sign in as super_admin.
  2. Open /admin/users.
  3. 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

FieldDatabase columnEncryption
All BYOK credentialsworkspaces.byok_keys (JSON map)encrypted:array via APP_KEY
Global flagapp_settings.byok_enabled_globally (boolean)Plain (it's a public flag)
Per-user overrideusers.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:

  1. DB column encryption. Reading workspaces.byok_keys outside the application returns a Crypt envelope, not the plaintext token.
  2. Tenant resolution. Every call to ByokResolver::keysFor(...) takes a Workspace model that comes from CurrentWorkspace::get(). That helper resolves the workspace from the authenticated admin's default_workspace_id OR the widget JWT's agent.workspace_id โ€” never from request body input. There's no global lookup that could leak the wrong tenant.
  3. Container binding lifetime. OpenAiClient and QdrantClient are bound scoped() in AppServiceProvider, not singleton(). Each HTTP request rebuilds the client. Workspace A's keys never persist into worker memory for workspace B's next request, even under Octane.
  4. Mutation surface. ByokKeysController::clear and ::update resolve the workspace from CurrentWorkspace, never from a ?workspace_id= param. Workspace A can't wipe or read workspace B's keys via any documented API call.
  5. Negative regression tests. tests/Feature/Byok/ByokTenantIsolationTest.php pins 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:

  1. Resolve CurrentWorkspace from the widget JWT.
  2. Call ByokResolver::isUnlockedFor($user, $workspace) (visitor flow has no user, so the resolver checks only the global flag plus the workspace's keys).
  3. If unlocked AND workspaces.byok_keys has the matching provider's keys, build the client with those credentials.
  4. 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.
  5. 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-keys as 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.