artif4ct
artif4ct is a single-binary Go web app for creating, publishing, backing up, and restoring markdown-backed artifacts. An artifact can be a normal public web document, a form, a quiz source, or context for an LLM-powered chat. The application keeps editable content in SQLite, materializes portable artifact folders on disk, serves a plain HTML/CSS/JS admin UI, and can use OpenAI or Ollama to generate artifacts, quizzes, and chat responses.
The project intentionally has no Node build step. The frontend is static JavaScript and CSS in public/; the backend starts in cmd/artif4ct and is organized under internal/.
Architecture
See ARCH.md for an overview of Artif4ct's internal architecture, subsystems, dependencies, request flow, and startup flow.
Quick Start
make install
artif4ct init
artif4ct serve
Open http://localhost:8217/login.
For local development without installing:
make init
make serve
For development with admin auth bypassed:
go run ./cmd/artif4ct serve --dev
--dev redirects / to /admin and bypasses the admin login. Do not use it for a public deployment.
Commands
artif4ct init
Creates config/.env, data/, and public/uploads/ if they do not exist, opens the SQLite database, and runs migrations.
artif4ct serve
Loads config, opens SQLite, runs migrations, materializes artifact directories under data/artifacts/, builds the route mux, and serves the app on PORT or 8217.
artif4ct smoke
Loads config, opens the database, runs migrations, materializes artifacts, and exits after printing Smoke check passed.
Useful flags:
--port 4000overrides the listen port.--devenables the development auth bypass.
Make targets:
make buildbuildsbin/artif4ct.make installrunsgo install ./cmd/artif4ct.make init,make serve, andmake smokeproxy to the CLI.make testrunsgo test ./....make releasefillsrelease/with macOS, Linux, and Windows amd64/arm64 binaries namedartif4ct-<os>-<arch>.make cleanremovesbin/andrelease/.
Runtime Layout
The important project paths are:
cmd/artif4ct/: the executable entry point.internal/: application packages for the server, feature repositories, configuration, storage, LLM providers, and usage pricing.public/: static browser assets served directly by Go.public/uploads/: uploaded shared image assets.config/.env: local runtime configuration.data/main.sqlite: the default SQLite database.data/artifacts/<public_hash>/: materialized artifact directories.data/exports/artif4ct-YYYYMMDD-HHMMSS/: backup directories created from the admin Data page.release/: generated cross-platform binaries frommake release.
Generated/local directories are ignored by git: bin/, release/, data/, config/.env, public/uploads/, test-results/, and node_modules/.
Configuration
artif4ct init writes config/.env with these values:
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-me-now
OPENAI_API_KEY=
SESSION_SECRET=<random hex>
Required values are ADMIN_USERNAME, ADMIN_PASSWORD, and SESSION_SECRET.
The application always reads runtime config from config/.env and always stores SQLite data at data/main.sqlite.
The public host is fixed to https://artif4ct.com; no host environment variable is needed. This URL is used when public links are generated and when markdown links are normalized.
Provider, model, and Ollama connection choices are application settings, not environment configuration. Sign in and open Settings to choose OpenAI or Ollama, the model for each provider, and the Ollama server URL. OpenAI is the only provider secret: it reads OPENAI_API_KEY from config/.env.
How Startup Works
The entrypoint is main.go.
main.gocallsserver.Run(os.Args).findRoot()walks upward from the current directory until it findsgo.modandpublic/.parseArgs()chooses the command, port, and dev mode.initcreates directories, writes config if needed, opens SQLite, and migrates.serveandsmokecallnewServer().newServer()loads config, opens SQLite with foreign keys on, migrates, creates shared server state, and callsmaterializeAllArtifactDirs().serveregisters routes and startshttp.ListenAndServe.
The server struct is the central runtime object. It holds the root path, config, SQLite handle, dev-mode flag, in-memory admin sessions, and a mutex for session access.
Database Model
SQLite is the source of truth. Migrations live in internal/storage and run on every startup.
Main tables:
artifacts: title, markdown, type, public hash, chat metadata when the artifact is a chat, timestamps, and active flag.assets: uploaded image metadata and URL.quizzes: generated quiz markdown, compiled HTML, answer key JSON, source context, and optional source artifact.quiz_submissions: public quiz submissions and scores.form_submissions: public form submissions, payload JSON, optional score, and timestamp.chats: compatibility mirror for public chat access codes and hashes. New chat configuration lives onartifactsrows whereartifact_type = 'chat'.chat_artifacts: links knowledge artifacts directly to chat artifacts.model_usage: one record for each external provider model request.model_usage_sources: reserved source-link table for future artifact-context attribution per model request.login_failuresandchat_login_failures: rate-limit failed admin and chat-code attempts.
The migration layer also handles older storage shapes, older project names in imports, missing public hashes, quiz-table rebuilds, artifact title deduplication, and retired tables.
Routing
Routes are registered in routes.go.
Static files:
/app.css,/nav.js,/editor.js,/quiz.js,/chat.js/logo.png,/logo.svg/uploads//artifact-files//artifact-ref-files/
Public pages:
/shows the landing page unless dev mode redirects to admin./artifact/<hash-or-id>renders a public artifact./artifact-ref/<parent_hash>/<child_hash>renders a preserved retired artifact snapshot./quiz/<hash>renders a public quiz./quiz/submitgrades and records quiz answers./chat/<hash>handles public chat login and chat messages.
Admin pages:
/loginand/logout/admin/admin/artifacts/admin/assets/admin/data/admin/usage/admin/llm/providers
Admin routes are wrapped by requireAuth(). In dev mode the wrapper lets requests through.
Artifacts
Artifacts are the main content unit. Each artifact has:
- A unique title.
- Markdown content.
- A type, currently
web,form, orchat. - A stable public hash used in public URLs.
- A materialized folder under
data/artifacts/<public_hash>/.
Web artifacts are edited as block-based markdown in the admin UI and rendered publicly through the markdown parser. The public artifact page does not inject a separate title heading; the markdown controls the visible document.
Form artifacts are markdown documents with JSON front matter and special question comments. They can be edited through the form editor, submitted publicly, and exported with their responses.
Artifacts can be created manually, uploaded from markdown, cloned, renamed, bulk deleted, previewed, or generated by an LLM request.
Materialized Artifact Directories
The database stores the canonical artifact record, but the app also writes each artifact to disk:
data/artifacts/<public_hash>/
artifact.md
assets/
responses.csv # only for form artifacts with responses
artifact.md contains JSON metadata front matter followed by the artifact markdown. This makes artifacts portable and inspectable outside SQLite.
When an artifact references an uploaded image from /uploads/, materializeArtifactDir() copies that file into the artifact's own assets/ folder and rewrites the markdown image URL to /artifact-files/<artifact_hash>/assets/<filename>. This makes published artifacts self-contained.
Artifact Retirement
Deleting artifacts goes through artifactLifecycle.
Before a live artifact is removed, the lifecycle code checks whether other live artifacts link to it. If a dependent artifact points at the one being deleted, the deleted artifact's materialized directory is copied into:
data/artifacts/<dependent_hash>/references/<deleted_hash>/
The dependent artifact's links are rewritten from /artifact/<deleted_hash> to /artifact-ref/<dependent_hash>/<deleted_hash>. This preserves old references without keeping the deleted artifact live in the database.
Bulk deletes require typing yes.
Assets
The asset library stores shared uploaded images.
Files live in public/uploads/. Metadata lives in the assets table. Uploaded files are limited by maxUploadBytes, currently 8 MiB.
Assets can be uploaded, renamed, given alt text, selected for LLM artifact context, and bulk deleted. Deleting a shared asset removes it from public/uploads/, but artifacts that already copied the file into their own materialized directory keep their private copy.
Forms
Forms are artifact type form.
Forms are managed from the main Artifacts page. There is no separate Forms index; filter the artifact list by Form to view form artifacts, and use each form row's Responses action to inspect submissions.
A form document starts with JSON front matter:
---
{"type":"form","version":1,"quiz":false}
---
Question blocks use comments:
<!-- artif4ct-form-question {"type":"multiple_choice","required":true,"correct":["o2"]} -->
### Question text
- [ ] Option one
- [x] Option two
<!-- /artif4ct-form-question -->
Supported question types:
multiple_choicetrue_falseselect_allshort_answerlong_answer
If front matter has "quiz":true, the form can score submissions using the stored correct answers. Public submissions are stored in form_submissions and appended to the artifact's responses.csv.
Legacy pipe-separated form syntax is still parsed for older content.
Quizzes
Quizzes are separate from form artifacts. They are generated from selected artifacts and optional source text, then stored in the quizzes table.
The quiz source is markdown with JSON front matter:
---
{
"type": "quiz",
"version": 1
}
---
The validator requires exactly five questions named q1 through q5. Each question must have four options named a through d, exactly one marked correct with [x].
compileQuizMarkdown() compiles that markdown into server-owned quiz HTML and extracts the answer key. Public submissions post to /quiz/submit; grading happens on the server, not in browser JavaScript.
Quiz generation retries up to three times if the LLM returns invalid markdown. Validation errors are fed back into the next attempt.
LLM Generation
LLM access is centralized in the model gateway formed by completeModel() in llm_provider.go and usage helpers in model_usage.go.
The app supports:
- OpenAI through the Responses API.
- Ollama through
/api/chat.
completeLLM() normalizes provider choice and dispatches to the provider-specific implementation. Provider status is exposed to admin UI code through /admin/llm/providers.
The system-wide default provider and models are stored in SQLite and managed at /admin/settings. Individual chat artifacts may override that default; public users cannot change either setting.
LLM generation is used for:
- Requested web artifacts.
- Requested form artifacts.
- Quizzes from selected artifacts/source text.
- Public chat replies.
Generation never trusts raw output blindly. Requested artifacts and quizzes are validated before they are saved.
External AI Usage Tracking
Every external AI request goes through the shared model gateway. Feature code asks for a model operation; the gateway handles provider selection, request execution, latency measurement, provider-reported usage parsing, and usage recording.
Current external provider accounting is implemented for OpenAI. Ollama requests still pass through the gateway, but they do not create billable external usage rows because local models do not incur external API token charges.
Each external request writes one model_usage row with:
- Optional
chat_id. - Provider and model.
- Operation label, such as
chat. - Input, output, cached input, and total tokens when the provider reports them.
- Estimated cost from the central pricing table.
- Pricing snapshot JSON used for that estimate.
- Request and response byte counts.
- Latency in milliseconds.
- Status, error text, and timestamp.
Usage records are the source of truth. Chat totals and system totals are both aggregates over model_usage; there is no separate monthly accounting table.
Chat-level usage is shown on each chat edit page. It totals only external-provider requests associated with that chat.
System-wide usage is available at /admin/usage. It groups usage by month, shows monthly request/token/cost totals, and breaks the selected month down by provider and model.
Pricing is centralized in model_usage.go and can be overridden from Settings with the custom model pricing JSON field. Updating prices does not require changes to chat, quiz, or artifact generation logic. Existing usage rows preserve the pricing snapshot used when the request was recorded.
model_usage_sources exists so a later change can record which artifacts contributed context bytes or estimated tokens to a model request.
Chats As Artifacts
Chats are public rooms protected by a short access code. A chat is represented as an artifact row with artifact_type = 'chat', so it has its own materialized directory under data/artifacts/<public_hash>/ and its behavior is self-describing in that directory's front matter.
Admins create chat artifacts from /admin/artifacts/new and manage them from the main /admin artifact list. There is no separate chat index page.
Each chat artifact has:
- A title.
- A public hash used in the URL.
- A generated code required for public access.
- An admin-selected LLM provider.
- An admin-selected LLM model.
- Optional linked knowledge artifacts through
chat_artifacts.
Public chat users cannot choose the provider or model. That setting belongs to the Artif4ct admin because external provider choice controls cost. When a public user sends chat messages, the server builds context from the chat's linked knowledge artifacts and recent conversation messages, then calls the chat artifact's configured model through the central gateway.
There is no separate persona system. If a chat needs a voice, role, policy, or behavioral instruction set, model it as a normal artifact and attach that artifact to the chat. The chat remains secure because public users only see the public chat route and only receive model responses scoped to the attached artifacts.
Public chat access uses a signed cookie separate from the admin session and has its own failed-code lockout table.
Import And Export
The Data admin page can export the current runtime state to:
data/exports/artif4ct-YYYYMMDD-HHMMSS/
The export contains:
manifest.jsonartifacts/with materialized artifact directoriesassets/with uploaded files andassets.jsonchats/chats.jsonquizzes/*.jsonresponses/*.csvusage/model_usage.jsonusage/model_usage_sources.json
Import validates the manifest, stages runtime files, clears current runtime tables, imports artifacts/assets/chats/quizzes/responses/usage with their original IDs, swaps runtime files into place, reruns migrations, and materializes all artifact directories.
Supported backup manifest formats include the current artif4ct format plus older compatible project names.
Frontend
There is no frontend build pipeline.
Static assets:
public/app.css: layout, admin UI, editor, public artifact, quiz, form, chat, and responsive styles.public/nav.js: admin navigation and shared dialogs/selection behavior.public/editor.js: block editor behavior for web and form artifacts.public/quiz.js: quiz/form page behavior.public/chat.js: public chat behavior. Provider and model selection is admin-owned and stored on the chat artifact.public/logo.*: branding assets.
HTML pages are generated in Go, mainly in pages.go and feature-specific handlers. Browser JavaScript enhances server-rendered forms rather than owning the whole app.
Code Map
main.go
Program entrypoint and top-level command dispatch.
cli.go
Root discovery, argument parsing, initApp(), and newServer().
types.go
Shared constants and data structs.
config.go
Environment parsing and SQLite opening.
routes.go
HTTP route registration.
auth.go
Admin sessions, signed cookies, and failed-login lockout.
store.go
Small SQLite read helpers for artifacts, assets, quizzes, and chats.
migrations.go
Schema creation, compatibility migrations, backfills, table repairs, and retired-table cleanup.
handlers_artifacts.go
Admin artifact creation, upload, preview, edit, clone, rename, delete, response pages, and save handling.
artifact_dirs.go
Materialized artifact folder paths, markdown metadata writing, artifact asset copying, and form response materialization.
artifact_lifecycle.go
Deletion rules, reference preservation, link rewriting, and artifact directory removal.
artifact_references.go
Serving artifact files and preserved retired-reference snapshots.
artifact_requests.go
LLM-assisted artifact creation, request context assembly, image context handling, and generated markdown validation.
markdown_blocks.go
Block editor parsing/compilation, markdown block utilities, upload text extraction, and request form parsing.
form_artifacts.go
Form markdown format, form parser, form renderer, answer scoring, and form payload serialization.
form_response_files.go
Form response CSV writing, reading, display, and SQL nullable score helper.
quiz_markdown.go
Quiz markdown validation, parser, HTML compiler, and answer extraction.
quiz_service.go
Quiz context assembly, referenced artifact traversal, LLM quiz generation, quiz-output cleanup, and legacy generated-HTML validation helpers.
handlers_quizzes.go
Admin quiz creation/response views plus public artifact, form, quiz, and quiz-submit handlers.
handlers_assets.go
Asset library pages, upload, rename, alt text updates, and deletion.
handlers_chats.go
Admin chat artifact management, public chat code login, message endpoint, chat cookies, and LLM reply generation.
handlers_data.go
Admin backup export and restore routes.
handlers_usage.go
Admin system-wide monthly usage page and reusable chat usage summary panel.
exportdir.go
Portable backup directory creation, import parsing, CSV/JSON helpers, file copy helpers, and slugging.
import_runtime_files.go
Staging uploaded/artifact files during import and atomically swapping runtime directories.
llm_provider.go
Provider availability, OpenAI completion, Ollama completion, model listing, and provider normalization.
model_usage.go
Model gateway request/result structs, usage insertion, pricing, cost estimation, monthly aggregation, chat aggregation, provider/model breakdowns, export structs, and usage display formatting.
pages.go
Server-rendered admin/public page templates and shared page shell.
branding.go
Shared display name and tagline constants.
util.go
Response helpers, random IDs, timestamps, URL builders, path helpers, and misc utilities.
main_test.go
End-to-end handler tests, parser tests, migration tests, import/export round trips, lifecycle tests, LLM provider behavior, and UI regression checks.
Extension Points
The project is easiest to extend by following the current file boundaries.
To add a new admin area:
- Add routes in
routes.go. - Add a focused
handlers_<feature>.go. - Add storage helpers in
store.goonly if they are shared. - Add schema changes in
migrations.go. - Add page HTML in the handler file or
pages.godepending on reuse. - Add tests in
main_test.goor split tests by feature if the file becomes too large.
To add a new artifact type:
- Extend
artifactTypeFromRequest()andartifactTypeLabel(). - Add markdown compile/parse/render logic in a dedicated file.
- Update artifact create, upload, preview, save, and public render paths.
- Update materialization if the type needs extra files.
- Include import/export behavior if the type stores separate data.
To add an LLM provider:
- Add provider constants and status handling in
llm_provider.go. - Implement provider-specific completion.
- Extend config loading.
- Keep callers going through
completeLLM(). - Add tests around availability, model selection, and error messages.
To add new backup content:
- Add tables or files to
exportStateDirectory(). - Add readers and import inserts in
importStateDirectory(). - Stage runtime files in
stageRuntimeFilesForImport()if needed. - Include compatibility behavior in migrations.
Design Notes
The app favors a small, inspectable architecture over framework layering. A single server object and feature-split files keep behavior easy to trace.
SQLite is the durable source of truth, but materialized artifact directories make content portable and readable.
Public URLs use random hashes instead of database IDs where possible.
The frontend is deliberately static and server-rendered. This avoids a JavaScript dependency chain and keeps release builds to a Go cross-compile.
LLM output is treated as untrusted input. The app validates generated artifacts and quizzes before saving them.
Deletes are conservative. Artifact deletion preserves dependent snapshots when live artifacts link to retired content.
Current Quality Checks
Run:
make test
make build
make release
make test covers the core flows: admin auth, dev mode, artifact editing, forms, quizzes, chats, assets, migrations, import/export, and artifact retirement.