The client, the steering committee, your manager — none of them read your document, they look at it first. A wall of unstyled text reads as a draft. A cover page, a clear structure and a chart that makes the point read as work someone thought about.
Getting there normally costs an afternoon: a chatbot hands you a wall of markdown, Word hands you a blank page and a style menu. Docyfier hands you the finished document — cover, table of contents, callouts, tables, charts, diagrams, stat cards — from a prompt, or from the draft you already have. Then you edit it like a real document, because it is one, in a real block editor. Not a chat transcript you reformat by hand.
- Formatting is the product. Everything else — templates, themes, blocks, the "make it pretty" pass — exists so a document leaves looking designed.
- Bring your own model. Any OpenAI-compatible server, local or hosted. Your drafts and prompts never have to leave your network.
- Leaves in the shape they asked for. Word, Markdown, Confluence, Notion, Trilium, PDF. Adding another target is one file.
- Yours to run. One container, one volume, AGPL. No seat pricing, no document quota, no telemetry.
Writing with AI
- Generate from a prompt — describe a document, get it drafted and formatted straight into the editor.
- Document assistant — whole-document operations: restructure, "make it pretty", shorten, change tone, add sections, via chat or one-click actions.
- Selection rewrites — select text, get a floating menu with rephrase / shorten / expand / formal, or apply a free prompt to that selection only.
- Two assistants, never both at once — a writer owns the words (tone, length, what is said) and a layout designer owns the shape (which block carries what, changing no word). Which one answers is read off the button you pressed, so nothing is spent deciding; the other is one click away.
- Diff review — AI edits arrive as a reviewable diff, applied or rejected block by block, never as a silent overwrite.
The editor
- Block editor built on Tiptap / ProseMirror: headings, bold / italic / strike /
underline / inline code, links, alignment, lists, blockquotes, code blocks,
tables, and colored callouts (note / tip / warn / danger). Type
/for the insert menu; drag handles reorder anything. - Presentation blocks: card grids, columns, key-figure rows, dashboard stat cards, timelines, step lists, pyramids, and bar & line charts rendered as inline SVG.
- Diagrams: flows, architectures, sequences, hierarchies and phase axes. You say what relates to what and the app decides where every box lands, so an AI-written diagram arrives placed. Rename a box where you read it, recolour it, drop it, or drag it somewhere you like better — a moved box moves alone, so nudging one never rearranges the picture around it, and Realign hands the whole drawing back to the layout.
- Images: paste, drag-drop or pick a file, re-rendered at 25 / 50 / 75 / 100 % of the text column.
- Cover page and table of contents, with print control for both.
- Themes: presets that are adjustable token sets — accent color, font pair, corner radius, density — edited live in a Design panel. Themes are presentation only and never touch document content.
- House identity: the instance sets the theme new documents start in and saves its own presets, which sit beside the built-in ones. A saved preset is referenced, not copied: changing the house accent repaints every document wearing it.
- Writing style: emoji policy, keyword bolding, statuses as badges and a writing language — set once, applied to everything the assistant writes.
- Autosave, debounced, with a save indicator.
Getting content in and out
- Templates: meeting notes, project one-pager, technical spec, status report, roadmap, incident postmortem, decision note — each a real document with its own theme preset.
- Import:
.md,.markdown,.txtand.docxbecome editable documents with their structure intact. PDF import is deliberately out of scope — a PDF carries layout, not structure. - Export: Word (
.docx), Markdown, Confluence, Notion, Trilium, and PDF through the browser print dialog against an A4 stylesheet. Targets are plugins: one adapter insrc/infrastructure/publishing/targets/plus a line in the registry. - Compose: short-form writing that ends in the clipboard instead of a document — an email from a brief in a chosen tone, or a ticket in the markup Jira, ServiceNow or GitLab expects.
Documents are stored as ProseMirror JSON, not markdown. That is what makes reliable formatting, targeted AI edits and multi-format export possible.
docker run -p 3000:3000 \
-v docyfier-data:/data \
-e DOCYFIER_LLM_BASE_URL=http://<your-model-server>:1234/v1 \
ghcr.io/<owner>/docyfier:latestOpen http://localhost:3000. Everything else is configurable from the Settings page once the app is running.
Requires Node 24.18.0 (pinned in .nvmrc).
nvm use
npm install
npm run dev # http://localhost:3000With just: just setup && just dev.
Every setting below resolves in the same order: Settings page > environment
variables > defaults. The Settings page (/settings) is split by scope —
ai, storage, style, exports, access — and writes to settings.json next to
the document store, so nothing here requires a rebuild or a restart.
Calls go through the Vercel AI SDK to any OpenAI-compatible endpoint — LM Studio, Ollama, vLLM, llama.cpp, or a hosted API that speaks the same protocol. Point the base URL at your server, and the model picker on the Settings page lists what it offers (with a connection test).
Several providers can be configured side by side — a local model and a
hosted one, or two accounts — each with its own model, key, token ceiling and
structured-output setting. One is active; switch from /settings/ai or from the
picker in the app header when a quota runs out or a task needs the other model.
The environment variables below describe the first provider, the one a fresh
deployment starts with.
Speed. Two settings decide most of the wait, and both are per provider.
Max output tokens caps how long an answer may get. Reasoning effort tells a
model that thinks before it writes how hard to think; leaving it on default
lets a reasoning model deliberate over a one-line rewrite, which is often most
of the seconds you counted. Docyfier already asks for low on the small
surfaces — a selection rewrite, a caret insert — and a value set here overrides
that everywhere, so pick it deliberately. Each call is also sent only the block
vocabulary its surface may produce, which is why rewording a paragraph carries a
third of the prompt a whole-document edit does.
To see where the time actually goes, start the app with DOCYFIER_LOG_USAGE=1:
every call prints its duration, tokens in and out, tokens spent thinking, and
how much of the prompt the provider served from a prefix it had already seen.
| Variable | Default | Purpose |
|---|---|---|
DOCYFIER_LLM_BASE_URL |
http://localhost:1234/v1 |
OpenAI-compatible endpoint |
DOCYFIER_LLM_MODEL |
first model on the server | Model id |
DOCYFIER_LLM_API_KEY |
— | Key for providers that require one |
DOCYFIER_LLM_MAX_TOKENS |
32768 |
Max tokens per response |
Every credential entered in Settings — LLM API keys, the database password, and
any export option a target declares secret — is stored encrypted
(AES-256-GCM) in settings.json and never sent back to the browser: an empty
password field means "keep the stored one". Set DOCYFIER_SECRET_KEY to 32
bytes of hex or base64 (openssl rand -hex 32) to control the encryption key;
without it, secret.key is generated next to settings.json and belongs to
your backups — lose it and the credentials have to be entered again.
Every AI response is validated against the editor schema server-side (invalid output gets one retry) before it can touch a document.
| Backend | Where | Notes |
|---|---|---|
| Files (default) | JSON files under data/documents/ |
Override with DOCYFIER_DATA_DIR |
| PostgreSQL | documents table |
Needs an existing database; the table is created on first connection |
| MySQL | documents table |
Same |
Connection settings always stay in settings.json — they cannot live in the
database they configure. The password is encrypted there like every other
credential. Saving a database configuration is refused if the
connection fails, so a typo cannot take the app down. After switching, Import
documents from files copies what is still on disk into the database, skipping
ids already present and never deleting the source files.
| Variable | Default | Purpose |
|---|---|---|
DOCYFIER_DB_DRIVER |
files |
files, postgres or mysql |
DOCYFIER_DB_HOST |
localhost |
Database host |
DOCYFIER_DB_PORT |
5432 / 3306 |
Defaults to the driver's port |
DOCYFIER_DB_USER |
— | Database user |
DOCYFIER_DB_PASSWORD |
— | Database password |
DOCYFIER_DB_NAME |
— | Database name |
DOCYFIER_DB_SSL |
0 |
1 connects over TLS (certificates verified) |
/settings/style holds what every document inherits: the theme new documents
start in, the presets this instance saved, and how the assistant writes — emoji
policy, keyword bolding, statuses as badges, writing language. There is no
environment variable for any of it; it is a look, and a look is chosen on
screen. A document from a template keeps the template's preset, and a generated
document keeps the theme its plan chose: the house theme dresses what nobody
else dressed.
Docyfier is currently single-user: one password guarding the instance, off by default and enabled as soon as a password exists. Multi-tenant access is on the roadmap, not in the build — do not expose an instance to the open internet expecting per-user isolation.
| Variable | Default | Purpose |
|---|---|---|
DOCYFIER_AUTH |
auto | On as soon as a password exists; 1 forces the setup form on first visit, 0 forces it off |
DOCYFIER_AUTH_PASSWORD |
— | Sets the password from the environment instead of the setup form |
DOCYFIER_AUTH_SECRET |
derived | Session signing key; without it one is derived and kept with the data |
The Dockerfile builds a small runtime image: dependencies and the Next.js
build happen in throwaway stages, and only the output: "standalone" server
bundle reaches the final layer. It runs as a non-root user on port 3000 and
exposes a single volume, /data, holding documents, uploads and settings —
everything that must survive an image upgrade.
just docker-build # docker build -t docyfier:latest ..github/workflows/docker.yml publishes to the GitHub Container Registry as
ghcr.io/<owner>/docyfier. It runs on version tags only — pushing v0.2.0
yields 0.2.0, 0.2, latest and the short commit sha — because a tag is the
signal that a build is meant to be deployed. workflow_dispatch covers the
occasional build from a branch. The workflow authenticates with the
repository's own GITHUB_TOKEN, so a fork needs no configuration.
A host pulling a private image needs a personal access token with the
read:packages scope:
docker login ghcr.io -u <github-user> --password-stdincompose.yaml is a ready deployment stack. Point the volume at a directory on
the host, keep secrets in a .env file beside it (never commit it), then:
docker compose pull && docker compose up -dPin image: to a release tag rather than latest, so a broken build cannot
reach the host on the next pull. Upgrading is pull + up -d; the /data
volume carries over untouched.
One recurring trap: DOCYFIER_LLM_BASE_URL must name a host the container
can reach. A model server on the machine running Docker is not localhost from
inside the container — use its LAN address, and make sure the server listens on
0.0.0.0 rather than loopback.
| Command | Purpose |
|---|---|
just setup |
Install dependencies |
just dev |
Dev server with hot reload |
just build |
Production build |
just start |
Serve the production build |
just serve |
build + start |
just check |
typecheck + lint |
just docker-build |
Build the Docker image |
just clean |
Remove .next |
Ports are overridable: just dev 4000. Each recipe maps to an npm script, so
just is a convenience, not a requirement.
src/components/Editor.tsx— Tiptap editor, toolbar, autosave.src/infrastructure/editor/— the Tiptap adapter: custom nodes (callouts, charts, cards…) and the schema AI output is validated against.src/components/AiPanel.tsx/SelectionAiMenu.tsx/GenerateHero.tsx— the three AI surfaces.src/lib/ai/— provider, prompts, schema validation, services.src/lib/store/— document store: facade + files / PostgreSQL / MySQL drivers.src/infrastructure/publishing/targets/— export targets, one adapter each, listed insrc/lib/export/registry.ts.src/infrastructure/rendering/— the document renderers: HTML, Markdown, Jira, plain text.src/domain/composing/composers/— email and ticket composers, same plugin shape.src/domain/documents//src/domain/authoring/— the document rules with no I/O in them: chart data, accepted import files, the deterministic formatter, the block diff behind AI review.src/lib/import.ts— file import, over the conversion adapters insrc/infrastructure/documents/.src/app/actions.ts/ai-actions.ts— server actions.src/app/globals.css— design system, editor chrome, A4 print rules.
Adding an export target or a composer means writing one file and registering it; that is the intended extension point.
Shipped so far: the editor, the AI surfaces, themes, templates, import/export, the composers, single-user auth, the instance's own identity and writing style, diagrams — flows, architectures, sequences, hierarchies and phase axes, placed by the app and then edited where you read them — and a model-facing document format that is markdown rather than raw JSON, which cut what an AI call carries to a third of the characters in both directions. Next up: comments, suggestions and version history; then multi-tenant workspaces with per-user permissions, and print-quality PDF through headless Chromium — today's PDF goes through the browser print dialog on purpose.
PLAN.md holds the ordered roadmap; docs/vision.md holds the product intent behind it.
GNU AGPL-3.0-only. Use it, modify it, self-host it freely. The one obligation that matters: if you run a modified version as a network service, its users are entitled to your source.
Issues and pull requests are welcome. Conventions: all artifacts in English
(code, comments, docs, commit messages), Conventional
Commits for messages, and small focused
diffs — one concern per commit. Run just check before opening a PR.