A self-hosted web application that uses AI agents to collaboratively write books. Seven specialized agents — Story Bible, Characters, Plot Threads, Chapter Outlines, Writer, Checker, and Editor — work together under your direction, streaming their progress in real time and pausing to ask clarifying questions when needed.
/plan / /continue in Telegram)GET /health reports whether the database is reachable (for Docker/uptime checks)/mcp; connect Claude Desktop, VS Code Copilot, or any MCP client using a per-user API tokenAgentSettings__MaxConcurrentRuns[React SPA — served as static files from ASP.NET wwwroot]
↕ REST API + SignalR
[ASP.NET Core 10 API]
↕ Direct provider SDKs ↕ EF Core 10 + Npgsql
[LLM (Ollama / OpenAI / Google)] [PostgreSQL 16 (pgvector in-DB)]
React is built at image-build time and served from wwwroot/ — there is no separate frontend container at runtime.
docker-compose up -d
# App is available at
open http://localhost:5000
On first launch the app shows a Create Admin Account setup screen — the first account registered automatically becomes admin. After signing in, go to Settings to configure your LLM provider and pull an Ollama model.
docker run -d \
-p 5000:8080 \
-e ConnectionStrings__DefaultConnection="Host=<postgres-host>;Port=5432;Database=abook;Username=abook;Password=abook" \
--add-host host.docker.internal:host-gateway \
jncchds/abook:latest
PostgreSQL with the pgvector extension must be reachable. The compose file below starts it automatically.
services:
abook-api:
image: jncchds/abook:latest
ports:
- "5000:8080"
environment:
- ConnectionStrings__DefaultConnection=Host=postgres;Port=5432;Database=abook;Username=abook;Password=abook
- ASPNETCORE_ENVIRONMENT=Production
depends_on:
postgres:
condition: service_healthy
extra_hosts:
- "host.docker.internal:host-gateway"
restart: unless-stopped
postgres:
image: pgvector/pgvector:pg16
environment:
POSTGRES_DB: abook
POSTGRES_USER: abook
POSTGRES_PASSWORD: abook
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U abook -d abook"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
volumes:
postgres_data:
| Variable | Default | Description |
|---|---|---|
ConnectionStrings__DefaultConnection | — | PostgreSQL connection string |
ASPNETCORE_ENVIRONMENT | Development | Production disables Swagger |
LlmDefaults__Provider | Ollama | Provider of the "Default" preset each new user gets (Ollama, OpenAI, OpenAICompatible, GoogleAIStudio); unset = new users start without a preset. The LlmDefaults__* values only seed that preset — books never fall back to them |
LlmDefaults__ModelName | llama3 | Default model name |
LlmDefaults__Endpoint | http://host.docker.internal:11434 | Default LLM endpoint |
LlmDefaults__ApiKey | — | API key (required for OpenAI / GoogleAIStudio; optional for Ollama) |
LlmDefaults__EmbeddingModelName | — | Embedding model for RAG (optional; falls back to chat model) |
LlmDefaults__ImageEndpoint | — | Image server URL for illustrations (e.g. http://gpu-box:8000); applied with LlmDefaults__Provider |
LlmDefaults__ImageProvider | ZImage | ZImage or UnslothStudio |
LlmDefaults__ImageApiKey | — | Image server API key (Unsloth Studio sk-unsloth-…) |
LlmDefaults__ImageModel | — | Unsloth Studio: image model to load when none is loaded |
LlmDefaults__ImageVisualStyle | — | Default visual style for illustrations |
LlmDefaults__ImagesPerChapter | 2 | Base number of illustrations per chapter (the illustrator may plan more when a chapter needs them) |
Jwt__SigningKey | generated | Key that signs sign-in tokens — any random string of 32+ characters (e.g. openssl rand -base64 48). If unset, ABook generates one and stores it in the database, logs a ⚠️ warning at startup and shows admins a banner; see Sign-in |
Jwt__AccessTokenMinutes | 15 | Lifetime of an access token |
Jwt__RefreshTokenDays | 30 | How long a browser stays signed in without using ABook |
AgentSettings__MaxConcurrentRuns | 3 | Max simultaneous agent runs across all books/users |
PublicMode | false | Enable public library (anonymous access to published books) |
TELEGRAM_BOT_TOKEN | — | Token from @BotFather; turns the Telegram bot on |
ABOOK_HOSTNAME | — | Public host name (e.g. book.example.com): the bot gets updates by webhook at https://{ABOOK_HOSTNAME}/api/telegram/webhook. Unset = long polling |
TELEGRAM_WEBHOOK_SECRET | random | Secret Telegram sends with each webhook call (A-Z a-z 0-9 _ -); unset = a new random one at each start |
TELEGRAM_API_URL | https://api.telegram.org | A self-hosted Bot API server instead — raises the file limit from 50 MB to 2 GB |
Changing
AgentSettings__MaxConcurrentRunsrequires restarting the API process/container.
For local development, copy src/ABook.Api/appsettings.Local.example.json → appsettings.Local.json and fill in your values.
⚠️ Since v0.3.0 ABook uses JWT sign-in instead of cookie sessions. Everyone has to sign in once after updating; books, presets and MCP API tokens are not affected.
The browser gets a short-lived access token (kept in memory, never in storage) and a refresh token in an httpOnly cookie that is only sent to /api/auth, rotated on every use and revocable (sign-out, password change). A browser stays signed in for Jwt__RefreshTokenDays after its last visit.
Tokens are signed with Jwt__SigningKey. You don't have to set it — if it is missing, ABook generates a random key on first start and keeps it in its database, so sessions survive restarts and updates. It does log a ⚠️ warning at every startup and shows admins a banner, because anyone with a copy of the database could then forge sign-ins. To move the key into your configuration:
docker compose exec abook-api dotnet ABook.Api.dll secrets show
Copy the printed Jwt__SigningKey=… line into the environment: section of abook-api, then docker compose up -d (any other random 32+ character value works just as well). Changing the key never signs anyone out: it only invalidates access tokens, and browsers fetch a new one with their refresh cookie.
LLM settings are kept as presets (the 🔑 Presets page) and per book (each book's Settings page):
| Provider | Notes |
|---|---|
| Ollama | Default. Runs locally; host.docker.internal resolves to the host from inside Docker. |
| OpenAI | Provide an API key and model name (e.g. gpt-4o). Leave endpoint blank for the real OpenAI API; set a custom endpoint for any OpenAI-compatible API (Groq, Together, LM Studio at http://host.docker.internal:1234/v1, etc.). |
| Google AI Studio | Native Gemini connector. Requires an API key from aistudio.google.com. Suggested models: gemini-2.0-flash, gemini-2.5-pro. Embedding model: text-embedding-004. |
| OpenAI Compatible | For LM Studio, OpenRouter, vLLM and similar. Reads the stream directly, so non-standard fields such as reasoning_content are captured; reasoning_effort is never sent. API key optional. |
How the settings are resolved:
LlmDefaults__* environment variables, if those are set.Local models often ignore the JSON schema the planning agents send — returning a chapter number as 1.0 or "3", or a text field as a list. The parsers coerce those instead of failing, drop only the entries they genuinely cannot read, and post a ⚠️ note in the book's chat naming each dropped entry and the JSON behind it. If a planning phase does fail outright, the error in the chat quotes what the model actually returned.
Illustrations are rendered by one of two image providers, chosen under 🎨 Image Generation in a book's LLM settings or in a preset; use Test connection after setting the URL.
compose/zimage folder (POST /generate, GET /images/{id}, GET /health)./v1) and an API key from Studio's Settings → API. Studio keeps only one model on the GPU, so if it also serves your text model (as an OpenAI Compatible provider at http://<host>:<port>/v1), set Image model too — e.g. unsloth/Z-Image-Turbo-GGUF:z-image-turbo-Q8_0.gguf (a Hub repo id, optionally : plus a checkpoint file). ABook then loads it before rendering whenever no image model is loaded, and Studio unloads the text model to make room. Leave it blank to use whatever is loaded on Studio's Images page.| Setting | Default | Notes |
|---|---|---|
| Endpoint | — | Blank disables rendering (illustrations can still be planned) |
| Image API key | — | Unsloth Studio only |
| Image model | — | Unsloth Studio only — loaded when Studio has no image model loaded |
| Visual style | — | Written into every prompt by the LLM; blank lets it choose one for the genre |
| Steps / CFG | 8 / 1.0 (Unsloth Studio: 9 / 0) | Z-Image Turbo values; blank on Unsloth Studio lets Studio choose, so set both for other Studio models (e.g. FLUX). Negative prompts only take effect with CFG above 1.0 |
| Portrait / landscape size | 768×1152 / 1152×768 | Square images use the portrait area |
| Timeout | 300000 ms | Per image |
| Unload the LLM before rendering | off | Ollama only (keep_alive: 0); Unsloth Studio swaps models by itself |
Workflow: enable Illustrate this book in the book settings → write the book → open 🎨 Illustrations, review or edit the plan → Render portraits and re-roll any you don't like → Illustrate book.
ABook includes a built-in Model Context Protocol server at /mcp. Any MCP-compatible client can connect to read and write book content and trigger agent workflows.
Tools (each tool's description says exactly what it does):
| Area | Tools |
|---|---|
| Account & LLM | get_current_user, get_llm_config, set_llm_config, list_presets, apply_preset |
| Books | list_books, get_book, create_book, update_book, delete_book, generate_book, export_book (html / fb2 / epub / metadata, or one chapter), get_agent_messages, get_agent_status, get_token_usage |
| Planning | list_clarifications, add_clarification, update_clarification, complete_planning_phase, reopen_planning_phase, clear_planning_phase |
| Content | get_story_bible, update_story_bible, list_characters, create_character, update_character, delete_character, list_plot_threads, create_plot_thread, update_plot_thread, delete_plot_thread |
| Sections & chapters | list_sections, update_section, move_section, archive_section, restore_section, list_chapters, get_chapter, create_chapter, update_chapter, move_chapter, move_chapter_to_section, split_chapter, archive_chapter, restore_chapter |
| History | list_chapter_versions, get_chapter_version, activate_chapter_version, archive_character, restore_character, list_character_versions, restore_character_version, archive_plot_thread, restore_plot_thread, list_plot_thread_versions, restore_plot_thread_version, list_clarification_versions, restore_clarification_version, list_story_bible_versions, restore_story_bible_version |
| Runs (Autonomous books) | start_planning, continue_planning, start_workflow, continue_workflow, stop_workflow, break_down_section, write_section, write_chapter, edit_chapter, run_continuity_check (book, section or chapter), answer_agent_question |
| Book chat (Interactive books) | get_book_chat, start_book_chat, send_book_chat_message, revise_book_chat_suggestions, retry_book_chat_message, apply_book_chat_proposals, reject_book_chat_proposals, undo_book_chat_proposal, `l |
Content type
Image
Digest
sha256:b62da7398…
Size
250 MB
Last updated
about 14 hours ago
docker pull jncchds/abook