Liteskill
Liteskill is an event-sourced chat application built with Phoenix 1.8 and Elixir. It provides multi-model LLM integration, MCP tool support, RAG (Retrieval-Augmented Generation), an agent studio, collaborative reports, a built-in wiki, and scheduled agent runs.
Key Capabilities
- Multi-model chat — Connect any LLM provider (AWS Bedrock, OpenRouter, OpenAI-compatible endpoints) and switch models per conversation.
- MCP tool calling — Register MCP servers and let the LLM call external tools during conversation streaming, with optional user approval.
- RAG — Ingest documents, generate embeddings via Cohere/OpenAI, and augment conversations with semantic search results.
- Agent Studio — Define reusable AI agents with system prompts, model assignments, and scoped tool/data-source access via ACLs.
- Teams & Runs — Compose agents into teams with topologies (sequential, parallel, supervisor) and execute them as runs.
- Reports — Structured documents with nested sections, markdown rendering, and a comment/review workflow.
- Wiki — Built-in collaborative wiki with hierarchical pages, ACL-based sharing, and automatic RAG indexing.
- Schedules — Cron-based recurring execution of agent runs.
- Usage tracking — Per-user, per-model, per-conversation token and cost accounting.
Tech Stack
| Layer | Technology |
|---|---|
| Language | Elixir 1.18 / Erlang/OTP 28 |
| Web framework | Phoenix 1.8.3, Phoenix LiveView 1.1 |
| HTTP server | Bandit |
| Database | PostgreSQL 16 with pgvector |
| HTTP client | Req + ReqLLM |
| Background jobs | Oban |
| CSS | Tailwind CSS v4 |
| Auth | Ueberauth (OIDC) + Argon2 (password) |
| Encryption | AES-256-GCM via Liteskill.Crypto |
Current version: 0.2.37
So What’s the Difference?
If you’re evaluating self-hosted AI tools, you’ve probably noticed the space is crowded. Chat UIs, API gateways, workflow engines, wiki platforms, agent frameworks - there’s no shortage of options, and each one solves a piece of the puzzle. Liteskill was built to be the whole puzzle.
This page explains what makes Liteskill different from the categories of tools you’ll encounter, and why those differences matter.
vs. Chat Interfaces
Most open-source LLM chat UIs give you a clean frontend for talking to models. They handle streaming, multi-model switching, and sometimes RAG. But they’re built on standard CRUD persistence - conversations are mutable rows in a database.
What Liteskill does differently:
- Event-sourced conversations. Every state change is captured as an immutable event. You get a full audit trail, state replay, and temporal queries. For regulated industries, this is table stakes.
- Per-resource access control. Most chat UIs offer role-based permissions (admin vs. user). Liteskill provides granular ACLs on individual conversations, reports, agents, wiki spaces, and more - with owner, manager, editor, and viewer roles, plus group-based grants.
- Native MCP tool integration. Rather than a proprietary plugin system, Liteskill speaks the open Model Context Protocol over JSON-RPC 2.0. Any MCP-compliant tool server works without custom integration code.
- Agent Studio & Teams. Beyond personas and system prompts, Liteskill provides composable agents with strategies (ReAct, chain-of-thought, tree-of-thoughts), team topologies (sequential, parallel, supervisor), and full execution tracking with cost limits.
- Conversation forking. Branch off at any point in a conversation to explore alternate paths without losing the original thread.
vs. LLM Proxy Gateways
API gateways sit between your application and LLM providers, offering a unified interface, load balancing, rate limiting, and cost tracking. They’re useful infrastructure - but they’re not applications.
What Liteskill does differently:
- It’s a complete platform. A proxy gateway has no UI, no conversation management, no persistence, and no user-facing experience. Liteskill is something your team can actually use.
- Built-in provider management. Liteskill handles multi-provider routing, circuit breaking, concurrency gating, and token-bucket rate limiting through its own LLM Gateway layer. A separate proxy in front is optional, not required.
- No enterprise paywall for auth. Proxy gateways commonly gate SSO, audit logs, and advanced RBAC behind paid tiers. Liteskill ships all authentication and authorization features in the open-source release.
- Compatible, not competing. If you already run a proxy gateway, Liteskill can use it as a provider - they complement each other.
vs. Workflow Automation Platforms
Visual workflow builders let you connect triggers and actions into automated pipelines. Some have added AI nodes for LLM interactions. But AI is a bolt-on capability, not the core architecture.
What Liteskill does differently:
- Purpose-built for AI conversations. Streaming token-by-token responses, tool calling during generation, conversation forking, and message editing are all first-class features - not nodes wired into a generic workflow DAG.
- Event-sourced state. Workflow engines store execution history as flat records. Liteskill captures every conversation state change as an immutable event, enabling replay, auditing, and stream recovery.
- Open tool protocol. Workflow platforms typically connect to services through proprietary node/connector systems. Liteskill uses the open MCP protocol - any compliant tool server works without writing platform-specific integration code.
- Apache 2.0 licensing. Some workflow platforms use restrictive licenses that prohibit commercial embedding or redistribution. Liteskill is Apache 2.0 - use it however you want.
- SSO and RBAC included. No paid enterprise plan required for OIDC SSO, RBAC, or entity-level access controls.
vs. Wiki & Knowledge Platforms
Wiki platforms excel at structured documentation with hierarchical content, collaborative editing, and content-level permissions. Some have mature SSO support. But they’re static knowledge bases.
What Liteskill does differently:
- AI-native knowledge. Liteskill’s built-in wiki isn’t just a documentation tool - it’s automatically synced to the RAG pipeline, so your documentation becomes searchable context for AI conversations. Knowledge isn’t static; it’s active.
- Wiki is one feature, not the product. Conversations, agents, reports, MCP tools, RAG, and the wiki all live in the same platform under the same unified access control system.
- Event-sourced audit trail. Traditional wikis use mutable ORM persistence. Liteskill’s append-only event store provides an immutable record of every state change across the entire platform.
vs. Agent Frameworks
Agent frameworks provide libraries and abstractions for building multi-agent AI systems in code. Agents have roles, goals, and tools. Some support MCP. But they’re developer tools, not deployed applications.
What Liteskill does differently:
- It’s a platform, not a library. Agent frameworks require you to build an application around them - writing your own UI, user management, persistence layer, and deployment infrastructure. Liteskill is a deployed application with a full web interface, real-time streaming, user accounts, and persistent conversation history.
- Integrated chat + agents. Agents and teams are defined through the Agent Studio UI, then invoked within the same platform where conversations happen. No separate application to build and maintain.
- Enterprise auth built in. Most agent frameworks have no auth, no RBAC, and no SSO in their open-source releases. Enterprise tiers that add these features often come with execution quotas and significant cost. Liteskill includes OIDC SSO, RBAC, entity ACLs, and group-based authorization in the open-source release with no quotas.
- Event-sourced execution tracking. Agent runs are tracked with structured logs, usage metrics, cost limits, and full event histories - not just basic logging.
- Elixir’s concurrency model. Liteskill runs on the BEAM VM with lightweight processes, preemptive scheduling, and fault-tolerant supervision trees. Parallel agent topologies, concurrent tool calls, and streaming responses all benefit from these concurrency primitives without thread-safety complexity.
What Liteskill Brings Together
| Capability | Chat UIs | LLM Proxies | Workflow Engines | Wiki Platforms | Agent Frameworks | Liteskill |
|---|---|---|---|---|---|---|
| LLM Chat UI | Yes | - | - | - | - | Yes |
| Event Sourcing | - | - | - | - | - | Yes |
| Conversation ACLs | - | - | - | - | - | Yes |
| RBAC (open source) | Basic | Paid | Paid | Some | Paid | Yes |
| OIDC SSO (open source) | Some | Paid | Paid | - | Paid | Yes |
| Native MCP Tools | - | - | - | - | Some | Yes |
| Agent Orchestration | - | - | Basic | - | Yes | Yes |
| RAG Pipeline | Some | - | - | - | - | Yes |
| Built-in Wiki | - | - | - | Yes | - | Yes |
| Structured Reports | - | - | - | - | - | Yes |
| Conversation Forking | - | - | - | - | - | Yes |
| Encryption at Rest | - | - | - | - | - | Yes |
| Multi-Provider LLM | Yes | Yes | Via plugins | - | Yes | 56+ |
| Apache 2.0 License | Varies | Varies | Restrictive | Varies | Varies | Yes |
The Bottom Line
Most tools in this space solve one slice of the problem. You get a chat interface, or a provider gateway, or a workflow engine, or a wiki, or an agent framework - then stitch them together yourself.
Liteskill combines all of these into a single, event-sourced platform with enterprise access controls. And it ships everything under Apache 2.0 with no feature paywalls, no branding requirements, and no execution quotas.
Hello World Tutorial
Welcome! This tutorial gets you up and going with LiteSkill.
Docker Compose
The fastest way to get up and running is with Docker Compose. Ensure it’s installed on your system and then run the following:
git clone https://github.com/liteskill-ai/liteskill-oss.git && cd liteskill-oss
Before starting the app, generate the required secrets:
export SECRET_KEY_BASE=$(openssl rand -base64 64)
export ENCRYPTION_KEY=$(openssl rand -base64 32)
Then start everything:
docker compose up
This starts a PostgreSQL 16 database with pgvector and the Liteskill application. The app container uses host networking so it can reach the database on localhost.
Admin Setup
Open a browser and navigate to http://localhost:4000/ and follow the setup instructions.
For testing purposes, we recommend using OpenRouter as your model provider. For model selection, we recommend Claude Sonnet for inference and OpenAI: Text Embedding 3 Small for embeddings.
Your First Conversation
- Navigate back to the main page by clicking
Chatin the sidebar. - Click the “+” icon to the right of “Conversations” in the top left corner of the screen.
- Type a message and hit Enter!
You should see the LLM stream its response in real-time. Congratulations — you’ve got Liteskill running!
Adding your first MCP Server
MCP (Model Context Protocol) servers give your LLM access to external tools. When an MCP server is connected, the LLM can request tool calls during conversation streaming, and Liteskill will execute them automatically or with user approval.
Prerequisites
- Liteskill running (see Initial Setup)
- An MCP server accessible via HTTP(S)
Register an MCP Server
- Navigate to
Toolsin the sidebar (or go to/mcp). - Click the “Add Server” button.
- Fill in the server details:
- Name: A display name for the server (e.g. “My Tools”)
- URL: The HTTP(S) endpoint of your MCP server
- API Key (optional): If your server requires authentication
- Custom Headers (optional): Any additional headers your server needs
- Click Save.
Liteskill will connect to the server and discover its available tools via the tools/list JSON-RPC call.
Select the Server for Conversations
After registering the server, you need to select it for use in your conversations:
- Start or continue a conversation.
- Click the Tool icon to the left of “Type a message…” in the chat window.
- Select the tools you’d like to use.
- By default, tool calls execute automatically (
auto_confirm: true). - When the LLM determines it needs to use a tool, it will emit a tool call request.
Tool calls appear in the conversation as collapsible sections showing the tool name, arguments, and result.
Docker Networking
If you’re running Liteskill via Docker Compose (which uses network_mode: host), MCP servers on your host machine are reachable at localhost:
http://localhost:4005
This requires Allow private MCP URLs to be enabled in server settings (Admin > Setup), since localhost resolves to a private address.
SSRF Protection
By default, only HTTPS URLs are accepted for MCP servers, and private/reserved addresses are blocked. To allow private URLs for self-hosted MCP servers, enable Allow private MCP URLs in the admin settings.
Configuring a RAG Data Source
RAG (Retrieval-Augmented Generation) lets you feed your own documents into conversations. When RAG is enabled, Liteskill searches your document collections for relevant context and includes it with your messages to the LLM.
Prerequisites
- Liteskill running (see Initial Setup)
- An LLM provider configured with an embedding model
- Documents you want to make searchable
Create a Wiki Space
- Navigate to
Wikiin the sidebar (or go to/wiki). - Click “New Space”.
- Give it a name (e.g. “Engineering Docs”).
Add Documents
You can add nested documents in this page, called “children”.
External Connectors
For bulk document sync, configure an external data source:
- Navigate to
Sourcesand click “Add Source”. - Select a source type:
- Google Drive — Service account JSON + folder ID
- SharePoint — Tenant ID, site URL, client credentials
- Confluence — Base URL, username, API token, space key
- Jira — Base URL, username, API token, project key
- GitHub — Personal access token, repository
- GitLab — Personal access token, project path
- Fill in the credentials and click Save.
- Click “Sync” to pull documents from the source.
Using RAG in Conversations
Once your documents are embedded, RAG context is automatically injected into conversations:
- Start a new conversation.
- Ask a question related to your documents.
- Liteskill embeds your query, searches across your accessible collections, and includes the most relevant chunks as context for the LLM.
- RAG sources are displayed alongside the assistant’s response for transparency.
Wiki Integration
Wiki pages are automatically indexed for RAG. When you create or edit a wiki page, a background job updates the corresponding RAG document and re-embeds its chunks. See the Wiki documentation for details.
Prerequisites
Required Tools
Liteskill uses mise for tool version management. The mise.toml at the project root pins:
| Tool | Version |
|---|---|
| Elixir | 1.18 |
| Erlang/OTP | 28 |
| Node.js | 24 |
| mdbook | latest |
Install mise and run mise install to get the correct versions.
System Dependencies
A C compiler and make are required to build native extensions (e.g. argon2_elixir). Install your platform’s build tools before running mix deps.compile:
- Debian/Ubuntu:
sudo apt install build-essential - Fedora/RHEL:
sudo dnf groupinstall "Development Tools" - macOS:
xcode-select --install - Arch Linux:
sudo pacman -S base-devel
PostgreSQL
PostgreSQL 16 with the pgvector extension is required.
Option A: Local install
Install PostgreSQL and pgvector via your system package manager. The default dev config expects:
- Host:
localhost - Port:
5432 - User/password:
postgres/postgres
Option B: Docker
Use the included test script which starts a disposable Postgres container:
./scripts/test-with-docker.sh test
Or use the production docker-compose.yml which provides a pgvector/pgvector:pg16 service.
Quick Start (Docker Compose)
If you just want to run Liteskill without setting up a development environment:
export SECRET_KEY_BASE=$(openssl rand -base64 64)
export ENCRYPTION_KEY=$(openssl rand -base64 32)
docker compose up
This starts PostgreSQL with pgvector and the Liteskill application on port 4000.
Local Development
Quick Start
# Install tool versions
mise install
# Install deps, create DB, run migrations, build assets
mix setup
# Start the dev server
mix phx.server
The app will be available at http://localhost:4000.
Without Local Postgres
If you don’t have PostgreSQL installed locally, use the Docker-based scripts:
# Run tests with a temporary Docker Postgres
./scripts/test-with-docker.sh test
# Full precommit with Docker Postgres
./scripts/test-with-docker.sh precommit
Single-User Mode
For desktop or self-hosted single-user setups:
SINGLE_USER_MODE=true mix phx.server
Or use the mise task:
mise run singleuser
This skips the login screen and auto-provisions an admin user.
Desktop Mode
Liteskill can run as a desktop application via Tauri (ex_tauri):
LITESKILL_DESKTOP=true mix phx.server
In desktop mode:
- A bundled PostgreSQL instance is started automatically
- An encryption key and secret key base are auto-generated and stored in
desktop_config.json - Single-user mode is enabled automatically
- Data is stored in platform-specific directories:
- macOS:
~/Library/Application Support/Liteskill - Linux:
$XDG_DATA_HOME/liteskill(default:~/.local/share/liteskill) - Windows:
%APPDATA%/Liteskill
- macOS:
Docker Compose
For production-like local development:
export SECRET_KEY_BASE=$(openssl rand -base64 64)
export ENCRYPTION_KEY=$(openssl rand -base64 32)
docker compose up
The docker-compose.yml includes:
- db —
pgvector/pgvector:pg16with persistent volume and health checks - app — The Liteskill release, using
network_mode: hostfor direct database access - migrate — One-shot migration runner (activated with
docker compose run migrate)
First Run
Initial Setup
On first boot, Liteskill:
- Validates the encryption key (
ENCRYPTION_KEYenv var or auto-generated in desktop mode) - Creates ETS tables for rate limiting and LLM token buckets
- Ensures a root admin user exists (
[email protected]) - Bootstraps system RBAC roles
- Ensures environment-configured providers are created (e.g. AWS Bedrock via
AWS_BEARER_TOKEN_BEDROCK) - Loads application settings
Admin Setup
Navigate to http://localhost:4000/setup to set the admin password. This route is only available when the admin user hasn’t been configured yet.
After setup, log in at http://localhost:4000/login.
Configuring LLM Providers
Before you can chat, configure at least one LLM provider:
- Go to Admin > Providers (
/admin/providers) - Add a provider (e.g. Amazon Bedrock, OpenRouter, or any OpenAI-compatible endpoint)
- Go to Admin > Models (
/admin/models) and add models under that provider
Users can also add personal providers at Profile > Providers (/profile/providers).
Configuring MCP Servers
To enable tool calling:
- Go to MCP Servers (
/mcp) - Add an MCP server URL with optional API key and custom headers
- The server’s tools will be available in conversations when selected
Configuring RAG
To enable document-augmented conversations:
- Go to Admin > RAG (
/admin/rag) and configure the default embedding model - Navigate to Sources (
/sources) to create collections and add documents - Documents are automatically chunked and embedded for semantic search
Architecture Overview
Liteskill is an event-sourced Phoenix application organized around bounded contexts with enforced boundaries (via the boundary library).
High-Level Architecture
┌─────────────────────────────────────────────────────────┐
│ LiteskillWeb │
│ LiveView UI │ REST API │ Auth Plugs │ Rate Limiting │
└──────────┬──────────────┬────────────────────────────────┘
│ │
┌──────────▼──────────────▼────────────────────────────────┐
│ Context Layer │
│ Chat │ Accounts │ Authorization │ Groups │ LLM │ ... │
└──────────┬───────────────────────────────────────────────┘
│
┌──────────▼───────────────────────────────────────────────┐
│ Infrastructure Layer │
│ EventStore │ Aggregate │ Crypto │ Repo │ PubSub │
└──────────────────────────────────────────────────────────┘
Bounded Contexts
Each context is a top-level Boundary module that declares its dependencies and exports:
| Context | Responsibility |
|---|---|
Liteskill.Chat | Conversations, messages, streaming, tool calls |
Liteskill.Accounts | Users, OIDC, password auth, invitations |
Liteskill.Authorization | Entity ACLs, role hierarchy, access queries |
Liteskill.Groups | Group management and memberships |
Liteskill.LLM | LLM completions, stream orchestration |
Liteskill.LlmProviders | Provider CRUD, env bootstrapping |
Liteskill.LlmModels | Model CRUD, provider options |
Liteskill.LlmGateway | Per-provider circuit breaker, concurrency gates, token buckets |
Liteskill.McpServers | MCP server CRUD, tool selection |
Liteskill.Rag | Collections, sources, documents, embeddings, search |
Liteskill.Agents | Agent definitions, tool/source ACLs |
Liteskill.Teams | Team definitions with agent composition |
Liteskill.Runs | Run execution, tasks, logs |
Liteskill.Reports | Reports with nested sections and comments |
Liteskill.Schedules | Cron-based recurring runs |
Liteskill.DataSources | External data connectors (Google Drive, Confluence, etc.) |
Liteskill.Usage | Token/cost tracking and aggregation |
Liteskill.Crypto | AES-256-GCM encryption at rest |
Liteskill.Rbac | Role-based permission checks |
Liteskill.Settings | Application-wide settings |
Key Design Decisions
- Event sourcing for chat — Conversations are modeled as event streams for full auditability and state replay. All other domains use standard CRUD with Ecto.
- CQRS — Write operations go through the event sourcing pipeline; reads query projection tables directly.
- Boundary enforcement — The
boundarylibrary enforces context dependencies at compile time, preventing unauthorized cross-context calls. - ReqLLM for all LLM transport — No direct HTTP calls to LLM providers. ReqLLM abstracts provider differences.
- Unified ACL table — A single
entity_aclstable handles access control for all entity types (conversations, reports, wiki spaces, agents, etc.). - Oban for background jobs — Embedding, data source sync, and agent runs are processed as Oban jobs with configurable queues and concurrency.
Event Sourcing
Liteskill uses event sourcing for the chat domain. Conversations are modeled as event streams rather than mutable rows.
Flow
Command → Aggregate → Event → EventStore (append) → PubSub broadcast → Projector → Projection tables
- A context function (e.g.
Chat.send_message/4) constructs a command tuple Aggregate.Loader.execute/3loads the aggregate state by replaying events (or from snapshot), executes the command, and appends resulting eventsEventStore.Postgres.append_events/3inserts events in a transaction with optimistic concurrency (unique index onstream_id, stream_version)- After successful append, events are broadcast via
Phoenix.PubSubon topic"event_store:<stream_id>" Chat.Projector(a GenServer in the supervision tree) receives events and updates projection tables
EventStore
Liteskill.EventStore.Postgres implements the Liteskill.EventStore behaviour:
append_events(stream_id, expected_version, events_data)— Append with optimistic concurrencyread_stream_forward(stream_id)— Read all events for a streamstream_version(stream_id)— Get the current versionsubscribe(stream_id)— Subscribe to PubSub notificationssave_snapshot/4andget_latest_snapshot/1— Snapshot support for performance
ConversationAggregate
State machine with states: :created → :active ↔ :streaming → :archived
Commands:
:create_conversation— Creates a new conversation:add_user_message— Adds a user message:start_stream— Begins LLM streaming:add_chunk— Appends a streaming chunk:complete_stream— Marks stream as complete:fail_stream— Records a stream failure:start_tool_call/:complete_tool_call— Tool calling lifecycle:archive— Archives the conversation:update_title— Updates the title:truncate_conversation— Truncates at a message boundary
Events
The system defines 12 domain event types:
| Event | Description |
|---|---|
ConversationCreated | Initial conversation setup |
UserMessageAdded | User sends a message |
AssistantStreamStarted | LLM response begins |
AssistantChunkReceived | Text delta received during streaming |
AssistantStreamCompleted | Stream finished successfully |
AssistantStreamFailed | Stream error occurred |
ToolCallStarted | LLM requests a tool call |
ToolCallCompleted | Tool execution finished |
ConversationForked | Branch from parent conversation |
ConversationTitleUpdated | Title changed |
ConversationArchived | Conversation closed |
ConversationTruncated | Messages truncated at a boundary |
Event Serialization
Events are stored with string keys (via stringify_keys). Deserialization back to structs happens through Events.deserialize/1. The event type string (e.g. "ConversationCreated") is mapped to its module via Events.module_for/1.
Stream IDs
Stream IDs follow the format "conversation-<uuid>".
Projector
Liteskill.Chat.Projector is a GenServer in the main supervision tree that projects events into read-model tables:
- Synchronous —
project_events/2blocks until projection is complete (used for write operations that need immediate read consistency) - Asynchronous —
project_events_async/2casts without waiting (used for streaming chunks where eventual consistency is acceptable) - Rebuild —
rebuild_projections/0replays all events from scratch to reconstruct projection tables
Snapshots
The aggregate loader supports snapshots for performance. When loading an aggregate, it first checks for the latest snapshot and replays only events after the snapshot version, avoiding full event replay for long-lived streams.
Supervision Tree
Liteskill uses a rest_for_one supervision strategy. If an infrastructure child (like Repo or PubSub) crashes, all children started after it restart too, re-establishing subscriptions and connections.
Child Process Order
The supervision tree starts children in this order:
LiteskillWeb.Telemetry— Telemetry metrics and pollerLiteskill.Desktop.PostgresManager— (Desktop mode only) Starts bundled PostgreSQLLiteskill.Repo— Ecto repository / database connection poolDNSCluster— DNS-based node clusteringPhoenix.PubSub— PubSub server (Liteskill.PubSub)Liteskill.Rag.EmbedQueue— Embedding request queue (skipped in test)Oban— Background job processing (queues:default,rag_ingest,data_sync,agent_runs)- Boot tasks — (non-test) Ensures admin user, system roles, env providers, and settings; auto-provisions admin in single-user mode
Liteskill.OpenRouter.StateStore— OAuth PKCE state storage for OpenRouterLiteskillWeb.Plugs.RateLimiter.Sweeper— Periodic ETS cleanup for rate limiterTask.Supervisor(Liteskill.TaskSupervisor) — For LLM streaming and async workLiteskill.Chat.StreamRegistry— Monitors active LLM stream tasks, triggers recovery on crashRegistry(Liteskill.LlmGateway.GateRegistry) — Per-provider gate registryDynamicSupervisor(Liteskill.LlmGateway.GateSupervisor) — Dynamic circuit breaker gatesLiteskill.LlmGateway.TokenBucket.Sweeper— Periodic ETS cleanup for token bucketsLiteskill.Chat.Projector— Event → projection table updaterLiteskill.Chat.StreamRecovery— Periodic sweep for stuck streaming conversationsLiteskill.Schedules.ScheduleTick— (non-test) Periodic check for due schedulesLiteskillWeb.Endpoint— Phoenix HTTP endpoint (started last)
LLM Gateway
The LLM Gateway provides per-provider concurrency control and circuit breaking:
GateRegistry— Unique registry keyed by provider IDGateSupervisor— Dynamically spawnsGateprocesses per providerTokenBucket— ETS-based rate limiting with periodic sweeper
Stream Registry
Liteskill.Chat.StreamRegistry monitors active LLM streaming tasks. If a streaming task crashes, the registry detects the :DOWN message and triggers stream recovery, preventing conversations from getting stuck in the :streaming state.
Stream Recovery
Liteskill.Chat.StreamRecovery is a periodic sweeper that detects conversations stuck in streaming state for more than 5 minutes and automatically recovers them by failing the stream.
Database
Liteskill uses PostgreSQL 16 with the pgvector extension for vector similarity search.
Conventions
- Binary UUIDs for all primary keys
:utc_datetimefor all timestamps- All schemas use
field :name, :stringeven for text columns - Foreign keys (e.g.
user_id) are set programmatically, never incast - Custom Postgrex types registered via
Liteskill.Repo.PostgrexTypes
Key Tables
Event Store
events— Append-only event log with(stream_id, stream_version)unique indexsnapshots— Aggregate snapshots for performance
Projections (Chat)
conversations— Current conversation state (stream_id, title, status, model, message_count)messages— Projected messages (role, content, status, token counts, position)message_chunks— Streaming chunks (delta_text, delta_type, chunk_index)tool_calls— Tool call records (name, arguments, status, result)
Accounts & Auth
users— User records (OIDC and password auth)invitations— Admin-created invite tokensentity_acls— Unified ACL table for all entity typesrbac_roles/rbac_role_permissions/rbac_user_roles— Role-based access control
LLM
llm_providers— Provider configurations (API keys encrypted, regions, types)llm_models— Model definitions linked to providers (costs, context window, type)usage_records— Token/cost tracking per API call
RAG
rag_collections— Top-level grouping for embeddings (configurable dimension)rag_sources— Sources within collectionsrag_documents— Documents to be chunked and embeddedrag_chunks— Chunked text with pgvector embeddings
Data Sources
data_sources— External connectors (Google Drive, Confluence, Jira, GitHub, GitLab, SharePoint)data_source_documents— Documents synced from connectors
Agents & Teams
agent_definitions— AI agent configurations (backstory, strategy, model)team_definitions— Agent team compositionsteam_members— Position-ordered team membership with roles
Runs
runs— Agent/team execution records (status, prompt, topology)run_tasks— Individual task tracking within a runrun_logs— Structured execution logs (level, step, message, metadata)
Features
reports/report_sections/section_comments— Structured report documentsschedules— Cron-based scheduled runsmcp_servers— MCP server registrationsuser_tool_selections— Per-user MCP server selectionssettings— Application-wide settings
Background Jobs
oban_jobs/oban_peers— Oban background job queue tables
Conversations
Conversations are the core feature of Liteskill. Each conversation is an event-sourced stream that records every user message, LLM response, and tool call.
Creating Conversations
Users create conversations from the main UI. Each conversation has:
- A title (auto-generated or user-set)
- A model selection (from configured LLM models)
- An optional system prompt
- An optional LLM model override
Messaging
When a user sends a message:
- A
UserMessageAddedevent is appended to the conversation stream - The LLM
StreamHandlerstarts a streaming response - Chunks arrive as
AssistantChunkReceivedevents - The stream completes with
AssistantStreamCompleted
Tool Calling
During streaming, the LLM may request tool calls:
- Auto-confirm mode: Tool calls execute automatically via MCP servers
- Manual mode: The UI pauses for user approval before executing each tool call
Tool calls are recorded as ToolCallStarted and ToolCallCompleted events.
RAG Context
When RAG is configured, the user’s message is embedded and searched against accessible collections. Relevant document chunks are included as context for the LLM. RAG sources are tracked per message for transparency.
Forking
Conversations can be forked at any message position, creating a new independent conversation with the history up to that point. The fork creates a new event stream with copied events and remapped IDs.
Editing Messages
Users can edit a previously sent message. This truncates the conversation at that message position and re-sends with the new content, effectively creating a new branch point.
Sharing
Conversation access is controlled via ACLs:
- The creator is automatically the owner
- Owners can grant manager access to other users or groups
- Shared conversations appear in the recipient’s conversation list
Archiving
Conversations can be archived individually or in bulk. Archived conversations are hidden from the default list but retain their full event history.
Stream Recovery
A periodic sweeper (Liteskill.Chat.StreamRecovery) detects conversations stuck in streaming state for more than 5 minutes and automatically recovers them by failing the stream. Additionally, the StreamRegistry monitors active streaming tasks and triggers immediate recovery if a task crashes.
LLM Providers
Liteskill supports multiple LLM providers through a pluggable provider system powered by ReqLLM.
Provider Types
Providers are configured in the database via the admin UI. ReqLLM supports 56+ providers including:
- Amazon Bedrock — AWS-hosted models (Claude, Llama, etc.)
- OpenRouter — Multi-model gateway with OAuth PKCE support
- OpenAI-compatible — Any endpoint that speaks the OpenAI API format (vLLM, LiteLLM, etc.)
- Anthropic — Direct Anthropic API access
- Google — Gemini models via Google AI
- Azure OpenAI — Azure-hosted OpenAI models
Provider Configuration
Each provider record stores:
- Name — Display name
- Provider type — Determines the API protocol
- API key — Encrypted at rest via
Liteskill.Crypto - Base URL — Optional override for custom endpoints (e.g. LiteLLM proxies, local vLLM)
- Provider config — Type-specific settings (e.g. AWS region, Azure deployment ID)
- Instance-wide flag — If true, available to all users
- Status — Active or inactive
Access Control
- Instance-wide providers are available to all users
- User-owned providers are private to their creator (configured at
/profile/providers) - Admin-granted access — Admins can grant
viewerrole on a provider to specific users via ACLs
Models
Models are defined under providers. Each model specifies:
- Model ID — The provider’s model identifier (e.g.
anthropic/claude-3-5-haiku) - Display name — Shown in the UI
- Model type —
inference,embedding, orrerank - Cost rates — Input/output cost per million tokens
- Context window — Maximum token limit
- Instance-wide flag — Available to all users when set
- Active/inactive status
Users select models when creating conversations or configuring agents.
Environment-Based Providers
On boot, Liteskill checks for environment variables and auto-creates providers:
AWS_BEARER_TOKEN_BEDROCK+AWS_REGION— Creates an instance-wide Bedrock provider
LLM Gateway
The LLM Gateway provides per-provider infrastructure:
- Circuit breaker — Tracks failures per provider and opens the circuit after a threshold, preventing cascading failures
- Concurrency gates — Limits concurrent requests per provider
- Token bucket — ETS-based rate limiting with periodic cleanup
ReqLLM Configuration
ReqLLM connection pools and timeouts are configured in config/runtime.exs:
- Stream receive timeout: 120 seconds (accommodates tool-calling rounds)
- HTTP/1.1 pool with 25 connections
MCP Tools
Liteskill integrates with Model Context Protocol (MCP) servers to give LLMs access to external tools.
How It Works
- Register an MCP server with its HTTP URL
- Liteskill discovers available tools via
tools/list - During conversation streaming, the LLM can request tool calls
- Liteskill executes tool calls via
tools/calland feeds results back
MCP Client
Liteskill.McpServers.Client implements the MCP JSON-RPC 2.0 Streamable HTTP transport:
- Initialize — Sends
initializerequest, receives session ID - Initialized — Sends
notifications/initializednotification - Request — Sends
tools/listortools/callwith the session ID
The client supports:
- Automatic retry with exponential backoff on 429/5xx errors (up to 2 retries)
- Session ID tracking via
mcp-session-idheader - Custom headers per server
- API key authentication (sent as
Authorization: Bearer <key>) - SSE response parsing
Server Management
- Users can create, update, and delete their own MCP servers
- Global servers (set by admin) are available to all users
- Servers can be shared via ACLs
Tool Selections
Users select which MCP servers are active for their conversations:
- Selections are persisted per user via
UserToolSelection - Selected servers’ tools are included in LLM requests
- Stale selections (for deleted servers) are automatically pruned
Built-in Servers
Virtual servers prefixed with builtin: are defined in code (Liteskill.BuiltinTools) and merged into the server list. They cannot be modified or deleted. These provide tools for built-in features like reports and wiki.
SSRF Protection
Server URLs are validated to prevent Server-Side Request Forgery (SSRF). By default:
- Only HTTPS URLs are accepted
- Private and reserved addresses are blocked (
localhost,127.*,10.*,172.16-31.*,192.168.*,169.254.*, IPv6 loopback, andhost.docker.internal)
To allow private URLs (e.g. for self-hosted MCP servers), enable Allow private MCP URLs in server settings.
Docker Networking
The Liteskill app container uses network_mode: host, which means it shares the host’s network stack directly. MCP servers running on the host machine are reachable at localhost:
http://localhost:4005
This requires Allow private MCP URLs enabled in server settings, since localhost resolves to a private address.
RAG (Retrieval-Augmented Generation)
Liteskill includes a full RAG pipeline: document ingestion, chunking, embedding generation, semantic search, and reranking.
Pipeline
- Ingest — Documents are added to a collection via URL, manual upload, or data source sync
- Chunk — Documents are split into chunks using a recursive text splitter (paragraph → line → sentence → word boundaries)
- Embed — Chunks are embedded using a configured embedding model (Cohere or OpenAI-compatible)
- Search — User queries are embedded and matched against chunks using pgvector cosine similarity
- Rerank — Search results are optionally reranked using a Cohere rerank model
Data Model
- Collection — Top-level grouping (e.g. “Wiki”, “Engineering Docs”) with configurable embedding dimension (256–1536, default 1024)
- Source — A source within a collection (e.g. “wiki”, “manual”)
- Document — A single document with content, metadata, and content hash
- Chunk — A text chunk with position, token count, and its pgvector embedding
Embedding
Embeddings are generated via Liteskill.Rag.EmbedQueue, a GenServer that batches requests and manages throughput. Two client implementations:
CohereClient— For Cohere’s embed and rerank APIs (including Cohere on AWS Bedrock)OpenAIEmbeddingClient— For OpenAI-compatible embedding endpoints
Context Augmentation
During conversations, RAG context is injected automatically:
- The user’s message is embedded
- All accessible collections are searched
- Top results are optionally reranked
- Relevant chunks are included as context for the LLM
- RAG sources are tracked per message for citation
Wiki Integration
Wiki pages are automatically synced to RAG collections. When a wiki page is created or updated, a background job (WikiSyncWorker) updates the corresponding RAG document and re-embeds its chunks.
URL Ingestion
The IngestWorker (Oban job) handles URL-based document ingestion:
- Fetches URL via Req
- Validates content type (rejects binary content)
- Auto-creates a source from the domain name
- Chunks the response body
- Enqueues embedding via
EmbedQueue - Retries up to 3 times on transient failures
Re-embedding
Admins can trigger a full re-embedding of all documents (e.g. after changing the embedding model) via the ReembedWorker.
Agent Studio
The Agent Studio lets users define reusable AI agents with specific configurations, tool access, and data source access.
Agent Definitions
Each agent definition includes:
- Name and backstory — Identity and persona
- Strategy — Behavioral approach (
react,chain_of_thought,tree_of_thoughts,direct) - Opinions — Behavioral guidance and perspective
- LLM model — The model the agent uses for inference
- System prompt — Optional per-agent system prompt override
- Tool access — ACL-controlled access to specific MCP servers
- Data source access — ACL-controlled access to specific data sources for RAG
Tool Access
Agents are granted access to MCP servers via ACLs on the mcp_server entity type. During execution, the ToolResolver resolves which tools the agent can use based on its ACLs.
Data Source Access
Similarly, agents can be granted access to data sources. During runs, the agent’s RAG context is scoped to only the data sources it has access to.
Teams
Agents can be composed into teams with different topologies:
- Sequential (Pipeline) — Agents execute in order, passing results forward. Later agents see the accumulated context from prior stages.
- Parallel — Agents execute concurrently
- Supervisor — A supervisor agent coordinates other agents
Team members are position-ordered and assigned roles (lead, analyst, reviewer, editor).
Execution
Agents execute through the Runs system. A run tracks the full lifecycle of an agent or team execution, including:
- Prompt — Input for the pipeline
- Logs — Structured execution logs with level, step, message, and metadata
- Tasks — Individual task tracking within a run
- Usage — Token/cost accounting per run
- Report — Optional deliverable report generated from the run output
Jido Integration
Under the hood, agent execution uses the Jido framework (v2.0) for structured agent actions and workflows. The LlmGenerate action provides a synchronous agentic loop with tool calling, context pruning, and cost limits.
Access Control
Agent definitions use the standard ACL system:
- Creator is the owner
- Access can be granted to other users
- Only accessible agents appear in the agent list
Routes
/agents— Agent Studio landing page/agents/list— List all agents/agents/new— Create a new agent/agents/:agent_id— View agent details/agents/:agent_id/edit— Edit an agent/teams— List teams/teams/new— Create a new team/teams/:team_id— View/edit a team/runs— List runs/runs/new— Start a new run/runs/:run_id— View run details and logs
Reports
Reports are structured documents with hierarchical sections, markdown rendering, and a comment/review workflow.
Structure
- Report — Top-level document with a title and owner
- Sections — Infinitely nested sections with titles, content, and position ordering
- Comments — Threaded comments on sections or the report itself
Section Management
Sections are addressed by path (e.g. "Executive Summary > Key Findings"). The path-based API supports:
- Upsert — Create or update a section at a path
- Delete — Remove a section and its children
- Move — Reposition a section among its siblings
Batch operations via modify_sections/3 allow multiple actions in a single transaction.
Comments
Comments support a review workflow:
- Comments have an
author_type("user"or"agent") - Comments can be open or addressed
- Resolving a comment creates a reply and marks it as addressed
- Comments can be threaded (replies to replies)
An LLM agent can be instructed to address open comments, making edits and resolving each one.
Markdown Rendering
render_markdown/2 produces a complete markdown document from a report:
- Section titles become
#,##,###headers based on nesting depth - Comments are rendered as blockquotes with status labels
- Comments can be included or excluded
Access Control
Reports use the standard ACL system:
- Creator is the owner
- Access can be granted to other users at the manager level
- Shared reports appear in the recipient’s report list
Run Integration
Reports can be created as part of an agent run, linking the report to its source run for traceability. The ReportBuilder in the runs system generates structured report output from pipeline execution.
Routes
/reports— List all reports/reports/:report_id— View/edit a report
Wiki
Liteskill includes a built-in collaborative wiki for documentation and knowledge management.
Structure
The wiki uses a hierarchical document model:
- Spaces — Top-level wiki documents (root pages with no parent)
- Pages — Child documents nested under spaces
- Pages can be nested to arbitrary depth
All wiki documents use the built-in "builtin:wiki" source reference.
Creating Content
- Create a new wiki space from the wiki page (
/wiki) - Add child pages under any space or page
- Edit pages with markdown content
- Pages have titles, slugs, and optional content
Sharing
Wiki spaces use ACL-based sharing:
- The space creator is the owner
- Owners can grant viewer, editor, or manager access to other users
- Editors can create and edit pages within the space
- Managers can also delete pages and manage access
- Shared spaces appear in the recipient’s wiki
RAG Integration
Wiki content is automatically indexed for RAG:
- When a page is created or updated, a
WikiSyncWorkerjob is enqueued - The worker creates/updates a RAG document and re-embeds chunks
- Wiki collections are shared with users who have access to the wiki space
- This means RAG search results include relevant wiki content from shared spaces
Export & Import
- Wiki spaces can be exported via the
/wiki/:space_id/exportroute - Reports can be exported to wiki pages via
DataSources.export_report_to_wiki/3 - Wiki content can be imported from external sources
Routes
/wiki— Wiki home (list all accessible spaces)/wiki/:document_id— View/edit a wiki page
Schedules
Schedules enable cron-based recurring execution of agent runs.
How It Works
- Create a schedule with a cron expression, timezone, and run template
ScheduleTick(a periodic GenServer) checks for due schedules every minute- When a schedule is due, it enqueues a
ScheduleWorkerOban job - The worker creates and starts a run based on the schedule’s template
next_run_atis recalculated for the next occurrence
Schedule Configuration
Each schedule defines:
- Name — Display name
- Cron expression — Standard 5-field format (minute, hour, day-of-month, month, day-of-week)
- Timezone — The cron expression is interpreted in this timezone
- Team definition — The agent team to execute
- Enabled flag — Schedules can be toggled on/off
- Status —
activeor other lifecycle states
Cron Parser
The built-in cron parser supports:
- Wildcards (
*) - Step values (
*/5) - Exact values (
30) - Comma-separated lists (
1,15,30)
Access Control
Schedules use the standard ACL system. Only the owner can update or delete a schedule.
Routes
/schedules— List all schedules/schedules/new— Create a new schedule/schedules/:schedule_id— View/edit a schedule
Chat Context
Liteskill.Chat is the primary context for conversation management. It provides write and read APIs backed by event sourcing.
Boundary
use Boundary,
top_level?: true,
deps: [Liteskill.Aggregate, Liteskill.Authorization, Liteskill.EventStore, Liteskill.Rbac, Liteskill.LlmModels],
exports: [Conversation, ConversationAggregate, Events, Message, MessageBuilder, MessageChunk, Projector, StreamRecovery, StreamRegistry, ToolCall]
Write API
All write operations go through the event sourcing pipeline: command → aggregate → event store → projector.
| Function | Description |
|---|---|
create_conversation(params) | Creates a new conversation with RBAC check |
send_message(conversation_id, user_id, content, opts) | Adds a user message |
fork_conversation(conversation_id, user_id, at_message_position) | Forks at a message boundary |
archive_conversation(conversation_id, user_id) | Archives a conversation |
update_title(conversation_id, user_id, title) | Updates the title |
truncate_conversation(conversation_id, user_id, message_id) | Truncates at a message |
edit_message(conversation_id, user_id, message_id, new_content, opts) | Truncates then re-sends |
Read API
Read operations query the projection tables directly.
| Function | Description |
|---|---|
list_conversations(user_id, opts) | Lists accessible conversations (paginated, searchable) |
count_conversations(user_id, opts) | Counts accessible conversations |
get_conversation(id, user_id) | Gets a conversation with messages |
list_messages(conversation_id, user_id, opts) | Lists messages (paginated) |
get_conversation_tree(conversation_id, user_id) | Gets fork tree |
replay_conversation(conversation_id, user_id) | Replays aggregate state |
ACL Management
Delegates to Liteskill.Authorization:
grant_conversation_access/4— Grant user access (normalizes “member” to “manager”)revoke_conversation_access/3— Revoke user accessleave_conversation/2— User leaves a shared conversationgrant_group_access/4— Grant group access
Streaming
Streaming is initiated after a user message is sent. The StreamHandler (in Liteskill.LLM) manages the LLM streaming lifecycle, emitting events back to the conversation’s event stream. The Projector updates projection tables as events arrive, and LiveView receives real-time updates via PubSub.
Message Builder
Liteskill.Chat.MessageBuilder constructs the message history for LLM requests from the conversation’s projected messages, including tool call results and RAG context.
Accounts Context
Liteskill.Accounts manages user records, authentication, and invitations.
Boundary
use Boundary, top_level?: true, deps: [], exports: [User, Invitation]
Authentication
Liteskill supports dual authentication:
OIDC (via Ueberauth)
find_or_create_from_oidc(attrs)— Finds or creates a user by OIDC subject + issuer- Idempotent — safe to call on every login callback
Password (via Argon2)
register_user(attrs)— Creates a user with email/passwordauthenticate_by_email_password(email, password)— Validates credentialschange_password(user, current_password, new_password)— Changes password (requires current)setup_admin_password(user, password)— First-time admin setup (no current password required)set_temporary_password(user, password)— Admin sets a temp password (forces change on login)
User Management
| Function | Description |
|---|---|
get_user!(id) | Gets user by ID (raises if not found) |
get_user(id) | Gets user by ID (returns nil) |
get_user_by_email(email) | Finds user by email |
list_users() | Lists all users ordered by email |
search_users(query, opts) | Searches by name or email |
update_user_role(user_id, role) | Updates role (prevents demoting root admin) |
update_preferences(user, new_prefs) | Merges new preferences |
Root Admin
On boot, ensure_admin_user/0 creates [email protected] if missing and forces its role to "admin".
Single-User Mode
When SINGLE_USER_MODE=true, SingleUser.auto_provision_admin/0 runs on boot to ensure the admin user is ready without manual setup.
Invitations
create_invitation(email, admin_user_id)— Creates an invite tokenget_invitation_by_token(token)— Looks up an invitationaccept_invitation(token, attrs)— Accepts: validates token, creates user, marks as used (transactional)revoke_invitation(id)— Deletes a pending invitationlist_invitations()— Lists all invitations
Authorization Context
Liteskill.Authorization provides centralized access control for all entity types via a single entity_acls table.
Boundary
use Boundary, top_level?: true, deps: [Liteskill.Groups], exports: [EntityAcl, Roles]
Role Hierarchy
viewer < editor < manager < owner
| Role | Permissions |
|---|---|
| viewer | Read-only access |
| editor | Can edit content (wiki spaces only) |
| manager | Edit + grant/revoke viewer/editor/manager access |
| owner | Full control (delete, demote anyone, transfer ownership) |
Access Checks
| Function | Description |
|---|---|
has_access?(entity_type, entity_id, user_id) | Any access (direct or group-based) |
get_role(entity_type, entity_id, user_id) | Highest role across direct + group ACLs |
can_manage?(entity_type, entity_id, user_id) | Manager or owner |
can_edit?(entity_type, entity_id, user_id) | Editor, manager, or owner |
owner?(entity_type, entity_id, user_id) | Owner only |
ACL Management
| Function | Description |
|---|---|
create_owner_acl(entity_type, entity_id, user_id) | Auto-created on resource creation |
grant_access(entity_type, entity_id, grantor_id, grantee_user_id, role) | Grant user access |
grant_group_access(entity_type, entity_id, grantor_id, group_id, role) | Grant group access |
update_role(entity_type, entity_id, grantor_id, target_user_id, new_role) | Change a user’s role |
revoke_access(entity_type, entity_id, revoker_id, target_user_id) | Revoke user access |
leave(entity_type, entity_id, user_id) | User leaves (owners cannot leave) |
Query Helpers
accessible_entity_ids(entity_type, user_id)— Subquery of entity IDs the user can accessusage_accessible_entity_ids(entity_type, user_id)— Same but excludes “owner” role
Entity Types
The ACL system is used across: conversation, report, wiki_space, mcp_server, agent_definition, team_definition, run, schedule, source, llm_provider, llm_model.
Groups Context
Liteskill.Groups manages user groups and memberships, used for group-based ACL authorization.
Boundary
use Boundary, top_level?: true, deps: [], exports: [Group, GroupMembership]
Group Management
| Function | Description |
|---|---|
create_group(name, creator_id) | Creates a group; creator becomes owner member |
list_groups(user_id) | Lists groups the user belongs to |
get_group(id, user_id) | Gets a group (requires membership) |
delete_group(group_id, user_id) | Deletes a group (creator only) |
Membership Management
| Function | Description |
|---|---|
add_member(group_id, requester_id, target_user_id, role) | Adds a member (creator only) |
remove_member(group_id, requester_id, target_user_id) | Removes a member (creator only, cannot remove owner) |
leave_group(group_id, user_id) | User leaves (creator cannot leave) |
Admin Functions
Admin functions bypass creator/membership checks:
list_all_groups/0— Lists all groups with memberships and creator preloadedadmin_get_group/1,admin_get_group_by_name/1admin_list_members/1,admin_add_member/3,admin_remove_member/2admin_delete_group/1
Usage with ACLs
Groups are used in the Authorization context for group-based access. When a group is granted access to an entity (e.g. a conversation), all members of that group inherit that access via a join with group_memberships.
LLM Context
Liteskill.LLM is the public API for LLM interactions, providing both streaming and non-streaming completions.
Boundary
use Boundary,
top_level?: true,
deps: [Liteskill.Chat, Liteskill.Aggregate, Liteskill.EventStore, Liteskill.Usage, Liteskill.LlmModels, Liteskill.LlmGateway, Liteskill.McpServers],
exports: [StreamHandler, ToolUtils, RagContext]
Non-Streaming Completions
LLM.complete(messages, opts) sends a single-turn completion request via ReqLLM. Used for tasks like auto-generating conversation titles.
Options:
:llm_model— A%LlmModel{}struct with full provider config:model_id— Model ID string (requires:provider_options):max_tokens,:temperature,:system— Standard LLM parameters:generate_fn— Override for testing
Streaming
Streaming is handled by LLM.StreamHandler, which orchestrates:
- Starting a streaming request via ReqLLM
- Appending events to the conversation’s event stream
- Retrying on 429/503/408 errors and transport failures (up to 3 retries with exponential backoff)
- Executing tool calls (auto-confirm or pause for approval)
- Recording usage after each round
Model Resolution
Models are configured in the database via LlmModels and LlmProviders:
available_models(user_id)— Returns active inference models accessible to the user- No hardcoded model IDs or env-var fallbacks for model selection
LLM Gateway
The gateway provides per-provider infrastructure:
- Circuit breaker — Tracks failures and opens the circuit after threshold
- Concurrency gates — Limits concurrent requests per provider
- Token bucket — ETS-based rate limiting with periodic cleanup
Usage Recording
After each streaming round, usage is recorded via Liteskill.Usage.record_from_response/2. This captures input tokens, output tokens, reasoning tokens, cached tokens, costs, and latency.
RagContext
Liteskill.LLM.RagContext builds RAG context for prompts:
- Searches accessible RAG collections for relevant documents
- Injects top-k chunks as system context
- Tracks RAG sources for citation and traceability
ToolUtils
Liteskill.LLM.ToolUtils provides shared utilities:
- Convert tool specs to ReqLLM format
- Dispatch tool execution to MCP servers
- Format tool outputs for LLM consumption
- Normalize tool call formats across providers
MCP Servers Context
Liteskill.McpServers manages MCP server registrations and user tool selections.
Boundary
use Boundary,
top_level?: true,
deps: [Liteskill.Authorization, Liteskill.Rbac, Liteskill.BuiltinTools, Liteskill.Settings],
exports: [McpServer, Client, UserToolSelection]
Server CRUD
| Function | Description |
|---|---|
list_servers(user_id) | Lists accessible servers (owned + global + ACL’d) plus built-in |
get_server(id, user_id) | Gets a server by ID with access check |
create_server(attrs) | Creates a server with RBAC check and owner ACL |
update_server(server, user_id, attrs) | Updates (owner only) |
delete_server(id, user_id) | Deletes (owner only) |
Built-in Servers
Virtual servers prefixed with builtin: are defined in code (Liteskill.BuiltinTools) and merged into the server list. They cannot be modified or deleted.
Tool Selections
Users select which servers are active for their conversations:
| Function | Description |
|---|---|
load_selected_server_ids(user_id) | Loads selections, prunes stale entries |
select_server(user_id, server_id) | Persists a selection (idempotent) |
deselect_server(user_id, server_id) | Removes a selection |
clear_selected_servers(user_id) | Removes all selections |
Client
Liteskill.McpServers.Client implements JSON-RPC 2.0 over HTTP:
list_tools(server, opts)— Discovers available toolscall_tool(server, tool_name, arguments, opts)— Executes a tool call
Features:
- MCP initialization handshake (initialize → initialized → request)
- Automatic retry with exponential backoff (up to 2 retries)
- Session ID tracking via
mcp-session-idheader - Custom header support with blocked header filtering for security
- API key authentication via
Authorization: Bearer <key>header
RAG Context
Liteskill.Rag manages the RAG pipeline: collections, sources, documents, chunks, embeddings, and search.
Boundary
use Boundary,
top_level?: true,
deps: [Liteskill.Authorization, Liteskill.DataSources, Liteskill.LlmModels, Liteskill.LlmProviders, Liteskill.Settings],
exports: [Collection, Source, Document, Chunk, Chunker, CohereClient, DocumentSyncWorker, EmbedQueue, EmbeddingClient, EmbeddingRequest, IngestWorker, OpenAIEmbeddingClient, Pipeline, ReembedWorker, WikiSyncWorker]
Data Model
- Collection — Top-level grouping, per user, with configurable embedding dimension (256–1536, default 1024)
- Source — A source within a collection (e.g. “wiki”, “manual”)
- Document — Content with title, status, metadata, and content hash
- Chunk — Text chunk with position, token count, and pgvector embedding
Collection & Source CRUD
Standard CRUD with user ownership checks. Collections and sources are scoped to the creating user.
Embedding
embed_chunks(document_id, chunks, user_id, opts):
- Validates ownership chain (document → source → collection)
- Sends texts to
EmbedQueuefor embedding - Inserts chunk rows with pgvector embeddings in a transaction
- Updates document status to
"embedded"
Search
| Function | Description |
|---|---|
search(collection_id, query, user_id, opts) | Vector search within a collection |
rerank(query, chunks, opts) | Rerank results via Cohere |
search_and_rerank(collection_id, query, user_id, opts) | Combined search + rerank |
search_accessible(collection_id, query, user_id, opts) | ACL-aware search for shared collections |
augment_context(query, user_id, opts) | Cross-collection search for conversation context |
Wiki Integration
find_or_create_wiki_collection(user_id)— Gets or creates the “Wiki” collectionfind_or_create_wiki_source(collection_id, user_id)— Gets or creates the “wiki” sourcefind_rag_document_by_wiki_id(wiki_document_id, user_id)— Finds RAG doc by wiki doc ID
URL Ingestion
The IngestWorker (Oban job) handles URL-based ingestion:
- Fetches URL content via Req
- Validates text content (rejects binary types)
- Auto-creates source from domain name
- Chunks content and enqueues for embedding
- Retries up to 3 times on transient failures
Background Workers
| Worker | Queue | Purpose |
|---|---|---|
IngestWorker | rag_ingest | URL-based document ingestion |
WikiSyncWorker | rag_ingest | Sync wiki pages to RAG |
DocumentSyncWorker | rag_ingest | Sync data source documents to RAG |
ReembedWorker | rag_ingest | Re-embed all documents after model change |
Reports Context
Liteskill.Reports manages structured reports with nested sections and a comment/review workflow.
Boundary
use Boundary,
top_level?: true,
deps: [Liteskill.Accounts, Liteskill.Authorization, Liteskill.Rbac],
exports: [Report, ReportSection, SectionComment]
Report CRUD
| Function | Description |
|---|---|
create_report(user_id, title, opts) | Creates a report with RBAC check and owner ACL |
list_reports(user_id) | Lists accessible reports |
list_reports_paginated(user_id, page) | Paginated list (20 per page) |
get_report(report_id, user_id) | Gets report with ordered sections |
delete_report(report_id, user_id) | Deletes (owner only) |
Section Management
Sections are addressed by > delimited paths (e.g. "Summary > Findings").
| Function | Description |
|---|---|
upsert_section(report_id, user_id, path, content) | Create or update a section |
upsert_sections(report_id, user_id, sections) | Batch upsert |
modify_sections(report_id, user_id, actions) | Batch upsert/delete/move |
update_section_content(section_id, user_id, attrs) | Direct update |
delete_section(section_id, user_id) | Delete a section |
Comments
| Function | Description |
|---|---|
add_comment(section_id, user_id, body, author_type) | Add a section comment |
add_report_comment(report_id, user_id, body, author_type) | Add a report-level comment |
resolve_comment(comment_id, user_id, body) | Resolve with a reply |
reply_to_comment(comment_id, user_id, body, author_type) | Thread a reply |
manage_comments(report_id, user_id, actions) | Batch add/resolve |
Rendering
render_markdown(report, opts)— Full markdown with headers and optional commentssection_tree(report)— Nested tree structure for UI rendering
Agents Context
Liteskill.Agents manages agent definitions — the “character sheets” for AI agents.
Boundary
use Boundary,
top_level?: true,
deps: [Liteskill.Authorization, Liteskill.Rbac, Liteskill.McpServers, Liteskill.LLM, Liteskill.LlmGateway, Liteskill.Usage, Liteskill.LlmModels, Liteskill.Rag, Liteskill.DataSources],
exports: [AgentDefinition, ToolResolver, JidoAgent, Actions.LlmGenerate]
Agent CRUD
| Function | Description |
|---|---|
create_agent(attrs) | Creates with RBAC check, model validation, and owner ACL |
update_agent(id, user_id, attrs) | Updates (owner only) |
delete_agent(id, user_id) | Deletes (owner only) |
list_agents(user_id) | Lists accessible agents (owned + ACL’d) |
get_agent(id, user_id) | Gets with access check |
Tool Access
Agents are granted access to MCP servers via entity ACLs:
| Function | Description |
|---|---|
grant_tool_access(agent_id, mcp_server_id, user_id) | Grant MCP server access |
revoke_tool_access(agent_id, mcp_server_id, user_id) | Revoke MCP server access |
list_tool_server_ids(agent_id) | List accessible server IDs |
list_accessible_servers(agent_id) | List accessible server structs |
Data Source Access
Agents can also be granted access to data sources for RAG:
| Function | Description |
|---|---|
grant_source_access(agent_id, source_id, user_id) | Grant data source access |
revoke_source_access(agent_id, source_id, user_id) | Revoke data source access |
list_source_ids(agent_id) | List accessible source IDs |
Model Validation
When creating or updating an agent with an llm_model_id, the context validates that the user has access to that model via LlmModels.get_model/2.
Tool Resolution
Liteskill.Agents.ToolResolver resolves the set of tools available to an agent at execution time by querying its MCP server ACLs and fetching tool lists from each accessible server.
Execution
Agent execution is handled by Liteskill.Agents.Actions.LlmGenerate, which provides a synchronous agentic loop:
- Build messages from context
- Send to LLM with available tools
- If LLM requests tool calls, execute them and loop
- Apply context pruning if needed
- Enforce cost limits per run
Usage Context
Liteskill.Usage records and queries LLM token usage and costs.
Boundary
use Boundary,
top_level?: true,
deps: [Liteskill.Groups, Liteskill.LlmModels, Liteskill.Rag],
exports: [UsageRecord, CostCalculator]
Recording Usage
| Function | Description |
|---|---|
record_usage(attrs) | Inserts a raw usage record |
record_from_response(usage, opts) | Builds and inserts from an API response map |
record_from_response/2 resolves costs via CostCalculator, assembles all fields, and inserts. No-ops when user_id is nil.
Each record tracks: input_tokens, output_tokens, total_tokens, reasoning_tokens, cached_tokens, cache_creation_tokens, input_cost, output_cost, reasoning_cost, total_cost, latency_ms, call_type, and tool_round.
Aggregation Queries
| Function | Description |
|---|---|
usage_by_conversation(conversation_id) | Totals for a conversation |
usage_by_user(user_id, opts) | Totals for a user (with time filters) |
usage_by_user_and_model(user_id, opts) | Per-model breakdown for a user |
usage_by_group(group_id, opts) | Totals for all group members |
usage_by_groups(group_ids, opts) | Batch group totals |
usage_by_run(run_id) | Totals for a run |
usage_by_run_since(run_id, since) | Run usage since a timestamp |
usage_by_run_and_model(run_id) | Per-model breakdown for a run |
usage_summary(opts) | Flexible query builder with grouping |
instance_totals(opts) | Instance-wide totals |
daily_totals(opts) | Daily aggregates |
Cost Limits
check_cost_limit(:conversation, id, limit)— Checks if conversation cost exceeds limitcheck_cost_limit(:run, id, limit)— Checks if run cost exceeds limit
Cost Calculator
Liteskill.Usage.CostCalculator computes costs from token counts and model pricing:
- Resolves per-model input/output rates from the
LlmModelrecord - Calculates separate costs for input, output, and reasoning tokens
- Handles cached token pricing
Embedding Usage
Separate tracking for embedding API calls:
embedding_totals(opts)— Aggregate embedding usage
Data Sources Context
Liteskill.DataSources manages external data connectors and their documents.
Boundary
use Boundary,
top_level?: true,
deps: [Liteskill.Authorization, Liteskill.Rbac, Liteskill.BuiltinSources, Liteskill.Reports],
exports: [Source, Document, SyncWorker, Connector, ConnectorRegistry, ContentExtractor, Connectors.GoogleDrive, Connectors.Wiki, WikiExport, WikiImport]
Supported Source Types
| Type | Config Fields |
|---|---|
| Google Drive | Service account JSON, folder ID |
| SharePoint | Tenant ID, site URL, client ID, client secret |
| Confluence | Base URL, username, API token, space key |
| Jira | Base URL, username, API token, project key |
| GitHub | Personal access token, repository |
| GitLab | Personal access token, project path |
Source CRUD
| Function | Description |
|---|---|
list_sources(user_id) | Lists sources (owned + ACL’d + built-in) |
list_sources_with_counts(user_id) | Same with document counts |
get_source(id, user_id) | Gets with access check |
create_source(attrs, user_id) | Creates with RBAC and owner ACL |
update_source(id, attrs, user_id) | Updates (built-in sources cannot be updated) |
delete_source(id, user_id) | Deletes source and its documents |
Document Management
| Function | Description |
|---|---|
list_documents(source_ref, user_id) | Lists documents for a source |
list_documents_paginated(source_ref, user_id, opts) | Paginated with search |
get_document(id, user_id) | Gets with ownership or wiki ACL check |
create_document(source_ref, attrs, user_id) | Creates (wiki docs auto-create ACLs) |
create_child_document(source_ref, parent_id, attrs, user_id) | Creates nested document |
update_document(id, attrs, user_id) | Updates (wiki docs trigger RAG sync) |
delete_document(id, user_id) | Deletes with role checks for wiki docs |
Sync Pipeline
start_sync(source_id, user_id)— Enqueues aSyncWorkerOban jobSyncWorkeruses theConnectorRegistryto dispatch to the correct connector (Google Drive, SharePoint, etc.)- Each connector fetches updated documents, creates/updates local records, and triggers RAG re-embedding
Wiki Integration
Connectors.Wikihandles built-in wiki documentsWikiExportexports wiki spaces as structured documentsWikiImportimports wiki content from external sourcesexport_report_to_wiki/3converts a report to wiki pages
Content Extraction
ContentExtractor handles converting various document formats to plain text for embedding.
Built-in Sources
Liteskill.BuiltinSources defines virtual sources that exist in code (e.g. the Wiki source). These appear in the source list but cannot be modified or deleted.
Router
LiteskillWeb.Router defines all routes for the application. Routes are organized into scopes with different pipelines.
Pipelines
| Pipeline | Purpose |
|---|---|
:browser | HTML requests with session, CSRF, LiveView flash |
:api | JSON requests with session, auth, and rate limiting (1000 req/min) |
:require_auth | Requires authenticated user |
Auth Routes (/auth)
| Route | Description |
|---|---|
GET /auth/session | Session bridge for LiveView auth |
DELETE /auth/logout | Logout |
GET /auth/openrouter | OpenRouter OAuth PKCE start |
GET /auth/openrouter/callback | OpenRouter OAuth callback |
POST /auth/register | Password registration (API) |
POST /auth/login | Password login (API) |
GET /auth/:provider | OIDC provider redirect |
GET /auth/:provider/callback | OIDC callback |
Public LiveView Routes
| Route | Description |
|---|---|
/login | Login page |
/register | Registration page |
/invite/:token | Invitation acceptance |
/setup | First-time admin setup |
Admin LiveView Routes
Require admin role via require_admin mount hook.
| Route | Description |
|---|---|
/admin | Admin dashboard (defaults to usage) |
/admin/usage | Usage analytics |
/admin/servers | MCP server registry |
/admin/users | User management |
/admin/groups | Group management |
/admin/providers | LLM provider configuration |
/admin/models | LLM model configuration |
/admin/roles | Role management |
/admin/rag | RAG configuration |
/admin/setup | Application setup |
/settings/* | Settings pages (single-user mode unified settings) |
Authenticated LiveView Routes
Chat
| Route | Description |
|---|---|
/ | Main chat interface |
/conversations | Conversation list |
/c/:conversation_id | Single conversation |
Profile
| Route | Description |
|---|---|
/profile | User info |
/profile/password | Password change |
/profile/providers | User LLM providers |
/profile/models | User LLM models |
Wiki
| Route | Description |
|---|---|
/wiki | Wiki home |
/wiki/:document_id | View/edit a wiki page |
/wiki/:space_id/export | Export a wiki space (browser download) |
Sources & RAG
| Route | Description |
|---|---|
/sources | Source/collection list |
/sources/pipeline | RAG ingestion pipeline |
/sources/:source_id | Source details |
/sources/:source_id/:document_id | Document details |
MCP Servers
| Route | Description |
|---|---|
/mcp | MCP server management |
Reports
| Route | Description |
|---|---|
/reports | Report list |
/reports/:report_id | View/edit a report |
Agent Studio
| Route | Description |
|---|---|
/agents | Agent Studio landing |
/agents/list | Agent list |
/agents/new | Create agent |
/agents/:agent_id | View agent |
/agents/:agent_id/edit | Edit agent |
/teams | Team list |
/teams/new | Create team |
/teams/:team_id | View/edit team |
/runs | Run list |
/runs/new | Start a run |
/runs/:run_id | View run details |
/runs/:run_id/logs/:log_id | View run log |
/schedules | Schedule list |
/schedules/new | Create schedule |
/schedules/:schedule_id | View/edit schedule |
LiveView
Liteskill’s UI is built entirely with Phoenix LiveView. The interface is composed of several LiveView modules, each handling a different feature area.
Live Sessions
The router defines several live sessions with different auth requirements:
| Session | Mount Hook | Purpose |
|---|---|---|
:auth | redirect_if_authenticated | Login/register (redirects away if already logged in) |
:setup | require_setup_needed | First-time admin setup |
:admin | require_admin | Admin-only routes |
:chat | require_authenticated | All authenticated user routes |
LiveView Modules
ChatLive
The primary chat interface. Handles:
- Message list with real-time streaming updates
- Tool call approval UI
- RAG source display
- Conversation forking and editing
- Model selection
AdminLive
Multi-tab admin dashboard with separate tab modules:
UsageTab— Usage analytics and reportingServerTab— MCP server registryUsersTab— User management and invitationsGroupsTab— Group managementProvidersTab— LLM provider configurationModelsTab— LLM model configurationRolesTab— RBAC role managementRagTab— RAG settings and collection management
SetupLive
Setup wizard used for both first-time admin setup (/setup) and admin re-run (/admin/setup).
Dual-mode: :initial skips existing config; :admin_rerun loads current DB state.
AgentStudioLive
Agent, team, run, and schedule management with live actions:
- Agent CRUD (list, new, show, edit)
- Team composition
- Run execution with real-time log streaming
- Schedule management
ProfileLive
User account settings: profile info, password change, personal providers and models.
ReportsLive
Report viewing and editing with section management and comment threads.
SourcesLive
RAG collection browser with document and chunk management.
PipelineLive
RAG ingestion pipeline UI for URL-based document ingestion.
WikiLive
Wiki editor and browser for creating and editing wiki spaces and pages.
McpLive
MCP server management: CRUD, tool listing, and per-user tool selection.
AuthLive
Login, registration, and invitation acceptance forms.
SetupLive
First-time admin setup wizard.
Auth Hooks
LiteskillWeb.Plugs.LiveAuth provides on_mount callbacks:
:require_authenticated— Redirects to/loginif not authenticated:redirect_if_authenticated— Redirects to/if already logged in:require_admin— Requires admin role:require_setup_needed— Only allows access during initial setup
Real-Time Updates
LiveView receives real-time updates via PubSub:
- Streaming chunks — LLM response chunks update the UI in real-time
- Tool call status — Tool call progress and results
- Run updates — Agent run status and log entries
- Conversation list — New messages update conversation metadata
API
Liteskill provides a JSON REST API at /api for programmatic access.
Authentication
API routes require session-based authentication. The :api pipeline:
- Accepts JSON (
"application/json") - Fetches session
- Loads current user via
LiteskillWeb.Plugs.Auth - Applies rate limiting (1000 requests per 60 seconds)
The :require_auth pipeline then verifies an authenticated user is present.
Endpoints
Conversations
| Method | Path | Description |
|---|---|---|
GET | /api/conversations | List conversations |
POST | /api/conversations | Create a conversation |
GET | /api/conversations/:id | Get a conversation |
POST | /api/conversations/:id/messages | Send a message |
POST | /api/conversations/:id/fork | Fork a conversation |
POST | /api/conversations/:id/acls | Grant access |
DELETE | /api/conversations/:id/acls/:target_user_id | Revoke access |
DELETE | /api/conversations/:id/membership | Leave conversation |
Groups
| Method | Path | Description |
|---|---|---|
GET | /api/groups | List groups |
POST | /api/groups | Create a group |
GET | /api/groups/:id | Get a group |
DELETE | /api/groups/:id | Delete a group |
POST | /api/groups/:id/members | Add a member |
DELETE | /api/groups/:id/members/:user_id | Remove a member |
Rate Limiting
The API uses an ETS-based rate limiter (LiteskillWeb.Plugs.RateLimiter) with:
- Limit: 1000 requests per window
- Window: 60 seconds
- Periodic sweeper cleans stale ETS entries
Authentication
Liteskill supports two authentication methods: OIDC (OpenID Connect) and password-based.
Session-Based Auth
All authentication is session-based. The session flow:
- User authenticates via OIDC callback or password login
user_idis stored in the sessionLiteskillWeb.Plugs.Authloads the user from the session on each requestLiteskillWeb.Plugs.LiveAuthhandles the same for LiveView mounts
OIDC Authentication
Configured via environment variables:
OIDC_ISSUER— OpenID Connect issuer URLOIDC_CLIENT_ID— Client IDOIDC_CLIENT_SECRET— Client secret
Uses Ueberauth with the OIDCC strategy. On callback, Accounts.find_or_create_from_oidc/1 finds or creates the user by OIDC subject + issuer.
Password Authentication
Password auth uses Argon2 for hashing (configured with t_cost: 1, m_cost: 8 in test for speed).
Registration
- Available at
/register(LiveView) orPOST /auth/register(API) - Can be restricted to invited users only
Login
- Available at
/login(LiveView) orPOST /auth/login(API) - Returns session with user ID
Password Changes
- Users change passwords at
/profile/password - Requires current password verification
- Admins can set temporary passwords that force a change on next login
Single-User Mode
When SINGLE_USER_MODE=true:
- The login screen is skipped entirely
SingleUser.auto_user/0returns the admin user for every request- An admin user is auto-provisioned on boot if not present
Invitations
Admins can create invitation tokens for specific email addresses:
- Admin creates invitation at
/admin/users - Invitation generates a unique token
- User visits
/invite/:tokento register - Token is marked as used after acceptance
OpenRouter OAuth
Liteskill supports OpenRouter OAuth PKCE flow for connecting user-level OpenRouter accounts:
- User initiates at
GET /auth/openrouter - PKCE challenge is generated and state stored in
Liteskill.OpenRouter.StateStore - User authorizes on OpenRouter
- Callback at
GET /auth/openrouter/callbackexchanges the code for credentials
Helm Chart
Liteskill publishes a Helm chart for deploying to Kubernetes. The chart handles the application deployment, database migrations, ingress, and optional autoscaling.
Add the Helm Repository
helm repo add liteskill https://docs.liteskill.ai/helm
helm repo update
Prerequisites
Before installing, you need:
- A running Kubernetes cluster
- A PostgreSQL database accessible from the cluster
- The following secrets ready:
DATABASE_URL— Ecto connection string (ecto://USER:PASS@HOST/DATABASE)SECRET_KEY_BASE— generate withmix phx.gen.secretoropenssl rand -base64 64ENCRYPTION_KEY— generate withopenssl rand -base64 32AWS_BEARER_TOKEN_BEDROCK— your AWS Bedrock bearer token (if using Bedrock)AWS_REGION— e.g.us-east-1
Quick Install
helm install liteskill liteskill/liteskill \
--namespace liteskill --create-namespace \
--set config.phxHost=liteskill.example.com \
--set secrets.databaseUrl="ecto://user:pass@db-host/liteskill" \
--set secrets.secretKeyBase="$(openssl rand -base64 64)" \
--set secrets.encryptionKey="$(openssl rand -base64 32)" \
--set secrets.awsBearerToken="your-token" \
--set secrets.awsRegion="us-east-1"
Using a Values File
For production deployments, use a values file instead of inline --set flags.
# values-production.yaml
config:
phxHost: liteskill.example.com
port: "4000"
poolSize: "10"
ingress:
enabled: true
className: nginx
hosts:
- host: liteskill.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: liteskill-tls
hosts:
- liteskill.example.com
secrets:
existingSecret: liteskill-secrets
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
memory: 1Gi
helm install liteskill liteskill/liteskill \
--namespace liteskill --create-namespace \
-f values-production.yaml
Using an Existing Secret
Rather than passing secrets through Helm values (which end up in Helm release metadata), you can reference a pre-created Kubernetes Secret:
kubectl create secret generic liteskill-secrets \
--namespace liteskill \
--from-literal=DATABASE_URL="ecto://user:pass@db-host/liteskill" \
--from-literal=SECRET_KEY_BASE="$(openssl rand -base64 64)" \
--from-literal=ENCRYPTION_KEY="$(openssl rand -base64 32)" \
--from-literal=AWS_BEARER_TOKEN_BEDROCK="your-token" \
--from-literal=AWS_REGION="us-east-1"
Then reference it in your values:
secrets:
existingSecret: liteskill-secrets
The chart maps secret keys using existingSecretKeys, which defaults to:
existingSecretKeys:
databaseUrl: DATABASE_URL
secretKeyBase: SECRET_KEY_BASE
encryptionKey: ENCRYPTION_KEY
awsBearerToken: AWS_BEARER_TOKEN_BEDROCK
awsRegion: AWS_REGION
oidcClientSecret: OIDC_CLIENT_SECRET
Database Migrations
The chart includes a migration Job that runs as a Helm pre-install and pre-upgrade hook. It executes bin/liteskill eval Liteskill.Release.migrate() before the application starts.
Migrations are enabled by default. To configure:
migration:
enabled: true
backoffLimit: 3 # retry attempts on failure
activeDeadlineSeconds: 600 # timeout in seconds (default: 10 minutes)
Ingress
Enable ingress to expose the application externally:
ingress:
enabled: true
className: nginx # or your ingress controller class
annotations: {}
hosts:
- host: liteskill.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: liteskill-tls
hosts:
- liteskill.example.com
Autoscaling
Horizontal Pod Autoscaling is available but disabled by default:
replicaCount: 2
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 5
targetCPUUtilizationPercentage: 80
When using multiple replicas, set dnsClusterQuery to enable Erlang node clustering:
config:
dnsClusterQuery: "liteskill-headless.liteskill.svc.cluster.local"
Pod Disruption Budget
Enable a PodDisruptionBudget to ensure availability during voluntary disruptions (node drains, cluster upgrades):
podDisruptionBudget:
enabled: true
minAvailable: 1
OIDC Authentication
To enable OpenID Connect authentication:
oidc:
enabled: true
issuer: "https://your-idp.example.com"
clientId: "liteskill"
clientSecret: "your-client-secret"
Or with an existing secret that includes the OIDC_CLIENT_SECRET key:
oidc:
enabled: true
issuer: "https://your-idp.example.com"
clientId: "liteskill"
secrets:
existingSecret: liteskill-secrets
SAML Authentication
To enable SAML authentication, set saml.enabled: true and provide the IdP metadata file via a volume mount:
saml:
enabled: true
idpMetadataFile: "/saml/metadata.xml"
spId: "liteskill"
spEntityId: "urn:liteskill:sp"
spCertfile: "/saml/sp.crt"
spKeyfile: "/saml/sp.key"
idpId: "saml"
baseUrl: "https://liteskill.example.com/sso"
extraVolumes:
- name: saml-config
secret:
secretName: liteskill-saml
extraVolumeMounts:
- name: saml-config
mountPath: /saml
readOnly: true
Create the Kubernetes secret containing the SAML files:
kubectl create secret generic liteskill-saml \
--namespace liteskill \
--from-file=metadata.xml=./idp-metadata.xml \
--from-file=sp.crt=./sp-certificate.pem \
--from-file=sp.key=./sp-private-key.pem
Session Configuration
Configure session timeouts:
config:
sessionMaxAgeSeconds: "86400" # max session age (default: 14 days)
sessionIdleTimeoutSeconds: "3600" # idle timeout (default: none)
Single User Mode
Enable single user mode to skip authentication entirely (useful for personal/local deployments):
config:
singleUserMode: true
Security Context
The chart sets pod and container security contexts by default for hardened deployments:
# Pod-level (default)
securityContext:
runAsNonRoot: true
runAsUser: 65534
fsGroup: 65534
# Container-level (default)
containerSecurityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
To disable or customize, override the values:
securityContext: {}
containerSecurityContext: {}
Startup Probe
The chart includes a startup probe to handle slow-starting containers. This allows the application up to 60 seconds (30 failures x 2s period) to start before the liveness probe kicks in:
startupProbe:
httpGet:
path: /login
port: http
failureThreshold: 30
periodSeconds: 2
Extra Volumes
Mount additional volumes into the application and migration containers (e.g. for SAML certificates, CA bundles, or database TLS certificates):
extraVolumes:
- name: db-certs
secret:
secretName: db-tls-certs
extraVolumeMounts:
- name: db-certs
mountPath: /certs
readOnly: true
Topology Spread Constraints
Spread pods across failure domains for high availability:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app.kubernetes.io/name: liteskill
Upgrading
helm repo update
helm upgrade liteskill liteskill/liteskill \
--namespace liteskill \
-f values-production.yaml
The migration hook runs automatically before each upgrade.
ArgoCD
An example ArgoCD Application manifest is included in the repository at deploy/argocd/application.yaml:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: liteskill
namespace: argocd
spec:
project: default
source:
repoURL: https://docs.liteskill.ai/helm
chart: liteskill
targetRevision: 0.2.51
helm:
releaseName: liteskill
valuesObject:
config:
phxHost: liteskill.example.com
ingress:
enabled: true
className: nginx
hosts:
- host: liteskill.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: liteskill-tls
hosts:
- liteskill.example.com
secrets:
existingSecret: liteskill-secrets
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
memory: 1Gi
destination:
server: https://kubernetes.default.svc
namespace: liteskill
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
All Values
For a full reference of available values, see the chart’s values.yaml or run:
helm show values liteskill/liteskill
Environment Variables
Required (Production)
| Variable | Description |
|---|---|
DATABASE_URL | PostgreSQL connection string (e.g. ecto://USER:PASS@HOST/DATABASE) |
PHX_SERVER | Set to true to start the HTTP server (required for releases) |
Optional (Auto-generated if not set)
| Variable | Description |
|---|---|
SECRET_KEY_BASE | Phoenix secret key for cookie signing. Auto-generated and persisted to secrets.json on first boot if not set. Generate manually with mix phx.gen.secret or openssl rand -base64 64. |
LITESKILL_SECRETS_FILE | Override the path to the auto-generated secrets file (default: platform-specific data dir) |
Encryption (Off by Default)
| Variable | Description |
|---|---|
LITESKILL_ENCRYPTION | Set to true to enable at-rest encryption of sensitive fields |
ENCRYPTION_KEY | Required when encryption is enabled. Key for AES-256-GCM (generate with openssl rand -base64 32) |
Server
| Variable | Default | Description |
|---|---|---|
PORT | 4000 | HTTP port |
PHX_HOST | example.com | Hostname for URL generation |
ECTO_IPV6 | — | Set to true or 1 to enable IPv6 for database connections |
POOL_SIZE | 10 | Database connection pool size |
DNS_CLUSTER_QUERY | — | DNS query for node clustering |
FORCE_SSL | — | Set to false to disable SSL enforcement (e.g. for local Docker) |
Authentication
| Variable | Description |
|---|---|
OIDC_ISSUER | OpenID Connect issuer URL |
OIDC_CLIENT_ID | OIDC client ID |
OIDC_CLIENT_SECRET | OIDC client secret |
LLM
| Variable | Description |
|---|---|
AWS_BEARER_TOKEN_BEDROCK | AWS Bedrock bearer token (auto-creates instance-wide provider on boot) |
AWS_REGION | AWS region for Bedrock (default: us-east-1) |
Mode
| Variable | Description |
|---|---|
SINGLE_USER_MODE | Set to true, 1, or yes to enable single-user mode |
LITESKILL_DESKTOP | Set to true to enable desktop mode (bundled Postgres, auto-config) |
Desktop Mode
When LITESKILL_DESKTOP=true:
- A bundled PostgreSQL instance is managed automatically
- Secret key base is auto-generated and stored in
desktop_config.json - Single-user mode is enabled automatically
- Data is stored in platform-specific directories:
- macOS:
~/Library/Application Support/Liteskill - Linux:
$XDG_DATA_HOME/liteskill(default:~/.local/share/liteskill) - Windows:
%APPDATA%/Liteskill
- macOS:
Docker Compose Defaults
The docker-compose.yml uses these defaults (overridable via shell environment):
| Variable | Default |
|---|---|
POSTGRES_USER | liteskill |
POSTGRES_PASSWORD | liteskill |
POSTGRES_DB | liteskill |
PHX_HOST | localhost |
SECRET_KEY_BASE is auto-generated on first boot if not provided. To persist across container restarts, mount a volume and set LITESKILL_SECRETS_FILE.
Encryption
Liteskill can optionally encrypt sensitive data at rest using AES-256-GCM via Liteskill.Crypto. Encryption is off by default and must be explicitly enabled.
Enabling Encryption
Set both environment variables:
export LITESKILL_ENCRYPTION=true
export ENCRYPTION_KEY="$(openssl rand -base64 32)"
When LITESKILL_ENCRYPTION is not set to true, the Crypto module is a passthrough — values are stored and returned as-is with no encryption overhead.
How It Works
When enabled:
- A 32-byte encryption key is derived from the
ENCRYPTION_KEYconfig value via SHA-256 - Each encrypt operation generates a random 12-byte IV
- Ciphertext format:
IV (12 bytes) || tag (16 bytes) || ciphertext - The result is base64-encoded for storage in string columns
Validation
On application boot, Crypto.validate_key!/0 checks the configuration. If encryption is enabled but no key is set, the app will crash on startup rather than failing on first encrypt/decrypt. If encryption is not enabled, this is a no-op.
What’s Encrypted (When Enabled)
The following fields use encryption-aware Ecto types:
- MCP server API keys — via
Liteskill.Crypto.EncryptedFieldEcto type - LLM provider API keys — via
Liteskill.Crypto.EncryptedField - Data source credentials — via
Liteskill.Crypto.EncryptedMapEcto type
When encryption is off, these types store values as plaintext.
Ecto Custom Types
Liteskill.Crypto.EncryptedField— Encrypts/decrypts a single string valueLiteskill.Crypto.EncryptedMap— Encrypts/decrypts a JSON map (for structured credentials)
These types handle encryption transparently at the Ecto layer — data is encrypted before insert and decrypted after load.
Development Setup
Prerequisites
- Install mise
- Clone the repository
- Run
mise installto get Elixir 1.18, Erlang/OTP 28, Node.js 24, and mdbook
Setup
mix setup
This runs:
deps.get— Install Elixir dependenciesecto.create— Create the databaseecto.migrate— Run migrationsrun priv/repo/seeds.exs— Seed datanpm install --prefix assets— Install Node dependenciestailwind.install --if-missing— Install Tailwindesbuild.install --if-missing— Install esbuildgen.jr_prompt— Generate JSON render prompt- Compile and build assets
Database Reset
mix ecto.reset
Drops, creates, migrates, and seeds the database.
Running the Server
mix phx.server
Visit http://localhost:4000.
Docker-Based Development
If you don’t have PostgreSQL installed locally:
# Run tests with Docker Postgres
./scripts/test-with-docker.sh test
# Full precommit with Docker Postgres
./scripts/test-with-docker.sh precommit
The script starts a temporary pgvector/pgvector:pg16 container, sets DATABASE_URL, runs the specified command, and cleans up after.
Building the Docker Image
docker build -t liteskill .
The multi-stage Dockerfile:
- Stage 0 — Copies Node.js binaries from
node:24-bookworm-slim - Stage 1 (Builder) — Compiles the Elixir release with
mix release liteskill - Stage 2 (Runtime) — Minimal Debian image with the release binary
Running with Docker Compose
export SECRET_KEY_BASE=$(openssl rand -base64 64)
export ENCRYPTION_KEY=$(openssl rand -base64 32)
docker compose up
This starts:
- db — PostgreSQL 16 with pgvector
- app — Liteskill release with host networking
To run migrations separately:
docker compose run --rm migrate
Testing
Running Tests
# All tests
mix test
# Single file
mix test test/path_test.exs
# Re-run failures
mix test --failed
Coverage
100% test coverage is required, enforced by ExCoveralls. Files excluded from coverage are listed in coveralls.json.
Use # coveralls-ignore-start / # coveralls-ignore-stop for genuinely unreachable branches (e.g. desktop-only code paths, production-only error handling).
Test Configuration
DataCase
use Liteskill.DataCase, async: false
All database tests use a shared Ecto sandbox (async: false).
Unit Tests
Pure unit tests (aggregates, events, parsers) use:
use ExUnit.Case, async: true
Argon2
Test config uses t_cost: 1, m_cost: 8 for fast password hashing.
HTTP Mocking with Req.Test
Use plug: {Req.Test, ModuleName} to mock HTTP calls:
Req.Test.stub(Liteskill.McpServers.Client, fn conn ->
Req.Test.json(conn, %{"result" => %{"tools" => []}})
end)
Important: Req.Test does NOT trigger into: callbacks. Use the plug: option when constructing Req requests in production code to enable test substitution.
Projector
The Chat.Projector runs in the main supervision tree — never start_supervised! it in tests. It’s already running.
Process Synchronization
Chat context write functions include Process.sleep(50) for projection consistency. In tests, prefer synchronization over additional sleeps:
# Prefer this
_ = :sys.get_state(Liteskill.Chat.Projector)
# Or use Projector.sync/0
:ok = Liteskill.Chat.Projector.sync()
Stateful Stubs
Use Agent for varying responses across retries:
{:ok, counter} = Agent.start_link(fn -> 0 end)
Req.Test.stub(ModuleName, fn conn ->
call = Agent.get_and_update(counter, &{&1, &1 + 1})
case call do
0 -> Plug.Conn.send_resp(conn, 429, "rate limited")
_ -> Req.Test.json(conn, %{"result" => "ok"})
end
end)
Set backoff_ms: 1 for retry tests to avoid slow test runs.
MCP Client Testing
Req.Test.stub(Liteskill.McpServers.Client, fn conn ->
Req.Test.json(conn, %{
"jsonrpc" => "2.0",
"id" => 1,
"result" => %{"tools" => []}
})
end)
Docker-Based Testing
# Run tests with a temporary Docker Postgres
./scripts/test-with-docker.sh test
# Full precommit with Docker Postgres
./scripts/test-with-docker.sh precommit
Conventions
IDs and Timestamps
- Binary UUIDs for all primary keys
:utc_datetimefor all timestamp fields
Schemas
- All schemas use
field :name, :stringeven for text columns - Foreign keys (e.g.
user_id) are set programmatically, never included incast
HTTP
- Req library only for all HTTP calls — never httpoison, tesla, or httpc
- HTTP mocking via
Req.Testwithplug:option
Boundaries
Every context uses the Boundary library to declare:
top_level?: true— Marks it as a top-level bounded contextdeps: [...]— Allowed dependencies on other contextsexports: [...]— Modules visible to other contexts
The boundary compiler enforces these at compile time.
Event Sourcing
- Events stored with string keys via
stringify_keys - Deserialized back to structs via
Events.deserialize/1 - Stream IDs:
"conversation-<uuid>" - Optimistic concurrency via unique index on
(stream_id, stream_version)
Authorization
- All entity types share a single
entity_aclstable - Owner ACL auto-created on resource creation
- Role hierarchy:
viewer<editor<manager<owner - RBAC permissions checked via
Liteskill.Rbac.authorize/2
Tailwind CSS
- Tailwind v4 — No
tailwind.config.js - Uses
@import "tailwindcss"syntax inassets/css/app.css
Oban Queues
| Queue | Concurrency | Purpose |
|---|---|---|
default | 10 | General background jobs |
rag_ingest | 5 | RAG document ingestion and embedding |
data_sync | 3 | External data source synchronization |
agent_runs | 3 | Agent pipeline execution |
LLM Integration
- All LLM transport via ReqLLM (
req_llm ~> 1.5) ReqLLM.stream_text/3for streaming,ReqLLM.generate_text/3for single-turnReqLLM.embed/3for embeddings- No hardcoded model IDs — all models configured in the database
Security
- Sensitive fields encrypted at rest via
Liteskill.Crypto(AES-256-GCM) - SSRF protection on MCP server URLs (configurable)
- ETS-based rate limiting on API routes
- CSRF protection via Phoenix plugs
- Per-provider circuit breakers prevent cascading LLM failures
Commands
Development
| Command | Description |
|---|---|
mix setup | Install deps, create DB, run migrations, build assets |
mix phx.server | Start dev server on localhost:4000 |
mix ecto.reset | Drop + create + migrate + seed |
iex -S mix phx.server | Start with interactive shell |
mise run singleuser | Start in single-user mode |
Testing
| Command | Description |
|---|---|
mix test | Run all tests (auto-creates/migrates DB) |
mix test test/path_test.exs | Run a single test file |
mix test --failed | Re-run previously failed tests |
mix coveralls | Run tests with coverage report |
mix coveralls.html | Generate HTML coverage report |
Code Quality
| Command | Description |
|---|---|
mix precommit | Full quality check (see below) |
mix compile --warnings-as-errors | Compile with strict warnings |
mix format | Format all Elixir files |
mix credo --strict | Static analysis |
mix sobelow --config --exit low | Security analysis |
mix dialyzer | Type checking |
mix deps.unlock --unused | Remove unused dependency locks |
Precommit
mix precommit runs the full quality pipeline:
compile --warnings-as-errorsdeps.unlock --unusedformatcredo --strictsobelow --config --exit lowdialyzerecto.create --quiet && ecto.migrate --quietcoveralls(tests with 100% coverage check)mdbook build docs/
Always run mix precommit after completing changes.
Docker-Based
| Command | Description |
|---|---|
./scripts/test-with-docker.sh test | Run tests via Docker Postgres |
./scripts/test-with-docker.sh precommit | Full precommit via Docker |
docker compose up | Start full stack (DB + app) |
docker compose run --rm migrate | Run migrations only |
Desktop
| Command | Description |
|---|---|
mise run linux-appimage | Build Linux AppImage via Docker |
mise run local-tauri | Open Tauri dev window against localhost:4000 |