---
title: Rukh — Roadmap
date: 2026-08-27
lang: en-US
author: Julien Béranger
model: Claude Opus 5
source: https://julienberanger.com/rukh-roadmap
---

# Rukh — Roadmap

Covers both [`rukh`](https://github.com/w3hc/rukh) (the API) and [`rukh-ui`](https://github.com/w3hc/rukh-ui) (the web app). The two ship in lockstep, so milestones are defined once and split into an **API track** and a **UI track**.

Status: proposal. Nothing here is committed to a release date.

> **Priority: the teachers & pupils profile.** M0–M4 exist to serve it. The ZK API profile (M7) is explicitly _not_ a priority and carries no date.

---

## 1. Guiding principle

**The API stays generalist.** Rukh must never learn what a school is, what a pupil is, or what a lesson plan is. It learns a small number of generic primitives, and every vertical is expressed as _data_ filling those primitives — never as a branch in the API.

Concretely, that means the roadmap below is mostly one thing: **growing the context schema from "a bag of files" into "a declarative description of an assistant"**, then implementing the runtime that reads it.

Three consequences worth stating up front, because they constrain everything else:

1. **A context is data, not code.** Anything that defines behaviour must be serialisable and inspectable. No vertical-specific handlers.
2. **The same context definition must run on either backend.** Rukh and zk-api are two deployments of the same idea, not two products. A context should move between them by changing one field.
3. **Capabilities are declared, resolved, and enforced per audience.** The same context grants different things to its creator than to an anonymous visitor. This is the mechanism that makes a safe pupil surface possible without a "kids mode" flag in the API.

---

## 2. The context schema

Everything converges here. Today a context is `name`, `description`, an optional `model` override, a list of `files`, a list of `links`, and a `queries` log (see `src/dto/context.dto.ts` in the API, and `docs/CONTEXT_MANAGEMENT.md`).

Target shape:

```jsonc
{
  "name": "maths-4e",
  "description": "Maths homework help, French 4e level",
  "schemaVersion": 2,

  // How it behaves. Always injected, never subject to RAG selection.
  "instructions": "You are a Socratic tutor for a 4e class. Never give the final answer; ask the question that moves the pupil forward one step...",

  // What it can do. Whitelist, resolved per audience (see below).
  "capabilities": ["skills", "document"],

  // Who may talk to it, and how they authenticate.
  "audience": {
    "creator": { "capabilities": ["skills", "document", "web-reader"] },
    "public": false,
    "code": { "enabled": true, "capabilities": ["skills"] }
  },

  // What gets logged, and for how long.
  "retention": { "storeMessages": false, "queries": "30d" },

  // Which backend serves it. Resolved by the UI, not by the API.
  "routing": "rukh", // | "zk-api"

  // Who pays.
  "billing": "creator", // | "end-user"

  // Unchanged.
  "model": "anthropic",
  "files": [...],
  "links": [...]
}
```

Every field is optional and every default reproduces today's behaviour, so existing contexts keep working untouched. `schemaVersion` exists so the migration can be explicit rather than inferred.

---

## 3. The three profiles

A **profile** is a preset that fills the schema above. Profiles live in `rukh-ui` as plain JSON — **the API has no notion of them**. That is the whole point: adding a fourth vertical must require zero API changes.

|                           | Generalist         | **Teachers & pupils**           | ZK API                       |
| ------------------------- | ------------------ | ------------------------------- | ---------------------------- |
| Priority                  | maintained         | **the priority**                | not a priority               |
| `routing`                 | `rukh`             | `rukh`                          | `zk-api`                     |
| `billing`                 | `creator` (Stripe) | `creator` (Stripe, school pays) | `end-user` (onchain credits) |
| `audience.public`         | `true`             | `false`                         | `true`                       |
| Access for end users      | open link          | class code, no account          | anonymous, proof-gated       |
| `retention.storeMessages` | creator's choice   | `false`, enforced               | operator's policy            |
| Identity seen by operator | wallet / anonymous | teacher only                    | **unlinkable by proof**      |
| RAG                       | server-side        | server-side                     | server-side                  |
| Skills                    | yes                | yes                             | yes                          |
| Server-executed tools     | yes                | teacher audience only           | yes                          |
| Viable from               | M0                 | M2                              | M7                           |

The education profile is therefore **not a feature** — it is `instructions` + `capabilities` narrowed for the `code` audience + `retention.storeMessages: false` + `audience.public: false`. Nothing in the API changes to support it beyond the generic mechanisms.

---

## 4. Two backends, one context

zk-api is getting its own Rukh-style RAG, so the two backends converge on capability. What differs is **identity and payment**, not what the assistant can do.

- **Rukh** (`POST /ask`) authenticates the creator by SIWE, meters cost server-side, and bills through Stripe.
- **[zk-api](https://github.com/w3hc/zk-api)** (`POST /zk-api/request`) authenticates a _right to spend_ rather than a person: a Groth16 proof plus a one-time nullifier, so two requests from the same depositor cannot be correlated. Transport is ML-KEM-1024 + AES-256-GCM (`src/encryption/mlkem-encryption.service.ts`), with optional TEE attestation.

**What zk-api does and does not buy you.** Its own README is the honest statement: _"The operator sees valid proofs and the requests it forwards. It does not see who you are or link your requests together."_ So the operator **does** see request content — which is precisely why it can run RAG. The guarantee is unlinkability of _identity_, not blindness to _content_. Do not describe it in the UI as end-to-end private; describe it as unlinkable. (An earlier draft of this document wrongly assumed the operator was blind to payloads and built a client-side prompt-compilation requirement on top of that. That requirement is dropped.)

Because both backends see plaintext and both can do RAG, a context needs no special preparation to move between them. `routing` is a deployment choice, not a capability boundary.

---

## 5. Request routing and PII redaction

Routing happens in **`rukh-ui`, at the Next.js API route level** — not in the browser and not in Rukh. This layer does not exist yet: `src/utils/api.ts` currently calls the Rukh API straight from the browser using the public `NEXT_PUBLIC_RUKH_API_URL`, and there is no `route.ts` anywhere under `src/app/`.

Target shape:

```
browser ──► /api/ask (Next.js route handler)
                ├─ redact PII
                ├─ resolve routing from the context
                ├──► Rukh   POST /ask
                └──► zk-api POST /zk-api/request
```

What the layer buys, beyond routing:

- Backend URLs stop being public. `NEXT_PUBLIC_RUKH_API_URL` becomes a server-only `RUKH_API_URL`, and the browser no longer needs to know where anything lives.
- One place to redact, rate-limit, and enforce quotas, rather than one per call site.
- SIWE headers are forwarded rather than exposed; the signature is still produced client-side by W3PK, so the trust model is unchanged.

**One caveat to accept knowingly.** Proxying makes the Next.js server a party that sees every prompt and every IP. For the Rukh/teacher path that is fine — it is already a trusted first-party server. For the zk-api path it is a genuine regression: it reintroduces exactly the observer that the proofs exist to eliminate. So for `routing: "zk-api"`, the Next.js route should return a _routing decision plus the operator's public key_ and let the browser talk to zk-api directly, redacting client-side before it sends. The package is zero-dependency regex, so it runs in the browser despite the `node` in its name.

### PII redaction

Use [`@redactpii/node`](https://www.npmjs.com/package/@redactpii/node) (MIT, v1.0.17, zero dependencies, fully offline):

```ts
import { Redactor } from '@redactpii/node'

const redactor = new Redactor()
const clean = redactor.redact(message) // "Hi PERSON_NAME, call PHONE_NUMBER"
```

It also offers `redactObject()` for whole payloads and `hasPII()` for detection without rewriting, and rules can be enabled selectively (`new Redactor({ rules: { EMAIL: true } })`).

**Be honest about what this is worth.** It is defence in depth, not a compliance guarantee, and it must never be sold to a school as "we remove personal data". Three concrete limits:

1. **The built-in patterns are US-centric.** Emails and credit cards travel, but phone matching targets US formats and SSN is a US identifier. French mobile numbers (`06 12 34 56 78`), the NIR, the INE, and French postal addresses are not covered. Extending the rule set for FR/EU identifiers is part of the work, not an afterthought.
2. **Name detection is greeting-based.** It catches `Hi David Johnson`. It will not catch `je m'appelle Léa` or `Léa, 4e B` — which is exactly how a French pupil introduces themselves. Expect close to zero name coverage out of the box in French.
3. **False positives are a real hazard in this product.** A maths context is full of long digit strings that regexes happily mistake for phone numbers or card numbers. Redacting the numbers in a maths exercise is worse than not redacting at all. Since rules are global (below), the mitigations have to be global too.

#### Rules are global configuration

Redaction rules are **configured once for the deployment**, not per context. A teacher configures an assistant's _behaviour_; they do not configure the privacy floor, and they must not be able to lower it for everyone by editing a context. So the rule set lives in `rukh-ui` as deployment config — a `src/config/redaction.ts` module with environment overrides — read by the Next.js route layer on every request, with a read-only view in `/settings` so an admin can see what is actually active.

That decision puts the whole weight of the false-positive problem on the defaults, so they have to carry it:

- **Rules are individually toggleable.** `EMAIL` and `CREDIT_CARD` are high-value and low-noise; `PHONE` and `SSN` are the ones that misfire on numeric content. Being able to disable one rule without disabling redaction is the difference between a usable default and one every operator turns off wholesale.
- **Exclude fenced code blocks and maths expressions before matching.** A global pre-pass that skips fenced code blocks and `$...$` / `\(...\)` spans removes most of the maths damage without touching the rules themselves.
- **Ship a dry-run screen.** Let an admin paste real lesson content and see exactly what would be redacted, before it is switched on. This is the cheapest possible safeguard and the one most likely to be skipped.
- **Log redaction counts per rule, never the matched text.** Counts make misfires visible and stay consistent with M0's rule that no message content is stored.

The actual privacy guarantee stays what it is in M0: **the message is never stored.** Redaction reduces what transits; not storing is what makes it unrecoverable. Also verify that the package's optional compliance-dashboard integration stays off, and pin that with a test — an outbound call from a redaction library would be a spectacular own goal.

---

## 6. Milestones

### M0 — Schema and privacy foundations

The single most important milestone. Everything else fills in fields defined here.

**API**

- Add `instructions` to `CreateContextDto` / `ContextMetadataDto`, persist it in `index.json`, and inject it at the head of the system prompt **outside** RAG selection (currently the only mechanism is the hardcoded `REQUIRED_FILES = ['instruction-file.md']` in `src/rag/rag.service.ts:21`). Keep the magic filename as a deprecated fallback.
- Stop writing the raw user message in `recordContextQuery` (`src/app.service.ts:283`). It currently persists the full prompt verbatim into `data/contexts/<name>/index.json`, under a `message` field that `ContextQueryDto` does not even declare, with no retention limit. Replace with `timestamp` + `contextFilesUsed` only.
- Add `retention` (`storeMessages`, `queries`) with a purge job. Default `storeMessages: false`.
- Declare `capabilities`, `audience`, `routing`, `billing` and `schemaVersion` in the schema and in Swagger. They may be inert at this stage — declaring them early is what lets the UI and the migration be written once.
- Write the migration for existing contexts in `data/contexts/`.

**UI**

- Introduce the Next.js route layer (§5): `/api/ask` and the context calls, moving `src/utils/api.ts` off `NEXT_PUBLIC_RUKH_API_URL`.
- Wire `@redactpii/node` into that layer, reading the global rule config, with the FR/EU rule extensions, the code/maths exclusion pre-pass, and the dry-run screen.
- Context edit page: a first-class "How should this assistant behave?" field, visually separate from Documents and Links.
- A retention control, defaulting to "don't store messages".
- Surface `schemaVersion` mismatches instead of failing silently.

**Done when** a context can define behaviour without a magic filename, no user message is written to disk anywhere, and every outbound prompt passes through one redaction point.

---

### M1 — Ingestion

Rukh accepts markdown. Teachers have PDFs, `.docx`, and photographs of textbook pages. Until a photo of a page becomes a context, nothing downstream matters.

**API**

- Accept PDF, `.docx` and images on `POST /context/upload`; convert to markdown on ingest and store the markdown (keeping the original is optional and costs privacy).
- Images: use the vision path rather than OCR where the model is available.
- Extend `src/validators/file.validator.ts` and `src/config/file-upload.config.ts` for the new types and size limits.

**UI**

- Drag-and-drop upload, conversion progress, preview and edit of the converted markdown before it is saved. Never save a silent bad conversion.

**Done when** a teacher can photograph a page and get a usable context without touching markdown.

---

### M2 — Audience and account-free access

Unlocks the education profile. Also the milestone with the clearest regulatory driver: the CNIL explicitly tells schools not to collect _"les informations nécessitant la création d'un compte individuel lorsqu'un compte de classe suffit"_ — information requiring an individual account where a class account would do.

**API**

- Implement `audience`: `creator` (SIWE, as today), `public` (open), `code` (a short shared token, rate-limited, revocable, rotatable).
- Resolve capabilities per audience on every request — this is the enforcement point that makes a restricted surface real rather than cosmetic.
- Per-audience throttling on top of the existing `ThrottlerGuard`.

**UI**

- Teacher: generate, display, rotate and revoke a class code; QR code for projection.
- Pupil: a route that takes a code and opens a chat with **no authentication, no passkey, no account, nothing persisted**. W3PK stays the teacher's alone — a class of thirty on shared tablets is the worst possible case for device-bound passkeys.

**Done when** a class can use a context without a single pupil account existing.

---

### M3 — First capability: document export

The deliverable of a teacher's workflow is a worksheet, a quiz or a marking grid — printed or dropped in the ENT. Chat text is not the deliverable.

**API**

- A capability registry: each capability is a NestJS provider exposing a declaration and a handler, resolved against the context's `capabilities` and the caller's audience.
- Implement `document`: markdown → PDF/docx.
- Expose the existing `WebReaderService` as the `web-reader` capability (it is already written; it is simply not reachable as one).

**UI**

- Export action on any assistant response; format choice; a capability picker on the edit page that shows what each audience gets.

**Done when** a teacher can go from a photographed page to a printable differentiated worksheet.

---

### M4 — Skills as files

The portable half of "capabilities", and the piece that works on **all three profiles**.

**API**

- A `skills/` subfolder in a context; each file is a named procedure (`make-a-quiz.md`, `grade-with-rubric.md`, `differentiate-three-levels.md`).
- Skills are **declarative markdown, never code**. Keeping them as data is what lets them be inspected by a teacher, shared as a file, and reused unchanged on either backend.
- Selection: named invocation first (deterministic, cheap); model-chosen selection later if it earns its keep.

**UI**

- Browse, edit and create skills as a distinct tab from Documents.
- Import/export a skill as a file. Teachers already trade lesson plans; a marketplace of _content_ fits their practice far better than a marketplace of _code_, and it is a much cheaper network effect to build.

**Done when** a teacher can write a reusable procedure once, invoke it across contexts, and share it with a colleague as a file.

---

### M5 — Stripe: creators pay

**API**

- Per-context, per-creator usage metering. `CostTracker` (`src/memory/cost-tracking.service.ts`) already computes per-request cost — this is aggregation and persistence, not new measurement.
- Stripe customer keyed to the creator's wallet address, metered subscription, quota enforcement with a clear over-quota error, and webhook handling.
- Quotas must be enforceable _before_ the expensive capabilities land in M6.

**UI**

- Billing section in `/settings`: plan, current usage, per-context breakdown, payment method.
- Over-quota state on the context page that is honest about what stopped and why.

**Note on the two payment rails.** Stripe is identified, invoiced, recurring — right for creators, and the only thing a school's accounting department can process. zk-api credits are anonymous and prepaid — right for privacy-first end users. They are not competing; they serve opposite ends of the same product. A school pays by card while its pupils stay anonymous, and that is a coherent story, not a contradiction.

**Done when** a creator can exceed a free tier and be charged for it.

---

### M6 — The tool loop

Deliberately late, and deliberately not universal.

**API**

- An agentic loop over the declared capabilities, gated by the audience resolution from M2.
- Depth, cost and wall-clock ceilings per request.
- Full trace of tool calls for the creator.

**UI**

- Render tool calls as they happen; let the creator inspect and replay a trace.

**Deliberate non-goal: the pupil assistant is never agentic.** "It cannot browse, it cannot act, it does not remember you after the lesson" is the sentence that gets the product past a headteacher and a DPO. That is the pitch, not a limitation. Agency belongs on the teacher's preparation surface, where the value is high and the risk is nil.

---

### M7 — The ZK API profile

**Not a priority. No date.** Listed so the schema stays honest about where it is heading, and so nothing in M0–M6 forecloses it.

**API**

- Nothing, ideally. If `routing: "zk-api"` requires an API change beyond serving the field, the schema is wrong.

**UI**

- Route `routing: "zk-api"` contexts to zk-api from the Next.js layer, browser-direct for the reasons in §5.
- Proof generation via W3PK, `POST /zk-api/estimate-cost` before sending, `POST /zk-api/request` to send, refund redemption.
- A deposit/credits screen, and an honest explanation of the boundary: unlinkable _who_, not private _what_.

**Done when** the same context definition runs on both backends with no change beyond one field.

---

## 7. Explicit non-goals

- **MCP, for now.** It is the right long-term interface and the wrong foundation today: arbitrary remote servers plus per-user credentials plus minors is an indefensible compliance surface, and no teacher will ever paste a server URL into a form. Revisit after M6 as _one capability among others_, teacher-only, gated by the same whitelist.
- **A third-party plugin marketplace.** First-party, audited capabilities only until the security model is proven.
- **Vertical-specific API endpoints.** If a milestone seems to need one, the schema is wrong.
- **Autonomous-agent registries (ERC-8004 and similar).** Interesting; out of scope for a classroom.
- **Presenting redaction as compliance.** See §5.

## 8. Open questions

- Should `instructions` be a single field or a small set (persona / constraints / refusals)? A single field is simpler; a structured one is easier to validate and to preset per profile.
- Where does the class-code session live so that "nothing persisted" stays true, while a lesson still survives a page reload?
- Does the `queries` aggregate (M0) give a teacher anything genuinely useful once messages are gone, or should the "what did the class struggle with" view be a derived skill instead?
- The homepage previously grouped contexts into Free / ZK API / For kids / Agentic (see `CHANGELOG.md` 0.2.0) using a `category` field that the API refactor dropped. Do categories come back as a first-class field, or are they derived from `routing` + `billing` + `audience`? Deriving them is more consistent with §1.

## 9. Related documents

- [`CONTEXT_MANAGEMENT.md`](https://github.com/w3hc/rukh/blob/main/docs/CONTEXT_MANAGEMENT.md) — how contexts work today
- [`MODELS.md`](https://github.com/w3hc/rukh/blob/main/docs/MODELS.md) — provider and model support
- [Rukh : se spécialiser pour l'école](https://julienberanger.com/rukh-pour-l-ecole) — the reasoning behind the education profile, with the regulatory sources
