Skip to content

Server libraries

You can install Vroxy by pasting the snippet into your layout by hand — see Installing the widget. The server libraries do the same thing for you, and add the part that’s tedious to hand-roll: telling Vroxy who the signed-in user is, with a server-side signature so access-gated bot tools actually unlock.

There are four:

Package For Version
vroxy (Ruby gem) Rails 7.1+ 0.6.1
@vroxy/node Express / Connect, plus a Next.js component 0.1.0
vroxy (Python) Django and Flask 0.1.0
vroxy-support-chat WordPress 6.8+, PHP 7.4+ 0.1.0

None of these are published yet. Vroxy is pre-launch, so the packages are private: the Node package is flagged private and is not on npm, the Python package carries a Private :: Do Not Upload classifier that makes PyPI reject it, the WordPress plugin is not in the wp.org directory, and the gem is not on RubyGems.

Ask us and we’ll get you the package for your stack — installed from a repository or an archive, exactly the same code that will ship at launch. Public registry installs land when we launch.

Set your workspace’s public key (from the workspace Embed page) and every text/html response your app returns gets two script tags appended before </body>:

<script src="https://vroxy.ai/widget.js?tenant=YOUR_PUBLIC_KEY" async></script>
<script>
(function(){
window.vroxy = window.vroxy || function(){ (window.vroxy.q = window.vroxy.q || []).push(arguments); };
window.vroxy("identify", {"email":"ada@example.com","name":"Ada Lovelace","external_id":"42","meta":{"role":"admin"}});
})();
</script>

The first tag is the widget loader. The second identifies the current user through the same public vroxy(...) API a hand-rolled install would use — which is why moving between a hand-rolled snippet and an SDK (or between SDKs) is a no-op on the widget side. The snippet output and the identity signatures are byte-identical across the libraries, pinned by shared test vectors generated from the Ruby implementation.

The public key is not a secret; it’s in the page source of every site running the widget. The identity secret is a secret and never leaves your server.

The server SDKs only rewrite a response when all of the following hold, and pass the original bytes through untouched otherwise:

  • The library is enabled and an API key is set.
  • Auto-injection is on.
  • The request path isn’t on your excluded-paths list.
  • The Content-Type is text/html.
  • The status is 2xx (not 204, not a redirect, not an error page).
  • The body contains a literal </body>.
  • You didn’t already render the snippet yourself on this request.

Python adds one more: streaming and direct-passthrough responses (Django streaming responses, Flask static files) are never touched.

Add the gem to your Gemfile — until launch that’s a path: or git: source pointing at the copy we send you rather than a bare gem "vroxy" — then:

Terminal window
bundle install
bin/rails generate vroxy:install
Terminal window
VROXY_API_KEY=your_public_key

That’s it — every HTML response now carries the loader and an identify() call.

Identity resolution. By default the gem reads controller.current_user and pulls external_id from user.id, email from user.email, name from full_namenamefirst_name + last_name, and role from user.roleuser.roles.first. Missing accessors are skipped, so a bare User(id, email) still produces a useful payload. Everything other than email, name and external_id travels under meta.

Custom resolver:

Vroxy.configure do |config|
config.identify = ->(controller) {
user = controller.current_user
next nil unless user
{
external_id: user.id.to_s,
email: user.email,
name: user.display_name,
role: user.admin? ? "admin" : "basic",
meta: { plan: user.subscription&.plan_name }
}
}
end

Return nil to stay anonymous for a request — useful during impersonation, where you don’t want the impersonator’s identity leaking to the widget.

Configuration:

Key Default Purpose
api_key ENV["VROXY_API_KEY"] Your workspace public key.
endpoint ENV["VROXY_ENDPOINT"] or https://vroxy.ai Base URL of the Vroxy deployment.
enabled true iff api_key present Master kill switch.
auto_inject true Append the snippet before </body>.
identify auto-detect ->(controller) { {...} }; nil stays anonymous.
exclude_paths [] Strings or Regexps matched against request.path.
csp_nonce nil ->(controller) { ... } for strict CSP.
identity_secret ENV["VROXY_IDENTITY_SECRET"] Signs the identify payload’s access level.
admin_roles %w[admin owner] Roles that sign as level: "admin".
report_errors nil (auto: production + api_key) Ship exceptions to your Errors page.
error_ignore RecordNotFound, RoutingError, … Exception class names never reported.
secret_token ENV["VROXY_SECRET_TOKEN"] A workspace API token with tenant:write, for server-to-server calls. Not the public key.
glossary_admin_url nil ->(term) { ... } building the admin deep-link template for a term.
glossary_extra [] Extra glossary entries to sync alongside the ones mined from i18n.

The last three exist for one task: bin/rails vroxy:sync_glossary mines your activerecord.models translations for nouns whose display label differs from the model name and pushes them to your workspace’s glossary. See Bot settings for what the glossary is used for.

Manual placement: set config.auto_inject = false and put <%= vroxy_snippet %> before </body> in your layout. Even with auto-injection on, calling the helper suppresses the middleware for that request, so you can never get two copies.

Rails only: the gem also loads the in-page feedback inspector for users whose resolved role is in admin_roles, along with the trail of partials Rails rendered for that request. That’s what powers the feedback flow in Feedback to fix. The Node, Python and WordPress packages don’t do this.

import { configure, vroxyMiddleware } from "@vroxy/node";
configure({ apiKey: process.env.VROXY_API_KEY });
app.use(vroxyMiddleware());

VROXY_API_KEY, VROXY_ENDPOINT and VROXY_IDENTITY_SECRET are read from the environment automatically — configure() is only needed for overrides and callbacks.

Identity resolution. Reads req.user (Passport and friends): external_id from user.id, email from user.email, name from full_namenamefirst_name + last_name, role from user.roleuser.roles[0].

configure({
identify: ({ req }) => {
const user = req.user;
if (!user) return null;
return {
external_id: String(user.id),
email: user.email,
name: user.displayName,
role: user.isAdmin ? "admin" : "basic",
};
},
});

Configuration: apiKey, endpoint, enabled, autoInject, identify, excludePaths, cspNonce, adminRoles (default ["admin", "owner"]), identitySecret, reportErrors, errorIgnore — same meanings as the Ruby table above.

Manual placement: vroxySnippet(identity) returns the snippet HTML for an explicit identity (null for anonymous, or no arguments to run the configured resolver). Using it during a request suppresses the middleware for that response.

Next.js. Body-rewriting middleware doesn’t fit Next, so use the component:

import { VroxyScript } from "@vroxy/node/next";
<body>
{children}
<VroxyScript identify={await identityForCurrentUser()} />
</body>

Render it server-side only — in a server component or _document. The identity secret must never be bundled for the client, so don’t mount it inside a "use client" tree.

No runtime dependencies.

Django:

INSTALLED_APPS = [
# ...
"vroxy.django",
]
MIDDLEWARE = [
# ... after AuthenticationMiddleware, so request.user is resolved
"vroxy.django.middleware.VroxyMiddleware",
]

Django identity resolution reads request.user: external_id from str(user.pk), email from user.email, name from get_full_name(), and role of "admin" when the user is is_staff or is_superuser. Anonymous requests get the loader tag only.

Flask:

from flask import Flask
from vroxy.flask import Vroxy
app = Flask(__name__)
Vroxy(app)

Flask has no universal current-user convention, so identify stays anonymous until you set a resolver.

FastAPI is not supported. FastAPI apps are API-first and rarely serve HTML; embed the snippet by hand on any HTML you do serve.

Custom resolver:

import vroxy
def identify(request):
user = getattr(request, "user", None)
if user is None or not user.is_authenticated:
return None
return {
"external_id": str(user.pk),
"email": user.email,
"name": user.get_full_name(),
"role": "admin" if user.is_staff else "basic",
}
vroxy.configure(identify=identify)

Configuration: api_key, endpoint, enabled, auto_inject, identify, exclude_paths (exact strings or compiled regexes), csp_nonce, identity_secret, admin_roles (default ["admin", "owner"]), report_errors, error_ignore.

Manual placement: {% vroxy_snippet %} in a Django template (requires the request context processor) or {{ vroxy_snippet() }} in Jinja.

The plugin injects the standard snippet in wp_footer and identifies logged-in WordPress users. Install it, then go to Settings → Vroxy and paste your workspace public key. Optionally paste the identity secret from the same Embed page to unlock access-gated bot tools.

The identify payload carries the WP user id as external_id, the user’s email, their display name, their WordPress role under meta.role, and — with the secret set — a signed access level of admin for users with the manage_options capability and user for everyone else who is logged in.

Cache safety is the plugin’s central design constraint, and it works differently from the other SDKs for that reason. A signed identify payload is per-user; a full-page cache serving one into another visitor’s page would leak identity. So the plugin never renders identity into page HTML:

  1. wp_footer emits only bytes that are identical for every visitor — the loader tag and a static bootstrap that installs the window.vroxy command queue.
  2. Only if the browser holds a WordPress login cookie does the bootstrap fetch admin-ajax.php?action=vroxy_identify with same-origin credentials.
  3. That endpoint returns the current user’s signed payload with Cache-Control: no-store, private and Vary: Cookie (and {} for anonymous callers), and the bootstrap forwards it through the queue.

One requirement: your cache must not cache admin-ajax.php. No mainstream WordPress cache plugin does by default, but if you run a custom Varnish, nginx or CDN page cache, exclude /wp-admin/admin-ajax.php explicitly and make sure the CDN honors Cache-Control: no-store on it. The settings page detects common cache plugins and reminds you.

Excluded paths: one per line, exact match, or end a line with * for a prefix match.

Every SDK signs the identify payload when you give it the workspace identity secret (Embed page → Identity verification). With the secret set, the payload gains a leveladmin when the resolved role is in admin_roles, otherwise user — plus an HMAC-SHA256 signature over external_id|email|level.

Only a verified level unlocks the user and admin tiers of custom bot tools. Unsigned identify calls still personalize the conversation, they just don’t grant anything. The secret stays server-side; only the derived signature for that one identity reaches the page.

Full details, including the exact string that gets signed for hand-rolled integrations, are in Identifying visitors.

Every SDK can ship exceptions from your app to your workspace’s Errors page — no separate error service needed. It’s on automatically in production when an API key is set, and can be forced either way.

Delivery is fire-and-forget on a background thread with 3-second timeouts and a 60-per-minute throttle, so reporting can never slow down or break your app. Reports go to <endpoint>/ingest/errors with your public key in the X-Vroxy-Tenant header.

  • Ruby — subscribes to Rails’ error reporter, so every unhandled request or job exception is delivered. Report a handled one with Vroxy.report_error(e, context: { order_id: order.id }).
  • NodereportError(e, { context: {...} }) for handled errors; installProcessHandlers() opts in to uncaughtException (via the non-intrusive monitor hook) and unhandledRejection, both reported as unhandled, with default crash semantics preserved.
  • Pythonvroxy.report_error(error, context={...}); vroxy.install_excepthook() and vroxy.VroxyLoggingHandler() are opt-in for unhandled and logged exceptions.
  • WordPress — optional; when enabled, fatal PHP errors are reported.

Each SDK has an ignore list of exception classes that are never reported (subclasses included) and that you can extend.

See Error reporting for what the endpoint accepts, how errors are grouped, and the retention limits.