← Back to list

I Built an Open-Source AI Chatbot You Can Add to Any Website with One Line of Code

An open-source, multi-tenant RAG chatbot for websites — embeddable with one script tag, powered by Rust, AWS Lambda, and real-time…

Jiyao Weng · 2026-05-01 03:40 · 2 claps · 6.5 min read
#ai-chatbot #rags #chatbots #rust #vector-database
Open on Medium ↗
Wiki topics: RAG · RAG & Retrieval ☁️ · DevOps & Cloud 🔓 · Open Source

I Built an Open-Source AI Chatbot You Can Add to Any Website with One Line of Code

An open-source, multi-tenant RAG chatbot for websites — embeddable with one script tag, powered by Rust, AWS Lambda, and real-time streaming

Open by Design — Like a Papaya, Everything Inside Is Visible

Open by Design — Like a Papaya, Everything Inside Is Visible

Most website chatbots are closed-source, expensive, and lock you into a platform. I wanted something different: a chatbot that answers questions from your own documents, streams responses in real time, and can be embedded with a single script tag.

So I built one.

The frontend is open source, the backend is written in Rust, and the whole thing runs on serverless AWS infrastructure.

This is Papaya Assist — a multi-tenant, RAG-powered chatbot for websites.

In this article, I’ll walk through the full architecture, explain why I chose Rust for the backend, and show two live examples: one embedded on a plain HTML site, and one running through a WordPress plugin.

What It Does

You upload documents (PDF, DOCX, or plain text). The system chunks them, generates embeddings, and stores the vectors.

When a visitor asks a question, the chatbot retrieves the most relevant chunks, feeds them to an LLM as context, and streams the answer back token by token.

From the website owner’s perspective, setup looks like this:

  1. Sign up and get a tenant ID
  2. Upload your documents
  3. Paste one script tag into your HTML

That’s it.

The chat bubble appears in the bottom-right corner of the page.

The Embed: One Script Tag

The widget is a self-contained JavaScript file.

No framework. No dependencies. No build step.

You add it to any page like this:

<script
  src="https://your-host/chatbot-widget.min.js"
  data-stream-url="https://your-stream-endpoint/"
  data-tenant-id="your-tenant-id"
  data-title="Chat with us"
  data-primary-color="#4f46e5">
</script>

The script creates a <div>, attaches a closed Shadow DOM, and renders the entire chat UI inside it.

Shadow DOM means the widget’s CSS never leaks into the host page, and the host page’s CSS never breaks the widget. It works on any site regardless of what framework or stylesheet is already loaded.

Configuration happens entirely through data-* attributes — title, color, endpoints, tenant ID.

No JavaScript API to learn. No initialization call.

On mobile, the chat window automatically goes full-screen.

On desktop, it opens as a 380x520px panel anchored to the bubble.

The widget keeps the last 10 messages in memory and sends them with each request so the model has conversation context, but nothing is persisted server-side — when the visitor refreshes, the conversation resets.

Messages stream in real time via Server-Sent Events.

The widget reads each data: {"token": "..."} line as it arrives and appends the token to the message bubble, so the visitor sees the response being typed out word by word.

If streaming isn’t available, it falls back gracefully to a REST endpoint that returns the full response at once.

You can see this in action on weng.ca — click the chat bubble in the corner.

Why Rust for the Backend

The backend runs on AWS Lambda.

Every millisecond of cold start and every megabyte of memory costs real money in a serverless environment.

Rust compiles to native code, has no garbage collector, and produces small binaries.

A Rust Lambda function starts in under 50ms, uses minimal memory, and handles concurrent requests efficiently.

But performance wasn’t the only reason.

Rust’s type system catches entire categories of bugs at compile time.

When you’re building a multi-tenant system where one tenant’s data must never leak into another’s, having the compiler enforce invariants is genuinely valuable.

Serde handles JSON serialization, the AWS SDK for Rust talks to DynamoDB and S3, and Axum provides a clean async web framework.

The ecosystem is mature enough for production backend work.

The backend is organized as a Cargo workspace with four crates:

  • shared — the core library. DynamoDB operations, S3 presigned URLs, OpenAI API calls, Pinecone vector queries, and the RAG pipeline itself
  • chat-rest — a Lambda function that handles non-streaming chat
  • chat-stream — an Axum server that streams responses as SSE
  • admin-rust — tenant management, uploads, auth, ingestion, and payment webhooks

All three Lambda functions use Docker-based multi-stage builds:

Compile on rust:1-slim, then copy just the binary to a minimal debian:trixie-slim image with the Lambda Web Adapter.

This keeps the final image small and cold starts fast.

The whole stack — Lambdas, DynamoDB tables, S3 buckets, and API Gateway — is defined in a single SAM template and deploys with:

sam build && sam deploy

How RAG Works in This System

RAG (Retrieval-Augmented Generation) is what makes the chatbot actually useful.

Instead of relying on what the LLM already knows, you retrieve relevant content from your own documents and include it in the prompt.

Here’s how it works inside Papaya Assist.

Ingestion (when documents are uploaded)

  1. Files are uploaded to S3 via presigned URLs — the browser uploads directly to storage
  2. The admin API downloads the file, extracts text, and splits it into chunks of roughly 500 tokens with 50-token overlap
  3. Chunks are sent to OpenAI’s embedding API in batches of 100
  4. Each chunk becomes a 1536-dimensional vector stored in Pinecone with tenant metadata

Query (when a visitor asks a question)

  1. If conversation history exists, the question is first contextualized using a lightweight LLM call
  2. The contextualized question is embedded into a vector
  3. Pinecone returns the top 5 most semantically similar chunks from that tenant’s namespace
  4. Those chunks are injected into the system prompt as context
  5. GPT-4o-mini generates the answer, either streamed or returned fully

The key design decision is namespacing.

Each tenant’s vectors live inside their own Pinecone namespace.

That means there is zero chance of cross-tenant data leakage.

A question asked on Site A will never surface documents uploaded by Site B.

Multi-Tenant Architecture

Everything in the system is keyed on tenant_id.

The DynamoDB Tenant table stores:

  • display name
  • system prompt
  • pricing tier
  • message counts
  • usage tracking

A separate Users table handles authentication.

Pricing tiers control quotas:

  • Free → 50 messages/month + 2 documents
  • Standard → 500 messages + 20 documents
  • Pro → 5,000 messages + unlimited documents

Usage resets monthly based on each tenant’s billing cycle day.

Payments are handled through Lemon Squeezy webhooks.

When a subscription event comes in, the admin API verifies the webhook signature and updates the tenant’s tier in DynamoDB.

The admin API itself is protected by Firebase Authentication.

The frontend sends a Google OAuth ID token with each request.

The Rust backend verifies it using Firebase public keys and validates the JWT.

For programmatic access (like the WordPress plugin), there is also an API key path.

The WordPress Plugin

Not everyone wants to edit HTML.

For WordPress users, there’s a plugin that wraps the entire experience into a native admin interface.

Install the plugin, click Sign Up / Log In on the settings page, and a popup handles account creation.

The plugin auto-generates a tenant ID from your domain.

For example:

example.com → example-com

A documents page lets you upload files, trigger ingestion, and manage your knowledge base — all without leaving WordPress.

Under the hood, the plugin:

  • stores credentials in wp_options
  • communicates using wp_remote_request
  • injects the widget JavaScript in wp_footer
  • protects AJAX calls with WordPress nonces

The beauty of this approach is that the WordPress site owner never touches code.

They never see embed snippets, API keys, or endpoint URLs.

The plugin handles everything internally.

You can see the WordPress integration live demo at Weng Photography. You can also install the Papaya Assist WordPress plugin directly from WordPress.org.

The chat bubble there is powered entirely by the plugin.

What’s Open Source

The frontend widget and WordPress plugin are open source on GitHub.

The widget JavaScript, plugin PHP, and demo page are all MIT-licensed.

You can fork the widget, restyle it, add features, or point it at your own backend.

The backend code is not included in the open-source repository, but the architecture is fully documented.

If you want to self-host, the README covers:

  • deploying the Rust Lambdas with AWS SAM
  • setting up DynamoDB tables
  • configuring Pinecone
  • wiring everything together

The Stack at a Glance

Visitor --> Widget (JS) --> API Gateway (REST) -----> Lambda (Rust) --> OpenAI
                         --> Function URL (Stream) --> Lambda (Rust)       |
                                                           |           Pinecone
                                                       DynamoDB
| Layer            | Technology                                |
|------------------|-------------------------------------------|
| Chat widget      | Vanilla JS, Shadow DOM, SSE               |
| Chat API (REST)  | Rust, lambda_http                         |
| Chat (streaming) | Rust, Axum, Lambda Web Adapter            |
| Admin API        | Rust, Axum, Firebase Auth                 |
| Vector search    | Pinecone (1536-d, text-embedding-ada-002) |
| LLM              | GPT-4o-mini                               |
| Storage          | DynamoDB, S3                              |
| Infrastructure   | AWS SAM, Docker, Lambda                   |
| WordPress plugin | PHP                                       | 
| Payments         | Lemon Squeezy                             |

LayerTechnologyChat widgetVanilla JS, Shadow DOM, SSEChat API (REST)Rust, lambda_httpChat (streaming)Rust, Axum, Lambda Web AdapterAdmin APIRust, Axum, Firebase AuthVector searchPineconeLLMGPT-4o-miniStorageDynamoDB, S3InfrastructureAWS SAM, Docker, LambdaWordPress pluginPHPPaymentsLemon Squeezy

Try It

See it on a website

Visit **weng.ca** and click the chat bubble

See it on WordPress

Visit Weng Photography and click the chat bubble

Install from WordPress.org

Download and install the Papaya Assist plugin directly from WordPress.org

Read the code

GitHub: Papaya-Assist---AI-Chatbot-for-Websites

Add it to your site

Sign up to get your tenant ID, or open demo/index.html from the repo to test locally

Building this project taught me that the barrier between “demo” and “production” in serverless is surprisingly thin.

Rust’s performance characteristics mean you don’t need to over-provision.

Pinecone’s namespace isolation makes multi-tenancy straightforward.

And Shadow DOM lets you ship a widget that works on literally any website without worrying about CSS conflicts.

If you’re building something similar, I hope this architecture walkthrough saves you time.

If you have questions or want to contribute, the GitHub repo is the place.

Issues and PRs are welcome.


메타데이터
post_id
00f7fdc0d80c
slug
i-built-an-open-source-ai-chatbot-you-can-add-to-any-website-with-one-line-of-code-00f7fdc0d80c
url
https://medium.com/@j.y.weng/i-built-an-open-source-ai-chatbot-you-can-add-to-any-website-with-one-line-of-code-00f7fdc0d80c
canonical_url
https://medium.com/@j.y.weng/i-built-an-open-source-ai-chatbot-you-can-add-to-any-website-with-one-line-of-code-00f7fdc0d80c
author_url
https://medium.com/@j.y.weng
status
ok
fetched_at
2026-06-21 19:25:17