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.
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, 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) |
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.
Setup:
Authorization: Bearer <token>Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"abook": {
"type": "http",
"url": "http://localhost:5000/mcp",
"headers": { "Authorization": "Bearer YOUR_TOKEN" }
}
}
}
VS Code / GitHub Copilot (.vscode/mcp.json):
{
"servers": {
"abook": {
"type": "http",
"url": "http://localhost:5000/mcp",
"headers": { "Authorization": "Bearer YOUR_TOKEN" }
}
}
}
User creates book (title, premise, genre, target chapters)
│
├─ optional: choose a base book (settings + context copied)
│
▼
┌──────────────────────────────────────────────────────┐
│ Planner — 4-phase pipeline │
│ Phase 1: Story Bible (world-building, tone, rules) │
│ Phase 2: Character Cards (roles, arcs, goals) │
│ Phase 3: Plot Threads (subplots, themes, arcs) │
│ Phase 4: Chapter Outlines (title + synopsis each) │
│ │
│ Asks clarifying questions up front; optionally │
│ pauses after each phase in Human-assisted mode │
└──────────────────────────────────────────────────────┘
│
│ ← "Plan Only" stops here so you can review
│ and edit outlines before clicking "Continue"
▼
For each chapter:
[Checker] ── pre-write: checks outline for contradictions
│
▼
[Writer] ── writes full chapter prose
│
▼
[Checker] ── continuity + style review → structured JSON patches
│
▼
[Editor] ── applies patches mechanically (skipped if no issues)
│
│ ← optional human pause in assisted mode: read the patched
│ chapter, edit anything, and steer the rewrite
▼
[Editor] ── creative rewrite — only when the Checker asked for one
│ or you gave instructions during the pause
▼
Done ✓
Agents stream tokens via SignalR as they write.
Workflow controls:
| Button | Behaviour |
|---|---|
| Plan Only | Runs the Planner and stops so you can review outlines |
| Write Book | Full pipeline from scratch (idempotent — skips completed phases and Done chapters) |
| Continue | Resumes from the first non-Done chapter |
| Continue Planning | Re-runs only the incomplete planning phases |
| Stop | Cancels any running agent cleanly |
In Human-assisted mode the app also pauses after each planning phase, and once per chapter — after the Editor's mechanical fixes are applied but before the creative rewrite. While it is paused nothing is generating, so you are free to edit the chapter (title, outline and prose), the characters, the plot threads or the story bible; the next step re-reads all of it. Whatever you type in the answer box is passed to the rewrite as author instructions, and the rewrite is skipped altogether when the Checker found nothing to rewrite and you left the box empty.
Chapter edits you make by hand are re-embedded in the background, so retrieval for later chapters sees your text rather than the version the agent wrote.
For books created from a base book, chapter-level RAG retrieval includes embeddings from all ancestor books in the continuation chain.
# Start PostgreSQL
docker-compose up postgres -d
# Start the UI dev server (proxies API calls to localhost:5000)
cd src/abook-ui
npm install
npm run dev
# Start the API (second terminal)
cd src/ABook.Api
dotnet run --urls http://localhost:5000
The React dev server runs at http://localhost:5173 and proxies /api and /hubs to the ASP.NET server.
dotnet ef migrations add <MigrationName> --project src/ABook.Infrastructure --startup-project src/ABook.Api
dotnet ef database update --project src/ABook.Infrastructure --startup-project src/ABook.Api
Always use the dotnet ef CLI — never create migration files by hand.
docker build -t abook .
The multi-stage Dockerfile builds the React app (Node 20), compiles the .NET API (.NET 10 SDK), and produces a minimal runtime image (ASP.NET 10).
Set LLM_DEBUG_LOGGING=true to print the full chat history and LLM responses to the application log at Information level.
| Layer | Technology |
|---|---|
| Frontend | React 19, TypeScript, Vite, Zustand, react-markdown |
| Backend | ASP.NET Core 10, C# |
| LLM | Ollama, OpenAI SDK, Google AI SDK (per-provider direct calls) |
| Database | PostgreSQL 16 via EF Core 10 + Npgsql |
| Vector store | pgvector (in-DB, Pgvector.EntityFrameworkCore) |
| Real-time | SignalR |
| Auth | JWT (JwtBearer) + rotating refresh tokens, IPasswordHasher<T>, Bearer API token for MCP |
| MCP | ModelContextProtocol.AspNetCore 1.2.0 — HTTP/SSE transport, 37 tools |
| Container | Docker, Docker Compose |
Content type
Image
Digest
sha256:528b243f5…
Size
248.8 MB
Last updated
4 days ago
docker pull jncchds/abook