- Python 71.8%
- TypeScript 27.4%
- CSS 0.3%
- Dockerfile 0.2%
- Shell 0.2%
- Other 0.1%
| .forgejo/workflows | ||
| backend | ||
| dev/keycloak | ||
| docs | ||
| frontend | ||
| mcp-server | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| CONTRIBUTING.md | ||
| docker-compose.prod.yml | ||
| docker-compose.sso.yml | ||
| docker-compose.yml | ||
| GEMINI.md | ||
| LICENSE | ||
| README.md | ||
| SECURITY.md | ||
Normwerk
Open-Source Compliance Management
Self-hostable, AI-assisted compliance management. ISO 37301 is the baseline; the Harmonized Structure (clauses 4–10) is modelled once and reused by extension packs such as ISO/IEC 27001, ISO 9001 and ISO 14001. A guided interview asks plain-language questions, saves every answer immediately and turns it into reviewable draft records.
Status: early foundation (v0.1). Not yet production-ready.
Features
- Guided AI interview in plain business language (English and German). Every answer is saved immediately; an LLM turns it into draft records you review and approve.
- Process map with management, core and support processes, subprocesses, custom swimlanes on every level (e.g. business areas within core processes) and hand-overs. Risks, controls, documents and KPIs are linked to the processes they belong to, and weak spots are flagged live.
- One management system, many standards. Each record (risk, control, policy, audit, …) exists once and is mapped to the requirements of every enabled standard.
- Coverage and gap analysis per clause of the Harmonized Structure, with an applicability decision (and justification) per requirement as the basis for a Statement of Applicability.
- Review schedule (Wiedervorlage): assignees, review intervals and deadlines on every record; a daily job sends in-app reminders and an e-mail digest before things are due.
- Self-hosted and model-agnostic. Docker Compose, local models via Ollama/vLLM or cloud APIs (Anthropic, OpenAI) via LiteLLM.
- Users, roles and single sign-on: invitations, four roles, OpenID Connect (Keycloak, Authentik, Entra ID, …) with group → role mapping, personal API tokens and a full audit log.
- English and German UI, light and dark mode.
Screenshots
| Overview | Guided interview |
|---|---|
![]() |
![]() |
| Subprocess map with swimlanes (dark mode) | Management system records |
![]() |
![]() |
| Gap analysis | German UI |
![]() |
![]() |
Quick start
cp .env.example .env # set DJANGO_SECRET_KEY and the LLM settings
docker compose up -d # postgres (pgvector), valkey, backend, celery, frontend
docker compose logs backend | grep "Generated password" # initial admin password
- App: http://localhost:5173. Sign in as
DJANGO_SUPERUSER_USERNAME(defaultadmin). IfDJANGO_SUPERUSER_PASSWORDis empty, a random password is generated on first start and printed once in the backend log. It only appears on the first start; if you missed it:docker compose exec backend python manage.py changepassword admin. - API docs (OpenAPI): http://localhost:8000/api/docs
- Django admin: http://localhost:8000/admin
When the backend starts, it runs the migrations, compiles translations, creates the admin user and loads the standard catalogue and the interview question bank.
Production: see docs/deployment.md (docker-compose.prod.yml: gunicorn, nginx, no
development servers or bind mounts).
Demo data: to explore the app with a sample company (processes, risks, controls, an interview):
docker compose exec backend python manage.py seed_demo --user admin --language en # or: de
AI models
The platform talks to models through LiteLLM, so local and cloud models are configured the same way:
| Setup | .env |
|---|---|
| Local (Ollama) | LLM_MODEL=ollama/llama3.1, LLM_API_BASE=http://ollama:11434, then docker compose --profile ollama up -d and docker compose exec ollama ollama pull llama3.1 |
| Anthropic Claude | LLM_MODEL=anthropic/claude-sonnet-5-5, ANTHROPIC_API_KEY=…, LLM_API_BASE= |
| OpenAI | LLM_MODEL=openai/gpt-4o, OPENAI_API_KEY=…, LLM_API_BASE= |
| No AI | LLM_MODEL= (answers are stored, nothing is extracted) |
Semantic requirement matching needs an embedding model (EMBEDDING_MODEL, e.g.
ollama/nomic-embed-text). After configuring it, run
docker compose exec backend python manage.py index_requirements.
Data protection: with a cloud model, interview answers are sent to that provider. Check your data processing agreement (GDPR), or use a local model for confidential information. Model weights (e.g. Llama) come with their own licenses.
Users, roles and single sign-on
| Role | Can do |
|---|---|
| Owner | everything, incl. members, invitations and SSO settings |
| Compliance officer | create and edit records, approve them, view the audit log |
| Contributor | create and edit records (drafts, in review) |
| Auditor | read everything incl. the audit log, change nothing |
Every change (create, update, approve, delete, sign-ins, member and token changes) is recorded in the audit log with the field-level differences (Settings → Audit log).
Single sign-on (OpenID Connect):
- Register a confidential client at your identity provider with the redirect URI
<APP_BASE_URL>/oidc/callback/and agroupsclaim in the user info. - Set
OIDC_RP_CLIENT_ID,OIDC_RP_CLIENT_SECRETandOIDC_DISCOVERY_URLin.envand restart the backend. - Map identity-provider groups to roles under Settings → Single sign-on.
Accounts are linked by issuer and subject, not by e-mail. Set LOCAL_LOGIN_ENABLED=false for
SSO-only operation (administrators keep local access as a fallback). To try it locally with a
prepared Keycloak:
docker compose -f docker-compose.yml -f docker-compose.sso.yml up -d
# sign in with sso-alice / sso-alice-dev-pw (dev credentials only)
API tokens (My account → API tokens), for scripts and AI assistants:
curl -H "Authorization: Bearer nw_…" http://localhost:8000/api/organizations/
Tokens have one of three access modes: read-only (never changes data), AI assistant (may only create and edit drafts, marked as AI-generated; approving and everything else is refused by the server) and full access.
AI assistants (MCP)
Normwerk includes an MCP server, so assistants such as Claude
can read your management system and propose drafts. It runs as its own container and is served
at /mcp (Streamable HTTP); locally at http://localhost:8100/mcp.
-
Create a token with access AI assistant under My account → API tokens.
-
Add the server to your MCP client, e.g. Claude Code:
claude mcp add --transport http normwerk https://normwerk.example.com/mcp \ --header "Authorization: Bearer nw_…"
Tools: organizations, coverage overview, process map (per level), gaps, due reviews and deadlines, requirement search with applicability, record types and their fields, list/get records, and create/update draft records. The assistant acts as you: it sees only your organizations, and every change appears in the audit log as "AI via (API token: …)". Drafts are approved by people in Normwerk.
Architecture
backend/ Django 5 + Django Ninja, Celery + Valkey, PostgreSQL + pgvector
apps/core tenancy (Organization, Membership), ComplianceRecord base, audit trail,
auth API
apps/accounts invitations, API tokens, OIDC single sign-on, SSO group mappings
apps/standards Standard → Clause → StandardRequirement catalogue, pack activation
data/*.json standard packs (paraphrased requirement summaries, EN/DE)
apps/compliance unified HS models for clauses 4–10, process map, generated CRUD API,
coverage, gap analysis, requirement mapping, AI policy drafting
apps/interviews question bank, sessions, answers, AI extraction into draft records
question_bank/ plain-language interview questions (EN/DE)
apps/ai LiteLLM gateway, embeddings, pgvector retrieval, prompt templates
locale/de/ German translations (gettext)
frontend/ React + TypeScript + Vite, Tailwind CSS v4, shadcn/ui, TanStack Query,
Zustand, React Flow
src/components/ui shadcn primitives
src/features/* dashboard, process map, interview, records, gaps, standards
Key design rule: there are no per-standard models. Each HS concept, such as ComplianceRisk,
exists once per organization. It is linked to the StandardRequirements it covers through a
many-to-many relation, so a single risk register serves ISO 37301 and ISO 27001 at the same time.
Process map
Every record can be linked to the business processes it belongs to (processes). The process map
(/processes) shows management, core and support processes and the hand-overs between them.
Double-click a process (or use “Open subprocess map”) to drill into its subprocesses. Sub-maps have
swimlanes you define yourself, for example departments or roles. Subprocesses can hand over to
processes on other maps: those appear in an extra lane, and on the top-level map the hand-over is
shown dashed between the parent processes. Evidence linked to a subprocess also counts for its
parents.
The map flags weak spots live: no owner, no assessed risks, risks without controls, no KPI, outsourced without a partner record, or a core process with no hand-overs. From the map you can start a process interview that asks the risk, control, partner and KPI questions for that one process and links everything it creates to the process.
Interview flow
POST /api/interviews/{org}/sessionsstarts a session, either for one HS clause or for one process (process_id). Early on, the organization interview asks for the main workflows and createsBusinessProcessdrafts.- Each answer is saved straight away (
InterviewAnswer, statussaved). - A Celery task asks the LLM to extract structured records for the question's target model.
The results are validated against the model and stored as draft records with
ai_generated=True. - Records are linked to requirements: deterministically through
evidence_modelsin the pack data, and semantically through pgvector if embeddings are available. - Gap analysis (
POST /api/compliance/{org}/gaps/run) lists requirements of the enabled standards that have no evidence, or only draft evidence.
Development
docker compose exec backend pytest # backend tests
docker compose exec backend ruff check . && docker compose exec backend ruff format .
docker compose exec backend python manage.py makemigrations
docker compose exec backend python manage.py makemessages -l de # after adding strings
cd frontend && npm install && npm run build && npm run lint
See AGENTS.md for architecture and conventions and CONTRIBUTING.md for how to contribute. Report security issues privately, see SECURITY.md.
Disclaimer
This project is not affiliated with, endorsed by or certified by the International Organization for Standardization (ISO). "ISO" and standard names are used only to describe compatibility. The platform does not contain the text of any ISO standard; requirement summaries are our own paraphrases. To implement or certify against a standard you need the official standard document. Using this software does not guarantee certification or legal compliance.
License
Copyright (C) 2026 Kevin Heyer and contributors.
Licensed under the GNU Affero General Public License v3.0 or later. If you run a
modified version as a network service, you must offer its source code to its users (AGPL §13).
Set VITE_SOURCE_CODE_URL to your repository so the link in the app points to it.






