A lightweight, self-hosted LLM chat interface built with React Router, MySQL, and Docker. Features real-time streaming responses, user management, and admin-configurable model access.
- User Authentication - Registration, login, and session-based authentication with HTTP-only cookies
- Admin Configuration - Manage OpenAI API credentials and control which models users can access
- Real-time Streaming - SSE-based streaming responses for a smooth chat experience
- Model Whitelist - Admins control which OpenAI models are available to users
- Persistent Chat - Chat history is saved and accessible across sessions
- Responsive Design - Works on desktop and mobile devices
- Framework: React Router 7 (Framework Mode)
- Frontend: React 19, Tailwind CSS 4
- Backend: React Router loaders/actions with cookie-based sessions
- Database: MySQL 8 with Prisma ORM
- AI Provider: OpenAI API with streaming support
- Runtime: Node.js 22+
- Container: Docker & Docker Compose
- Node.js >= 22.0.0
- npm (comes with Node.js)
- Docker & Docker Compose
docker-compose up -d mysqlcp .env.example .env
# Edit .env if needed (default values work with docker-compose)npm install# Generate Prisma client
npm run db:generate
# Run migrations
npm run db:migrate
# Seed admin user
npm run db:seednpm run devVisit http://localhost:3000
After seeding, login with:
- Username:
admin - Password:
admin123
Warning: Change this password immediately in production!
- Login with the admin account
- Go to Admin Settings (accessible from the sidebar or directly at
/admin) - Configure your OpenAI API key (required, must be non-empty)
- Add allowed models (one per line), e.g.:
gpt-4o-mini gpt-4o gpt-4-turbo - Save settings - the system will show "Ready for chat" when properly configured
- Navigate to New Chat and start chatting!
npm run dev- Start development servernpm run build- Build for productionnpm run start- Start production servernpm run typecheck- Run TypeScript type checkingnpm run db:migrate- Run database migrationsnpm run db:generate- Generate Prisma clientnpm run db:seed- Seed database with initial datanpm run db:studio- Open Prisma Studionpm run db:reset- Reset database and re-seed
.
├── app/
│ ├── lib/
│ │ └── server/ # Server-only modules (db, env, config, auth)
│ ├── routes/ # Application routes
│ │ ├── auth/ # Login, Register, Logout
│ │ ├── chat/ # Chat interface with streaming
│ │ └── admin/ # Admin settings
│ ├── app.css # Global styles
│ ├── root.tsx # Root layout
│ ├── routes.ts # Route configuration
│ └── sessions.ts # Session configuration
├── prisma/
│ ├── schema.prisma # Database schema
│ └── seed.ts # Database seed script
├── docker-compose.yml # Docker services
├── Dockerfile # App container
└── .env.example # Environment template
The chat interface uses Server-Sent Events (SSE) for real-time streaming:
- User message is persisted immediately
- Assistant response streams token-by-token
- Message is only saved after streaming completes
- Errors are displayed without creating fake messages
- API Key: Required, stored securely server-side
- Base URL: Optional, for custom OpenAI-compatible endpoints
- Allowed Models: Whitelist of models users can select from
- Server-only modules: All sensitive operations in
app/lib/server/*marked withserver-onlypackage - HTTP-only cookies: Session tokens not accessible to JavaScript
- API key protection: OpenAI credentials never exposed to browser
- Ownership checks: Users can only access their own chat sessions
- Role-based access: Admin-only routes protected at server level
Required variables (see .env.example):
| Variable | Description | Default |
|---|---|---|
DATABASE_URL |
MySQL connection string | mysql://user:pass@localhost:3306/owu |
SHADOW_DATABASE_URL |
Dedicated Prisma shadow database for migrate dev |
mysql://user:pass@localhost:3306/owu_shadow |
SESSION_SECRET |
Secret for signing cookies | Generate a random string |
APP_PORT |
Server port | 3000 |
NODE_ENV |
Environment | development |
Ensure MySQL is running: docker-compose ps
This project uses a dedicated SHADOW_DATABASE_URL so prisma migrate dev does not need CREATE DATABASE privileges on the application user. If you added this fix after MySQL was already initialized, run the SQL setup once in the running container or recreate the MySQL volume so docker/mysql/init/01-grant-prisma-shadow.sh can create the owu_shadow database and grants.
Admin needs to configure allowed models in Admin Settings
Check browser console for SSE connection errors. Ensure the server supports streaming responses.
SSE was chosen for streaming because:
- Simpler implementation with standard HTTP
- Automatic reconnection handling
- Works through most proxies/firewalls
- Perfect fit for one-way server-to-client streaming
- No JWT storage in localStorage (XSS protection)
- Automatic browser handling of session expiration
- Simple server-side session invalidation
Admins can add any OpenAI-compatible model identifier:
- OpenAI models:
gpt-4o,gpt-4o-mini,gpt-4-turbo - Compatible endpoints: Any model identifier your API supports
Key entities:
User- Accounts with role (admin/user)SystemConfig- Singleton configuration recordChatSession- Conversation containerChatMessage- Individual messages
Private - For internal use only