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 |
Availability
Section titled “Availability”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.
What they all do
Section titled “What they all do”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.
Middleware safety rules
Section titled “Middleware safety rules”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-Typeistext/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.
Ruby (Rails)
Section titled “Ruby (Rails)”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:
bundle installbin/rails generate vroxy:installVROXY_API_KEY=your_public_keyThat’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_name → name →
first_name + last_name, and role from user.role →
user.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 } } }endReturn 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_name → name → first_name + last_name, role from
user.role → user.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.
Python
Section titled “Python”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 Flaskfrom 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.
WordPress
Section titled “WordPress”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:
wp_footeremits only bytes that are identical for every visitor — the loader tag and a static bootstrap that installs thewindow.vroxycommand queue.- Only if the browser holds a WordPress login cookie does the
bootstrap fetch
admin-ajax.php?action=vroxy_identifywith same-origin credentials. - That endpoint returns the current user’s signed payload with
Cache-Control: no-store, privateandVary: 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.
Identity verification
Section titled “Identity verification”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 level — admin 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.
Error reporting
Section titled “Error reporting”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 }). - Node —
reportError(e, { context: {...} })for handled errors;installProcessHandlers()opts in touncaughtException(via the non-intrusive monitor hook) andunhandledRejection, both reported as unhandled, with default crash semantics preserved. - Python —
vroxy.report_error(error, context={...});vroxy.install_excepthook()andvroxy.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.