🚀 Live Demo | 📊 Benchmark Results | 📝 Blog Post
A real-time AI chatbot built with PHP 8.2+, Swoole, Mezzio, and Datastar. Features streaming responses, document/artifact generation, and a modern reactive UI—all without JavaScript frameworks.
🎯 Project Goal: This is a side-by-side comparison with the Vercel AI Chatbot (Next.js), demonstrating that a lean PHP stack can deliver the same features with dramatically less complexity and better performance.
This project exists to challenge the assumption that modern AI chat apps require heavy JavaScript stacks. We rebuilt the Vercel AI Chatbot using PHP—and the results speak for themselves.
Context: The Vercel AI Chatbot has 86 contributors and 600+ commits of optimization. This PHP port is a straightforward implementation with minimal optimization—yet outperforms on most metrics.
Desktop (Chrome 144):
| Metric | Next.js (Vercel) | PHP/Swoole | Difference |
|---|---|---|---|
| Lighthouse Score | 93 | 100 | 🏆 PHP |
| Time to Interactive | 1.6s | 0.3s | 5.3x faster |
| Total Blocking Time | 110ms | 0ms | ∞ better |
| JavaScript Sent | ~1,080 KB | 13.5 KB | 80x less |
| HTTP Requests | 36+ | 8 | 4.5x fewer |
| Page Weight | 1,107 KB | 42 KB | 26x smaller |
Mobile (Slow 4G + 4x CPU throttling):
| Metric | Next.js (Vercel) | PHP/Swoole | Difference |
|---|---|---|---|
| Lighthouse Score | 54 | 100 | 🏆 PHP |
| Time to Interactive | 8.2s | 1.1s | 7.5x faster |
| Total Blocking Time | 780ms | 0ms | ∞ better |
| Largest Contentful Paint | 8.1s | 1.1s | 7.4x faster |
| Aspect | Next.js | PHP/Swoole | Ratio |
|---|---|---|---|
| Dependencies (prod) | 799 packages | 69 packages | 11.6x fewer |
| node_modules / vendor | 793 MB | 25 MB | 31.7x smaller |
| Build Step | Required | None | — |
| Hosting Cost | Usage-based | $20/year VPS | — |
The takeaway: Modern PHP with Swoole is a serious contender for real-time applications. No transpilation, no hydration, no serverless cold starts—just fast, efficient code.
⚠️ Feature Completeness: This is a working proof-of-concept, not a production-ready clone. Core features (chat, streaming, artifacts, auth, voting) work. Missing: file attachments, edit/regenerate messages. See the Vercel AI Chatbot for the full-featured original.
- Real-time AI Streaming - Token-by-token streaming via Server-Sent Events (SSE)
- Multiple AI Providers - Support for Anthropic (Claude) and OpenAI (GPT) models
- Document Artifacts - AI can create and edit code, text, spreadsheets, and images
- CQRS Architecture - Clean separation of commands, queries, and events
- Session-based Auth - Simple authentication with guest and registered user support
- Rate Limiting - Configurable daily message limits for guests and registered users
- Responsive UI - Mobile-friendly design with sidebar navigation
- No Build Required - Datastar provides reactivity without complex JS bundling
- Zero CDN Dependencies - All assets served locally (Open Props, marked.js, SVG icons)
- PHP 8.2 or higher
- Swoole extension (
pecl install swoole) - SQLite3 extension
- Composer
- Node.js 18+ (optional, for TypeScript development)
git clone <repository-url>
cd ai-chatbot
# Install PHP dependencies
composer install
# Install frontend dependencies (optional)
npm install# Copy the environment template and add your API keys
cp .env.example .envEdit .env with your API keys:
# Required: At least one AI provider API key
ANTHROPIC_API_KEY=sk-ant-api03-your-key-here
# OPENAI_API_KEY=sk-your-key-here
# Optional: Model and token configuration
AI_DEFAULT_MODEL=claude-sonnet-4-5
AI_MAX_TOKENS=4096Optionally copy the local PHP config for additional settings:
cp config/autoload/app.local.php.dist config/autoload/app.local.php# Initialize the SQLite database
composer db:init
# Or manually:
sqlite3 data/db.sqlite < data/schema.sql# Start the Swoole server (runs on http://localhost:8080)
composer serveVisit http://localhost:8080 in your browser.
┌─────────────────────────────────────────────────────────────────┐
│ Frontend (Datastar + TypeScript) │
│ └── SSE connection to /updates for real-time DOM patching │
├─────────────────────────────────────────────────────────────────┤
│ Infrastructure Layer │
│ ├── Http/Handler/Command/ → POST/PUT/DELETE mutations │
│ ├── Http/Handler/Query/ → GET read operations │
│ ├── Http/Listener/ → SseRequestListener for streaming │
│ └── AI/ → LLPhantAIService, streaming tools │
├─────────────────────────────────────────────────────────────────┤
│ Application Layer (Events) │
│ ├── Domain/Event/ → MessageStreamingEvent, ChatUpdated│
│ └── EventBus/ → SwooleEventBus for SSE broadcasts │
├─────────────────────────────────────────────────────────────────┤
│ Domain Layer │
│ ├── Model/ → Chat, Message, Document, User │
│ ├── Service/ → AIServiceInterface, RateLimitSvc │
│ └── Repository/ → Interface definitions │
├─────────────────────────────────────────────────────────────────┤
│ Persistence (SQLite) │
│ └── data/db.sqlite │
└─────────────────────────────────────────────────────────────────┘
The application follows the Command Query Responsibility Segregation pattern:
- Commands (
/cmd/*) - POST/PUT/DELETE operations that modify state - Queries (
/api/*) - GET operations that read state - Events - Emitted when state changes, broadcast to clients via SSE
- User sends message via
POST /cmd/chat/{chatId}/message MessageCommandHandlercreates user + assistant message placeholders- Swoole coroutine starts
streamAiResponse()callingAIService::streamChat() - Server accumulates full content; each chunk emits
MessageStreamingEventwithfullContent SseRequestListenerrenders full markdown server-side and sendsPatchElements- Datastar morphs the DOM efficiently (only changed nodes update)
- Container auto-scrolls via
data-on:datastar-fetch__window
Why fat morph? Brotli compresses repetitive content ~same as deltas. Server-side markdown means no client scripts per chunk. DOM stays clean — just rendered HTML.
├── bin/
│ ├── init-db.php # Database initialization script
│ └── seed.php # Sample data seeder
├── config/
│ ├── config.php # Config aggregator
│ ├── container.php # DI container setup
│ ├── routes.php # Route definitions
│ ├── pipeline.php # Middleware pipeline
│ └── autoload/ # Environment-specific configs
├── data/
│ ├── schema.sql # Database schema
│ └── db.sqlite # SQLite database (created on init)
├── public/
│ ├── css/
│ │ ├── app.css # Custom styles
│ │ └── open-props-bundle.css # Open Props CSS (bundled)
│ ├── icons.svg # SVG icon sprite
│ └── js/
│ ├── app.js # Custom TypeScript (compiled)
│ ├── datastar.js # Datastar library
│ └── marked.min.js # Markdown parser
├── src/App/
│ ├── ConfigProvider.php # DI factories
│ ├── Domain/
│ │ ├── Event/ # Domain events
│ │ ├── Model/ # Entity classes (Chat, Message, Document, etc.)
│ │ ├── Repository/ # Repository interfaces
│ │ └── Service/ # Service interfaces
│ └── Infrastructure/
│ ├── AI/ # AI service implementations
│ ├── Auth/ # Authentication middleware
│ ├── EventBus/ # SSE event broadcasting
│ ├── Http/Handler/ # Request handlers
│ ├── Persistence/ # SQLite repositories
│ ├── Session/ # Swoole-based sessions
│ └── Template/ # Template renderer
├── templates/
│ ├── app/ # Page templates
│ ├── layout/ # Layout templates
│ └── partials/ # Reusable components
└── tests/
├── Feature/ # Integration tests
└── Unit/ # Unit tests
| Method | Endpoint | Description |
|---|---|---|
| POST | /auth/login |
Login with email/password |
| POST | /auth/register |
Register new account |
| POST | /auth/logout |
Logout current session |
| POST | /auth/upgrade |
Upgrade guest to registered |
| GET | /auth/status |
Get current auth status |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/chats |
List user's chats |
| GET | /api/chats/{id} |
Get chat details |
| GET | /api/chats/{id}/messages |
Get chat messages |
| GET | /api/documents/{id} |
Get document/artifact |
| Method | Endpoint | Description |
|---|---|---|
| POST | /cmd/chat |
Create new chat |
| DELETE | /cmd/chat/{id} |
Delete chat |
| PATCH | /cmd/chat/{id}/visibility |
Toggle public/private |
| POST | /cmd/chat/{chatId}/message |
Send message & generate response |
| POST | /cmd/chat/{chatId}/generate |
Regenerate AI response |
| POST | /cmd/chat/{chatId}/stop |
Stop streaming response |
| POST | /cmd/document |
Create document |
| PUT | /cmd/document/{id} |
Update document |
| DELETE | /cmd/document/{id} |
Delete document |
| PATCH | /cmd/vote/{chatId}/{messageId} |
Vote on message |
| Method | Endpoint | Description |
|---|---|---|
| GET | /updates |
SSE endpoint for real-time updates |
Anthropic (Claude 4.5)
claude-opus-4-5- Claude Opus 4.5 (Maximum intelligence)claude-sonnet-4-5- Claude Sonnet 4.5 (Best balance)claude-haiku-4-5- Claude Haiku 4.5 (Fast/Cheap)
Anthropic (Legacy)
claude-opus-4-1- Claude Opus 4.1claude-opus-4- Claude Opus 4claude-sonnet-4- Claude Sonnet 4claude-3-5-haiku-20241022- Claude Haiku 3.5claude-3-haiku-20240307- Claude Haiku 3 (Cheapest!)
OpenAI (GPT-5.x)
gpt-5.2/gpt-5.1/gpt-5- Full capabilitygpt-5-mini- Balanced (cost-effective)gpt-5-nano- Cheapest
OpenAI (GPT-4.x)
gpt-4.1/gpt-4.1-mini/gpt-4.1-nanogpt-4o/gpt-4o-mini
The AI can use tools to create and update documents:
- CreateDocument - Create code, text, spreadsheet, or image artifacts
- UpdateDocument - Modify existing artifacts
-- Users (session-based auth)
users (id, email, password_hash, is_guest, created_at)
-- Chats (conversations)
chats (id, user_id, title, model, visibility, created_at, updated_at)
-- Messages
messages (id, chat_id, role, content, parts, created_at)
-- Documents/Artifacts
documents (id, chat_id, message_id, kind, title, language, created_at, updated_at)
-- Document versions (undo/redo)
document_versions (id, document_id, content, version, created_at)
-- Message votes
votes (id, chat_id, message_id, user_id, is_upvote, created_at)
-- AI suggestions
suggestions (id, document_id, content, status, created_at)# Server
composer serve # Start Swoole server at :8080
composer stop # Stop Swoole server
composer reload # Reload Swoole workers
# Database
composer db:init # Initialize database schema
composer db:seed # Seed with sample data
# Testing
composer test # Run Pest tests
composer test:coverage # Run tests with coverage
# Code Quality
composer cs # Check code style (dry-run)
composer cs:fix # Fix code style issues
composer stan # Run PHPStan static analysis
# Frontend (optional)
npm run build # Build TypeScript with esbuild
npm run watch # Watch mode for development
npm run typecheck # TypeScript type checkingTests use Pest PHP with in-memory SQLite:
# Run all tests
composer test
# Run specific test file
./vendor/bin/pest tests/Unit/ChatTest.php
# Run with coverage
composer test:coverageThis project uses PHP-CS-Fixer with PSR-12 style:
# Check for issues
composer cs
# Auto-fix issues
composer cs:fixPHPStan is configured at level 6:
composer stanThe frontend uses Datastar for reactive UI without JavaScript frameworks.
- Signals - Client-side state (form inputs, UI flags)
- PatchElements - Server-sent HTML fragments that update DOM
- ExecuteScript - Server-sent JavaScript execution
- Actions - Declarative HTTP requests (
@post,@get, etc.)
<!-- SSE connection for real-time updates -->
<div data-init="@get('/updates')">
<!-- Form with signal binding -->
<input type="text" data-model="$message" />
<!-- Action on click -->
<button data-on:click="@post('/cmd/chat/123/message')">
Send
</button>
<!-- Conditional rendering -->
<div data-show="$isGenerating">
Generating...
</div>Create a .env file from the example:
cp .env.example .envAvailable environment variables:
# AI Provider API Keys (at least one required)
ANTHROPIC_API_KEY=sk-ant-api03-your-key-here
OPENAI_API_KEY=sk-your-key-here
# AI Model Configuration
AI_DEFAULT_MODEL=claude-3-haiku-20240307
AI_MAX_TOKENS=2048
# Context compression settings
AI_CONTEXT_RECENT_MESSAGES=6
AI_CONTEXT_MAX_OLDER_CHARS=500
# Application Settings
APP_ENV=development
APP_DEBUG=true
# Rate Limits
RATE_LIMIT_GUEST_HOURLY=10
RATE_LIMIT_GUEST_DAILY=20
RATE_LIMIT_USER_HOURLY=30
RATE_LIMIT_USER_DAILY=100For additional PHP configuration overrides, create config/autoload/app.local.php:
<?php
return [
'database' => [
'path' => getcwd() . '/data/db.sqlite',
],
'templates' => [
'paths' => [
'' => getcwd() . '/templates',
],
],
];Default Swoole settings can be overridden in config/autoload/swoole.local.php:
<?php
return [
'mezzio-swoole' => [
'swoole-http-server' => [
'host' => '0.0.0.0',
'port' => 8080,
'options' => [
'worker_num' => 4,
'task_worker_num' => 2,
'max_request' => 10000,
],
],
],
];- Set
debugtofalsein configuration - Use strong session secrets
- Configure proper rate limits
- Set up SSL/TLS termination (nginx/Caddy)
- Configure log rotation
server {
listen 443 ssl http2;
server_name chat.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
}
# SSE endpoint needs special handling
location /updates {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding off;
}
}FROM php:8.2-cli
RUN pecl install openswoole && docker-php-ext-enable openswoole
RUN docker-php-ext-install pdo pdo_sqlite
WORKDIR /app
COPY . .
RUN composer install --no-dev --optimize-autoloader
EXPOSE 8080
CMD ["php", "vendor/bin/laminas", "mezzio:swoole:start"]MIT License - see LICENSE for details.
- Baseline: Vercel AI Chatbot — the Next.js reference implementation we're comparing against
- AI Integration: LLPhant — PHP library for LLM interactions
- Reactivity: Datastar — HTML-over-the-wire without the JS framework tax
Tired of JavaScript complexity? zwei und eins gmbh builds high-performance PHP applications that compete with (and often outperform) modern JS stacks.