OwnWire

End-to-end encrypted WebSocket messaging

Docs

Setup, widget, SDK, internal client WebSocket, reverse proxy, styling, and CDN assets.

Problem

HTTPS is common, but TLS private keys are often held by reverse proxies (CDNs, load balancers, etc.). Those systems terminate TLS, so traffic is visible to them.

OwnWire adds an additional public-key session encryption layer on top of WebSockets, so only your frontend and your internal WS client can decrypt messages. The reverse proxy in the middle only sees ciphertext.

Two WebSockets
  • Public WebSocket -- browsers / SDK / embeddable widget
  • Internal WebSocket -- your backend worker / bot / LLM client

Local test / demo

Show options
CLI help
ownwire --help
Start OwnWire
Local run
./ownwire --log-level=info
Run the example internal client (Ruby)
Connects to internal WS and replies
  1. Put your OpenAI API key into a file: .openai_key
  2. Install Ruby + Bundler: bundle install
  3. Start the WS client:
    bundle exec ruby internal_ws_client.rb --openai-api-key-fn=.openai_key
Example client archive: internal_ws_client.tar.gz
Open the demo page
Widget embed example
http://localhost:8080/embed_example.html

Setting up in production

Install binary
Put it on PATH
cp ownwire /usr/local/bin/
chmod +x /usr/local/bin/ownwire
systemd service
Run as a daemon
[Unit]
Description=OwnWire secure messaging service
After=network.target

[Service]
ExecStart=/usr/local/bin/ownwire --log-level=warn
Restart=always
User=www-data
Group=www-data
WorkingDirectory=/var/lib/ownwire
Environment=OWNWIRE_PORT=8080

[Install]
WantedBy=multi-user.target
Enable service
Reload + enable + start
systemctl daemon-reload
systemctl enable ownwire
systemctl start ownwire

Reverse proxy (nginx)

Why you may want this

Use a reverse proxy when you want TLS termination, a stable public origin, and a single entry point for both your site and OwnWire. nginx can handle certs and routing, while OwnWire still keeps message contents end-to-end encrypted above TLS.

  • TLS + public origin -- browsers connect to HTTPS/WSS.
  • One front door -- route /ws and assets cleanly.
  • Same model -- proxy forwards ciphertext.
nginx config
WSS upgrade + proxying
server {
  listen 443 ssl;
  server_name ownwire.yourwebsite.com;

  ssl_certificate     /path/to/fullchain.pem;
  ssl_certificate_key /path/to/privkey.pem;

  proxy_read_timeout 75s;
  proxy_send_timeout 75s;

  location = /ws {
      proxy_pass http://127.0.0.1:8080;
      proxy_http_version 1.1;

      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";

      proxy_set_header Host   $http_host;
      proxy_set_header Origin $http_origin;

      proxy_set_header X-Real-IP         $remote_addr;
      proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;

      proxy_buffering off;
  }

  location / {
      proxy_pass http://127.0.0.1:8080;

      proxy_set_header Host   $http_host;
      proxy_set_header Origin $http_origin;

      proxy_set_header X-Real-IP         $remote_addr;
      proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
  }
}
Point ws_url to wss://ownwire.yourwebsite.com/ws. Do not manually set Sec-WebSocket-* headers in nginx.
Enable nginx site
Symlink + test + reload
ln -s /etc/nginx/sites-available/ownwire /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginx

Secure origins

The widget and SDK only work on https://* or on http://localhost. Insecure origins are not supported.

Using the JavaScript SDK

Include the SDK
One script tag
<script src="https://ownwire.yourwebsite.com/js/ownwire.js"></script>
Connect + send
Minimal example
let ownwire = new Ownwire("wss://ownwire.yourwebsite.com/ws");
ownwire.onMessage = (msg) => console.log("received:", msg);
ownwire.connect();
ownwire.send("What's the weather in Brazil?");

Adding the chat widget

Embed
Script + init
<script src="https://ownwire.yourwebsite.com/js/ownwire_widget.js"></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
    ownwireWidget({
        ws_url: "wss://ownwire.yourwebsite.com/ws",
        metadata: "username:user1",
        title: "Chat with our agent",
        widget_origin: "https://ownwire.yourwebsite.com",
        widget_path: "/",
        minimized_mode: "bar", // "bar" (default) or "icon"
        icon_url: "https://ownwire.yourwebsite.com/assets/chat-icon.png" // optional
    });
});
</script>
Behavior

The widget appears in the corner and communicates securely through OwnWire. You can choose a minimized bar (default) or an icon launcher. If icon_url is omitted, OwnWire uses its default icon.

Working with the internal websocket

Concept
Two sockets, routed by session_id
browser  →  OwnWire public WS  →  OwnWire internal WS  →  your client
browser  ←  OwnWire public WS  ←  OwnWire internal WS  ←  your client
Example URLs
Local vs production
ws://127.0.0.1:8081/ws
wss://ownwire.yourwebsite.com/ws
Message format
Text frames containing JSON
{
  "content":    "... message text ...",
  "session_id": "UUID string",
  "metadata":   "optional metadata string"
}
Notes
  • Your client connects once and keeps the connection open.
  • All sessions arrive on the same internal WS. Always route using session_id.
  • OwnWire may send pings; most WS libraries handle pongs automatically.
Pseudocode
Language agnostic
function handle_incoming(raw_json):
  data       = parse_json(raw_json)
  content    = data["content"]
  session_id = data["session_id"]
  metadata   = data.get("metadata")

  if content is empty:
      log("empty message, ignoring")
      return

  reply_text = process_message(content, metadata)

  reply = {
    "content":    reply_text,
    "session_id": session_id,
    "metadata":   metadata
  }

  ws.send(encode_json(reply))


main():
  ws_url = "ws://127.0.0.1:8081/ws"   # or wss://... in production

  ws = connect_websocket(ws_url)

  ws.on_open = function():
      log("internal websocket connected")

  ws.on_message = function(event):
      try:
          handle_incoming(event.data)
      except error as e:
          log("error processing message: " + e)

  ws.on_close = function(code, reason):
      log("internal websocket closed: " + code + " / " + reason)

  ws.on_error = function(err):
      log("websocket error: " + err)

Styling the chat widget

Two ways
  • Recommended: append overrides using --custom-css.
  • Advanced: replace /css/style.css at the nginx layer.
Simple
Append overrides
./ownwire --custom-css=/path/to/custom.css
Advanced
Full replacement via nginx
location = /css/style.css {
    alias /var/www/ownwire-custom/style.css;
    add_header Cache-Control "no-cache";
}
Example overrides
Green theme
/* custom.css: green theme overrides */
.chat { background-color: #2f3b30 !important; }
.chat header { background: #3a4a3a !important; }
.chat .message:not(.reply) { background: #57d38c !important; color: #0d2214 !important; }
.chat .message.reply { background: #465646 !important; }
.chat .send-message { background: linear-gradient(#4fdc87,#3cbf72) !important; }
.chat .send-message:hover { background: linear-gradient(#5aed95,#43d67f) !important; }
.chat .typing-dot { background: #c4f5d6 !important; }
Isolation

The widget is isolated in an iframe, so your widget CSS will not affect the rest of your website.

Serving assets from a CDN

Extract assets
Dump embedded JS/CSS
./ownwire --extract-assets=./public
After extracting, OwnWire exits. Upload ownwire_assets/ to your CDN.
Example CDN layout
URLs
https://mycdn.com/mycompany/ownwire/css/style.css
https://mycdn.com/mycompany/ownwire/js/application.js
https://mycdn.com/mycompany/ownwire/js/ownwire.js
https://mycdn.com/mycompany/ownwire/js/ownwire_widget.js
Start OwnWire with assets root
Rewrites HTML to CDN
./ownwire --assets-url="https://mycdn.com/mycompany/ownwire/"
Before
Local assets
<link href="/css/style.css" rel="stylesheet">
<script src="/js/application.js" type="module"></script>
After
CDN assets
<link href="https://mycdn.com/mycompany/ownwire/css/style.css" rel="stylesheet">
<script src="https://mycdn.com/mycompany/ownwire/js/application.js" type="module"></script>
Widget embed with CDN script
Load widget JS from CDN
<script src="https://mycdn.com/mycompany/ownwire/js/ownwire_widget.js"></script>
<script>
  ownwireWidget({
    ws_url: "https://ownwire.yourwebsite.com/ws",
    metadata: "username:user1",
    title: "Chat with our agent",
    widget_origin: "",
    widget_path: "/"
  });
</script>
After this, CSS/JS comes from the CDN, while OwnWire handles WebSocket traffic only.