It's your epic. Every source leaves a mark.
Nornis is a world memory engine for tabletop roleplaying games.
You feed it the raw material of your game — session notes, handwritten pages, uploads, images, maps — and it reads that material and proposes structured knowledge: characters, locations, factions, items, events, storylines, and the facts and relationships that connect them. Nothing enters the record without a human accepting it.
What accumulates is a searchable, cited record of your world that you can ask questions of in plain language, browse as a codex or a graph, walk along a timeline, trace across a map, and selectively share with your players.
Nornis is not a wiki. Sources are raw material; artifacts are memory. The product is the transformation between them.
Capture a source
↓
Async AI extraction
↓
Review proposals ──→ Accept / Edit / Reject
↓
Artifacts, facts, and relationships update
↓
Ask the Loremaster
You paste a session note:
We questioned Captain Voss in Black Harbor. He denied knowing about the missing caravan, but Tavrin found the Silver Key in his quarters.
Nornis proposes — and waits for you to decide on each one:
- Create or update
Captain Voss, and connect him toBlack Harbor - Add the claim "Captain Voss denied knowing about the missing caravan"
- Create or update
Silver Key, with the fact "found in Voss's quarters" - Open the storyline
Missing Caravan, linking Voss, Black Harbor, and the Silver Key
Accept, edit, or reject each proposal. What you accept becomes canon, and every accepted claim keeps a link back to the sentence in the source that produced it.
| Area | What it gives you |
|---|---|
| Capture | Session notes, GM prep, handwritten pages (transcribed), an in-app ink canvas, images (vision-read), PDFs and documents, maps (place names and pins read off the image), and links. |
| Review | Every AI suggestion lands in a queue with its confidence, its source, and its rationale. Accept, edit the values first, or reject — individually or in dependency-safe bulk. |
| Codex | Browse everything the world knows as cards, a collapsible tree, or a live force-directed graph. Every artifact shows its facts, truth states, relationships, open questions, and source excerpts. |
| Ask the Loremaster | Ask your world questions in plain language. Answers are grounded strictly in your accepted record plus your indexed library, every claim cited, with a confidence rating — and it says so when it doesn't know. |
| Timeline, journey & locations | Storylines laid out over the real session calendar, the party's trail across a map walked session by session, and the reverse view — pick a place, get every session that visited it. |
| World digest | A short read on where the world stands, on the dashboard, written twice: the full record for GMs, and a party version drawn only from what the party can see. |
| Secrets & reveals | Everything carries a scope — Private, GM only, or Party visible. When the fiction discloses a secret, the GM ticks exactly what the party now learns; Nornis checks the reveal leaves no dangling references. One-way. A convergence gauge ranks what the party is ready to learn next, and players get a "what you learned" view of everything disclosed since they last looked. |
| Campaigns & characters | Campaigns are runs of play with their own page — cast, places, and record assembled from what their sessions cite. A character is somewhere to go: what the record knows about them, grouped for reading about a person, beside a free-form sheet Nornis stores but never interprets and dated snapshots of the paper one. |
| Library | Upload sourcebooks, maps, and handouts. PDFs are indexed into passages so the Loremaster can quote them with page citations. |
| Continuity health | A read on how coherent the record is — contradictions, dangling threads, stale storylines, timeline conflicts, summary drift, and duplicates — each finding with a severity, a jump to the artifact, and most drafting into a reviewable fix. |
| Onboarding | A demo world of five already-extracted sessions plus a sixth left as raw notes, and a two-chapter checklist that detects its own completion from actual state. Dismissable permanently. |
| Sharing | Invite players by link with a role. Optionally give the world a public address for a read-only, party-visible-only view, with a public Ask the Loremaster behind a monthly cap the GM sets. |
| Cost visibility | Every AI call is metered by operation, model, and user, against a per-world daily budget. |
Roles are GM, Player, and Observer — and they see genuinely different worlds. A player and a GM asking the Loremaster the same question get different answers, because retrieval respects visibility.
A fuller tour lives on the in-app Features page at /features. Per-feature design docs
are in docs/features/, numbered in build order.
Three independently deployable services over a shared Clean Architecture core:
nornis-web Blazor Web App (MudBlazor) — the UI
nornis-api ASP.NET Core — the HTTP API
nornis-worker Background processor — drains the extraction queue
| Project | Role |
|---|---|
src/Nornis.Domain |
Entities, enums, repository interfaces. No EF Core, no Azure, no UI. |
src/Nornis.Application |
Use cases, services, authorization, AI orchestration. Depends on interfaces only. |
src/Nornis.Infrastructure |
EF Core repositories, migrations, blob storage, Service Bus, Azure OpenAI. |
src/Nornis.Api |
Controllers, request/response contracts, auth filters. |
src/Nornis.Web |
Blazor pages and components, API client. |
src/Nornis.Worker |
Queue-driven extraction, indexing, and transcription jobs. |
Stack: .NET 10 · Blazor · ASP.NET Core · Azure SQL (EF Core, repository pattern) · Azure Blob Storage · Azure Service Bus · Azure OpenAI · Auth0 (Discord) · Azure Container Apps · GitHub Actions.
Every project has a matching test project under tests/; NUnit throughout.
Prerequisites: .NET 10 SDK (see
global.json), Docker Desktop, PowerShell 7+.
The local stack runs SQL Server and the Azure Service Bus emulator in Docker, applies migrations, and launches all three apps with connection strings pointed at the containers — so a local run can never touch cloud SQL or compete with the cloud worker for queue messages.
# Everything: containers, migrations, API + Worker + Web
./scripts/start-local.ps1
# Just SQL + Service Bus + migrations
./scripts/start-local.ps1 -InfraOnly
# Tear down
docker compose -f compose.local.yaml down- Web: http://localhost:5100
- API: http://localhost:5000 (dev-auth bypass active — no Auth0 round trip)
The only calls that leave your machine are to Azure OpenAI, so the AI does real work. The API and Worker have separate user-secret stores and separate deployments — extraction runs in the Worker, everything conversational runs in the API:
# Worker — extraction (deployment: nornis-extract)
dotnet user-secrets --project src/Nornis.Worker set "Extraction:AiEndpoint" "https://<resource>.openai.azure.com/"
dotnet user-secrets --project src/Nornis.Worker set "Extraction:AiApiKey" "<your-key>"
# API — Ask the Loremaster, continuity health, retrospectives, library
# retrieval (deployment: nornis-ask)
dotnet user-secrets --project src/Nornis.Api set "Loremaster:AiEndpoint" "https://<resource>.openai.azure.com/"
dotnet user-secrets --project src/Nornis.Api set "Loremaster:AiKey" "<your-key>"Neither is required to boot. Without them the app runs fine — capture, browse, review, share — and the AI paths fail with an explicit "not configured" message rather than a crash.
The
AiModelvalues inappsettings.jsonare Azure OpenAI deployment names, not model names, and theModelPricingkey must match, or cost tracking silently records $0.
dotnet build Nornis.sln
dotnet test --solution Nornis.sln
# One project
dotnet test --project tests/Nornis.Application.Tests/
# One fixture
dotnet test --project tests/Nornis.Application.Tests/ --filter "FullyQualifiedName~LibraryServiceTests"Warnings are errors (Directory.Build.props), so a clean build is
the bar.
Coverage is signal here, never a gate — see
.kiro/steering/testing-strategy.md.
./scripts/coverage.ps1 # collect, merge, open the HTML report
./scripts/coverage.ps1 -Projects Domain,Application
./scripts/crap-report.ps1 # rank methods by complexity against coverageEvery push to main publishes both to https://status.nornis.app — per-assembly trend and
the CRAP hotspot table, which is the test-writing backlog ordered by risk rather than by
whatever a percentage is shouting about. The full annotated report stays a build artifact on
the workflow run.
Pushing to main deploys. .github/workflows/deploy.yml
builds the three images in parallel with the test run, rolls the Azure Container Apps
forward, then polls /health until the new revision serves and fails loudly if it never
does — gated on tests passing, so images for a failing commit are tagged but never deployed.
Pull requests run ci.yml instead: restore, build, test,
vulnerable-package scan, format.
EF migrations are not applied by the pipeline. They are run by hand, before pushing the commit that needs them, and must stay additive so the old revision keeps serving through the rollout:
dotnet ef database update --project src/Nornis.Infrastructure --startup-project src/Nornis.ApiThe design-time factory reads the API's user secrets. Set ConnectionStrings:DefaultConnection
there to the production server and database with Authentication=Active Directory Default and
no password: your az login is then the credential, and it must be the SQL server's Entra admin
(or a contained user). If Visual Studio is signed in as a different account it sits ahead of the
CLI in the credential chain and the login fails — pin the chain with
AZURE_TOKEN_CREDENTIALS=AzureCliCredential, which .claude/launch.json already does for the
local API. Since 2026-09-08 the deployed apps reach SQL, Blob Storage and Service Bus the same
way, as their managed identities — see .kiro/steering/azure-hosting.md.
Miss that step and /health returns 503 until it is run — see
docs/runbooks/migration-missed.md.
Infrastructure is provisioned by scripts/provision-azure.ps1.
| Surface | Answers |
|---|---|
GET /health (API) |
Is this deploy broken? Pending migrations only; names whatever is failing. Backs the Container Apps readiness probe, the deploy poll, and the ping-nornis-api-health availability test. |
GET /status (API) |
Are the dependencies healthy? SQL, blob storage, Service Bus, Azure OpenAI, worker heartbeat. Anonymous; names and verdicts only. |
| https://status.nornis.app | Both dashboard faces. Hosted on GitHub Pages, deliberately outside Azure, so it still loads when the system it reports on does not. |
docs/runbooks/ has one doc per nameable failure mode, and every
Azure alert links to its own from its description. ./scripts/dlq.ps1 peeks, resubmits and
purges dead-lettered messages; ./scripts/ai-pause.ps1 stops every paid AI call without a
redeploy. The apps hold no credentials for SQL, Blob Storage or Service Bus — each reaches
them as its managed identity, and so do these scripts, as your az login.
src/ Application source (see the table above)
tests/ One test project per source project
ci/pages/ The static engineering dashboard published to status.nornis.app
docs/features/ Per-feature design docs, numbered in build order (index in its README)
docs/plans/ Backlog specs; docs/future-features.md holds the execution order
docs/runbooks/ One doc per nameable failure mode, linked from every Azure alert
docs/ Images
scripts/ Local stack, provisioning, note import, coverage, CRAP, dead-letter queue
.kiro/steering/ Product vision, architecture, and standards that guide the build
Two documents are worth reading before changing anything:
.kiro/steering/product-vision.md for what Nornis is
trying to be, and .kiro/steering/coding-standards.md
for how it's built.
- Domain vocabulary is deliberate: Storyline (never "Thread"), Source (never "Evidence"), Artifact, Fact, Relationship, Canon, Reveal.
- Repository pattern over EF Core — application services never touch
DbContext. - Authorization is enforced server-side, in application services.
- AI proposes; a human decides. Nothing mutates canon on its own.