# NeuralRepo documentation
---
---
> Idea manager with semantic search and duplicate detection. GitHub, Google or email-link sign-in; capture ideas from Claude (MCP), the nrepo CLI, the web app, the REST API or email, then tag, link and track them on a kanban board or mind map.
---
---
# NeuralRepo Documentation
Source: https://docs.neuralconfig.com/neuralrepo/
## Capture from anywhere
**Web App**
Create and manage ideas from the dashboard with kanban boards and mind maps.
**CLI**
`nrepo push "idea"` — capture from your terminal in seconds.
**Claude AI (MCP)**
Save ideas directly from Claude conversations via the MCP protocol.
**Siri Shortcuts**
"Hey Siri, NeuralRepo" — capture by voice on iPhone, Mac, or Apple Watch.
**Email**
Mail an idea to your personal capture address from any device.
**iOS App (not yet released)**
A native app with a share sheet, a widget, and an offline capture queue, still in testing.
## Organize with intelligence
**Semantic Search**
Find ideas by meaning, not just keywords. Powered by vector embeddings.
**Duplicate Detection**
Automatic similarity detection catches duplicates before they pile up.
**Idea Relations**
Connect ideas with typed relations — blocks, inspires, parent, and more.
**Mind Map**
Visualize your idea graph as an interactive network.
**Review Queue**
A short daily queue: possible duplicates, new connections, and ideas that went quiet.
## Explore the docs
- [Getting Started](https://docs.neuralconfig.com/neuralrepo/getting-started/introduction/): What NeuralRepo is, how it works, and what each plan includes.
- [Capture Ideas](https://docs.neuralconfig.com/neuralrepo/capture/web/): Every way in — web, CLI, Claude, Siri, email, and the REST API, plus the iOS app still in testing.
- [Organize & Explore](https://docs.neuralconfig.com/neuralrepo/organize/search/): Search, tags, statuses, relations, duplicates, the review queue, the mind map, and deleting data.
- [AI Agent](https://docs.neuralconfig.com/neuralrepo/agent/overview/): The Pro agent that searches, links, and reorganizes your repo for you.
- [CLI Reference](https://docs.neuralconfig.com/neuralrepo/cli/installation/): Full reference for all 20+ nrepo commands.
- [API Reference](https://docs.neuralconfig.com/neuralrepo/api/overview/): REST API documentation with request/response examples.
- [Integrations](https://docs.neuralconfig.com/neuralrepo/integrations/mcp/): Claude via MCP, Claude Code, BYOK providers, Apple Shortcuts, and the weekly digest.
- [Claude Connector](https://docs.neuralconfig.com/neuralrepo/integrations/claude-connector/): Add NeuralRepo to Claude at https://neuralrepo.com/mcp/v2 and see what its 22 tools do on each plan.
- [Guides](https://docs.neuralconfig.com/neuralrepo/guides/idea-to-project/): End-to-end workflows: idea to project, the idea graph, CI/CD, and migration.
- [Architecture](https://docs.neuralconfig.com/neuralrepo/architecture/tech-stack/): How NeuralRepo is built on Cloudflare — data model, queues, search, and security.
---
# Artifacts & Visualization
Source: https://docs.neuralconfig.com/neuralrepo/agent/artifacts/
When the agent calls a tool, the result is rendered as a **visual artifact** rather than plain text. Artifacts give you interactive, scannable views of your ideas — and in graph mode, they animate directly onto the mind map.
## Artifact Types
Six tools emit an artifact the canvas knows how to draw:
| Type | Produced by | Description |
|------|-------------|-------------|
| `idea_grid` | `search_ideas` (browse or search) | Grid of idea cards with title, status, and tags |
| `idea_card` | `get_idea`, `create_idea`, `update_idea` | Single idea, in detail / created / updated mode |
| `mini_graph` | `discover_connections` | Small force graph showing newly discovered connections |
| `brainstorm_cluster` | `brainstorm` | Clustered display of generated idea variations |
| `tag_cloud` | `suggest_tags` | Tag suggestions with applied/unapplied state |
| `summary` | `summarize` | Summary text with extracted themes |
| `connection_line` | `link_ideas` | Animated edge drawn on the mind map (no canvas card) |
**Note:**
The canvas can also render `mermaid` diagrams, side-by-side `comparison` cards, and
`insights` panels, but **no tool emits those types today** — they are renderers waiting
for a producer. Likewise, `score_idea` and `draft_document` draw on the **mind map** (score
cards over the graph, a new node for the document) and produce **no canvas artifact** in
the Agent view; their results arrive as tool output there.
## How Artifacts Render
### The Agent View
In the Agent view's split pane, artifacts appear in the **canvas panel** on the right. Each artifact is wrapped in a collapsible **artifact card** with:
- A header showing the artifact type icon, a title, and collapse/pin/dismiss controls
- A **pin** button (📌) to keep the artifact visible even as the conversation continues
- A **dismiss** button (×) to remove the artifact from the canvas
Multiple artifacts accumulate in the canvas as the conversation progresses. Pinned artifacts stay at the top; unpinned artifacts can be scrolled through.
### Mind Map Chat Panel
In the mind map chat panel, artifacts render differently depending on their type:
**Graph mutations** — tool calls that change the graph animate onto the canvas:
| Artifact | Graph effect |
|----------|-------------|
| `connection_line` | New edge draws with animation between source and target nodes |
| `idea_card` (created) | New node appears with a pulse animation |
| `idea_card` (updated) | Existing node highlights briefly to show the change |
| `mini_graph` | New nodes and edges for all discovered connections |
**Non-graph artifacts** render as collapsible **ToolResultCards** in the chat message:
- A single header line shows the tool icon and a summary (e.g., "Found 5 ideas for 'auth'")
- Clicking the chevron expands to show idea reference chips, detail content, or a grid
### Idea Reference Chips
When an artifact references specific ideas, they appear as **color-coded chips** with the idea title. Clicking a chip in the mind map chat panel highlights that node on the graph and scrolls it into view.
## Graph Mutations in Detail
In the mind map, every tool result is translated into one or more **graph mutations** — the
vocabulary the map understands:
| Mutation | What happens |
|----------|-------------|
| `add_nodes` / `remove_nodes` | Adds or removes ideas on the graph (removal is how undo reverses a creation) |
| `add_edges` / `remove_edges` | Draws or erases relations |
| `highlight_nodes` | Pulses the named ideas in a colour, for a set duration |
| `focus_node` / `focus_bbox` | Pans and zooms to one idea, or frames a whole set |
| `focus_cluster` / `clear_focus_cluster` | Dims everything outside a set of ideas, then releases it |
| `arrange_nodes` / `clear_arrangement` | Applies an agent-defined layout, or drops back to force layout |
| `semantic_layout_weights` | Feeds pairwise similarities into the simulation so like clusters with like |
| `show_score_cards` | Overlays `score_idea` results on the graph |
| `show_tension_edges` | Draws red contradiction edges between opposing ideas |
| `show_proposed_edges` | Draws suggested-but-not-yet-created connections |
| `move_agent_cursor` / `hide_agent_cursor` | Walks a visible cursor across the graph while the agent works |
Each tool has its own choreography — `search_ideas` highlights its hits and dims the rest,
`discover_connections` adds edges then frames them, `create_idea` adds a node and highlights it
in the accent colour.
## Undo Stack
Every agent action that **writes** data is recorded in an **undo stack** in the chat panel.
The undo button in the chat header reverses the most recent one:
- `create_idea`, `synthesize_ideas`, `draft_document` — deletes the idea it created
- `link_ideas`, `discover_connections` — removes the relations it created
Layout changes are not undoable — clear an arrangement instead. The undo stack is
per-session and cleared when the page reloads.
**Tip:**
Use undo liberally when exploring — the agent can create many connections quickly via `discover_connections`. If the results are not what you wanted, undo rolls back the graph to its previous state.
## ToolResultCard
Non-graph artifacts in the mind map panel render using the `ToolResultCard` component. The card has:
- **Icon** — a monospace emoji or symbol indicating the tool type
- **Summary** — a single-line description of the result (e.g., "Merged 3 tags → machine-learning")
- **Expand chevron** — click to reveal the full detail view
- **Idea reference chips** — clickable chips for each idea referenced in the result
The card uses the accent colour (Ember) for icons and a raised surface with a 1px border, in both the dark and light themes.
## Next Steps
- [Agent Tools Reference](https://docs.neuralconfig.com/neuralrepo/agent/tools/): Every tool, its parameters, and the artifact or mutation it produces.
- [AI Agent Overview](https://docs.neuralconfig.com/neuralrepo/agent/overview/): Models, code mode, graph mode, and the prompt palette.
- [Mind Map](https://docs.neuralconfig.com/neuralrepo/organize/mind-map/): The canvas these mutations animate on — edges, filters, and force controls.
- [Idea Relations](https://docs.neuralconfig.com/neuralrepo/organize/relations/): What an agent-created edge means, and how it differs from one you drew.
---
# AI Agent Overview
Source: https://docs.neuralconfig.com/neuralrepo/agent/overview/
The NeuralRepo AI agent is a conversational assistant embedded directly in the web app. It can search your ideas, create new ones, discover connections, brainstorm, summarize, and perform advanced analysis — all through natural language, with results rendered as visual artifacts.
**Caution:**
The AI agent is a **Pro feature** — the WebSocket endpoint refuses free accounts outright,
and the Agent view shows an upgrade panel instead. No API key of your own is needed: the
two Workers AI models run on NeuralRepo's account. A BYOK Anthropic key unlocks the three
Claude models on top of that.
## Two Interfaces
The agent is available in two places:
**Agent view**
A full split-pane view with a chat panel on the left and an artifact canvas on the right. Open it from **Agent** in the sidebar. Best for deep exploration sessions where you want to see multiple artifacts side-by-side.
**Mind Map Chat Panel**
A floating, draggable chat panel that hovers over the mind map. Tool results animate directly onto the graph — nodes appear, edges draw, and clusters form in real time. Best for exploring your idea graph while conversing with the agent.
## Model Registry
Five models are available. The three Anthropic models require a [BYOK
key](https://docs.neuralconfig.com/neuralrepo/integrations/byok/); the two Workers AI models need no key at all.
| Key | Label | Provider | BYOK | Context | Tool steps | Notes |
|-----|-------|----------|:----:|---------|:----------:|-------|
| `gpt-oss-120b` | GPT-OSS 120B | Workers AI | No | 128K | 12 | **The default.** Runs in code mode |
| `qwen-30b` | Qwen 30B | Workers AI | No | 32K | 6 | Compact prompt; also the creative sub-agent |
| `sonnet` | Sonnet | Anthropic | Yes | 1M | 10 | Balanced reasoning |
| `haiku` | Haiku | Anthropic | Yes | 200K | 8 | Faster, lower cost |
| `opus` | Opus | Anthropic | Yes | 1M | 10 | Most capable |
The Anthropic keys are tiers, not fixed models: the agent uses the newest Sonnet, Haiku or
Opus available, never older than `claude-sonnet-5`, `claude-haiku-4-5` or `claude-opus-5`. See
[BYOK](https://docs.neuralconfig.com/neuralrepo/integrations/byok/#agent-model-registry) for how the model is chosen.
Every model is capped at 8,192 output tokens except GPT-OSS 120B, which gets 16,384. "Tool
steps" is the hard stop on how many tool calls the model may chain in one turn — a request
that needs more than its budget stops mid-way rather than looping.
Selecting an Anthropic model without a key configured returns a message telling you to pick
a Workers AI model or add a key; it does not silently fall back. Your last-used model is
remembered in `localStorage`.
All Anthropic requests are routed through **Cloudflare AI Gateway** — providing logging, caching, and rate limiting without exposing your key to additional parties.
### Code Mode
GPT-OSS 120B runs in **code mode** by default: instead of calling tools one at a time, it
receives a single `codemode` tool and writes JavaScript that calls the tools as functions,
executed in a sandboxed Worker. Multi-step work — search, filter, then link — becomes one
round trip.
You can force code mode on or off for any model with the `code_mode` user setting:
```bash
curl -X PATCH https://neuralrepo.com/api/v1/user/me \
-H "X-API-Key: nrp_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"settings_json": "{\"code_mode\": true}"}'
```
The tools available are identical either way — code mode only changes how the model reaches
them.
## How It Works
The agent is a **Cloudflare Durable Object** (`NeuralRepoAgent`) built on the [Agents SDK](https://developers.cloudflare.com/agents/) `AIChatAgent` class. It persists conversation history per user across sessions.
```mermaid
flowchart TD
browser["Browser"] -->|WebSocket| ws["/api/v1/agent/ws"]
ws --> do["Durable Object (NeuralRepoAgent)"]
do --> stream["streamText (Vercel AI SDK)"]
stream -->|via AI Gateway| anthropic["Anthropic (BYOK)"]
stream -->|via AI binding| workersai["Workers AI"]
```
Messages stream back over the WebSocket connection. The frontend uses the Agents SDK client to manage the connection and reconstruct streaming state.
### Thinking Blocks
Anthropic models run with **adaptive thinking**: the model decides for itself whether to reason before responding, and for how long. There is no fixed thinking budget. In the chat UI, a collapsible "Thought process" block appears above the response when thinking is active. While the model is still generating, an animated neural icon shows it is working. The Workers AI models do not produce thinking blocks.
### System Prompt
Each conversation begins with a system prompt that includes:
- The user's total idea count (active ideas only — archived ones are excluded)
- 5 most recent ideas, with number, title, and status
- Up to 20 tags with their idea counts, and a note of how many more exist
Qwen 30B gets a **compact** version of that prompt — 3 recent ideas, 10 tags, and terser
rules — because of its smaller context window.
In Graph Mode (mind map chat panel), the system prompt also includes a note that the graph is the primary output, encouraging the agent to use tools aggressively and keep prose brief.
### Conversation History
The Durable Object keeps your conversation across sessions. For models with a context window
below 200K, history is trimmed from the **oldest** message forward until it fits the model's
budget, so a long-running conversation quietly loses its beginning on Qwen 30B and GPT-OSS
120B while the Anthropic models keep everything.
## Graph Mode
When using the chat panel embedded in the mind map, tool results animate directly onto the graph:
- **New ideas** — nodes appear with a pulse animation
- **New connections** — edges draw with an animation along the path
- **Search results** — matching nodes highlight
- **Semantic layout** — nodes re-cluster with a physics re-heat
Every graph mutation is added to an **undo stack**. The undo button in the chat panel header reverses the last agent action (deleting created relations, removing created ideas, etc.).
## Working Memory Bar
At the top of the Agent view's chat, a **working memory bar** shows a summary of what the agent has loaded in the current session:
- Number of unique ideas referenced in artifacts
- Number of connections made
- Last search query
This gives you a quick sense of the agent's context without scrolling through the conversation.
## Prompt Palette
Both interfaces show a **prompt palette** — a set of contextual quick-action chips that update based on what you have selected or what the agent last did:
- **Default chips** — "What am I working on?", "Find missing connections", "Weekly insights", "Surprise me"
- **Single selection** — Expand, Find related, Brainstorm, Suggest tags, Summarize
- **Multi-selection** — Connect these, Compare, Summarize cluster, Visualize
- **Post-action** — context-specific follow-ups after brainstorm, search, scoring, etc.
Click any chip to send that prompt immediately.
## Selection Awareness
When you have nodes selected in the mind map, the agent automatically receives a list of selected idea IDs and titles as additional context. This allows prompts like "compare these" or "link these ideas" to work without you naming the ideas explicitly.
---
# Agent Tools Reference
Source: https://docs.neuralconfig.com/neuralrepo/agent/tools/
The agent's capabilities are exposed as **tools** — typed functions the model can call to interact with your idea repository. Tools are defined with Zod schemas and execute server-side with full database and Vectorize access.
The reference below is split into two groups: twelve **core tools** that read and write your
repository, and five **intelligence tools** that analyse it. Because the agent itself is
Pro-only, **everyone who can open it has all seventeen** — the split is about what each tool
is for, not about who gets it.
**Note:**
The agent selects tools automatically based on your request. You do not need to name tools explicitly — just describe what you want.
Every idea the agent creates — with `create_idea`, `synthesize_ideas`, or `draft_document` —
is recorded with source `api`, and goes through the same background processing as any other
capture: embedding, duplicate detection, and auto-tagging.
## Core Tools
### `search_ideas`
Search and browse ideas. With a query, performs semantic search (Vectorize embeddings + reranking). Without a query, lists ideas by recency. Supports status, tag, and limit filters.
| Parameter | Type | Required | Description |
|-----------|------|:--------:|-------------|
| `query` | string | No | Search query. Omit to browse all ideas. |
| `status` | enum | No | Filter: `captured`, `exploring`, `building`, `shipped`, `shelved` |
| `tag` | string | No | Filter by tag name |
| `limit` | number | No | Max results — default **5** with a query, **20** while browsing; 100 is the ceiling |
Search uses **semantic + FTS fallback**: Vectorize is queried first; if it returns no results above the score threshold, the tool falls back to SQLite FTS5 full-text search automatically. This is the agent's own behaviour — [the REST API and the CLI do not fall back](https://docs.neuralconfig.com/neuralrepo/organize/search/#default-behavior--and-where-the-fallback-actually-is).
Filtering by a tag that does not exist returns an empty result with a hint naming the
mistake, rather than silently returning everything.
Returns an `idea_grid` artifact.
---
### `get_idea`
Get full details of a single idea by ID, including tags, links, and relations.
| Parameter | Type | Required | Description |
|-----------|------|:--------:|-------------|
| `id` | number | Yes | Idea ID |
| `include_related` | boolean | No | Include related ideas and links (default: true) |
Returns an `idea_card` artifact.
---
### `create_idea`
Create a new idea. Triggers background queue processing for embedding generation and auto-tagging.
| Parameter | Type | Required | Description |
|-----------|------|:--------:|-------------|
| `title` | string | Yes | Concise idea title |
| `body` | string | No | Detailed description (Markdown) |
| `tags` | string[] | No | Tags (lowercase, hyphenated) |
| `status` | enum | No | Initial status (default: `captured`) |
Returns an `idea_card` artifact. In graph mode, the new node appears on the mind map.
---
### `update_idea`
Update an existing idea's title, body, status, or tags.
| Parameter | Type | Required | Description |
|-----------|------|:--------:|-------------|
| `id` | number | Yes | Idea ID to update |
| `title` | string | No | New title |
| `body` | string | No | New body |
| `status` | enum | No | New status |
| `tags` | string[] | No | Replace all tags with this list |
| `add_tags` | string[] | No | Tags to add (cannot combine with `tags`) |
| `remove_tags` | string[] | No | Tags to remove (cannot combine with `tags`) |
Returns an `idea_card` artifact. Triggers re-embedding if title or body changed.
---
### `link_ideas`
Create a typed relation between two ideas. Runs cycle detection before creating the relation.
| Parameter | Type | Required | Description |
|-----------|------|:--------:|-------------|
| `source_id` | number | Yes | Source idea ID |
| `target_id` | number | Yes | Target idea ID |
| `relation_type` | enum | No | `related` (default), `blocks`, `inspires`, `parent`, `supersedes` |
| `note` | string | No | Optional note about the relationship (max 500 chars) |
If `relation_type` is omitted, the agent selects the most appropriate type based on the ideas' content. `duplicate` is not offered — see [Relations](https://docs.neuralconfig.com/neuralrepo/organize/relations/). Cycle detection only *blocks* the hard-blocked types (`blocks`, `parent`); a `supersedes` cycle goes through. Linking an idea to itself, or re-linking a pair that is already connected, returns an error rather than a duplicate edge.
Returns a `connection_line` artifact that animates on the mind map.
---
### `discover_connections`
Find hidden connections between ideas using Vectorize semantic similarity. Scans ideas, identifies missing relations, and uses AI to reason about relationship types before creating them.
| Parameter | Type | Required | Description |
|-----------|------|:--------:|-------------|
| `scope` | string | No | `'all'` for all ideas, `'recent'` (default, last 7 days), or comma-separated idea IDs |
| `min_score` | number | No | Minimum similarity score 0–1 (default 0.5) |
Scope caps what is scanned: `'all'` takes your 100 most recent ideas, `'recent'` filters
your 50 most recent down to those touched in the last 7 days — falling back to the 10 most
recent if that window is empty.
Pairs that already have a relation in either direction are skipped. The top 10 remaining
candidates go to an AI pass that names the relation type; any it does not judge becomes
`related`. The relations it writes are marked system-created, so the mind map draws them
faintly. Returns a `mini_graph` artifact showing the new connections.
---
### `summarize`
Summarize one or more ideas, extracting common themes and a connecting narrative.
| Parameter | Type | Required | Description |
|-----------|------|:--------:|-------------|
| `idea_ids` | number[] | Yes | 1–20 idea IDs to summarize |
| `write_back` | boolean | No | Save the summary to the idea's `source_summary` field (single idea only, default false) |
Returns a `summary` artifact with the summary text and extracted themes.
---
### `brainstorm`
Generate creative variations or extensions of an idea using the Qwen 30B sub-agent.
| Parameter | Type | Required | Description |
|-----------|------|:--------:|-------------|
| `idea_id` | number | No | Base idea to brainstorm from |
| `prompt` | string | No | Freeform brainstorm prompt |
| `count` | number | No | Number of suggestions, 1–5 (default 3) |
Either `idea_id` or `prompt` must be provided. Returns a `brainstorm_cluster` artifact.
---
### `suggest_tags`
Get AI-suggested tags for an idea. Prefers existing user tags to avoid tag sprawl.
| Parameter | Type | Required | Description |
|-----------|------|:--------:|-------------|
| `idea_id` | number | Yes | Idea to suggest tags for |
Returns a `tag_cloud` artifact showing suggested tags with an indicator for which are already applied.
---
### `cleanup_tags`
Find and merge similar or duplicate tags using tag embeddings. In dry-run mode (default), returns proposed merges for review. With `dry_run: false`, executes the merges.
| Parameter | Type | Required | Description |
|-----------|------|:--------:|-------------|
| `dry_run` | boolean | No | If true (default), show proposals without executing. If false, execute merges. |
Uses a similarity threshold of 0.85. The most-used tag in each cluster becomes the canonical tag. Comparison needs tag embeddings, which are generated as tags are used, so a brand-new tag may be skipped on the first pass.
---
### `arrange_nodes`
Arrange mind map nodes into visual clusters by tags, status, or custom groupings.
| Parameter | Type | Required | Description |
|-----------|------|:--------:|-------------|
| `mode` | enum | Yes | `by_tags`, `by_status`, or `by_clusters` |
| `clusters` | object[] | No | Custom cluster definitions (required for `by_clusters` mode). Each object has `label` (string) and `ids` (number[]). |
| `idea_ids` | number[] | No | Specific idea IDs to arrange (max 200). Omit to arrange all ideas. |
Modes:
- **`by_tags`** — groups ideas by shared tags
- **`by_status`** — arranges ideas in status columns
- **`by_clusters`** — uses custom groupings you define in the `clusters` parameter
---
### `clear_arrangement`
Clear any agent-arranged node positions, letting nodes return to the natural force-directed layout. No parameters.
---
## Intelligence Tools
Analysis rather than bookkeeping. Same availability as the core tools — the agent is Pro-only,
so every session that can reach a tool can reach all of these.
### `semantic_layout`
Rearrange the mind map so similar ideas cluster together. Computes pairwise vector similarities across up to 100 ideas and applies the weights as force-graph spring strengths.
| Parameter | Type | Required | Description |
|-----------|------|:--------:|-------------|
| `idea_ids` | number[] | No | Specific idea IDs to lay out. Omit to use all visible ideas (up to 100). |
Triggers a physics re-heat on the mind map. Semantically similar ideas pull toward each other; dissimilar ideas repel.
---
### `synthesize_ideas`
Combine two ideas into a new synthesis idea, linked to both originals with `inspires` relations.
| Parameter | Type | Required | Description |
|-----------|------|:--------:|-------------|
| `idea_id_1` | number | Yes | First idea ID |
| `idea_id_2` | number | Yes | Second idea ID |
| `synthesis_title` | string | No | Optional title. If omitted, AI generates one. |
The synthesis body is AI-generated (2–3 sentences), capturing the best elements of both source ideas. The new idea is created with status `exploring`.
---
### `score_idea`
Score ideas on 5 dimensions using AI evaluation.
| Parameter | Type | Required | Description |
|-----------|------|:--------:|-------------|
| `idea_ids` | number[] | Yes | 1–10 idea IDs to score |
**Scoring dimensions** (0–10 each), all produced by a single AI call:
| Dimension | What it measures |
|-----------|-----------------|
| Novelty | How original and unique the idea is |
| Feasibility | How practical and achievable it is |
| Impact | How significant the potential impact could be |
| Clarity | How well-defined and clearly articulated it is |
| Momentum | How much energy and excitement the idea has |
In the mind map, the scores render as cards over the graph and the scored ideas are framed.
In the Agent view they arrive as tool output rather than a canvas artifact.
**Caution:**
If the model returns something the scorer cannot parse, every dimension falls back to
**5/10** rather than reporting a failure. A set of ideas that all score a flat 5 across
the board is that fallback, not a verdict — ask again.
---
### `find_contradictions`
Detect idea pairs with opposing viewpoints or mutually exclusive approaches. Shows tension edges on the mind map.
| Parameter | Type | Required | Description |
|-----------|------|:--------:|-------------|
| `limit` | number | No | Max contradiction pairs to find, 1–20 (default 5) |
Candidates come from your 100 most recent ideas, of which the newest 30 are compared against
their nearest neighbours. Only pairs in a **0.6–0.95 similarity band** are considered — ideas
too far apart to be about the same thing are ignored, and near-identical ones are duplicates
rather than contradictions. The AI pass then keeps only pairs that are genuinely mutually
exclusive, dropping anything it scores below 0.3.
Returns contradiction pairs with AI-generated explanations and strength scores, drawn as red
tension edges on the mind map.
---
### `draft_document`
Generate a structured document from one or more ideas as source material.
| Parameter | Type | Required | Description |
|-----------|------|:--------:|-------------|
| `idea_ids` | number[] | Yes | 1–20 idea IDs to use as source material |
| `format` | enum | Yes | Document format |
**Available formats:**
| Format | Description | Outline structure |
|--------|-------------|-------------------|
| `prd` | Product Requirements Document | Overview, Problem, Goals, Requirements, Non-Goals, Success Metrics |
| `blog_post` | Blog post | Title, Introduction, three main points, Conclusion |
| `memo` | Internal memo | To/From/Subject/Date, Executive Summary, Key Points |
| `research_brief` | Research brief | Background, Key Findings, Implications, Next Steps |
| `feature_spec` | Feature specification | User Story, Acceptance Criteria, Technical Notes, Out of Scope |
The generated document is saved as a new idea — status `exploring`, tagged `document`, and
linked to every source idea with an `inspires` relation. In the mind map it appears as a new
node; in the Agent view it arrives as tool output. Undo deletes the generated idea.
---
## Sub-Agents
Several tools use lightweight **sub-agents** — secondary AI calls to a fixed Workers AI model — rather than the primary conversation model. This is why a tool behaves the same whichever model you have selected:
| Sub-agent | Model | Used by |
|-----------|-------|---------|
| Brainstorm | Qwen 30B (creative) | `brainstorm` |
| Contradiction detection | Qwen 30B (creative) | `find_contradictions` |
| Document drafting | Qwen 30B (creative) | `draft_document` |
| Summarize cluster | IBM Granite Micro (utility) | `summarize` |
| Suggest tags | IBM Granite Micro (utility) | `suggest_tags` |
| Find hidden connections | IBM Granite Micro (utility) | `discover_connections` |
| Synthesis and scoring | IBM Granite Micro (utility) | `synthesize_ideas`, `score_idea` |
Sub-agents are called server-side and their results are returned to the primary agent as tool output. They do not appear in the conversation history.
## Next Steps
- [AI Agent Overview](https://docs.neuralconfig.com/neuralrepo/agent/overview/): Models, code mode, the system prompt, and how the agent is wired.
- [Artifacts & Visualization](https://docs.neuralconfig.com/neuralrepo/agent/artifacts/): What each tool result looks like in the canvas and on the graph.
- [Idea Relations](https://docs.neuralconfig.com/neuralrepo/organize/relations/): The relation types link_ideas and discover_connections write.
- [BYOK](https://docs.neuralconfig.com/neuralrepo/integrations/byok/): Add an Anthropic key to run these tools on Sonnet, Haiku, or Opus.
---
# Authentication
Source: https://docs.neuralconfig.com/neuralrepo/api/authentication/
All API endpoints (except `/api/v1/health`) require authentication. NeuralRepo accepts a
session token, an API key, or a session cookie, and resolves them in a fixed order.
## How a request is authenticated
Every `/api/v1/*` request runs through the same middleware chain before your endpoint sees it:
```mermaid
flowchart TD
start["Request to /api/v1/*"] --> bearer{"Authorization: Bearer present?"}
bearer -- yes --> sess["Look up SHA-256 of the token in sessions, unexpired only"]
sess -- hit --> refresh["Session user scopes = null, expiry pushed to +30 days"]
sess -- miss --> keyb["Look up the same hash in api_keys"]
keyb -- hit --> keyu["API-key user scopes from the key row, last_used_at updated"]
keyb -- miss --> xkey
bearer -- no --> xkey{"X-API-Key starting with nrp_ ?"}
xkey -- yes --> keyh["Look up SHA-256 of the key in api_keys"]
keyh -- hit --> keyu
keyh -- miss --> cookie
xkey -- no --> cookie{"Cookie nrepo_session present?"}
cookie -- yes --> cs["Look up SHA-256 in sessions refresh expiry and re-set the cookie"]
cs -- hit --> refresh
cs -- miss --> deny["401 Unauthorized"]
cookie -- no --> deny
refresh --> plan["Load plan from users"]
keyu --> plan
plan --> rl["Rate limit API-key and Bearer requests only"]
rl --> route["Endpoint handler"]
```
In prose, so nothing here lives only in the diagram:
1. If an `Authorization: Bearer` header is present, its SHA-256 hash is looked up in the
**sessions** table first, then in the **api\_keys** table. That is why a bearer token can be
*either* credential.
2. Otherwise an `X-API-Key` header is tried, but **only if it starts with `nrp_`** — a key sent
under that header without the prefix is ignored entirely.
3. Otherwise the `nrepo_session` cookie is tried.
4. If none matches, the request is rejected with `401` and the body `{ "error": "Unauthorized" }`.
The message is the same whether the credential was missing, malformed, revoked, or expired.
5. A match sets the user and their plan; a session match also slides its expiry 30 days forward.
6. The rate limiter then runs, but only for API-key and bearer-token requests.
## Session Token
When you sign in through the NeuralRepo web app, a session token is stored in a secure
HTTP-only cookie. You can also pass it explicitly:
```
Authorization: Bearer
```
Sessions last **30 days** and slide: every authenticated request pushes the expiry another 30
days out, so an active session effectively never expires while a dormant one lapses after a
month. Signing out via `/auth/logout` deletes the row immediately.
**Note:**
Session tokens are the credential the web app uses. For scripts, prefer an API key — cookie
and bearer *session* auth are the two paths the rate limiter treats differently, and a session
token pasted into a script is bearer auth, so it counts against your daily quota.
## API Key
API keys provide long-lived access for scripts, CI pipelines, and third-party integrations.
Every key is the prefix `nrp_` followed by 64 hexadecimal characters (32 random bytes):
```
X-API-Key: nrp_a1b2c3d4e5f6...
```
Keys do not expire. Only the SHA-256 hash is stored, so a lost key cannot be recovered — only
revoked and replaced.
### Generating an API Key
**Pro plan required:**
`POST /user/api-keys` is gated on the Pro plan. On Free it returns `403` with
`{ "pro_required": true, "feature": "API access" }` and no key is created. Signing in with
`nrepo login` issues its own key labelled `CLI (auto-generated)` through the OAuth callback,
which is a separate code path — see [CLI authentication](https://docs.neuralconfig.com/neuralrepo/cli/authentication/).
**curl**
```bash
curl -X POST https://neuralrepo.com/api/v1/user/api-keys \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{"label": "CI Pipeline"}'
```
**JavaScript**
```js
const res = await fetch("https://neuralrepo.com/api/v1/user/api-keys", {
method: "POST",
headers: {
Authorization: "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({ label: "CI Pipeline" }),
});
const key = await res.json();
console.log(key);
```
`label` is optional and defaults to `default`; it is capped at 100 characters.
**Response `201 Created`**
```json
{
"id": "3f2a91c47b0e4d5aa8c61e0f2b7d4c93",
"key": "nrp_a1b2c3d4e5f67890...",
"label": "CI Pipeline",
"created_at": "2026-03-24 12:00:00"
}
```
The `id` is a 32-character hex string — it is what you pass to the delete endpoint, and it is
not the key.
:::caution
The full key value is only returned once at creation time. Store it securely.
:::
### Listing Keys
```
GET /api/v1/user/api-keys
```
Returns every key on the account, newest first. The response carries **no key material at all**
— not a masked value, not a prefix. Each entry is `id`, `label`, `scopes`, `source`,
`last_used_at`, and `created_at`. `source` is `manual` for keys you created here and `mcp` for
keys minted by the MCP OAuth flow.
### Revoking a Key
```
DELETE /api/v1/user/api-keys/:id
```
Deletes the row, so the key stops working on the next request. Returns `200 OK` with
`{ "success": true }`, or `404` if the id does not belong to you. Unlike key creation, revoking
is available on every plan.
## Scopes
Scopes are stored per key and are **null for every key you create yourself**, which means full
access. Only tokens minted by the MCP OAuth flow carry a scope string:
| Scope | Granted to | Description |
|-------|------------|-------------|
| `ideas:read` | MCP OAuth tokens | Read ideas, tags, relations, links, search, map, duplicates |
| `ideas:write` | MCP OAuth tokens | Create, update, archive, merge, and develop ideas; manage tags, links, and relations |
A token whose scope string omits the scope an endpoint requires gets
`403 { "error": "Insufficient scope: requires ideas:write" }`. Session tokens and manually
created API keys never see that error, because they are unscoped.
**Note:**
This is the opposite of the usual arrangement: the *narrow* credential is the OAuth token an
AI client holds, and the key you paste into a script is unrestricted. There is currently no
way to create a scope-limited key from the dashboard or the API.
## MCP OAuth
NeuralRepo issues MCP (Model Context Protocol) tokens for AI assistant integrations. The flow is
separate from standard API authentication and is driven by the client, not by you:
1. You add `https://neuralrepo.com/mcp/v2` (or the original `https://neuralrepo.com/mcp/`) as an MCP server in a compatible client.
2. The client discovers the endpoints from `/.well-known/oauth-authorization-server`, registers
itself, and walks the authorization-code + PKCE flow at `/mcp/authorize` and `/mcp/token`.
PKCE is required for every new authorization, with `code_challenge_method=S256` stated
explicitly; anything else gets `400` `invalid_request`.
3. The token it receives is written to the same `api_keys` table with `source = "mcp"`, a label
of `MCP ()`, and the scopes you approved.
`GET /api/v1/user/mcp-tokens` **lists** the tokens issued this way — it does not create one.
Revoke them with the same `DELETE /api/v1/user/api-keys/:id` endpoint used for manual keys.
## Error Responses
| Status | Body | Meaning |
|--------|------|---------|
| `401 Unauthorized` | `{ "error": "Unauthorized" }` | Missing, malformed, revoked, or expired credential |
| `403 Forbidden` | `{ "error": "Insufficient scope: requires ideas:write" }` | Valid MCP token, wrong scope |
| `403 Forbidden` | `{ "error": "API access requires a Pro plan", "pro_required": true, ... }` | Valid credential, plan gate |
There is no distinct "invalid API key" message — every authentication failure returns the same
`401` body, by design.
## Best Practices
- Use **API keys** for server-side scripts and automations.
- Use **session tokens** only from browser-based code.
- Rotate API keys periodically and revoke unused keys — `last_used_at` on the list endpoint
tells you which are dormant.
- Never commit API keys to version control.
## Next Steps
- [User & Settings](https://docs.neuralconfig.com/neuralrepo/api/user/): Manage keys, MCP tokens, BYOK provider keys, profile settings, and data exports.
- [Rate Limits](https://docs.neuralconfig.com/neuralrepo/api/rate-limits/): Which credentials count against the daily quota and which are exempt.
- [Error Handling](https://docs.neuralconfig.com/neuralrepo/api/errors/): The full status-code vocabulary, including the two different 403 bodies.
- [MCP Integration](https://docs.neuralconfig.com/neuralrepo/integrations/mcp/): Connect NeuralRepo to Claude and other MCP clients, and see which tools each scope unlocks.
---
# Duplicates
Source: https://docs.neuralconfig.com/neuralrepo/api/duplicates/
NeuralRepo compares each new idea against your existing ones and records pairs that score above
your duplicate threshold. This API lists those detections and resolves them.
**Free accounts always get an empty list:**
`GET /ideas/duplicates` returns `{ "duplicates": [] }` for every Free account, before it
touches the database — not because none were found. Detection keeps running in the background,
so the backlog appears the moment you upgrade. On Free, an empty array proves nothing.
## How detections appear
Detection is asynchronous and happens after capture, not during it:
1. `POST /ideas` returns immediately with `processing: true`.
2. The queue embeds the idea and compares it against your existing vectors.
3. Any pair scoring above `dedup_threshold` (default **0.75**) is written as a `pending`
detection.
Nothing in the create response tells you a duplicate was found — there is no synchronous warning
and no blocking prompt. Poll this endpoint (or `GET /review`) a few seconds later instead.
## List Duplicates
`GET /api/v1/ideas/duplicates`
Returns every **pending** detection where both ideas are still unarchived, highest similarity
first. Dismissed and merged detections are never returned, and there is no pagination.
**curl**
```bash
curl https://neuralrepo.com/api/v1/ideas/duplicates \
-H "X-API-Key: nrp_YOUR_KEY"
```
**JavaScript**
```js
const res = await fetch(
"https://neuralrepo.com/api/v1/ideas/duplicates",
{ headers: { "X-API-Key": "nrp_YOUR_KEY" } }
);
const { duplicates } = await res.json();
```
**Response `200 OK`**
```json
{
"duplicates": [
{
"id": 7,
"user_id": "3f2a91c47b0e4d5aa8c61e0f2b7d4c93",
"idea_id": 158,
"idea_number": 58,
"idea_title": "Add SSO support for enterprise accounts",
"idea_body": "Larger customers keep asking for SAML...",
"idea_status": "captured",
"idea_source": "cli",
"idea_created_at": "2026-03-22 13:58:00",
"duplicate_of_id": 137,
"duplicate_number": 42,
"duplicate_title": "Enterprise SSO integration",
"duplicate_body": "SAML and OIDC for teams...",
"duplicate_status": "exploring",
"duplicate_source": "web",
"duplicate_created_at": "2026-02-11 09:04:00",
"similarity_score": 0.94,
"status": "pending",
"created_at": "2026-03-22 14:00:00"
}
]
}
```
| Field | Meaning |
|---|---|
| `id` | The **detection** id — this is what the dismiss and merge endpoints take |
| `idea_id` | The idea whose capture or edit triggered the detection — normally the newer one |
| `duplicate_of_id` | The older idea it matched |
| `idea_body`, `duplicate_body` | First **280 characters** only, for previewing |
| `similarity_score` | Cosine similarity, 0–1 |
| `status` | Always `pending` in this listing |
`idea_number` and `duplicate_number` are display numbers for showing the user; every write
endpoint here takes the detection `id`, and the merge acts on the two idea `id`s.
## Dismiss Duplicate
`POST /api/v1/ideas/duplicates/:id/dismiss`
Marks the detection as `dismissed`. Both ideas are left exactly as they are, and the pair never
appears in the list again.
**curl**
```bash
curl -X POST https://neuralrepo.com/api/v1/ideas/duplicates/7/dismiss \
-H "X-API-Key: nrp_YOUR_KEY"
```
**JavaScript**
```js
const res = await fetch(
"https://neuralrepo.com/api/v1/ideas/duplicates/7/dismiss",
{ method: "POST", headers: { "X-API-Key": "nrp_YOUR_KEY" } }
);
```
**Response `200 OK`**
```json
{
"success": true
}
```
Only a `pending` detection can be dismissed. Dismissing one that is already dismissed or merged
returns `404 { "error": "Duplicate not found or already resolved" }` — the same body as an id
that does not exist.
## Merge Duplicate
`POST /api/v1/ideas/duplicates/:id/merge`
Folds the two ideas together and marks the detection `merged`.
**The newer idea survives, and relations do not move:**
This merge keeps `idea_id` — the idea that *triggered* the detection — and archives
`duplicate_of_id`, the older one. That is the opposite of what most people assume, and it is
also the opposite survivor from
[`POST /ideas/:id/merge`](https://docs.neuralconfig.com/neuralrepo/api/ideas/#merge-ideas), where you choose the keeper.
Relations on the archived idea are **not** repointed here, so its connections are lost from the
graph.
```bash
curl -X POST https://neuralrepo.com/api/v1/ideas/duplicates/7/merge \
-H "X-API-Key: nrp_YOUR_KEY"
```
Exactly what happens, compared with the general merge:
| Aspect | `duplicates/:id/merge` | `ideas/:id/merge` |
|---|---|---|
| Survivor | `idea_id` — the newer idea | The id in the path; you choose |
| Body | Both joined with `\n\n---\n\n` | Both joined with `\n\n---\n\n` |
| Tags | Union of both | Union of both |
| `created_at` | The earlier of the two | The earlier of the two |
| Relations | **Not transferred** | Repointed onto the survivor |
| Links | Not transferred | Not transferred |
| Loser's status | Unchanged, just archived | Set to `shelved`, then archived |
| Detection row | Marked `merged` | Untouched |
| Response | `{ "success": true }` | The surviving idea object |
The loser is archived (you can [restore](https://docs.neuralconfig.com/neuralrepo/api/ideas/#restore-idea) it, but the merge itself isn't reversed), so pick deliberately: if the older idea is the one with the relations,
the history, or the better title, dismiss the detection and use
[`POST /ideas/:id/merge`](https://docs.neuralconfig.com/neuralrepo/api/ideas/#merge-ideas) instead, naming the keeper
yourself.
**Response `200 OK`**
```json
{
"success": true
}
```
## Workflow
1. **List** pending detections with `GET /ideas/duplicates`.
2. Compare `idea_title`/`idea_body` against `duplicate_title`/`duplicate_body`.
3. **Dismiss** false positives — nothing changes.
4. For true duplicates, decide which idea should survive:
- the newer one → `POST /ideas/duplicates/:id/merge`;
- the older one, or you want relations preserved → dismiss, then
`POST /ideas/:id/merge` with the keeper in the path.
Tune how many detections you get with the `dedup_threshold`
[user setting](https://docs.neuralconfig.com/neuralrepo/api/user/#user-settings): raise it for fewer, stricter matches.
## Status Codes
| Status | Meaning |
|--------|---------|
| `200 OK` | Listed, dismissed, or merged |
| `400 Bad Request` | `:id` is not a number |
| `401 Unauthorized` | Missing or invalid auth |
| `404 Not Found` | Detection not found, not yours, or already dismissed/merged |
No endpoint here returns `403` on Free — the list simply comes back empty.
## Next Steps
- [Duplicate detection concepts](https://docs.neuralconfig.com/neuralrepo/organize/duplicates/): How the threshold works, what the background pass compares, and how to tune it.
- [Review Queue](https://docs.neuralconfig.com/neuralrepo/api/review/): The same detections alongside stale ideas and fresh connections, in one call.
- [Ideas](https://docs.neuralconfig.com/neuralrepo/api/ideas/#merge-ideas): The general merge, where you choose which idea survives.
- [User & Settings](https://docs.neuralconfig.com/neuralrepo/api/user/#user-settings): Set dedup_threshold and the other similarity thresholds.
---
# Error Handling
Source: https://docs.neuralconfig.com/neuralrepo/api/errors/
Every NeuralRepo API error returns a JSON object with an `error` field. The HTTP status gives
the category; the body sometimes carries more.
## Error Response Format
Most errors are a single string:
```json
{
"error": "Idea not found"
}
```
**`error` is not always a string:**
Validation failures on **ideas** and **links** return the validator's field map instead, so
`error` is an *object*:
```json
{
"error": {
"title": ["Too small: expected string to have >=1 characters"],
"tags": ["Too big: expected array to have <=20 items"]
}
}
```
Code that does `console.error(error)` or string-concatenates it will print `[object Object]`
and lose the reason. Test the type before formatting:
```js
const msg = typeof error === "string"
? error
: Object.entries(error).map(([f, msgs]) => `${f}: ${msgs.join(", ")}`).join("; ");
```
Tags and user/profile endpoints return the first validation message as a plain string, so the
same request shape can give you either form depending on which resource you hit.
Some errors add fields alongside `error`. Where they do, `error` may be a **machine-readable
code** rather than a sentence — `POST /ideas` over the Free-plan cap returns
`"error": "idea_limit_reached"` with the human-readable text in `message`.
## Status Codes
### `400 Bad Request`
The path, body, or query parameters failed validation.
```json
{
"error": {
"title": ["Too small: expected string to have >=1 characters"]
}
}
```
Common causes:
- Missing required fields (`title` on idea creation, `q` on search)
- A field over its limit (body > 50,000 characters, more than 20 tags, more than 50 bulk ids)
- Invalid format (colour not `#rrggbb`, unparseable URL, unknown `mode` or `link_type`)
- A non-numeric `:id` in the path — `{ "error": "Invalid idea ID" }`
- **A relation that would create a cycle** — despite the wording of the message, this is a `400`
### `401 Unauthorized`
Authentication is missing or invalid. The body is always identical:
```json
{
"error": "Unauthorized"
}
```
There is no separate message for expired, revoked, malformed, or absent credentials — all four
produce this exact response. Common causes:
- No `Authorization`, `X-API-Key`, or cookie credential
- An `X-API-Key` value that does not begin with `nrp_` (it is ignored entirely)
- A session older than 30 days with no activity
- A revoked key
### `403 Forbidden`
Authenticated, but not allowed. Three distinct bodies:
**Plan gate** — `POST /user/api-keys`, BYOK save/test, relation writes, agent WebSocket:
```json
{
"error": "Idea relations requires a Pro plan",
"pro_required": true,
"feature": "Idea relations",
"upgrade_url": "https://neuralrepo.com/upgrade"
}
```
**Free-plan idea cap** — `POST /ideas` only:
```json
{
"error": "idea_limit_reached",
"message": "You've captured 50 ideas on the free plan. Upgrade to Pro for unlimited ideas.",
"currentCount": 50,
"limit": 50,
"upgradeUrl": "https://neuralrepo.com/upgrade",
"featureRequestUrl": "https://support.neuralrepo.com/feature-requests"
}
```
**Scope** — MCP OAuth tokens only:
```json
{
"error": "Insufficient scope: requires ideas:write"
}
```
Branch on `pro_required` and on `error === "idea_limit_reached"` rather than on the sentence.
### `404 Not Found`
The resource does not exist, or belongs to another account — the two are deliberately
indistinguishable.
```json
{
"error": "Idea not found"
}
```
Common causes:
- An id that is not yours
- **Passing a display number (`#42`) where the API wants a database `id`** — the single most
common cause of a spurious `404`
- A duplicate detection that has already been dismissed or merged
Note that some deletes do *not* 404 on a bad id: `DELETE /ideas/:id/links/:linkId` and
`DELETE /map/relations/:id` both return `{ "success": true }` regardless.
### `409 Conflict`
Only two situations produce a `409`:
```json
{
"error": "Tag name already exists"
}
```
- `POST /tags` with a name you already use.
- `POST /map/relations` when a user-created relation already exists between that pair in that
direction — `{ "error": "Relation already exists" }`. Uniqueness ignores the relation type, so
a second edge of a different type between the same two ideas also conflicts.
**Caution:**
A rejected cycle reads like a conflict and is not one — it comes back as `400`. Renaming a tag
onto an existing name is not one either; that surfaces as a `500`.
### `429 Too Many Requests`
The daily quota is exhausted. See [Rate Limits](https://docs.neuralconfig.com/neuralrepo/api/rate-limits/).
```json
{
"error": "Rate limit exceeded"
}
```
**No `Retry-After` header is sent** — the response has no rate-limit headers of any kind. The
window is a calendar day in UTC, so there is nothing to poll for until midnight.
### `500 Internal Server Error`
An unhandled exception. In production the body is generic:
```json
{
"error": "Internal server error"
}
```
The most reproducible cause is renaming a tag onto a name you already use — the unique
constraint is not caught, so it surfaces here rather than as a `409`.
### `502 Bad Gateway`
An upstream service the request depends on failed:
| Endpoint | Cause |
|---|---|
| `POST /ideas/:id/develop` | Your BYOK provider rejected or failed the call; body includes `provider` |
| `GET /user/support-link` | The support site could not mint a link |
### `503 Service Unavailable`
Returned only by `GET /api/v1/health`, when the database probe fails:
```json
{
"status": "error",
"error": "Database unavailable"
}
```
## Failures that are not error statuses
Several operations report failure inside a `2xx` response. Checking `res.ok` is not enough:
| Endpoint | Success status | What to check |
|---|---|---|
| `PATCH /ideas/bulk` | `200` | `errors` count and the per-idea `results` array |
| `POST /map/relations` (bulk) | `201` | `errors` count and the per-link `results` array |
| `POST /user/byok/:provider/test` | `200` | `ok` — a rejected key is still a `200` |
| `GET /ideas/search` | `200` | `semantic_limit_reached`, and `search_type` if you assumed semantic |
| `GET /ideas/duplicates` | `200` | On Free, the list is always empty regardless of what was detected |
## Handling Errors in Code
**JavaScript**
```js
function formatError(error) {
if (typeof error === "string") return error;
if (error && typeof error === "object") {
return Object.entries(error)
.map(([field, msgs]) => `${field}: ${[].concat(msgs).join(", ")}`)
.join("; ");
}
return "Unknown error";
}
const res = await fetch("https://neuralrepo.com/api/v1/ideas", {
method: "POST",
headers: {
"X-API-Key": "nrp_YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ title: "" }),
});
if (!res.ok) {
const body = await res.json();
switch (res.status) {
case 400:
console.error("Validation error:", formatError(body.error));
break;
case 401:
console.error("Auth error — check your API key");
break;
case 403:
console.error(
body.error === "idea_limit_reached"
? body.message
: body.pro_required
? `${body.feature} needs Pro: ${body.upgrade_url}`
: formatError(body.error)
);
break;
case 429:
// No Retry-After header exists — the quota resets at 00:00 UTC.
console.error("Daily rate limit exhausted");
break;
default:
console.error(`Error ${res.status}:`, formatError(body.error));
}
}
```
**curl**
```bash
# curl shows the HTTP status with -w and the body together
curl -s -w "\nHTTP Status: %{http_code}\n" \
-X POST https://neuralrepo.com/api/v1/ideas \
-H "X-API-Key: nrp_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"title": ""}'
```
## Summary Table
| Status | Name | Retryable | `error` type | Description |
|--------|------|-----------|--------------|-------------|
| `400` | Bad Request | No | String or object | Fix the request and resend; includes refused cycles |
| `401` | Unauthorized | No | String | Always `"Unauthorized"` |
| `403` | Forbidden | No | String or code | Plan gate, idea cap, or insufficient scope |
| `404` | Not Found | No | String | Wrong id — check you passed `id`, not `number` |
| `409` | Conflict | No | String | Tag name taken, or that relation pair already exists |
| `429` | Too Many Requests | Not today | String | Quota resets at 00:00 UTC; no `Retry-After` |
| `500` | Internal Server Error | Sometimes | String | Retry once; a tag rename collision will never succeed |
| `502` | Bad Gateway | Yes | String | Upstream AI provider or support service |
| `503` | Service Unavailable | Yes | String | Health check only — the database is unreachable |
## Next Steps
- [Rate Limits](https://docs.neuralconfig.com/neuralrepo/api/rate-limits/): What counts toward the daily quota, and why a 429 cannot be waited out.
- [Authentication](https://docs.neuralconfig.com/neuralrepo/api/authentication/): Why every auth failure looks the same, and how credentials are resolved.
- [Overview](https://docs.neuralconfig.com/neuralrepo/api/overview/#idea-identifiers): The id-versus-number rule behind most unexpected 404s.
- [Relations & Graph](https://docs.neuralconfig.com/neuralrepo/api/relations/): Cycle detection, the 409 pair rule, and bulk results that hide failures in a 201.
---
# Ideas
Source: https://docs.neuralconfig.com/neuralrepo/api/ideas/
Ideas are the core resource. Every endpoint on this page addresses an idea by its database
`id`, never by the `#N` display number.
**`:id` is the database id, not the number in your listings:**
An idea carries both an `id` (unique across all accounts) and a `number` (a counter within
your account). Listings, the CLI, and the web UI show the **number**; this API takes the
**id**. Read `id` out of the response body — the two match only for the very first account
ever created. See [Idea identifiers](https://docs.neuralconfig.com/neuralrepo/api/overview/#idea-identifiers).
## List Ideas
`GET /api/v1/ideas`
Returns unarchived ideas, newest first (`created_at` descending).
### Query Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `status` | string | One of `captured`, `exploring`, `building`, `shipped`, `shelved` |
| `tag` | string | Filter by tag name (exact match) |
| `connection_types` | string | Comma-separated relation types; returns ideas on either end of such a relation |
| `limit` | number | Items per page (default `20`, clamped to `100`) |
| `offset` | number | Items to skip (default `0`) |
`status` combines with either filter. `tag` and `connection_types` do **not** combine — if both
are supplied, `tag` wins and `connection_types` is ignored.
**curl**
```bash
curl "https://neuralrepo.com/api/v1/ideas?status=exploring&limit=10" \
-H "X-API-Key: nrp_YOUR_KEY"
```
**JavaScript**
```js
const res = await fetch(
"https://neuralrepo.com/api/v1/ideas?status=exploring&limit=10",
{ headers: { "X-API-Key": "nrp_YOUR_KEY" } }
);
const { ideas, has_more } = await res.json();
```
**Response `200 OK`**
```json
{
"ideas": [
{
"id": 137,
"number": 42,
"title": "Add dark mode support",
"body": "Users have requested a dark theme...",
"status": "exploring",
"source": "web",
"source_url": null,
"source_summary": null,
"parent_id": null,
"vectorize_id": "idea_137",
"is_archived": 0,
"tags": ["ui", "feature-request"],
"created_at": "2026-03-20 10:00:00",
"updated_at": "2026-03-22 14:30:00"
}
],
"has_more": true
}
```
`has_more` is computed as "this page is exactly `limit` long", so the last full page reports
`has_more: true` and the following request returns an empty array. Keep paging until `ideas` is
empty rather than until `has_more` is `false`.
## Create Idea
`POST /api/v1/ideas`
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | string | Yes | 1–200 characters |
| `body` | string | No | Max 50,000 characters |
| `tags` | string[] | No | Max 20 tags, each max 50 characters; unknown tags are created |
| `source` | string | No | One of `web`, `cli`, `claude-mcp`, `siri`, `email`, `api`, `shortcut`, `ios` |
| `source_url` | string | No | Valid URL, max 2,000 characters |
| `status` | string | No | Initial status (default `captured`) |
| `parent_id` | number | No | Parent idea **id** for nesting |
**`source` defaults to `web`, not `api`:**
The default is applied in the database layer and does not know which client called it. If you
want your integration's ideas to be distinguishable — in the web UI's source icons, in CSV
exports, or in your own reporting — send `"source": "api"` explicitly on every create.
**curl**
```bash
curl -X POST https://neuralrepo.com/api/v1/ideas \
-H "X-API-Key: nrp_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Implement webhook notifications",
"body": "Send HTTP callbacks when ideas change status.",
"tags": ["backend", "integrations"],
"source": "api"
}'
```
**JavaScript**
```js
const res = await fetch("https://neuralrepo.com/api/v1/ideas", {
method: "POST",
headers: {
"X-API-Key": "nrp_YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Implement webhook notifications",
body: "Send HTTP callbacks when ideas change status.",
tags: ["backend", "integrations"],
source: "api",
}),
});
const idea = await res.json();
```
**Response `201 Created`**
```json
{
"id": 138,
"number": 43,
"title": "Implement webhook notifications",
"body": "Send HTTP callbacks when ideas change status.",
"status": "captured",
"source": "api",
"source_url": null,
"parent_id": null,
"vectorize_id": null,
"is_archived": 0,
"tags": ["backend", "integrations"],
"processing": true,
"created_at": "2026-03-24 09:00:00",
"updated_at": "2026-03-24 09:00:00"
}
```
`processing: true` means the idea is saved but its embedding, auto-tags, related-idea links, and
duplicate check are still queued. `vectorize_id` stays `null` until that work completes — which
is why a brand-new idea is not immediately findable by semantic search. If the queue write
itself fails, the idea is still stored and an hourly recovery cron picks it up.
### Free-plan idea cap
`POST /ideas` is the one endpoint with a hard plan limit. Once a Free account holds **50
unarchived ideas**, further creates are refused before validation:
**Response `403 Forbidden`**
```json
{
"error": "idea_limit_reached",
"message": "You've captured 50 ideas on the free plan. Upgrade to Pro for unlimited ideas.",
"currentCount": 50,
"limit": 50,
"upgradeUrl": "https://neuralrepo.com/upgrade",
"featureRequestUrl": "https://support.neuralrepo.com/feature-requests"
}
```
Note that `error` here is a machine-readable code rather than a sentence — the human-readable
text is in `message`. Archiving an idea frees a slot, since the count is of unarchived ideas.
## Get Idea
`GET /api/v1/ideas/:id`
Returns a single idea with its tags, links, and relations. Archived ideas are still returned by
this endpoint — it is the one place they remain reachable.
**Response `200 OK`**
```json
{
"id": 138,
"number": 43,
"title": "Implement webhook notifications",
"body": "Send HTTP callbacks when ideas change status.",
"status": "captured",
"source": "api",
"source_url": null,
"parent_id": null,
"is_archived": 0,
"tags": ["backend", "integrations"],
"links": [],
"relations": [],
"created_at": "2026-03-24 09:00:00",
"updated_at": "2026-03-24 09:00:00"
}
```
`relations` here is the **flat** list — every relation touching this idea in either direction,
each carrying `related_idea_title` and `related_idea_number`, ordered by score. For the
outgoing/incoming split, use
[`GET /ideas/:id/relations`](https://docs.neuralconfig.com/neuralrepo/api/relations/#list-relations-for-an-idea).
## Update Idea
`PATCH /api/v1/ideas/:id`
All fields are optional. Only provided fields are updated.
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | string | No | 1–200 characters |
| `body` | string | No | Max 50,000 characters; replaces the body, never appends |
| `status` | string | No | New status value |
| `parent_id` | number \| null | No | Set or remove parent (pass `null` to detach) |
| `tags` | string[] | No | **Replaces** all tags; max 20, each max 50 characters |
Changing `title`, `body`, or `parent_id` re-queues the idea for embedding and relation
reconciliation. Changing only `status` or `tags` queues a lighter metadata update and leaves the
embedding alone.
**Caution:**
`is_archived` is not part of this schema. There is no field, on this endpoint or any other,
that clears the archived flag — see [Delete Idea](#delete-idea) below.
**Response `200 OK`** — the updated idea, with its tags.
## Bulk Update Ideas
`PATCH /api/v1/ideas/bulk`
Update status and/or tags for up to 50 ideas in one request.
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `ids` | number[] | Yes | Idea **ids** to update, 1–50 |
| `status` | string | No | New status for all ideas |
| `tags` | string[] | No | Replace all tags (cannot be combined with `add_tags`/`remove_tags`) |
| `add_tags` | string[] | No | Append tags without removing existing ones |
| `remove_tags` | string[] | No | Remove specific tags |
At least one of `status`, `tags`, `add_tags`, or `remove_tags` must be provided. Combining
`tags` with either of the other two returns `400`.
**This endpoint is not atomic:**
Ideas are processed one at a time, and a failure on one does not stop or roll back the others.
A response with `errors > 0` means the batch partially applied. **Read the `results` array** —
the HTTP status is `200` even when every single idea failed.
**curl**
```bash
curl -X PATCH https://neuralrepo.com/api/v1/ideas/bulk \
-H "X-API-Key: nrp_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"ids": [137, 138, 96],
"status": "building",
"add_tags": ["sprint-1"]
}'
```
**JavaScript**
```js
const res = await fetch("https://neuralrepo.com/api/v1/ideas/bulk", {
method: "PATCH",
headers: {
"X-API-Key": "nrp_YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
ids: [137, 138, 96],
status: "building",
add_tags: ["sprint-1"],
}),
});
const { updated, errors, results } = await res.json();
if (errors) console.warn(results.filter((r) => r.status === "error"));
```
**Response `200 OK`**
```json
{
"updated": 2,
"errors": 1,
"results": [
{ "id": 137, "status": "updated" },
{ "id": 138, "status": "updated" },
{ "id": 96, "status": "error", "error": "Idea 96 not found" }
]
}
```
An id that does not exist — or belongs to another account — comes back as a per-idea `error`
row, not a `404`.
## Delete Idea
`DELETE /api/v1/ideas/:id`
Archives the idea by setting `is_archived = 1`, then deletes its vector from the search index.
**Archiving can be undone:**
`is_archived` is absent from the update schema; [Restore Idea](#restore-idea) is the only way
back. The embedding is deleted rather than detached, so a restored idea is re-indexed. The row
survives until you [permanently delete it](#permanently-delete-archived-ideas).
What an archived idea disappears from:
| Surface | Effect |
|---|---|
| `GET /ideas`, `GET /map`, `GET /review` | Excluded |
| `GET /ideas/search` | Excluded from both keyword and semantic results |
| Relations | Filtered out of every relation query, in both directions |
| `GET /ideas/duplicates` | Detections referencing it are dropped |
| `POST /user/export` and the CSV/Markdown exports | Excluded — archiving before exporting loses the content |
| `GET /ideas/:id` | Still returns it, if you kept the id |
| Free-plan idea cap | Frees a slot; the cap counts unarchived ideas only |
**Response `200 OK`** — `{ "success": true }`. A `404` means the id is not yours or does not
exist; deleting an already-archived idea succeeds again and changes nothing.
## Restore Idea
`POST /api/v1/ideas/:id/restore`
Un-archives the idea and queues it to be re-embedded for semantic search. No request body.
Restoring an idea that is already active changes nothing and returns it as is.
**Response `200 OK`** — the idea object with its `tags`, as in [Get Idea](#get-idea) but without
`links` and `relations`.
| Status | When |
|---|---|
| `400` | The id is not a number |
| `403` | Free plan with 50 active ideas already: the same `idea_limit_reached` body as [Create Idea](#free-plan-idea-cap). An MCP token without `ideas:write` gets `Insufficient scope`. |
| `404` | The id is not yours or does not exist |
## List Archived Ideas
`GET /api/v1/ideas/archived`
Returns your archived ideas, most recently updated first. It is the list that permanent deletion
works from.
```json
{
"ideas": [
{ "id": 391, "number": 42, "title": "Rewrite auth flow", "updated_at": "2026-09-20 14:02:11" }
],
"count": 1
}
```
## Permanently Delete Archived Ideas
Two endpoints remove archived ideas for good. Each deleted idea takes its tags, links,
relations, duplicate records and search vector with it. A tag that no remaining idea uses is
deleted with its vector. A tag still in use has its vector re-embedded, through the background
queue, from the ideas that remain. Idea vectors are deleted before the idea rows, so if the
search index fails, nothing is deleted and a retry finds the same ideas. Vectors of orphaned tags
are deleted after the idea rows, just before those tag rows.
| Endpoint | Deletes |
|---|---|
| `DELETE /api/v1/ideas/:id/permanent` | One idea, which must already be archived |
| `DELETE /api/v1/ideas/archived` | Every archived idea you have (the web app's **Settings ▸ Delete** button) |
**Web session only:**
Both endpoints refuse API keys and MCP tokens, whatever their scopes, with `403`
`Sign in on the web to permanently delete ideas`. Only a signed-in browser session can delete
data irreversibly. See [Delete Data](https://docs.neuralconfig.com/neuralrepo/organize/delete-data/) for the web flow.
**Response `200 OK`:**
```json
{
"success": true,
"ideas_deleted": 1,
"idea_vectors_deleted": 1,
"tags_deleted": 0,
"tag_vectors_deleted": 0,
"tags_reembed_queued": 2
}
```
| Status | Body `error` | When |
|---|---|---|
| `400` | `Invalid idea ID` | `:id` is not a number |
| `403` | `Sign in on the web to permanently delete ideas` | Called with an API key or MCP token |
| `404` | `Idea not found` | The id is not yours or does not exist |
| `409` | `Only archived ideas can be permanently deleted. Archive it first.` | The idea is live — archive it with `DELETE /ideas/:id` first |
`DELETE /ideas/archived` with nothing archived returns `200` with every count at `0`.
## Merge Ideas
`POST /api/v1/ideas/:id/merge`
Folds another idea into this one. The idea in the path (`keep_id`) survives.
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `absorb_id` | number | Yes | **id** of the idea to absorb |
```bash
curl -X POST https://neuralrepo.com/api/v1/ideas/137/merge \
-H "X-API-Key: nrp_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"absorb_id": 138}'
```
Exactly what happens:
| Aspect | Behaviour |
|---|---|
| Body | Both bodies joined with a `\n\n---\n\n` separator, kept idea first |
| Tags | Union of both sets |
| Relations | Repointed from the absorbed idea to the kept idea; self-relations are removed |
| Links | **Not transferred** — they stay on the absorbed idea, which is then archived |
| `created_at` | The **earlier** of the two, so the merged idea keeps the original capture date |
| Absorbed idea | Set to `status: "shelved"` **and** archived — and therefore not recoverable |
| `supersedes` relation | Not created; archived ideas are filtered from relation queries, so it would be invisible |
Merging an idea with itself returns `400`. If either id is missing or not yours, the whole
operation returns `404` and nothing is changed.
**Response `200 OK`** — the surviving idea, with its merged tag list.
**Note:**
Resolving a duplicate detection is a **different** merge with different rules — it keeps the
newer idea and does not move relations. See
[Duplicates](https://docs.neuralconfig.com/neuralrepo/api/duplicates/#merge-duplicate).
## Develop Idea
`POST /api/v1/ideas/:id/develop`
Sends the idea, its tags, its related ideas, and its links to your configured AI provider and
asks for a buildable specification.
**This endpoint writes to the idea:**
The generated spec is not just returned — it is appended to the idea's `body` under an
`## AI-Generated Spec` heading, separated by `---`. Calling it twice appends twice. Save the
original body first if you need to compare.
Requires a [BYOK provider key](https://docs.neuralconfig.com/neuralrepo/integrations/byok/). Storing one is itself a Pro
feature, so in practice this endpoint is Pro-only.
```bash
curl -X POST https://neuralrepo.com/api/v1/ideas/137/develop \
-H "X-API-Key: nrp_YOUR_KEY"
```
**Response `200 OK`**
```json
{
"spec": "### Problem\n\n...",
"provider": "anthropic",
"model": "claude-sonnet-5-5",
"usage": {
"input_tokens": 2400,
"output_tokens": 1800
}
}
```
The response is capped at 4,096 output tokens and the prompt asks for under 1,500 words, laid
out as Problem / Proposed solution / Key features / Technical approach / Open questions — with a
Mermaid diagram in the technical section where one helps.
| Failure | Status | Body |
|---|---|---|
| No provider key stored | `400` | `{ "error": "No AI provider configured...", "needs_byok": true }` |
| Provider rejected the call or returned an error | `502` | `{ "error": "", "provider": "anthropic" }` |
| Idea id not found | `404` | `{ "error": "Idea not found" }` |
## Status Codes
| Status | Meaning |
|--------|---------|
| `200 OK` | Successful read, update, archive, merge, or develop |
| `201 Created` | Idea created |
| `400 Bad Request` | Validation error, non-numeric `:id`, self-merge, or a bad bulk payload |
| `401 Unauthorized` | Missing or invalid auth |
| `403 Forbidden` | Free-plan idea cap reached (`idea_limit_reached`) |
| `404 Not Found` | Idea not found, or not yours |
| `502 Bad Gateway` | Your AI provider failed during `develop` |
No endpoint on this page returns `409`.
## Next Steps
- [Search](https://docs.neuralconfig.com/neuralrepo/api/search/): Find ideas by keyword or meaning, and what the Free-plan semantic allowance changes.
- [Relations & Graph](https://docs.neuralconfig.com/neuralrepo/api/relations/): Link ideas to each other, in bulk, with cycle detection on the directional types.
- [Duplicates](https://docs.neuralconfig.com/neuralrepo/api/duplicates/): Review detections and merge them — with the other merge's rules.
- [Error Handling](https://docs.neuralconfig.com/neuralrepo/api/errors/): The two shapes an error body can take, and what each status code really means.
---
# Links
Source: https://docs.neuralconfig.com/neuralrepo/api/links/
Links attach external URLs to an idea — reference material, GitHub issues, design documents, or
any relevant resource. All three endpoints take the idea's database **id** in the path, not its
`#N` display number.
**Connecting two ideas is a relation, not a link:**
A link on this page is a URL on a single idea. A connection **between two ideas** (`related`,
`parent`, `blocks`, `inspires`, `supersedes`) is a **relation**: create it with
`POST /api/v1/map/relations`, which accepts up to 50 at once, or with `nrepo link` (`--batch`
reads many from stdin). Creating relations is a **Pro** feature and returns `403` on the Free
plan; reading them works on every plan. See [Relations](https://docs.neuralconfig.com/neuralrepo/api/relations/).
## List Links
`GET /api/v1/ideas/:id/links`
Returns every link attached to the idea, newest first. Available on all plans.
**curl**
```bash
curl https://neuralrepo.com/api/v1/ideas/137/links \
-H "X-API-Key: nrp_YOUR_KEY"
```
**JavaScript**
```js
const res = await fetch(
"https://neuralrepo.com/api/v1/ideas/137/links",
{ headers: { "X-API-Key": "nrp_YOUR_KEY" } }
);
const { links } = await res.json();
```
**Response `200 OK`**
```json
{
"links": [
{
"id": 1,
"idea_id": 137,
"url": "https://github.com/org/repo/issues/42",
"title": "GitHub Issue #42",
"link_type": "github-issue",
"created_at": "2026-03-20 10:00:00"
}
]
}
```
The same links are also embedded in the `links` array of `GET /ideas/:id`, so fetching an idea
already gives you them — this endpoint exists for when you only need the links.
## Add Link
`POST /api/v1/ideas/:id/links`
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `url` | string | Yes | Must parse as a URL; max 2,000 characters |
| `title` | string | No | Display title, max 200 characters |
| `link_type` | string | No | One of the five types below (default `url`) |
**curl**
```bash
curl -X POST https://neuralrepo.com/api/v1/ideas/137/links \
-H "X-API-Key: nrp_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://github.com/org/repo",
"title": "Project repository",
"link_type": "github-repo"
}'
```
**JavaScript**
```js
const res = await fetch(
"https://neuralrepo.com/api/v1/ideas/137/links",
{
method: "POST",
headers: {
"X-API-Key": "nrp_YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://github.com/org/repo",
title: "Project repository",
link_type: "github-repo",
}),
}
);
const { link } = await res.json();
```
**Response `201 Created`**
```json
{
"link": {
"id": 2,
"idea_id": 137,
"url": "https://github.com/org/repo",
"title": "Project repository",
"link_type": "github-repo",
"created_at": "2026-03-24 09:00:00"
}
}
```
Nothing is fetched, validated, or unfurled at the far end: the URL only has to parse. A link to a
page that 404s is stored exactly like one that works, and `title` is whatever you supply — it is
never read from the page. Duplicate URLs on the same idea are allowed and create separate rows.
## Delete Link
`DELETE /api/v1/ideas/:id/links/:linkId`
Removes a link from an idea. Both ids are required, and the link must belong to that idea.
```bash
curl -X DELETE https://neuralrepo.com/api/v1/ideas/137/links/2 \
-H "X-API-Key: nrp_YOUR_KEY"
```
**Response `200 OK`**
```json
{
"success": true
}
```
**A wrong link id still reports success:**
Only the **idea** is checked for existence and ownership. If the idea is yours, the delete
returns `{ "success": true }` whether or not `:linkId` matched anything. Confirm removal by
re-listing rather than by the status code.
## Link Types
`link_type` is a closed set — any other value is rejected with `400`:
| Type | Description |
|------|-------------|
| `url` | General web link (default) |
| `claude-chat` | Claude conversation link |
| `github-repo` | GitHub repository |
| `github-issue` | GitHub issue or PR |
| `attachment` | Accepted, but see below |
**`attachment` is a label, not a feature:**
The value is accepted and stored, but nothing in NeuralRepo produces or treats it specially —
there is no file-upload endpoint, and a link you tag `attachment` is stored and rendered
exactly like a `url`. Use it as your own marker if it helps; do not expect a file to be
hosted.
## Link Schema
| Field | Type | Description |
|-------|------|-------------|
| `id` | number | Unique link identifier — the `:linkId` for deletion |
| `idea_id` | number | The idea this link belongs to |
| `url` | string | The linked URL |
| `title` | string \| null | Display title |
| `link_type` | string | One of the five types above |
| `created_at` | string | UTC `YYYY-MM-DD HH:MM:SS` |
## Status Codes
| Status | Meaning |
|--------|---------|
| `200 OK` | Links listed, or delete accepted |
| `201 Created` | Link added |
| `400 Bad Request` | Non-numeric id, unparseable URL, title too long, or unknown `link_type` |
| `401 Unauthorized` | Missing or invalid auth |
| `404 Not Found` | The **idea** does not exist or is not yours |
## Next Steps
- [Ideas](https://docs.neuralconfig.com/neuralrepo/api/ideas/): Fetch an idea with its links, tags, and relations in one request.
- [Relations & Graph](https://docs.neuralconfig.com/neuralrepo/api/relations/): Link ideas to each other rather than to external URLs.
- [Claude Code integration](https://docs.neuralconfig.com/neuralrepo/integrations/claude-code/): Where claude-chat links come from, and what NeuralRepo does with them.
- [Error Handling](https://docs.neuralconfig.com/neuralrepo/api/errors/): Every status code the API returns and how to tell them apart.
---
# Mind Map
Source: https://docs.neuralconfig.com/neuralrepo/api/map/
The mind map endpoint returns all of your unarchived ideas and their relations as a graph
structure, ready for visualization. It is available on every plan.
## Get Map Data
`GET /api/v1/map`
Returns the complete graph: nodes (ideas) and edges (relations). There is no pagination and no
`limit` — you always get the whole graph, optionally narrowed by capture date.
### Query Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `since` | string | Only nodes whose `created_at` is at or after this value |
| `until` | string | Only nodes whose `created_at` is at or before this value |
Both are compared as strings against the stored `YYYY-MM-DD HH:MM:SS` timestamps, so
`?since=2026-03-01` works and an ISO string with a `Z` will not match the way you expect.
**Note:**
The window filters **nodes only**. Edges are fetched for the whole account, so a narrowed
window can return edges whose endpoints are not in `nodes`. Drop any edge whose `source` or
`target` is missing from the node set before rendering.
**curl**
```bash
curl "https://neuralrepo.com/api/v1/map?since=2026-03-01" \
-H "X-API-Key: nrp_YOUR_KEY"
```
**JavaScript**
```js
const res = await fetch("https://neuralrepo.com/api/v1/map", {
headers: { "X-API-Key": "nrp_YOUR_KEY" },
});
const { nodes, edges } = await res.json();
```
**Response `200 OK`**
```json
{
"nodes": [
{
"id": 137,
"number": 42,
"title": "Add dark mode support",
"status": "exploring",
"tags": ["ui", "feature-request"],
"connectionCount": 2,
"created_at": "2026-03-20 10:00:00"
},
{
"id": 138,
"number": 43,
"title": "Theme customization options",
"status": "building",
"tags": ["ui"],
"connectionCount": 1,
"created_at": "2026-03-18 08:00:00"
}
],
"edges": [
{
"id": 1,
"source": 137,
"target": 138,
"type": "related",
"score": 0.87,
"note": "Both deal with theming",
"created_by": "system",
"created_at": "2026-03-20 10:00:00"
},
{
"id": 2,
"source": 96,
"target": 137,
"type": "blocks",
"score": null,
"note": null,
"created_by": "user",
"created_at": "2026-03-19 08:00:00"
}
]
}
```
Nodes come back newest first; edges are ordered by descending `score`, which puts the
system-detected similarities before the ones you drew by hand. Ideas with no relations are
included, with `connectionCount: 0`.
## Node Schema
| Field | Type | Description |
|-------|------|-------------|
| `id` | number | Idea **id** — what edges reference and what every other endpoint takes |
| `number` | number | The `#N` display number, for labelling only |
| `title` | string | Idea title |
| `status` | string | `captured`, `exploring`, `building`, `shipped`, or `shelved` |
| `tags` | string[] | Associated tag names |
| `connectionCount` | number | Edges touching this node, counted from the edge list in this response |
| `created_at` | string | UTC `YYYY-MM-DD HH:MM:SS` |
## Edge Schema
| Field | Type | Description |
|-------|------|-------------|
| `id` | number | Relation identifier — pass this to `PATCH`/`DELETE /map/relations/:id` |
| `source` | number | Source idea id |
| `target` | number | Target idea id |
| `type` | string | `related`, `parent`, `blocks`, `inspires`, `supersedes`, or `duplicate` |
| `score` | number \| null | Similarity score if auto-detected, `null` if you created it |
| `note` | string \| null | Optional description |
| `created_by` | string | `system` for auto-detected edges, `user` for yours |
| `created_at` | string | UTC `YYYY-MM-DD HH:MM:SS` |
**Caution:**
The relation type arrives as `type` on an edge, but as `relation_type` everywhere else in the
API. Both name the same field in the database.
## Archived ideas
An archived idea is dropped from `nodes`, and any edge with an archived idea at **either** end is
dropped from `edges`. Since archiving cannot be undone, an idea that vanishes from your map is
gone from it permanently.
## Managing Relations
The map endpoint is read-only. To create, update, or delete relations, use the
[Relations](https://docs.neuralconfig.com/neuralrepo/api/relations/) endpoints — all three of which require Pro:
- `POST /api/v1/map/relations` — create one relation, or up to 50 at once
- `PATCH /api/v1/map/relations/:id` — update a relation
- `DELETE /api/v1/map/relations/:id` — delete a relation
## Usage Tips
- Use the node `tags` and `status` fields to filter or colour-code nodes.
- Style edges by `type` (dashed for `related`, solid arrows for `blocks`) and by `created_by`, so
a reader can tell suggested connections from deliberate ones.
- `connectionCount` is precomputed from this same response — sizing nodes by it needs no extra
pass.
## Status Codes
| Status | Meaning |
|--------|---------|
| `200 OK` | Graph data returned |
| `401 Unauthorized` | Missing or invalid auth |
## Next Steps
- [Relations & Graph](https://docs.neuralconfig.com/neuralrepo/api/relations/): Create, update, and delete the edges this endpoint returns.
- [Mind map concepts](https://docs.neuralconfig.com/neuralrepo/organize/mind-map/): How the in-app map lays the graph out and what each edge colour means.
- [Ideas](https://docs.neuralconfig.com/neuralrepo/api/ideas/): Fetch the full record behind a node, including its body and links.
- [Overview](https://docs.neuralconfig.com/neuralrepo/api/overview/): Pagination, timestamp format, and the id-versus-number rule that governs every node.
---
# API Overview
Source: https://docs.neuralconfig.com/neuralrepo/api/overview/
The NeuralRepo API is a RESTful JSON API that lets you programmatically manage ideas, tags,
relations, and user settings. Everything the web app, the CLI, and the MCP server do is done
through these endpoints.
## Base URL
All API requests are made to:
```
https://neuralrepo.com/api/v1
```
The API is versioned through the URL path. The current (and only) version is **v1**.
## Content Type
All request and response bodies use JSON. Set the `Content-Type` header on every request that includes a body:
```
Content-Type: application/json
```
## Authentication
Every endpoint under `/api/v1` except `/api/v1/health` requires authentication. Three
credentials are accepted:
| Method | Header | Format |
|--------|--------|--------|
| API key | `X-API-Key` | `nrp_` + 64 hexadecimal characters |
| API key or session token | `Authorization` | `Bearer ` |
| Session cookie | `Cookie` | `nrepo_session=` — set by the web app |
The `Authorization: Bearer` header accepts **either** a session token or an API key; the
server tries the session table first and falls back to the API-key table. See
[Authentication](https://docs.neuralconfig.com/neuralrepo/api/authentication/) for the exact resolution order, key
generation, and scopes.
## Idea identifiers
Every idea carries **two** integers, and they are not interchangeable:
| Field | What it is | Where you see it |
|---|---|---|
| `id` | Database primary key, unique across all accounts | Every `:id` path segment in this API |
| `number` | Per-account counter assigned at capture time | The `#42` in CLI listings and the web UI |
**Every endpoint in this API keys off `id`.** Nothing resolves a `number`, so passing the
`#42` you read in a listing either returns someone else's idea number range as a `404` or —
worse — silently addresses a *different* idea of yours.
**Never pass a display number to this API:**
`GET /api/v1/ideas/42` fetches the idea whose **`id`** is 42, not the one shown as `#42`.
The two values coincide only for the very first account ever created. Read `id` out of the
response body and pass that.
## Pagination
Pagination is **not** uniform across list endpoints — check the endpoint's own reference page:
| Endpoint | `limit` | `offset` | Notes |
|---|---|---|---|
| `GET /ideas` | default `20`, max `100` | default `0` | `has_more` is `true` when the page is exactly `limit` long |
| `GET /ideas/search` | default `20`, max `100` | not supported | Keyword mode can return up to **2×** `limit` |
| `GET /tags` | not supported | not supported | Returns every tag |
| `GET /ideas/duplicates` | not supported | not supported | Returns every pending detection |
| `GET /map` | not supported | not supported | Returns the whole graph; filter with `since`/`until` |
| `GET /review` | not supported | not supported | Returns the whole queue |
Example:
```
GET /api/v1/ideas?limit=50&offset=100
```
## Response Format
Successful responses return the resource wrapped in a named object (`{ "ideas": [...] }`,
`{ "tags": [...] }`, `{ "relation": {...} }`). Delete operations return `{ "success": true }`.
Error responses carry an `error` field — but note that on validation failures **`error` is an
object, not a string**. See [Error Handling](https://docs.neuralconfig.com/neuralrepo/api/errors/).
### Timestamps
Every `created_at` and `updated_at` value is a **UTC SQLite timestamp**, not ISO-8601:
```json
{ "created_at": "2026-03-24 09:00:00" }
```
There is no `T` separator and no `Z` suffix. `new Date("2026-03-24 09:00:00")` parses as
*local* time in most runtimes, so append `Z` yourself before parsing.
## Rate Limiting
Requests authenticated with an API key or bearer token are rate-limited per user and per day —
100/day on Free, 10,000/day on Pro. Cookie-authenticated requests from the web app are exempt.
Exceeding the limit returns `429`, and **no `Retry-After` header is sent**. See
[Rate Limits](https://docs.neuralconfig.com/neuralrepo/api/rate-limits/).
## Health Check
The health endpoint is the only unauthenticated route under `/api/v1`. Use it to verify the API
is reachable:
**curl**
```bash
curl https://neuralrepo.com/api/v1/health
```
**JavaScript**
```js
const res = await fetch("https://neuralrepo.com/api/v1/health");
const data = await res.json();
console.log(data);
```
**Response `200 OK`**
```json
{
"status": "ok",
"environment": "production",
"timestamp": "2026-03-25T12:00:00.000Z"
}
```
The health check runs a `SELECT 1` against the database. If that query fails, the endpoint
returns `503 Service Unavailable` with `{ "status": "error", "error": "Database unavailable" }`.
**Note:**
`timestamp` here *is* an ISO-8601 string — it is generated in the Worker rather than read
from the database. Resource timestamps follow the SQLite format described above.
## Field Limits
All input fields are validated against these maximum lengths:
| Field | Max | Behaviour when exceeded |
|-------|-----|-------------------------|
| Idea title | 200 characters | `400` |
| Idea body | 50,000 characters | `400` |
| Tags per idea | 20 | `400` |
| Tag name | 50 characters | `400` |
| Source URL | 2,000 characters | `400` |
| Display name | 100 characters | `400` |
| API key label | 100 characters | `400` |
| Settings JSON | 10,000 characters | `400` |
| Search query | 500 characters | `400` |
| List limit | 100 (default 20) | Clamped to 100, no error |
| Relation note | 500 characters | **Silently truncated**, no error |
| Bulk idea update | 50 ids | `400` |
| Bulk relation create | 50 links | `400` |
**Caution:**
Two of these do not behave like the rest. A `limit` above 100 is clamped rather than
rejected, and a relation `note` longer than 500 characters is stored truncated — the
response reports success and you lose the tail.
## Plan gates
Some endpoints require a Pro plan and return `403` with `pro_required: true` on Free:
| Endpoint | Gate |
|---|---|
| `POST /user/api-keys` | Pro |
| `PUT`/`POST` on `/user/byok/:provider` | Pro |
| `POST`, `PATCH`, `DELETE` on `/map/relations` | Pro |
| `GET /agent/ws` | Pro |
Two more limits are enforced by returning *fewer results* rather than an error, which is easy
to mistake for a bug:
- `GET /ideas/duplicates` returns `{ "duplicates": [] }` for every Free account.
- `GET /ideas/search` gives Free accounts 10 semantic searches per month, then quietly serves
keyword results.
And one is a hard cap on writes: `POST /ideas` returns `403 idea_limit_reached` once a Free
account holds 50 unarchived ideas.
## Versioning
The API is currently at **v1**. Breaking changes will be introduced under a new version prefix
(e.g., `/api/v2`). Non-breaking additions (new fields, new optional parameters) may be added to
v1 at any time.
## Endpoint index
| Resource | Endpoints |
|----------|-----------|
| [Ideas](https://docs.neuralconfig.com/neuralrepo/api/ideas/) | `GET` `POST` `/ideas`, `GET` `PATCH` `DELETE` `/ideas/:id`, `PATCH /ideas/bulk`, `POST /ideas/:id/merge`, `POST /ideas/:id/develop` |
| [Search](https://docs.neuralconfig.com/neuralrepo/api/search/) | `GET /ideas/search` |
| [Duplicates](https://docs.neuralconfig.com/neuralrepo/api/duplicates/) | `GET /ideas/duplicates`, `POST /ideas/duplicates/:id/dismiss`, `POST /ideas/duplicates/:id/merge` |
| [Tags](https://docs.neuralconfig.com/neuralrepo/api/tags/) | `GET` `POST` `/tags`, `PATCH` `DELETE` `/tags/:id`, `GET /tags/similar` |
| [Links](https://docs.neuralconfig.com/neuralrepo/api/links/) | `GET` `POST` `/ideas/:id/links`, `DELETE /ideas/:id/links/:linkId` |
| [Relations](https://docs.neuralconfig.com/neuralrepo/api/relations/) | `GET /ideas/:id/relations`, `POST /map/relations`, `PATCH` `DELETE` `/map/relations/:id` |
| [Mind Map](https://docs.neuralconfig.com/neuralrepo/api/map/) | `GET /map` |
| [Review Queue](https://docs.neuralconfig.com/neuralrepo/api/review/) | `GET /review`, `POST /review/:ideaId/keep` |
| [User & Settings](https://docs.neuralconfig.com/neuralrepo/api/user/) | `GET` `PATCH` `/user/me`, API keys, MCP tokens, BYOK, shortcuts, support link |
| [Export](https://docs.neuralconfig.com/neuralrepo/api/user/#data-export) | `POST /user/export`, `/user/export/csv`, `/user/export/markdown` |
Three further routes exist under `/api/v1` but are not part of the documented REST surface:
`GET /agent/ws` is a WebSocket upgrade used by the web app's [agent chat](https://docs.neuralconfig.com/neuralrepo/agent/overview/),
`POST /user/billing/checkout` and `/user/billing/portal` return single-use Stripe URLs for the
web app's upgrade flow, and `/api/v1/admin/*` is operator-only.
## Next Steps
- [Authentication](https://docs.neuralconfig.com/neuralrepo/api/authentication/): Generate an API key, understand scopes, and see the exact order credentials are resolved in.
- [Ideas](https://docs.neuralconfig.com/neuralrepo/api/ideas/): The core resource — create, read, update, archive, merge, and develop ideas.
- [Error Handling](https://docs.neuralconfig.com/neuralrepo/api/errors/): Every status code the API returns, the two shapes the error field can take, and what is retryable.
- [Rate Limits](https://docs.neuralconfig.com/neuralrepo/api/rate-limits/): Daily quotas per plan, what counts against them, and why there is no Retry-After header.
---
# Rate Limits
Source: https://docs.neuralconfig.com/neuralrepo/api/rate-limits/
NeuralRepo enforces a per-user daily request limit on programmatic access. The limit applies
only to requests authenticated with an **API key or a bearer token**. Requests carrying only the
`nrepo_session` cookie — that is, the web app — are exempt, because a single page load makes many
calls and the quota exists for scripted access.
## Daily Limits
| Plan | Limit |
|------|-------|
| Free | 100 requests per day |
| Pro | 10,000 requests per day |
The counter is keyed on your user id and the **UTC date**, so it resets at 00:00 UTC. It is a
single quota per account: every API key, MCP token, and bearer session you hold draws from the
same bucket.
## What counts
| Request | Counted? |
|---|---|
| Any `/api/v1/*` call with `X-API-Key` | Yes |
| Any `/api/v1/*` call with `Authorization: Bearer` | Yes — session tokens included |
| Any `/api/v1/*` call with only the session cookie | No |
| `GET /api/v1/health` | No — it runs before the middleware chain |
| A request that then fails with `400`/`404` | Yes — the counter increments before your handler runs |
| A request rejected with `429` | No — the counter is already at the limit and is not raised further |
Reads count exactly as much as writes. There is no separate write quota.
## Retry Behavior
When you exceed your daily limit, the API responds with `429 Too Many Requests`:
```json
{
"error": "Rate limit exceeded"
}
```
**There is no Retry-After header:**
The `429` response carries no `Retry-After`, no `X-RateLimit-Remaining`, and no reset
timestamp — reading them returns `null`. The only thing you know from a `429` is that you are
done until 00:00 UTC.
Because the window is a whole day, retrying within the same session will not succeed. Back off
so that a transient failure does not turn into a retry storm, but treat a sustained `429` as a
signal to stop for the day rather than to keep trying.
### Example: Retry with Backoff
```js
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const res = await fetch(url, options);
if (res.status !== 429) return res;
// No Retry-After is sent — back off on a fixed schedule.
const delay = Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
}
throw new Error("Rate limit exceeded after retries");
}
```
## Limits that are not rate limits
Three other caps are easy to mistake for rate limiting, because two of them return `200` with
less data rather than an error:
| Cap | Plan | What you see |
|---|---|---|
| 50 unarchived ideas | Free | `POST /ideas` returns `403 idea_limit_reached` |
| 10 semantic searches per month | Free | `GET /ideas/search` returns keyword results with `semantic_limit_reached: true` |
| Duplicate detections hidden | Free | `GET /ideas/duplicates` returns `{ "duplicates": [] }` |
The semantic-search counter is monthly (not daily) and is consumed *before* the search runs, so
a search that fails or matches nothing still spends one of the ten.
## Best Practices
- **Batch operations.** `PATCH /ideas/bulk` updates up to 50 ideas for one request, and
`POST /map/relations` accepts up to 50 links in one call.
- **Cache responses** when possible to reduce request volume.
- **Spread requests evenly** across the day rather than sending bursts.
- **Watch your own count.** Nothing in the response tells you how much quota is left, so track
request counts client-side if you are near the Free limit.
## Status Codes
| Status | Meaning |
|--------|---------|
| `429 Too Many Requests` | Daily quota exhausted; resets at 00:00 UTC |
## Next Steps
- [Ideas](https://docs.neuralconfig.com/neuralrepo/api/ideas/): Bulk endpoints that let you do more per request, and the Free-plan idea cap.
- [Search](https://docs.neuralconfig.com/neuralrepo/api/search/): How the monthly semantic-search allowance behaves once it runs out.
- [Error Handling](https://docs.neuralconfig.com/neuralrepo/api/errors/): Every status code the API returns and which of them are worth retrying.
- [Pricing](https://docs.neuralconfig.com/neuralrepo/getting-started/pricing/): What Free and Pro include, including trials.
---
# Relations & Graph
Source: https://docs.neuralconfig.com/neuralrepo/api/relations/
Relations connect ideas to each other, forming a navigable graph. Each relation has a source, a
target, a type, an optional note, and a `created_by` marker recording whether you made it or
NeuralRepo's similarity pass did.
**Writing relations is a Pro feature:**
`POST`, `PATCH`, and `DELETE` on `/map/relations` all return `403` with `pro_required: true`
on the Free plan. **Reading** relations — `GET /ideas/:id/relations` and `GET /map` — works on
every plan, so a Free account can see the connections the system found for it but cannot add
its own.
## List Relations for an Idea
`GET /api/v1/ideas/:id/relations`
Returns relations involving the idea, split by direction. Relations to or from an archived idea
are omitted from both lists.
**curl**
```bash
curl https://neuralrepo.com/api/v1/ideas/137/relations \
-H "X-API-Key: nrp_YOUR_KEY"
```
**JavaScript**
```js
const res = await fetch(
"https://neuralrepo.com/api/v1/ideas/137/relations",
{ headers: { "X-API-Key": "nrp_YOUR_KEY" } }
);
const { outgoing, incoming } = await res.json();
```
**Response `200 OK`**
```json
{
"outgoing": [
{
"id": 1,
"relation_type": "related",
"score": 0.87,
"note": "Both deal with theming",
"created_by": "system",
"created_at": "2026-03-20 10:00:00",
"idea_id": 138,
"idea_number": 43,
"idea_title": "Theme customization options",
"idea_status": "building"
}
],
"incoming": [
{
"id": 2,
"relation_type": "blocks",
"score": null,
"note": null,
"created_by": "user",
"created_at": "2026-03-19 08:00:00",
"idea_id": 96,
"idea_number": 12,
"idea_title": "Implement design tokens",
"idea_status": "captured"
}
]
}
```
The row is **flat** — the other side of the relation arrives as `idea_id`, `idea_number`,
`idea_title`, and `idea_status` rather than as a nested object, and it carries no tags. `id` is
the relation's own id, which is what `PATCH` and `DELETE` take. Rows are ordered by
`relation_type`, then by descending `score`.
## Create Relation
`POST /api/v1/map/relations`
Pro only. Accepts a single relation, or up to 50 at once.
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `source_idea_id` | number | Yes | **id** of the source idea |
| `target_idea_id` | number | Yes | **id** of the target idea |
| `relation_type` | string | No | `related` (default), `parent`, `blocks`, `inspires`, or `supersedes` |
| `note` | string | No | Free text; anything past 500 characters is silently truncated |
**Note:**
`duplicate` is a valid stored relation type but **cannot be created through this endpoint** —
it is rejected as invalid. It exists for detections written by the duplicate pass. See
[Duplicates](https://docs.neuralconfig.com/neuralrepo/api/duplicates/).
**curl**
```bash
curl -X POST https://neuralrepo.com/api/v1/map/relations \
-H "X-API-Key: nrp_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"source_idea_id": 137,
"target_idea_id": 138,
"relation_type": "related",
"note": "Both deal with theming"
}'
```
**JavaScript**
```js
const res = await fetch("https://neuralrepo.com/api/v1/map/relations", {
method: "POST",
headers: {
"X-API-Key": "nrp_YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
source_idea_id: 137,
target_idea_id: 138,
relation_type: "related",
note: "Both deal with theming",
}),
});
const { relation } = await res.json();
```
**Response `201 Created`**
```json
{
"relation": {
"id": 17,
"source_idea_id": 137,
"target_idea_id": 138,
"relation_type": "related",
"note": "Both deal with theming"
}
}
```
The single-relation response carries no `created_at` and no `score` — `score` is only ever set
by the automatic similarity pass, never on a relation you create. Re-read the relation through
`GET /ideas/:id/relations` if you need those fields.
### Creating many at once
Send a `links` array instead of a single body. Up to **50** per call.
```bash
curl -X POST https://neuralrepo.com/api/v1/map/relations \
-H "X-API-Key: nrp_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"links": [
{ "source_idea_id": 137, "target_idea_id": 138, "relation_type": "related" },
{ "source_idea_id": 137, "target_idea_id": 96, "relation_type": "blocks", "note": "needs tokens first" }
]
}'
```
**Response `201 Created`**
```json
{
"linked": 1,
"errors": 1,
"results": [
{
"source_idea_id": 137,
"target_idea_id": 138,
"relation_type": "related",
"status": "created",
"relation_id": 17
},
{
"source_idea_id": 137,
"target_idea_id": 96,
"relation_type": "blocks",
"status": "error",
"error": "This would create a circular blocking chain: #137 → #96 → #137"
}
]
}
```
**Bulk mode always returns 201, even when nothing was created:**
Every failure — a missing idea, a cycle, a duplicate pair, an invalid type — is reported as a
`status: "error"` row inside a `201` response. There is no rollback and no error status code.
Check `errors` and read `results`.
## Update Relation
`PATCH /api/v1/map/relations/:id`
Pro only. Changes the type, the note, or both.
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `relation_type` | string | No | `related`, `parent`, `blocks`, `inspires`, or `supersedes` |
| `note` | string \| null | No | Updated note; `null` clears it, over 500 characters truncates |
```bash
curl -X PATCH https://neuralrepo.com/api/v1/map/relations/17 \
-H "X-API-Key: nrp_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"relation_type": "blocks"}'
```
A body with neither field returns `400 Nothing to update`.
**Updating skips cycle detection entirely:**
The cycle checks described below run on **create only**. Changing an existing `related` edge
to `blocks` or `parent` can therefore produce exactly the cycle that a fresh create would have
refused. If you rely on the graph being acyclic, delete and re-create rather than patching the
type.
**Response `200 OK`** — the updated relation wrapped in `{ "relation": {...} }`, or `404` if the
relation id is not yours.
## Delete Relation
`DELETE /api/v1/map/relations/:id`
Pro only.
```bash
curl -X DELETE https://neuralrepo.com/api/v1/map/relations/17 \
-H "X-API-Key: nrp_YOUR_KEY"
```
**Response `200 OK`**
```json
{
"success": true
}
```
Deleting is idempotent and unverified: an id that does not exist, or belongs to another account,
still returns `{ "success": true }`. It never returns `404`.
## Duplicate pairs
The uniqueness rule is `(source_idea_id, target_idea_id, created_by)` — it does **not** include
the relation type. So:
- A second user-created relation from A → B is refused with `Relation already exists`, even if
you ask for a different type. Change the existing one with `PATCH` instead.
- A → B and B → A are two different pairs, and both may exist.
- A system-created relation between the same two ideas does not block your user-created one;
they coexist, and both appear in the graph.
That refusal is the one error on this page that really does return `409`.
## Cycle Detection
Creating a relation runs a breadth-first walk from the proposed target, following edges of the
**same type**, looking for a path back to the source. What happens then depends on the type:
| Types | Cycle behaviour |
|---|---|
| `blocks`, `parent` | **Hard block** — always refused; `?force=true` does not help |
| `supersedes` | **Soft block** — refused, but `?force=true` creates it anyway |
| `related`, `inspires` | Not checked at all — the walk is skipped |
The rejection message names the whole chain:
```json
{
"error": "This would create a circular blocking chain: #137 → #96 → #137"
}
```
For `supersedes` the same message ends with `Use ?force=true to bypass.`
**A rejected cycle returns 400, not 409:**
Despite the wording, the API classifies this as a validation failure: the response status is
`400 Bad Request`. Match on the status **and** the message, or you will treat a cycle as a
generic bad request. In bulk mode it is not a status at all — just an `error` row.
Two more properties of the check:
- The numbers in the message are **database ids** with a `#` prefix, even though `#N` means the
display number everywhere else in NeuralRepo. They are ids.
- The walk stops after 50 hops. A cycle that only closes beyond that depth is not detected and
the relation is created.
```bash
# Soft block: allowed through with ?force=true
curl -X POST "https://neuralrepo.com/api/v1/map/relations?force=true" \
-H "X-API-Key: nrp_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"source_idea_id": 138,
"target_idea_id": 137,
"relation_type": "supersedes"
}'
```
## Relation Schema
| Field | Type | Description |
|-------|------|-------------|
| `id` | number | Relation identifier — what `PATCH`/`DELETE` take |
| `source_idea_id` | number | Source idea **id** |
| `target_idea_id` | number | Target idea **id** |
| `relation_type` | string | `related`, `parent`, `blocks`, `inspires`, `supersedes`, or `duplicate` (system only) |
| `score` | number \| null | Similarity score for system-created relations; `null` for yours |
| `note` | string \| null | Optional description, stored truncated at 500 characters |
| `created_by` | string | `user` or `system` |
| `created_at` | string | UTC `YYYY-MM-DD HH:MM:SS` |
## Status Codes
| Status | Meaning |
|--------|---------|
| `200 OK` | Read, update, or delete succeeded |
| `201 Created` | Relation created — **including bulk calls where every link failed** |
| `400 Bad Request` | Validation error, self-link, unknown relation type, or a refused cycle |
| `401 Unauthorized` | Missing or invalid auth |
| `403 Forbidden` | Free plan — writing relations requires Pro |
| `404 Not Found` | An idea id in the request does not exist, or the relation id does not (`PATCH` only) |
| `409 Conflict` | A user-created relation already exists between that pair, in that direction |
## Next Steps
- [Mind Map](https://docs.neuralconfig.com/neuralrepo/api/map/): Fetch every node and edge at once for visualization.
- [Relations concepts](https://docs.neuralconfig.com/neuralrepo/organize/relations/): What each relation type means, and which ones the similarity pass creates for you.
- [Ideas](https://docs.neuralconfig.com/neuralrepo/api/ideas/#merge-ideas): Merging repoints relations — see exactly how before you fold two ideas together.
- [Error Handling](https://docs.neuralconfig.com/neuralrepo/api/errors/): Which failures are statuses and which are rows inside a 2xx response.
---
# Review Queue
Source: https://docs.neuralconfig.com/neuralrepo/api/review/
The review endpoints back the Review tab: one call that returns everything worth revisiting —
ideas that have gone quiet, connections found for you recently, and duplicate detections waiting
on a decision.
## Get the Review Queue
`GET /api/v1/review`
Returns all three groups plus their counts. Available on every plan, with one difference noted
below.
**curl**
```bash
curl https://neuralrepo.com/api/v1/review \
-H "X-API-Key: nrp_YOUR_KEY"
```
**JavaScript**
```js
const res = await fetch("https://neuralrepo.com/api/v1/review", {
headers: { "X-API-Key": "nrp_YOUR_KEY" },
});
const { stale, connections, duplicates, counts } = await res.json();
```
**Response `200 OK`**
```json
{
"stale": [
{
"id": 96,
"number": 12,
"title": "Implement design tokens",
"body": "Ship a token layer before theming...",
"status": "captured",
"source": "web",
"updated_at": "2026-01-04 11:12:00",
"days_stale": 79
}
],
"connections": [
{
"id": 31,
"relation_type": "related",
"score": 0.71,
"created_by": "system",
"created_at": "2026-03-22 06:00:00",
"source_idea_id": 137,
"source_number": 42,
"source_title": "Add dark mode support",
"target_idea_id": 138,
"target_number": 43,
"target_title": "Theme customization options"
}
],
"duplicates": [],
"counts": {
"stale": 1,
"connections": 1,
"duplicates": 0,
"total": 2
},
"stale_threshold_days": 30
}
```
| Group | What it contains |
|---|---|
| `stale` | Ideas still in `captured` whose `updated_at` is older than `stale_threshold_days`, oldest first, with `days_stale` precomputed |
| `connections` | Relations created in the **last 7 days**, newest first, with both endpoints' numbers and titles and a `created_by` telling you whether the system proposed it or you drew it |
| `duplicates` | Pending duplicate detections — the same rows as [`GET /ideas/duplicates`](https://docs.neuralconfig.com/neuralrepo/api/duplicates/) |
| `counts` | Sizes of each group and their `total`, for a badge |
| `stale_threshold_days` | The threshold actually applied, so you can label the list |
**`stale` and `connections` are capped at 20 each:**
Both lists stop at 20 rows, and there is no parameter to raise it or to page past it. `counts`
is computed from the returned arrays, so `counts.stale: 20` means "20 or more", not "20". Use
[`GET /ideas?status=captured`](https://docs.neuralconfig.com/neuralrepo/api/ideas/) if you need the full backlog.
`stale_threshold_days` comes from your `stale_threshold_days` [user setting](https://docs.neuralconfig.com/neuralrepo/api/user/#user-settings)
and defaults to **30**. The connections window is fixed at 7 days and is not configurable.
**Duplicates are empty on Free:**
The `duplicates` group follows the same rule as the duplicates endpoint: Free accounts always
get an empty array, and `counts.duplicates` is `0`. The other two groups are unaffected, so a
Free account still gets a useful queue — just never a duplicate in it.
Only `captured` ideas can be stale. An idea moved to `exploring`, `building`, `shipped`, or
`shelved` leaves the stale list permanently, whatever its date.
## Keep an Idea
`POST /api/v1/review/:ideaId/keep`
"Still relevant." Bumps the idea's `updated_at` to now, which drops it out of the stale group for
another `stale_threshold_days`. Nothing else changes — not the status, not the body, not the
tags.
```bash
curl -X POST https://neuralrepo.com/api/v1/review/96/keep \
-H "X-API-Key: nrp_YOUR_KEY"
```
**Response `200 OK`**
```json
{
"success": true
}
```
**Note:**
Like the other touch-only endpoints, this one does not verify the idea exists: an id that is
not yours still returns `{ "success": true }` and changes nothing. Only a non-numeric `:ideaId`
is rejected, with `400`.
## Resolving the rest of the queue
Only "keep" is a review-specific action. Everything else reuses the endpoints you already have:
| Item | Action | Endpoint |
|---|---|---|
| Stale idea | Still relevant | `POST /review/:ideaId/keep` |
| Stale idea | Move it along | `PATCH /ideas/:id` with a new `status` |
| Stale idea | Let it go | `DELETE /ideas/:id` — archives, permanently |
| Connection | Keep it | Nothing to do; it ages out of the 7-day window |
| Connection | Reject it | `DELETE /map/relations/:id` (Pro) |
| Duplicate | Not a duplicate | `POST /ideas/duplicates/:id/dismiss` |
| Duplicate | Merge | `POST /ideas/duplicates/:id/merge` |
## Status Codes
| Status | Meaning |
|--------|---------|
| `200 OK` | Queue returned, or keep recorded |
| `400 Bad Request` | `:ideaId` is not a number |
| `401 Unauthorized` | Missing or invalid auth |
| `404 Not Found` | The user record is missing (the queue endpoint only) |
## Next Steps
- [Review concepts](https://docs.neuralconfig.com/neuralrepo/organize/review/): How the Review tab presents this queue and the habits it is designed around.
- [Duplicates](https://docs.neuralconfig.com/neuralrepo/api/duplicates/): The full detection payload, and which idea survives each kind of merge.
- [User & Settings](https://docs.neuralconfig.com/neuralrepo/api/user/#user-settings): Change stale_threshold_days, or turn stale nudge emails off.
- [Relations & Graph](https://docs.neuralconfig.com/neuralrepo/api/relations/): Delete a proposed connection, or add your own.
---
# Search
Source: https://docs.neuralconfig.com/neuralrepo/api/search/
## Search Ideas
`GET /api/v1/ideas/search`
Searches your unarchived ideas by meaning (default) or by keyword. The two modes are separate
engines with separate result shapes — the response tells you which one ran.
### Query Parameters
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `q` | string | Yes | — | Search query, max 500 characters |
| `mode` | string | No | semantic | `keyword` or `semantic`; anything else returns `400` |
| `limit` | number | No | `20` | Clamped to `100`. Keyword mode can return up to **2×** this |
| `fts_mode` | string | No | `or` | Keyword matching: `or`, `and`, or `exact` |
| `min_score` | number | No | `0.2` | Semantic score floor; overrides your `search_threshold` setting |
There is no `offset` — search is not paginated. Raise `limit` instead.
**There is no automatic keyword fallback:**
A semantic search that finds nothing above the score floor returns **an empty `results` array**
with `search_type: "semantic"`. It does not silently retry as a keyword search. If you want
keyword behaviour, ask for it with `mode=keyword` — that is the only way to reach the
full-text index. (The MCP `search_ideas` tool and the agent *do* fall back; this endpoint and
the CLI do not.)
### Semantic Search
The default mode. The query is embedded, matched against your idea vectors, filtered by score,
and then **reranked** by a second model before being cut to `limit`.
```bash
curl "https://neuralrepo.com/api/v1/ideas/search?q=user+interface+theming" \
-H "X-API-Key: nrp_YOUR_KEY"
```
Two consequences worth knowing:
- Results are ordered by the reranker, not by raw `score`, so scores are **not** guaranteed to
descend down the list.
- Ideas captured seconds ago may be missing. An idea is only searchable once its queued
embedding lands (`vectorize_id` stops being `null`).
Only ideas scoring **strictly above** `min_score` survive. The default floor is `0.2`; your
`search_threshold` user setting replaces it, and an explicit `min_score` parameter overrides
both.
### Keyword Search
Keyword mode queries the SQLite FTS5 index over titles and bodies, ordered by BM25 rank.
**curl**
```bash
curl "https://neuralrepo.com/api/v1/ideas/search?q=dark+mode&mode=keyword" \
-H "X-API-Key: nrp_YOUR_KEY"
```
**JavaScript**
```js
const params = new URLSearchParams({
q: "dark mode",
mode: "keyword",
fts_mode: "and",
});
const res = await fetch(
`https://neuralrepo.com/api/v1/ideas/search?${params}`,
{ headers: { "X-API-Key": "nrp_YOUR_KEY" } }
);
const results = await res.json();
```
Before matching, the query is lowercased, stripped of punctuation, and split on whitespace.
Tokens of one character and common stop words (`the`, `and`, `for`, `what`, `this`, and about
sixty others) are dropped. **A query made entirely of stop words matches nothing** — `"how to
do it"` reduces to zero tokens and returns an empty array.
`fts_mode` controls how the surviving tokens are combined:
| `fts_mode` | Behaviour | `dark mode` becomes |
|---|---|---|
| `or` (default) | Any token matches | `"dark" OR "mode"` |
| `and` | Every token must match | `"dark" "mode"` |
| `exact` | The raw phrase, quoted; stop words are kept | `"dark mode"` |
**Keyword mode over-delivers:**
Keyword mode asks the index for `limit × 2` rows and returns all of them, so `limit=20` can
come back with 40 results. Slice client-side if the count matters.
### Response
**`200 OK`**
The response echoes the `query`, reports `search_type` (`semantic` or `fts`), and — in keyword
mode — the `fts_mode` that ran.
```json
{
"query": "user interface theming",
"search_type": "semantic",
"results": [
{
"id": 137,
"number": 42,
"title": "Add dark mode support",
"body": "Users have requested a dark theme...",
"status": "exploring",
"source": "web",
"source_url": null,
"is_archived": 0,
"tags": ["ui", "feature-request"],
"score": 0.92,
"created_at": "2026-03-20 10:00:00",
"updated_at": "2026-03-22 14:30:00"
}
]
}
```
### Score Field
| Mode | `score` |
|------|---------|
| `fts` | Always `null` — the BM25 rank orders the rows but is not returned |
| `semantic` | Cosine similarity between the query and the idea embedding |
## Free-plan semantic allowance
Free accounts get **10 semantic searches per calendar month**. Once they are spent, the endpoint
serves keyword results instead and says so:
```json
{
"query": "user interface theming",
"search_type": "fts",
"fts_mode": "or",
"results": [],
"semantic_limit_reached": true,
"semantic_limit_message": "You've used all 10 semantic searches this month. Showing keyword results instead. Upgrade to Pro for unlimited semantic search."
}
```
Three details that surprise people:
- The counter is incremented **before** the search runs, so a search that errors or matches
nothing still costs one of the ten.
- `mode=keyword` requests are free and never touch the counter.
- The count resets on the first of the month; the underlying counter is stored per
account-month with a 35-day expiry.
Pro accounts have no semantic-search limit. Both plans have unlimited keyword search.
## Comparison
| | Keyword (`mode=keyword`) | Semantic (default) |
|---|---|---|
| Engine | SQLite FTS5, BM25 rank | Vector index + reranker |
| Matches exact words | Yes | Not necessarily |
| Finds conceptual matches | No | Yes |
| Needs an embedding | No — searchable immediately | Yes — after the queue processes the idea |
| Returns a `score` | No (`null`) | Yes |
| Respects `limit` | Returns up to 2× | Yes |
| Free-plan limit | None | 10 per month |
## Status Codes
| Status | Meaning |
|--------|---------|
| `200 OK` | Search completed — including when `results` is empty |
| `400 Bad Request` | Missing `q`, or a `mode` other than `keyword`/`semantic` |
| `401 Unauthorized` | Missing or invalid auth |
## Next Steps
- [Search concepts](https://docs.neuralconfig.com/neuralrepo/organize/search/): How embedding, reranking, and thresholds fit together across every NeuralRepo surface.
- [User & Settings](https://docs.neuralconfig.com/neuralrepo/api/user/): Set search_threshold and the other similarity thresholds on your profile.
- [Ideas](https://docs.neuralconfig.com/neuralrepo/api/ideas/): List and filter ideas when you already know the status, tag, or connection you want.
- [Rate Limits](https://docs.neuralconfig.com/neuralrepo/api/rate-limits/): The daily request quota that applies on top of the monthly semantic allowance.
---
# Tags
Source: https://docs.neuralconfig.com/neuralrepo/api/tags/
Tags categorize and filter ideas. Each tag has a name, an optional colour, and — on the list
endpoint — a count of ideas using it. Tag names are unique per account, and tags are created
implicitly whenever you attach an unknown name to an idea.
## List Tags
`GET /api/v1/tags`
Returns every tag on the account, most-used first, then alphabetically. There is no pagination.
**curl**
```bash
curl https://neuralrepo.com/api/v1/tags \
-H "X-API-Key: nrp_YOUR_KEY"
```
**JavaScript**
```js
const res = await fetch("https://neuralrepo.com/api/v1/tags", {
headers: { "X-API-Key": "nrp_YOUR_KEY" },
});
const { tags } = await res.json();
```
**Response `200 OK`**
```json
{
"tags": [
{
"id": 1,
"user_id": "3f2a91c47b0e4d5aa8c61e0f2b7d4c93",
"name": "feature-request",
"color": "#4f46e5",
"vectorize_id": "tag_1",
"created_at": "2026-02-01 09:00:00",
"idea_count": 12
},
{
"id": 2,
"user_id": "3f2a91c47b0e4d5aa8c61e0f2b7d4c93",
"name": "bug",
"color": null,
"vectorize_id": null,
"created_at": "2026-02-03 11:20:00",
"idea_count": 5
}
]
}
```
`idea_count` counts **unarchived** ideas only, so archiving ideas lowers it and a tag can sit at
zero without being deleted. `vectorize_id` is `null` until the tag's embedding is generated in
the background — until then it cannot appear in similarity results.
## Find Similar Tags
`GET /api/v1/tags/similar`
Finds tags whose embeddings are close to a given tag's. Useful for spotting near-duplicate tags
(`ui` vs `frontend`) before they fragment your organization.
### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `tag` | string | Yes | Tag **name** (not id) to compare against |
| `limit` | number | No | Max results (default `5`, clamped to `20`) |
**curl**
```bash
curl "https://neuralrepo.com/api/v1/tags/similar?tag=frontend&limit=5" \
-H "X-API-Key: nrp_YOUR_KEY"
```
**Response `200 OK`**
```json
{
"similar": [
{ "name": "ui", "idea_count": 8, "similarity": 0.82 },
{ "name": "react", "idea_count": 5, "similarity": 0.76 },
{ "name": "css", "idea_count": 3, "similarity": 0.71 }
]
}
```
| Situation | Response |
|---|---|
| Tag name not found on your account | `404 { "error": "Tag \"frontend\" not found" }` |
| Tag exists but has no embedding yet | `200 { "similar": [], "message": "Tag has no embedding yet" }` |
| `tag` parameter omitted | `400 { "error": "Missing \"tag\" query parameter" }` |
There is no score floor — every neighbour the index returns is listed, so a small account with
unrelated tags still gets results with low `similarity` values. Judge by the number, not by
presence in the list.
## Create Tag
`POST /api/v1/tags`
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | 1–50 characters, unique within your account |
| `color` | string | No | Hex colour, exactly `#rrggbb` (six digits, `#f00` is rejected) |
**curl**
```bash
curl -X POST https://neuralrepo.com/api/v1/tags \
-H "X-API-Key: nrp_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "backend", "color": "#059669"}'
```
**JavaScript**
```js
const res = await fetch("https://neuralrepo.com/api/v1/tags", {
method: "POST",
headers: {
"X-API-Key": "nrp_YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "backend", color: "#059669" }),
});
const { tag } = await res.json();
```
**Response `201 Created`**
```json
{
"tag": {
"id": 3,
"user_id": "3f2a91c47b0e4d5aa8c61e0f2b7d4c93",
"name": "backend",
"color": "#059669",
"vectorize_id": null,
"created_at": "2026-03-24 09:00:00"
}
}
```
Creating a tag that already exists returns `409 { "error": "Tag name already exists" }`. You
rarely need this endpoint — attaching an unknown tag name to an idea creates it for you.
## Update Tag
`PATCH /api/v1/tags/:id`
Update the name, colour, or both. Only provided fields change. Takes the tag **id**, not its
name.
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | No | 1–50 characters |
| `color` | string | No | Hex colour in `#rrggbb` format |
```bash
curl -X PATCH https://neuralrepo.com/api/v1/tags/3 \
-H "X-API-Key: nrp_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"color": "#0ea5e9"}'
```
**Response `200 OK`**
```json
{
"tag": {
"id": 3,
"user_id": "3f2a91c47b0e4d5aa8c61e0f2b7d4c93",
"name": "backend",
"color": "#0ea5e9",
"vectorize_id": null,
"created_at": "2026-03-24 09:00:00"
}
}
```
**Renaming onto an existing name fails with a 500:**
The uniqueness conflict that `POST` reports as a clean `409` is not caught here: renaming tag
`backend` to a name you already use returns `500 { "error": "Internal server error" }` and
changes nothing. Check the tag list first, and treat a `500` from this endpoint as a name
collision rather than an outage. To combine two tags, retag the ideas and delete the empty one.
Renaming a tag does not re-embed it, so `GET /tags/similar` keeps comparing against the old
name's embedding until the tag is re-embedded in the background.
## Delete Tag
`DELETE /api/v1/tags/:id`
Deletes the tag and detaches it from every idea. The ideas themselves are untouched — no bodies,
statuses, or other tags change.
```bash
curl -X DELETE https://neuralrepo.com/api/v1/tags/3 \
-H "X-API-Key: nrp_YOUR_KEY"
```
**Response `200 OK`**
```json
{
"success": true
}
```
Deleting a tag that is not yours or does not exist returns `404`. There is no undo — recreating
the tag does not restore its associations.
**Unused tags are deleted for you:**
Every write that sets an idea's tags — `POST /ideas`, `PATCH /ideas/:id` with `tags`, and the
bulk endpoint — finishes by deleting **all** of your tags that are attached to no idea. A tag
you created here and have not used yet will disappear the next time any idea's tags are
replaced, taking its colour with it. Create tags by attaching them, not ahead of time.
## Tag Schema
| Field | Type | Description |
|-------|------|-------------|
| `id` | number | Unique tag identifier |
| `user_id` | string | Owning account |
| `name` | string | Display name, unique per account |
| `color` | string \| null | Hex colour code |
| `vectorize_id` | string \| null | Embedding id; `null` until the tag has been embedded |
| `created_at` | string | UTC `YYYY-MM-DD HH:MM:SS` |
| `idea_count` | number | Unarchived ideas using this tag — **list endpoint only** |
## Status Codes
| Status | Meaning |
|--------|---------|
| `200 OK` | Successful read, update, or delete |
| `201 Created` | Tag created |
| `400 Bad Request` | Validation error (name length, colour format, non-numeric id, missing `tag` param) |
| `401 Unauthorized` | Missing or invalid auth |
| `404 Not Found` | Tag not found, or not yours |
| `409 Conflict` | `POST` only — a tag with that name already exists |
| `500 Internal Server Error` | `PATCH` only — the new name collides with an existing tag |
## Next Steps
- [Ideas](https://docs.neuralconfig.com/neuralrepo/api/ideas/#bulk-update-ideas): Attach tags at capture time, replace them wholesale, or add and remove them in bulk.
- [Tag concepts](https://docs.neuralconfig.com/neuralrepo/organize/tags/): Auto-tagging, tag hygiene, and when merging two tags is worth the effort.
- [Search](https://docs.neuralconfig.com/neuralrepo/api/search/): Filtering by tag versus searching by meaning — and when each one finds more.
- [Error Handling](https://docs.neuralconfig.com/neuralrepo/api/errors/): What each status code means across the whole API.
---
# User & Settings
Source: https://docs.neuralconfig.com/neuralrepo/api/user/
Everything under `/api/v1/user` acts on the authenticated account. Three of these endpoints are
Pro-gated; the rest, including every export, work on any plan.
## Get Current User
`GET /api/v1/user/me`
Returns the profile, the current idea count, and trial state. Encrypted provider keys are
stripped and replaced by `has_*_key` booleans.
**curl**
```bash
curl https://neuralrepo.com/api/v1/user/me \
-H "X-API-Key: nrp_YOUR_KEY"
```
**JavaScript**
```js
const res = await fetch("https://neuralrepo.com/api/v1/user/me", {
headers: { "X-API-Key": "nrp_YOUR_KEY" },
});
const user = await res.json();
```
**Response `200 OK`**
```json
{
"id": "3f2a91c47b0e4d5aa8c61e0f2b7d4c93",
"email": "you@example.com",
"display_name": "Jane Doe",
"avatar_url": "https://avatars.githubusercontent.com/u/1?v=4",
"plan": "pro",
"settings_json": "{\"preferred_ai_provider\":\"anthropic\"}",
"has_anthropic_key": true,
"has_openai_key": false,
"has_openrouter_key": false,
"trial_ends_at": "2026-04-01 00:00:00",
"trial_used": 1,
"active_idea_count": 137,
"is_trialing": true,
"trial_days_remaining": 6,
"created_at": "2026-01-15 08:00:00",
"updated_at": "2026-03-20 12:00:00"
}
```
| Field | Meaning |
|---|---|
| `plan` | `free` or `pro`. **Trialing accounts read `pro`** — the gates treat them as Pro |
| `active_idea_count` | Unarchived ideas; compare with 50 to see how close a Free account is to its cap |
| `is_trialing` | `true` only when the plan is `pro`, the trial has not ended, and there is no Stripe subscription |
| `trial_days_remaining` | Whole days, rounded up; `null` when not trialing |
| `email_capture_bare_optin` | `1` if you opted in to the shared `idea@` address for the transition, else `0`. Your personal capture address is **not** in this response; see [Email Capture Address](#email-capture-address) |
**Note:**
`plan` alone cannot tell you whether someone is paying. A trial and a subscription both read
`pro`; `is_trialing` is what distinguishes them.
## Update Profile
`PATCH /api/v1/user/me`
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `display_name` | string | No | 1–100 characters |
| `settings_json` | string | No | A JSON **string**, max 10,000 characters. See [User Settings](#user-settings) |
```bash
curl -X PATCH https://neuralrepo.com/api/v1/user/me \
-H "X-API-Key: nrp_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"display_name": "Jane D.", "settings_json": "{\"search_threshold\":0.3}"}'
```
**settings_json replaces the whole blob:**
It is stored as an opaque string, so a partial write drops every setting you left out. Read
`settings_json` from `GET /user/me`, parse it, change one key, and send the whole object back.
Unknown or out-of-range keys are ignored when the settings are read, not rejected here.
**Response `200 OK`** — the updated user object, with the same stripped fields as `GET /user/me`
but without `active_idea_count` and the trial fields.
## API Keys
### List Keys
`GET /api/v1/user/api-keys`
Returns every key on the account, newest first, with **no key material** — not even a masked
prefix.
**Response `200 OK`**
```json
{
"api_keys": [
{
"id": "9c4f1b7e2d3a48f0b6e5c8d1a2f7b904",
"label": "CI Pipeline",
"scopes": null,
"source": "manual",
"last_used_at": "2026-03-24 08:12:00",
"created_at": "2026-03-20 10:00:00"
}
]
}
```
`source` is `manual` for keys made here and `mcp` for tokens minted by the MCP OAuth flow;
`scopes` is `null` on every manual key, which means full access.
### Create Key
`POST /api/v1/user/api-keys`
**Pro plan required:**
Returns `403 { "error": "API access requires a Pro plan", "pro_required": true, "feature": "API access", "upgrade_url": "..." }`
on Free, and no key is created.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `label` | string | No | Max 100 characters; defaults to `default` |
**Response `201 Created`**
```json
{
"id": "9c4f1b7e2d3a48f0b6e5c8d1a2f7b904",
"key": "nrp_a1b2c3d4...",
"label": "CI Pipeline",
"created_at": "2026-03-24 09:00:00"
}
```
:::caution
The full `key` value is only returned at creation time — only its hash is stored. Store it
securely; a lost key can be revoked but never recovered.
:::
### Delete Key
`DELETE /api/v1/user/api-keys/:id`
Revokes the key immediately, using the `id` from the list endpoint (a 32-character hex string,
not the key itself). Works on every plan. Returns `404` if the id is not yours.
**Response `200 OK`**
```json
{
"success": true
}
```
## MCP Tokens
`GET /api/v1/user/mcp-tokens`
Lists the tokens issued by the MCP OAuth flow when you authorize a client such as Claude. It
**does not create** tokens — the client does that during authorization.
**Response `200 OK`**
```json
{
"tokens": [
{
"id": "b17d0e59a4c34f8e91d2c6b7f0a35e82",
"label": "MCP (claude-ai)",
"scopes": "ideas:read ideas:write",
"source": "mcp",
"last_used_at": "2026-03-24 08:00:00",
"created_at": "2026-03-20 10:00:00"
}
]
}
```
The client's identity appears inside `label`; there is no separate `client_id` field. Revoke a
token with `DELETE /api/v1/user/api-keys/:id` — MCP tokens live in the same store as API keys.
Revoking one also revokes that client's refresh tokens, so the client has to authorize again.
An MCP token stops working **30 days** after it was issued. The client uses its refresh token
to get a new one, and the refresh deletes the old token, so the list normally holds one token
per connected client. The daily retention sweep removes tokens older than 30 days.
## BYOK (Bring Your Own Key)
AI features that run on your own provider account — `POST /ideas/:id/develop` in this API — need
a stored provider key. NeuralRepo encrypts it at rest and never returns it.
### Save Provider Key
`PUT /api/v1/user/byok/:provider`
Pro only. `:provider` must be `anthropic`, `openai`, or `openrouter`.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `api_key` | string | Yes | Your provider key; must be at least 10 characters |
```bash
curl -X PUT https://neuralrepo.com/api/v1/user/byok/anthropic \
-H "X-API-Key: nrp_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"api_key": "sk-ant-..."}'
```
The key is stored without being tested — a typo is accepted here and fails later at
`develop` time. Run the test endpoint below to confirm it works.
**Response `200 OK`**
```json
{
"success": true,
"provider": "anthropic"
}
```
### Delete Provider Key
`DELETE /api/v1/user/byok/:provider`
Removes the stored key. Unlike saving and testing, this is **not** Pro-gated — an account that
lapses to Free can still delete keys it stored while on Pro.
**Response `200 OK`**
```json
{
"success": true,
"provider": "anthropic"
}
```
### Test Provider Key
`POST /api/v1/user/byok/:provider/test`
Pro only. Decrypts the stored key and makes a minimal live call to the provider.
```bash
curl -X POST https://neuralrepo.com/api/v1/user/byok/anthropic/test \
-H "X-API-Key: nrp_YOUR_KEY"
```
**Response `200 OK`**
```json
{
"ok": true
}
```
A failure is also `200`, with the reason in the body: `{ "ok": false, "error": "No anthropic key configured" }`
when nothing is stored, or the provider's own message when the call is rejected. **Check `ok`,
not the status code.**
### Check All Providers
`GET /api/v1/user/byok/status`
Returns which providers have keys and which one will actually be used.
**Response `200 OK`**
```json
{
"active_provider": "anthropic",
"has_anthropic_key": true,
"has_openai_key": false,
"has_openrouter_key": false
}
```
`active_provider` resolves as: your `preferred_ai_provider` setting **if a key for it is
stored**, otherwise the first available in the fixed order Anthropic → OpenAI → OpenRouter. It is
`null` when no keys are stored — which is when `develop` returns `needs_byok`. Setting a
preference for a provider you have no key for is silently ignored rather than erroring.
## Shortcuts
`GET /api/v1/user/shortcuts`
Returns the iCloud Shortcut URL used for Siri capture. The same URL for every account.
```bash
curl https://neuralrepo.com/api/v1/user/shortcuts \
-H "X-API-Key: nrp_YOUR_KEY"
```
**Response `200 OK`**
```json
{
"quick_capture_url": "https://www.icloud.com/shortcuts/..."
}
```
## Support Link
`GET /api/v1/user/support-link`
Mints a single-use, pre-authenticated URL into the NeuralRepo support site for the account's
email address, so you arrive already signed in.
**Response `200 OK`**
```json
{
"url": "https://support.neuralrepo.com/..."
}
```
Returns `502 { "error": "Failed to generate support link" }` if the support service is
unreachable. Treat the URL as a credential — anyone holding it is signed in as you.
## Test Digest
`POST /api/v1/user/test-digest`
Sends the weekly digest email to your own address immediately, and returns the rendered content
so you can preview it without waiting for Sunday.
```bash
curl -X POST https://neuralrepo.com/api/v1/user/test-digest \
-H "X-API-Key: nrp_YOUR_KEY"
```
**Response `200 OK`**
```json
{
"success": true,
"content": "..."
}
```
If sending fails, the endpoint returns `500` with the underlying error message.
## User Settings
`settings_json` is a JSON string on the profile. Values outside the stated ranges are discarded
when the settings are parsed, and the default applies instead — nothing reports the mistake.
| Field | Type | Range | Default | Description |
|-------|------|-------|---------|-------------|
| `preferred_ai_provider` | string | `anthropic`, `openai`, `openrouter` | — | Provider for `develop`, if a key for it is stored |
| `search_threshold` | number | 0.1–0.9 | `0.2` | Semantic results must score **above** this |
| `dedup_threshold` | number | 0.1–0.9 | `0.75` | Score above which a pair is flagged as a duplicate |
| `related_threshold` | number | 0.1–0.9 | `0.55` | Score above which a `related` edge is created automatically |
| `code_mode` | boolean | — | — | Exposes the MCP code-mode tool surface |
| `weekly_digest` | boolean | — | opted in | Only an explicit `false` stops the Sunday digest |
| `stale_nudges` | boolean | — | opted in | Only an explicit `false` stops stale-idea nudge emails |
| `stale_threshold_days` | integer | 7–180 | `30` | Days before a `captured` idea counts as stale |
**Note:**
The three similarity thresholds cannot be set to 0 or 1. A value like `0` or `0.95` is dropped
and the default silently applies, so a search that "ignores your threshold" usually means the
value was out of range.
## Data Export
Export is available on **every plan**, and all three endpoints are `POST` with no body. Each
covers up to 10,000 ideas and **excludes archived ideas** — export before you archive, not after.
### JSON Export
`POST /api/v1/user/export`
The complete dump: profile, ideas with their tags, links, and relations, plus the tag list with
counts. Returned as a JSON body (not a file download), stamped with `exported_at`.
```bash
curl -X POST https://neuralrepo.com/api/v1/user/export \
-H "X-API-Key: nrp_YOUR_KEY" \
-o export.json
```
### CSV Export
`POST /api/v1/user/export/csv`
A flat table for spreadsheets: `id`, `number`, `title`, `body`, `status`, `tags` (semicolon
separated), `source`, `created_at`. Sent as a UTF-8 BOM'd `text/csv` attachment named
`neuralrepo-ideas-YYYY-MM-DD.csv`. Relations and links are not included.
```bash
curl -X POST https://neuralrepo.com/api/v1/user/export/csv \
-H "X-API-Key: nrp_YOUR_KEY" \
-o export.csv
```
### Markdown Export
`POST /api/v1/user/export/markdown`
A ZIP of one Markdown file per idea, named `NNNN-slug.md` from the idea's display number and
title. Each file carries YAML frontmatter (`number`, `title`, `status`, `source`, `tags`,
`created`, `updated`), the body, a Links section, and a Related section where **relations become
`[[wiki-links]]`** — so an Obsidian vault reproduces your mind map graph.
```bash
curl -X POST https://neuralrepo.com/api/v1/user/export/markdown \
-H "X-API-Key: nrp_YOUR_KEY" \
-o neuralrepo-markdown.zip
```
## Email Capture Address
The personal `idea+@neuralrepo.com` address that
[email capture](https://docs.neuralconfig.com/neuralrepo/capture/email/) delivers to. These routes work on every plan, but
mail is captured only on Pro.
| Endpoint | Does |
|---|---|
| `GET /api/v1/user/email-capture` | Returns your address, creating it on the first call |
| `POST /api/v1/user/email-capture/rotate` | Replaces the address. The old one is rejected from then on |
| `PUT /api/v1/user/email-capture/bare-address` | Body `{ "opt_in": true }` or `{ "opt_in": false }`: opts in to, or out of, the shared `idea@` address until the cut-off |
All three return the same shape:
```json
{
"address": "idea+3f9c0a1b2c3d4e5f6a7b8c9d@neuralrepo.com",
"bare_address": "idea@neuralrepo.com",
"bare_address_opt_in": false,
"bare_address_open": true,
"bare_address_sunset": "2026-10-24T00:00:00Z"
}
```
| Field | Meaning |
|---|---|
| `address` | Your personal capture address. Treat it as a secret: anyone who has it can add ideas to your repo |
| `bare_address_opt_in` | Whether mail from your account email to `idea@` is still captured |
| `bare_address_open` | `false` once `bare_address_sunset` has passed; after that `idea@` rejects everything |
| `bare_address_sunset` | When the shared address stops capturing |
`PUT /bare-address` returns `400` `opt_in (boolean) is required` for a missing or non-boolean
`opt_in`, and `409` `The shared address has been retired; use your personal capture address`
for `opt_in: true` after the cut-off. Opting **out** always works.
## Delete Account
`DELETE /api/v1/user/me`
Deletes the account and everything it owns. [Delete Data](https://docs.neuralconfig.com/neuralrepo/organize/delete-data/)
lists what is removed, what is kept, and what is out of reach. **Web session only:** an API key
or MCP token gets `403`.
**Request body:**
```json
{ "confirm_email": "you@example.com" }
```
`confirm_email` must match the account's email, ignoring letter case and surrounding spaces.
**Response `200 OK`:** `{ "success": true, ... }`, with counts of what was deleted
(`rows`, `vectors_deleted`, `kv_keys_deleted`), whether a subscription was canceled
(`subscription_canceled`), and the `out_of_reach` list. The session cookie is cleared.
| Status | Body `error` |
|---|---|
| `400` | `confirm_email is required`, or `The email you typed does not match your account` |
| `403` | `Sign in on the web to delete your account`, or `The admin account cannot be deleted here` |
| `404` | `User not found` |
| `502` | `Account deletion did not finish. You are still signed in; try again to complete it.` Nothing past the failed step was deleted, and a retry continues |
## Status Codes
| Status | Meaning |
|--------|---------|
| `200 OK` | Successful read, update, delete, account deletion, or export |
| `201 Created` | API key created |
| `400 Bad Request` | Validation error, or an unknown BYOK provider |
| `401 Unauthorized` | Missing or invalid auth |
| `403 Forbidden` | Pro-only endpoint on a Free plan (`pro_required: true`), or account deletion without a web session or on the admin account |
| `404 Not Found` | API key id not yours, or the user record is missing |
| `409 Conflict` | Opting in to the shared `idea@` address after its cut-off |
| `500 Internal Server Error` | `test-digest` failed to send |
| `502 Bad Gateway` | The support service could not mint a link, or account deletion did not finish |
## Next Steps
- [Authentication](https://docs.neuralconfig.com/neuralrepo/api/authentication/): How keys, sessions, and MCP tokens are resolved, and what scopes actually apply.
- [BYOK setup](https://docs.neuralconfig.com/neuralrepo/integrations/byok/): Which models each provider uses, and what NeuralRepo sends them.
- [Ideas](https://docs.neuralconfig.com/neuralrepo/api/ideas/): The develop endpoint that consumes your BYOK key, and the Free-plan idea cap.
- [Weekly digest](https://docs.neuralconfig.com/neuralrepo/integrations/weekly-digest/): What the Sunday email contains and how stale_threshold_days changes it.
---
# Data Model
Source: https://docs.neuralconfig.com/neuralrepo/architecture/data-model/
NeuralRepo uses **Cloudflare D1** (SQLite) as its primary database. The schema is designed for single-user data isolation, efficient full-text search, and flexible idea relations.
**Note:**
**Every `created_at` and `updated_at` in D1 is `datetime('now')`** — the SQLite format
`YYYY-MM-DD HH:MM:SS`, in UTC, with no `T`, no `Z`, and no offset. It is not ISO-8601, and
parsing it as such fails in some runtimes. See
[Timestamps](https://docs.neuralconfig.com/neuralrepo/api/overview/#timestamps).
## Entity Relationship Diagram
```mermaid
erDiagram
users ||--o{ auth_accounts : "has"
users ||--o{ sessions : "has"
users ||--o{ api_keys : "has"
users ||--o{ ideas : "owns"
users ||--o{ billing_events : "has"
ideas ||--o{ idea_tags : "has"
ideas ||--o{ idea_links : "has"
ideas ||--o{ idea_relations : "source"
ideas ||--o{ idea_relations : "target"
ideas ||--o{ duplicate_detections : "source"
ideas ||--o{ duplicate_detections : "target"
ideas ||--o{ attachments : "has"
ideas ||--o| ideas : "parent"
tags ||--o{ idea_tags : "has"
users ||--o{ oauth_authorization_codes : "has"
users ||--o{ oauth_refresh_tokens : "has"
```
`magic_links` and `schema_migrations` stand outside this graph: magic links are keyed by email
address rather than by user (the account may not exist yet), and migrations track the schema itself.
## Core Tables
### users
The central user record. All data queries are scoped by `user_id`.
| Column | Type | Description |
|--------|------|-------------|
| `id` | TEXT | Primary key — 32 lowercase hex characters from `randomblob(16)`, not a dashed UUID |
| `email` | TEXT | Unique email address |
| `display_name` | TEXT | Display name |
| `avatar_url` | TEXT | Profile avatar URL |
| `plan` | TEXT | `free` or `pro` |
| `stripe_customer_id` | TEXT | Stripe customer reference |
| `stripe_subscription_id` | TEXT | Stripe subscription reference |
| `anthropic_api_key_encrypted` | TEXT | Encrypted Anthropic API key (BYOK) |
| `openai_api_key_encrypted` | TEXT | Encrypted OpenAI API key (BYOK) |
| `openrouter_api_key_encrypted` | TEXT | Encrypted OpenRouter API key (BYOK) |
| `settings_json` | TEXT | User settings (JSON string, default `{}`) |
| `trial_ends_at` | TEXT | When the Pro trial lapses; the daily cron downgrades past this |
| `trial_used` | INTEGER | `1` once a trial has been taken, so it cannot be taken twice |
| `latest_accepted_terms_version` | TEXT | Terms version last accepted; the evidence is in `terms_acceptances` |
| `latest_accepted_privacy_version` | TEXT | Privacy Policy version last accepted |
| `latest_accepted_addenda_version` | TEXT | Addenda version last accepted |
| `latest_accepted_at` | TEXT | When those versions were accepted |
| `email_capture_token` | TEXT | Unique secret in the `idea+@neuralrepo.com` capture address, created on first view |
| `email_capture_bare_optin` | INTEGER | `1` if opted in to capture from the bare `idea@` address, until the 2026-10-24 sunset |
| `created_at` | TEXT | `YYYY-MM-DD HH:MM:SS` UTC |
| `updated_at` | TEXT | `YYYY-MM-DD HH:MM:SS` UTC |
### auth_accounts
Links external sign-in providers to a user.
| Column | Type | Description |
|--------|------|-------------|
| `id` | TEXT | Primary key |
| `user_id` | TEXT | FK to users |
| `provider` | TEXT | `github`, `google`, `apple`, or `email` |
| `provider_account_id` | TEXT | External account ID; unique together with `provider` |
| `provider_email` | TEXT | Email from the provider |
| `created_at` | TEXT | Creation timestamp |
### sessions
Active user sessions. Tokens are SHA-256 hashed before storage.
| Column | Type | Description |
|--------|------|-------------|
| `id` | TEXT | Primary key |
| `user_id` | TEXT | FK to users |
| `token_hash` | TEXT | SHA-256 hash of the session token (unique) |
| `expires_at` | TEXT | Rolling 30-day expiry, pushed forward on every authenticated request |
| `created_at` | TEXT | Creation timestamp |
### api_keys
API keys for programmatic access. Keys are hashed with SHA-256.
| Column | Type | Description |
|--------|------|-------------|
| `id` | TEXT | Primary key |
| `user_id` | TEXT | FK to users |
| `key_hash` | TEXT | SHA-256 hash of the `nrp_` prefixed key (unique) |
| `label` | TEXT | User-assigned label, default `default` |
| `scopes` | TEXT | Space-separated scope list. `NULL` — full access — on every key you create; scopes only appear on MCP OAuth tokens. |
| `source` | TEXT | Key source, default `manual` |
| `last_used_at` | TEXT | Timestamp of most recent use |
| `created_at` | TEXT | Creation timestamp |
### magic_links
One-time email sign-in tokens, keyed by email rather than user.
| Column | Type | Description |
|--------|------|-------------|
| `id` | TEXT | Primary key |
| `email` | TEXT | Recipient address |
| `token_hash` | TEXT | SHA-256 hash of the emailed token (unique) |
| `expires_at` | TEXT | 15 minutes after issue |
| `used_at` | TEXT | Set on redemption — the row is marked, not deleted |
| `created_at` | TEXT | Creation timestamp |
### ideas
The core content table.
| Column | Type | Description |
|--------|------|-------------|
| `id` | INTEGER | Auto-increment primary key — **globally unique**, and the identifier every API, CLI, and MCP call takes |
| `user_id` | TEXT | FK to users |
| `number` | INTEGER | **Per-user display number**, unique with `user_id`. This is the `#42` the UI and CLI print. Nothing resolves a number back to an idea. |
| `title` | TEXT | Idea title (max 200 chars) |
| `body` | TEXT | Markdown body (max 50,000 chars) |
| `status` | TEXT | `captured`, `exploring`, `building`, `shipped`, `shelved` |
| `source` | TEXT | Origin: `web`, `cli`, `claude-mcp`, `siri`, `email`, `api`, `shortcut`, `ios` |
| `source_url` | TEXT | Optional URL reference |
| `source_summary` | TEXT | Summary from source |
| `parent_id` | INTEGER | Self-referencing FK; `ON DELETE SET NULL` |
| `vectorize_id` | TEXT | Cloudflare Vectorize embedding ID (`idea_`). Null until the queue has processed the idea. |
| `is_archived` | INTEGER | Soft delete flag (0 = active, 1 = archived) |
| `created_at` | TEXT | Creation timestamp |
| `updated_at` | TEXT | Last update timestamp |
**Danger:**
**`id` and `number` are different numbers and only coincide for the first account ever created.**
`number` is `MAX(number) + 1` within one user; `id` is the global autoincrement. Every listing
prints `number`; every lookup takes `id`. See
[Idea identifiers](https://docs.neuralconfig.com/neuralrepo/api/overview/#idea-identifiers).
### tags
Unique tag definitions scoped by user.
| Column | Type | Description |
|--------|------|-------------|
| `id` | INTEGER | Primary key |
| `user_id` | TEXT | FK to users; unique together with `name` |
| `name` | TEXT | Tag name (lowercase, unique per user) |
| `color` | TEXT | Optional hex color |
| `vectorize_id` | TEXT | Vector ID for the tag's own embedding (`tag--`) |
| `created_at` | TEXT | Creation timestamp |
### idea_tags
Many-to-many join between ideas and tags. The composite primary key is `(idea_id, tag_id)`, so the same tag cannot be attached twice.
| Column | Type | Description |
|--------|------|-------------|
| `idea_id` | INTEGER | FK to ideas, cascade delete |
| `tag_id` | INTEGER | FK to tags, cascade delete |
### idea_links
URLs attached to ideas.
| Column | Type | Description |
|--------|------|-------------|
| `id` | INTEGER | Primary key |
| `idea_id` | INTEGER | FK to ideas |
| `url` | TEXT | Link URL |
| `title` | TEXT | Optional display title |
| `link_type` | TEXT | `url`, `claude-chat`, `github-repo`, `github-issue`, `attachment` |
| `created_at` | TEXT | Creation timestamp |
### idea_relations
Typed directional links between ideas — the mind map's edges.
| Column | Type | Description |
|--------|------|-------------|
| `id` | INTEGER | Primary key |
| `user_id` | TEXT | FK to users |
| `source_idea_id` | INTEGER | FK to ideas |
| `target_idea_id` | INTEGER | FK to ideas |
| `relation_type` | TEXT | `related`, `parent`, `blocks`, `inspires`, `supersedes` — plus `duplicate`, which the constraint permits but nothing writes |
| `score` | REAL | Similarity score (null for manual relations) |
| `note` | TEXT | Optional description |
| `created_by` | TEXT | `user` or `system` |
| `created_at` | TEXT | Creation timestamp |
Uniqueness is on `(source_idea_id, target_idea_id, created_by)`. That third column is load-bearing:
it lets a manual link and an auto-detected one coexist between the same pair, and it is why a second
*manual* link between two ideas fails while the system can still add its own.
### duplicate_detections
Records when the system detects similar ideas.
| Column | Type | Description |
|--------|------|-------------|
| `id` | INTEGER | Primary key |
| `user_id` | TEXT | FK to users |
| `idea_id` | INTEGER | The newer/suspected duplicate |
| `duplicate_of_id` | INTEGER | The existing original idea |
| `similarity_score` | REAL | Cosine similarity (0.0 to 1.0) |
| `status` | TEXT | `pending`, `merged`, `dismissed` |
| `created_at` | TEXT | Creation timestamp |
A unique index on `(idea_id, duplicate_of_id)` means re-running detection cannot create the same detection twice.
### attachments
File metadata, with the object itself in R2 under `r2_key`.
| Column | Type | Description |
|--------|------|-------------|
| `id` | TEXT | Primary key |
| `idea_id` | INTEGER | FK to ideas |
| `user_id` | TEXT | FK to users |
| `filename` | TEXT | Original filename |
| `content_type` | TEXT | MIME type |
| `size_bytes` | INTEGER | File size |
| `r2_key` | TEXT | Object key in the R2 bucket |
| `created_at` | TEXT | Creation timestamp |
**Caution:**
**Nothing writes to this table.** There is no upload endpoint anywhere in the product, and the
matching `attachment` link type has no producer either. The table and the R2 binding exist ahead
of the feature — treat file attachments as unimplemented rather than undocumented.
### billing_events
An append-only log of Stripe webhook events: `stripe_event_id` (unique, which is what makes webhook delivery idempotent), `event_type`, `amount_cents`, `currency`, the Stripe customer, subscription, and invoice ids, and a `metadata_json` blob.
### OAuth Tables
`oauth_authorization_codes` and `oauth_refresh_tokens` support the MCP OAuth flow. Both store SHA-256 hashes rather than the credential. Authorization codes are short-lived (10 minutes) and single-use via `used_at`; refresh tokens expire after 90 days and carry a `revoked_at` column. Both default their `scopes` column to `ideas:read ideas:write`.
### schema_migrations
One row per applied migration file (`version`, `applied_at`), so migrations are not re-run.
## Full-Text Search
The `ideas_fts` table is an **FTS5 virtual table** that mirrors the `title` and `body` columns of the `ideas` table:
```sql
CREATE VIRTUAL TABLE ideas_fts USING fts5(
title, body, content='ideas', content_rowid='id'
);
```
Three triggers keep it in sync — `ideas_ai` after insert, `ideas_ad` after delete, and `ideas_au`
after update (a delete row followed by a fresh insert).
**Note:**
Archiving is an `UPDATE`, not a `DELETE`, so an archived idea **stays in the FTS index** — the
archived-out filter happens in the query, not in the index. Its *vector* is deleted, though, which
is why archived ideas vanish from semantic results immediately and from keyword results only
because the query says so.
## Indexes
Key indexes for query performance:
| Table | Index | Columns |
|-------|-------|---------|
| ideas | `idx_ideas_user` | `user_id` |
| ideas | `idx_ideas_user_status` | `user_id`, `status` |
| ideas | `idx_ideas_user_created` | `user_id`, `created_at DESC` |
| tags | `idx_tags_user` | `user_id` |
| idea_tags | `idx_idea_tags_tag` | `tag_id` |
| idea_relations | `idx_relations_source` | `source_idea_id` |
| idea_relations | `idx_relations_target` | `target_idea_id` |
| idea_links | `idx_idea_links_idea` | `idea_id` |
| duplicate_detections | `idx_dupes_user` | `user_id`, `status` |
| duplicate_detections | `idx_dupes_pair` (unique) | `idea_id`, `duplicate_of_id` |
| sessions | `idx_sessions_token` | `token_hash` |
| api_keys | `idx_api_keys_hash` | `key_hash` |
| auth_accounts | `idx_auth_accounts_user` | `user_id` |
| attachments | `idx_attachments_idea` | `idea_id` |
## Soft Delete Pattern
Ideas use a **soft delete** pattern. The `is_archived` column is `0` for active ideas and `1` for archived ideas, and every listing query filters `is_archived = 0`. This preserves relation integrity: an archived idea's rows in `idea_relations` and `idea_tags` survive rather than cascading away.
**Caution:**
**Archiving drops the vector.** `is_archived` is absent from the update schema; only
`POST /ideas/:id/restore` (and MCP `restore_idea`) clears it, and restoring queues the idea to be
embedded again. Archived ideas are excluded from every export.
**Hard delete is a separate, explicit step.** Only archived ideas can be removed from the
database, from a web session: `DELETE /ideas/:id/permanent` or `DELETE /ideas/archived`. Deleting
the account removes the user's rows from every table except four. The `users` row is tombstoned
rather than deleted, because `terms_acceptances` cascades on user delete. `terms_acceptances`
(the consent record) and `billing_events` (the payment record) are kept. `ai_usage` rows are
detached (`user_id` set to `NULL`) and age out with the 90-day sweep. See
[Delete Data](https://docs.neuralconfig.com/neuralrepo/organize/delete-data/).
## Next Steps
- [Tech Stack](https://docs.neuralconfig.com/neuralrepo/architecture/tech-stack/): The Cloudflare services this schema sits on.
- [Semantic Search](https://docs.neuralconfig.com/neuralrepo/architecture/semantic-search/): How rows here become vectors, and what the metadata carries.
- [API Overview](https://docs.neuralconfig.com/neuralrepo/api/overview/): Idea identifiers, timestamps, and the field limits enforced above.
- [Security & Auth](https://docs.neuralconfig.com/neuralrepo/architecture/security/): What the sessions, keys, and OAuth tables protect.
---
# Queue Processing
Source: https://docs.neuralconfig.com/neuralrepo/architecture/queue-processing/
NeuralRepo processes ideas asynchronously using **Cloudflare Queues**. When you create or update an idea, the API returns immediately while background processing handles embedding, duplicate detection, auto-tagging, and vector indexing.
## Architecture Overview
The API request returns immediately (`processing: true`) while, on the same
request, a queue producer publishes a message to the Cloudflare Queue. A queue
consumer — a Workers handler — receives messages in batches of up to 10 and routes
each by its message type to one of five pipelines.
```mermaid
flowchart TD
api["API request (create / update idea)"] --> resp["Response returned immediately (processing: true)"]
api --> producer["Queue producer publishes to Cloudflare Queue"]
producer --> consumer["Queue consumer (Workers handler) batch of up to 10 · routes by message type"]
consumer --> created["idea_created"]
consumer --> updated["idea_updated"]
consumer --> meta["idea_metadata_updated"]
consumer --> backfill["backfill_vectors"]
consumer --> tagfill["backfill_tag_vectors"]
```
**Note:**
`processing: true` is a constant on the create response, not a stored column. Nothing ever writes
`processing: false` — there is no such field on the idea. The signal that the pipeline finished is
the idea's `vectorize_id` becoming non-null.
### Consumer configuration
| Setting | Value |
|---|---|
| Queue | `neuralrepo-processing` |
| Max batch size | 10 messages |
| Max batch timeout | 30 seconds |
| Max retries | 3 |
| Dead-letter queue | `neuralrepo-processing-dlq` |
## Message Types
Every message has the same envelope: a `type` and a `payload` object.
### idea_created
Triggered when a new idea is saved from any surface. This is the most comprehensive pipeline.
**Payload:**
```json
{
"type": "idea_created",
"payload": {
"idea_id": 138,
"user_id": "9f2c7a1b4e8d40c3ab55e1f0d7c92e64",
"title": "Browser extension for idea capture",
"body": "Highlight text on any page and save it..."
}
}
```
**Processing steps, in order:**
1. **Fetch the idea** from D1 and load the user's threshold settings.
2. **Create the parent relation**, if the idea was saved with a `parent_id` (a branch or a sub-idea). This is why a branch's relation appears seconds after the branch itself.
3. **Normalize text** — join title and body with a space, trim, collapse whitespace, truncate to 8,192 characters.
4. **Generate the embedding** via Workers AI (`@cf/baai/bge-m3`), with a 30-second timeout.
5. **Duplicate detection** — query Vectorize for the 5 nearest neighbours, excluding `shelved` ideas. Anything above the dedup threshold becomes a `duplicate_detections` row; anything above the related threshold becomes a system-created `related` relation.
6. **Auto-tag**, but only if the idea arrived with **no** tags at all.
7. **Upsert the vector** into Vectorize as `idea_`, with metadata for `user_id`, `idea_id`, `status`, `tags`, `source`, and `created_at`.
8. **Store `vectorize_id`** on the idea row.
9. **Ensure tag embeddings** exist for the idea's tags, creating any that are missing.
**Caution:**
Steps 6 and 7 are in that order deliberately: auto-tags have to be applied **before** the vector
is upserted, or the vector's `tags` metadata would be written empty and tag-filtered searches
would miss the idea until something else touched it.
**Auto-tagging is not a BYOK feature and it does not ask permission.** Both plans auto-tag, by
different means, and the tags are applied directly to the idea rather than stored as suggestions:
| Plan | How tags are chosen |
|---|---|
| Pro | An LLM classifier sees the title, body, your 20 most-used tags, and tags ranked by frequency across the idea's nearest neighbours. |
| Free | No AI call. Tags shared by **at least two** of the five nearest neighbours are borrowed, up to three — and only when the idea has at least two related neighbours to borrow from. |
### idea_updated
Triggered when an idea's title or body changes.
**Payload:**
```json
{
"type": "idea_updated",
"payload": {
"idea_id": 138,
"user_id": "9f2c7a1b4e8d40c3ab55e1f0d7c92e64",
"title": "Browser extension for idea capture",
"body": "Updated body text..."
}
}
```
**Processing steps:**
1. **Reconcile the parent relation** — delete existing parent relations for the idea, then re-create one if `parent_id` is still set.
2. **Re-generate the embedding** from the new text.
3. **Delete system-created relations** for the idea. Manual relations are left alone.
4. **Delete pending duplicate detections** for the idea.
5. **Re-run duplicate detection**, producing fresh detections and `related` relations.
6. **Auto-tag**, if the idea now has no tags.
7. **Re-upsert** the vector under the same ID and **refresh the tag embeddings**.
**Caution:**
Steps 3 and 4 mean an edit can make auto-detected relations and pending duplicate flags
**disappear** — they are re-derived from the new text, not merged with the old set. Manual links
survive any number of edits.
### idea_metadata_updated
Triggered when only status or tags change — no content change.
**Payload:**
```json
{
"type": "idea_metadata_updated",
"payload": {
"idea_id": 138,
"user_id": "9f2c7a1b4e8d40c3ab55e1f0d7c92e64"
}
}
```
**Processing steps:**
1. **Fetch the existing vector** by ID and reuse its values. If no vector exists yet, the message is dropped — an idea whose embedding has not landed cannot have its metadata refreshed.
2. **Re-upsert** with the same embedding and fresh metadata, so filtered searches (for example, "search within `building` status") reflect the change.
### backfill_vectors
Full re-indexing of one user's ideas.
**Payload:**
```json
{
"type": "backfill_vectors",
"payload": { "user_id": "9f2c7a1b4e8d40c3ab55e1f0d7c92e64" }
}
```
The handler does not embed anything itself. It selects every unarchived idea for the user and
**re-enqueues each one as an `idea_created` message**, so each goes through the full pipeline —
including duplicate detection and auto-tagging.
**Caution:**
Backfill is heavy and it fans out: one message becomes one message per idea. Because each replays
`idea_created`, it also re-runs duplicate detection across the account. It is not triggered by
normal usage.
### backfill_tag_vectors
Re-embeds a user's **tag** vectors — the per-tag embeddings used for tag-similarity features —
in batches of 20, each batch queueing the next until every tag is done.
```json
{
"type": "backfill_tag_vectors",
"payload": { "user_id": "9f2c7a1b4e8d40c3ab55e1f0d7c92e64", "offset": 20 }
}
```
Unlike the per-idea path, this one **forces** re-embedding of tags that already have a vector.
## Error Handling
The consumer wraps each message in a try/catch and treats every failure the same way:
| Outcome | Behavior |
|----------|----------|
| Handler succeeds | `message.ack()` — the message is done |
| Handler throws (Workers AI, Vectorize, D1, anything) | `message.retry()` — Cloudflare redelivers it |
| Retried more than 3 times | Cloudflare moves it to `neuralrepo-processing-dlq` |
| Unknown message type | Logged as an error and **acked** — the message is discarded, not retried |
| Idea no longer exists | The handler returns early and acks — archiving an idea mid-flight is not an error |
There is no per-error-class strategy and no application-level backoff; retry timing is Cloudflare's.
Failures are logged with the error message, and queue-send failures on the producer side are logged
and swallowed rather than failing the API request — which is exactly what the hourly backfill cron
below exists to repair.
## Cron Triggers
Four **Cloudflare Workers Cron Triggers** are live in production. The daily `0 4 * * *` trigger runs two jobs:
| Schedule | Job | Scope |
|---|---|---|
| `0 18 * * SUN` | Weekly digest | Pro users who have not opted out |
| `0 9 * * 1` | Stale-idea nudges | All plans, once per user per ~25 days |
| `0 4 * * *` | Trial expiry | Users whose trial has ended with no Stripe subscription |
| `0 4 * * *` | Data-retention sweep | Deletes expired sessions, spent OAuth codes, expired magic links, MCP access keys older than 30 days, dead refresh tokens, and `ai_usage` rows older than 90 days — see [Delete Data](https://docs.neuralconfig.com/neuralrepo/organize/delete-data/#what-is-cleaned-up-automatically) |
| `0 * * * *` | Backfill missed ideas | Up to 50 ideas per run |
**Caution:**
`docs/CRONS.md` in the source repo lists the stale-idea check as disabled with a "placeholder
handler". That is out of date: the trigger is in `wrangler.toml`, the handler is fully
implemented, and the emails send.
### Weekly digest
1. Queries Pro users who haven't opted out of the digest in **Settings ▸ Notifications**.
2. For each user, gathers the week's new ideas, pending duplicates, stale ideas, and totals.
3. If the user has a BYOK key, calls the AI provider for a narrative summary; otherwise formats the stats directly. If the AI call fails, it falls back to the stats summary rather than skipping the email.
4. Sends the digest — skipped entirely when there is nothing to report.
### Stale-idea nudges
Runs independently of the digest and covers **all plans**. It checks every user who has not opted
out, skips anyone with an active KV cooldown key, and emails a list of ideas sitting in `captured`
past the user's stale threshold (default 30 days, configurable 7–180). The cooldown is 25 days, so
the weekly cron delivers at most one nudge a month per user. The same stale definition feeds the
digest's stale section.
### Backfill missed ideas
The self-healing half of the queue. Every hour it selects up to 50 unarchived ideas that have
`vectorize_id IS NULL` and were created more than five minutes ago — the signature of a queue send
that failed — and re-enqueues each as `idea_created`. The five-minute floor keeps it from racing
ideas that are simply still in flight.
## Monitoring
Queue health can be monitored through the Cloudflare dashboard:
- **Messages in queue** — should be near zero during normal operation.
- **Messages processed per minute** — spikes after bulk imports and backfills.
- **Dead-letter count** — should be zero. Anything in `neuralrepo-processing-dlq` survived three retries and indicates a processing bug.
- **Consumer latency** — time from message publish to processing completion.
## Next Steps
- [Semantic Search](https://docs.neuralconfig.com/neuralrepo/architecture/semantic-search/): The embedding model, vector storage, and how search actually resolves.
- [Duplicate Detection](https://docs.neuralconfig.com/neuralrepo/organize/duplicates/): The user-facing side of the dedup step, including thresholds.
- [Weekly Digest](https://docs.neuralconfig.com/neuralrepo/integrations/weekly-digest/): What the two email crons send, and how to opt out.
- [Tags](https://docs.neuralconfig.com/neuralrepo/organize/tags/): Auto-tagging from the user's side, on both plans.
---
# Security & Auth
Source: https://docs.neuralconfig.com/neuralrepo/architecture/security/
NeuralRepo is designed with security at every layer. This page covers authentication, authorization, encryption, and data isolation.
## OAuth Authentication
NeuralRepo supports **four** sign-in methods. The three federated ones are implemented with the **Arctic** OAuth library:
| Provider | Protocol | Implementation |
|----------|----------|----------------|
| GitHub | OAuth 2.0 | Arctic |
| Google | OpenID Connect | Arctic |
| Apple | Sign in with Apple | Arctic |
| Magic Link | Email-based | Custom |
The `auth_accounts` table records which providers are linked to an account, with `provider` one of
`github`, `google`, `apple`, or `email`.
### OAuth Flow
1. User clicks "Sign in with GitHub" (or Google, or Apple).
2. The server generates a state parameter and redirects to the provider.
3. The provider authenticates the user and redirects back with an authorization code.
4. The server exchanges the code for an access token and retrieves the user profile.
5. If the email matches an existing user, the account is linked. Otherwise, a new user is created.
6. A session is created and returned as an HttpOnly cookie.
7. If the account has not accepted the current Terms of Service and Privacy Policy (a new
account, or one signed in after either was updated), the web app shows an acceptance page
before anything else. Until then, every `/api/*` call from that session returns `403`
`terms_acceptance_required`, except reading your profile and deleting your account.
The acceptance gate applies to **web sessions**. API keys and MCP tokens are not re-prompted when
the documents change. Instead, the flows that create them are gated when the key is created. The two browser flows,
`nrepo login` and connecting an MCP client, pass through the same acceptance page first. Native
Sign in with Apple on iOS (`POST /auth/apple/native`) returns `403` `terms_acceptance_required`
with the documents and no key, until the app sends the request again with `terms_accepted: true`.
### Magic Links
For users who prefer email-based auth:
1. User enters their email address.
2. The server generates a one-time token, stores its SHA-256 hash in the `magic_links` table, and sends the token via email.
3. The user clicks the link, which contains the token.
4. The server hashes the token, looks up a record that is unexpired (15 minutes) and unused, and creates a session.
5. The record is **marked used** with a `used_at` timestamp, not deleted — a replayed link finds the row and is rejected.
Sending links is rate-limited per UTC clock hour (the count resets on the hour, not an hour after the first link), so the sign-in form can't be used to flood an inbox:
| Limit | Per hour | Counted by |
|---|---|---|
| Per inbox | 5 links | The address, lowercased, with any `+tag` removed. For Gmail addresses, dots are ignored too, so `j.doe+x@gmail.com` and `jdoe@gmail.com` share one count. The link still goes to the address exactly as typed. |
| Per client IP | 20 links | The connecting IP address. IPv6 addresses are counted per `/64` network |
Over either limit, the request returns `429` with `Retry-After: 3600` and
`Too many sign-in links requested. Try again in an hour.` The answer is the same whether or not
the address has an account. The counters live in KV and are not atomic, so a burst of
simultaneous requests can slip a few past the limit.
## Session Management
Sessions are the primary authentication mechanism for the web app.
| Property | Value |
|----------|-------|
| Token format | 32 random bytes, hex-encoded (64 characters) |
| Cookie name | `nrepo_session` |
| Storage | SHA-256 hash stored in D1 `sessions` table |
| Expiry | 30 days |
| Cookie flags | `HttpOnly`, `Secure`, `SameSite=Lax`, `Path=/`, `Max-Age=2592000` |
| Auto-refresh | Rolling — **every** authenticated request pushes expiry back to 30 days from now |
The session is a **rolling** 30 days rather than a fixed one: any authenticated request extends it,
so an account in daily use never expires, and one that goes quiet for a month does. Requests
carrying the session as a `Bearer` token are refreshed in the database; requests carrying the cookie
also get a fresh `Set-Cookie`.
The raw session token is only ever sent to the client as a cookie. The server stores and compares only the SHA-256 hash. This means a database breach does not expose usable session tokens.
**Note:**
Session tokens are generated using the Web Crypto API (`crypto.getRandomValues`), ensuring cryptographic randomness.
## API Keys
API keys provide programmatic access for the CLI, Siri Shortcuts, CI/CD, and custom integrations.
| Property | Value |
|----------|-------|
| Format | `nrp_` prefix + 32 random bytes hex-encoded — 68 characters in total |
| Storage | SHA-256 hash stored in D1 `api_keys` table |
| Display | Never shown again; the settings list identifies keys by label and the last 4 characters of the key **record's id** |
| Scopes | `NULL` on keys you create — full access. Scopes only exist on MCP OAuth tokens. |
| Revocation | Immediate — the key record is deleted, and nothing is cached |
When a request includes an `X-API-Key` header:
1. The header is ignored unless the value starts with `nrp_`.
2. The server hashes the provided key with SHA-256.
3. It looks up the hash in the `api_keys` table.
4. If found, the request is authenticated as the key's owner, with the key's scopes.
5. The `last_used_at` timestamp is updated.
Credentials are checked in a fixed order: `Authorization: Bearer` (tried as a session token first,
then as an API key or MCP token), then `X-API-Key`, then the session cookie. MCP OAuth tokens are
not `nrp_`-prefixed, so they must be sent as `Bearer` — `X-API-Key` will not see them.
The full key is shown **only once** at creation time. It cannot be retrieved later.
**Note:**
Creating an API key requires **Pro**, and the **Settings ▸ Connections** tab that manages keys and
connected MCP clients is Pro-only. The exception is `nrepo login`, whose browser flow mints a key
with no plan check — which is why the CLI works on Free.
## BYOK Encryption
User-provided AI keys (Anthropic, OpenAI, OpenRouter) are encrypted before storage:
| Property | Value |
|----------|-------|
| Algorithm | AES-256-GCM (Web Crypto) |
| Key derivation | HKDF-SHA256 over the `ENCRYPTION_KEY` Workers secret, with a fixed salt and info string |
| IV | Fresh random 12-byte IV per encryption |
| Storage | A single base64 string of IV ‖ ciphertext ‖ auth tag in D1 |
The secret is never used as an AES key directly — it is stretched through HKDF first, so the stored
ciphertext does not depend on the secret's length or entropy distribution. The encryption key is a
Workers secret that never appears in code or logs. Decryption happens in-memory only when making an AI request, and the plaintext key is never written to disk or returned via API.
**Caution:**
If the `ENCRYPTION_KEY` is rotated, all stored BYOK keys become unreadable. A migration script must re-encrypt all keys with the new key.
## MCP OAuth with PKCE
The MCP integration uses a full **OAuth 2.0 with PKCE** flow to authorize Claude's access:
| Property | Value |
|----------|-------|
| Grant type | Authorization Code with PKCE, plus `refresh_token` |
| Challenge method | **S256 only, and required.** A missing `code_challenge`, a missing `code_challenge_method`, `plain`, or a challenge that isn't 43 base64url characters (a SHA-256 hash) is refused with `invalid_request` |
| Client registration | Dynamic Client Registration (RFC 7591) at `/mcp/register`, kept 30 days |
| Authorization code expiry | 10 minutes, single use |
| Access token expiry | 30 days |
| Refresh token expiry | 90 days, rotated on every refresh |
| Scopes | `ideas:read`, `ideas:write` |
The PKCE flow prevents authorization code interception attacks. The MCP client generates a random `code_verifier`, hashes it to create a `code_challenge`, and sends the challenge with the authorization request. When exchanging the code for tokens, the server verifies the original verifier matches the stored challenge.
Every **new** authorization must use PKCE with `code_challenge_method=S256` stated explicitly.
`GET /mcp/authorize` answers anything else with `400` `invalid_request`, and the consent form
redirects back with `error=invalid_request` and no code. Refresh grants carry no PKCE, so a
client that is already connected keeps refreshing. The server metadata at
`/.well-known/oauth-authorization-server` advertises `code_challenge_methods_supported: ["S256"]`.
Authorization codes and refresh tokens are both stored as SHA-256 hashes, in
`oauth_authorization_codes` and `oauth_refresh_tokens`. Scopes are enforced at the tool boundary:
every MCP tool that writes requires `ideas:write`, and a token carrying neither scope is refused.
**Caution:**
Connected MCP clients are revoked in **Settings ▸ Connections**, which is a **Pro-only** tab. A
Free account that has authorized an MCP client has no in-app way to revoke it.
## CORS Middleware
The API uses strict CORS configuration:
| Header | Value |
|--------|-------|
| `Access-Control-Allow-Origin` | The request origin if it is `https://neuralrepo.com` or any `*.neuralrepo.com` subdomain; otherwise `APP_URL` |
| `Access-Control-Allow-Methods` | `GET, POST, PATCH, DELETE, OPTIONS` |
| `Access-Control-Allow-Headers` | `Content-Type, Authorization, X-API-Key` |
| `Access-Control-Allow-Credentials` | `true` |
| `Access-Control-Max-Age` | `86400` (24 hours) |
Reflection is limited to NeuralRepo's own domains — an unrecognized origin is answered with the app
URL rather than itself, so the browser blocks it. Non-production environments additionally allow
`*.workers.dev` (staging) and `http://localhost:*`.
**Note:**
`PUT` is **not** in the allowed-methods list, even though `PUT /user/byok/:provider` exists. The
dashboard calls it same-origin, so no preflight is involved; a cross-origin browser client would
be blocked. Server-side callers are unaffected — CORS is a browser mechanism, and API
key-authenticated requests (CLI, Siri, CI) are not subject to it.
## Rate Limiting
Rate limits are enforced using **Cloudflare KV** as a daily counter store. Limits apply only to requests authenticated via **API key** (`X-API-Key` header) or **Bearer token**. Web UI sessions are exempt.
| Plan | Daily Limit |
|------|-------------|
| Free | 100 requests/day |
| Pro | 10,000 requests/day |
When a limit is exceeded, the API returns `429 Too Many Requests` — with **no `Retry-After` header**.
The counter key format is `rate:{user_id}:{YYYY-MM-DD}` with a 24-hour TTL. The date is UTC, so
counters reset at UTC midnight. The limit is per user, not per key: every API key, MCP client, and
CLI install on one account shares the same budget.
## Data Isolation
Every user-facing database query in NeuralRepo is scoped by `user_id`:
```sql
SELECT * FROM ideas WHERE user_id = ? AND is_archived = 0
```
Each user's data is isolated — you cannot access, search, or modify another user's ideas regardless of the authentication method. Ownership is checked on the read, not just at the route: fetching an idea by id requires the id **and** the user id to match, so guessing an id gets you a 404.
**Note:**
Vectorize queries are also filtered by `user_id` metadata, ensuring semantic search results only return the authenticated user's ideas.
**Note:**
One surface is deliberately account-wide: `/api/v1/admin`, gated to a **single** account id
configured as `ADMIN_USER_ID`. Any other authenticated user gets `403`. It exposes aggregate
counts — totals by status, source, and plan, billing sums, active session counts — and no idea
titles or bodies.
## Security Headers
Every response carries a **Content-Security-Policy**, set by middleware that rebuilds the response
so the header lands even on immutable responses passed through from the static-assets binding. The
Worker is deliberately configured to run *before* asset serving for exactly this reason — otherwise
the SPA's own HTML would be returned without a CSP.
| Directive | Value |
|---|---|
| `default-src` | `'self'` |
| `script-src` | `'self'` — no inline scripts, no CDNs |
| `style-src` | `'self' 'unsafe-inline'` plus Google Fonts |
| `font-src` | `'self'` plus Google Fonts |
| `img-src` | `'self'`, `data:`, `blob:`, and the Google/GitHub avatar hosts |
| `connect-src` | `'self'` plus Stripe |
| `frame-src` | `'self'` plus Stripe (checkout) |
| `object-src` | `'none'` |
| `base-uri` | `'self'` |
## Next Steps
- [API Authentication](https://docs.neuralconfig.com/neuralrepo/api/authentication/): Using each credential type against the REST API, with examples.
- [MCP Server](https://docs.neuralconfig.com/neuralrepo/integrations/mcp/): The OAuth flow from the client side, and how to revoke access.
- [Data Model](https://docs.neuralconfig.com/neuralrepo/architecture/data-model/): The tables behind sessions, keys, and OAuth grants.
- [Rate Limits](https://docs.neuralconfig.com/neuralrepo/api/rate-limits/): The daily budget, what counts against it, and what a 429 looks like.
---
# Semantic Search
Source: https://docs.neuralconfig.com/neuralrepo/architecture/semantic-search/
NeuralRepo goes beyond keyword matching. Every idea is converted into a vector embedding that captures its **meaning**, enabling search by concept rather than exact words.
## Embedding Model
NeuralRepo uses the **`@cf/baai/bge-m3`** model running on Cloudflare Workers AI:
| Property | Value |
|----------|-------|
| Model | `@cf/baai/bge-m3` |
| Dimensions | 1024 |
| Multilingual | Yes (100+ languages) |
| Input truncated at | 8,192 characters (~2,048 tokens) before the call |
| Timeout | 30 seconds, after which the queue message is retried |
| Runtime | Cloudflare Workers AI |
The BGE-M3 model was chosen for its strong multilingual performance, high dimension count for nuanced similarity, and native availability on Cloudflare's infrastructure.
## Vector Storage
Embeddings are stored in **Cloudflare Vectorize** (index `neuralrepo-ideas`):
| Setting | Value |
|---------|-------|
| Metric | Cosine similarity |
| Dimensions | 1024 |
| Vector ID | `idea_` — the database id, not the display number |
| Metadata | `user_id`, `idea_id`, `status`, `tags` (comma-joined), `source`, `created_at` |
Metadata is stored alongside each vector to enable filtered queries — every query filters on
`user_id`, and search adds `status` and tag filters on top.
Tags get vectors too, under `tag--`, embedded from the tag name plus the titles of
five of its ideas.
## Embedding Pipeline
When a new idea is created, the embedding process runs asynchronously:
```mermaid
flowchart TD
created["Idea created"] --> msg["Queue message (idea_created)"]
msg --> norm["Text normalization join title + body · trim · collapse whitespace · truncate to 8,192 chars"]
norm --> embed["Workers AI embedding @cf/baai/bge-m3 → 1024-dim vector"]
embed --> dup["Duplicate detection query topK=5 · exclude shelved · write detections + related links"]
dup --> tag["Auto-tag only if the idea has no tags"]
tag --> upsert["Vectorize upsert id idea_138 · 1024 floats · metadata incl. tags"]
upsert --> done["Store vectorize_id on the idea"]
```
The order matters: duplicate detection runs against the **query embedding before the new vector is
stored**, so an idea is never its own nearest neighbour, and auto-tagging happens **before** the
upsert so the vector's `tags` metadata is complete.
**Note:**
Embedding is asynchronous. The API returns `"processing": true` immediately. Semantic search
results for the new idea are available within a few seconds. Until then the idea's
`vectorize_id` is null — that field, not `processing`, is how you tell the pipeline finished.
## Search Pipeline
Semantic search is **not** a hybrid merge of keyword and vector results. It is a vector search with
a reranker, and keyword search only appears as a fallback:
```mermaid
flowchart TD
q["Search query"] --> embed["Embed the query @cf/baai/bge-m3"]
embed --> vec["Vectorize query topK = limit x 3, clamped to 20-50 · filter user_id"]
vec --> filter["Drop archived · apply status/tag filters drop anything at or below min_score"]
filter --> any{"Any survivors?"}
any -->|Yes| rerank["Rerank @cf/baai/bge-reranker-base · keep top limit"]
any -->|No| fts["FTS5 keyword search fetch limit x 2 · filter · slice to limit"]
rerank --> out["search_type: semantic"]
fts --> out2["search_type: fts_fallback"]
```
In words: the query is embedded, Vectorize returns between 20 and 50 candidates filtered to your
own ideas, archived ideas and anything failing your status or tag filter are dropped, and everything
scoring at or below `min_score` (default 0.2, or your `search_threshold` setting) is discarded. What
survives is reranked by `@cf/baai/bge-reranker-base` and truncated to your limit. If nothing
survives — or the vector search errored — keyword search over the FTS5 index answers instead.
Two details worth knowing:
- **The reranker is skipped when there is nothing to rerank.** With fewer candidates than the requested limit, results are returned in score order without a model call. If the rerank call fails, results also degrade to score order rather than falling back to keyword search.
- **The keyword fallback is per-surface.** Only the MCP tool and the AI agent fall back automatically. The REST API, the CLI, and the web app return an empty result set instead. See [Search](https://docs.neuralconfig.com/neuralrepo/organize/search/) for the full table.
## Duplicate Detection
When a new idea is embedded, NeuralRepo checks its five nearest neighbours — excluding ideas in
`shelved` status — and acts on each:
| Score | Action |
|-----------|--------|
| **> 0.75** (dedup threshold) | A `duplicate_detections` row with `pending` status — **and**, because the score also clears the related threshold, a system-created `related` relation |
| **> 0.55** (related threshold) | A system-created `related` relation |
| **≤ 0.55** | No action |
**Note:**
The bands are not exclusive. A pair above 0.75 gets **both** a duplicate detection and a `related`
relation — the two checks are independent comparisons against the same match. Both comparisons are
strictly greater-than, so a score of exactly 0.75 is not a duplicate.
Both thresholds are per-user settings (`dedup_threshold` and `related_threshold`), each clamped to
0.1–0.9. Detections are written with `INSERT OR IGNORE` against a unique index on the idea pair, so
re-running detection cannot duplicate a detection.
**Tip:**
Reviewing detections requires Pro — on Free, `GET /ideas/duplicates` always returns an empty list
even though detection itself still runs and still creates the rows.
## Text Normalization
Before embedding, text is normalized:
1. **Combine fields.** Title and body are joined with a **space**: `{title} {body}`.
2. **Trim.** Leading and trailing whitespace is removed.
3. **Collapse whitespace.** Runs of spaces, tabs, and newlines collapse to a single space — so Markdown structure contributes nothing to the embedding.
4. **Truncate.** The result is cut to **8,192 characters**. A 50,000-character body is embedded from its first ~8,000 characters only.
Ideas with only a title are still embedded. An idea that normalizes to an empty string throws, which
sends the queue message to a retry.
## Re-embedding
When an idea's title or body is updated, a new embedding is generated:
1. An `idea_updated` queue message is dispatched.
2. The consumer re-normalizes and re-embeds the updated text.
3. System-created relations and pending duplicate detections for the idea are **deleted**, then re-derived from the new embedding.
4. The existing Vectorize record is upserted (replaced) with the new vector.
Metadata-only changes (status, tags) dispatch an `idea_metadata_updated` message that rewrites the
Vectorize metadata using the stored vector, with no model call.
Archiving an idea **deletes its vector** (`VECTORIZE.deleteByIds(["idea_"])`), which is why an
archived idea stops appearing as anyone's neighbour immediately. Restoring an idea
(`POST /ideas/:id/restore`) clears `is_archived` and enqueues an `idea_updated` message, so the
vector is rebuilt rather than recovered.
## Backfill
Two queue messages rebuild vectors:
- **`backfill_vectors`** re-enqueues every unarchived idea for a user as `idea_created`, replaying the whole pipeline.
- **`backfill_tag_vectors`** force-re-embeds the user's tag vectors, 20 at a time.
An hourly cron also repairs individual gaps on its own, re-queueing up to 50 ideas that have no
`vectorize_id` more than five minutes after creation. In practice that catches the failure these
backfills exist for, without anyone asking.
**Note:**
Both backfills are triggered by the maintainer, not by an API you can call. The admin surface is
gated to a single account id and exposes aggregate statistics only.
## Next Steps
- [Queue Processing](https://docs.neuralconfig.com/neuralrepo/architecture/queue-processing/): The full pipeline this page's embedding step lives in.
- [Search](https://docs.neuralconfig.com/neuralrepo/organize/search/): Searching from each surface, including where the fallback is.
- [Duplicate Detection](https://docs.neuralconfig.com/neuralrepo/organize/duplicates/): Working through detections, merging, and tuning the thresholds.
- [Search API](https://docs.neuralconfig.com/neuralrepo/api/search/): GET /ideas/search — parameters, scoring, and response shape.
---
# Tech Stack
Source: https://docs.neuralconfig.com/neuralrepo/architecture/tech-stack/
NeuralRepo is an **edge-first** application built entirely on the Cloudflare developer platform. Every request is handled at the edge, keeping latency low regardless of where you are in the world.
## Cloudflare Services
**Workers + Hono**
The API server runs on Cloudflare Workers using the **Hono** framework. Hono provides Express-like routing with middleware support, optimized for the Workers runtime. Every API endpoint, MCP handler, and OAuth flow runs here — and the Worker is configured to run *before* static assets so security headers apply to the SPA's HTML too.
**D1 (SQLite)**
All structured data — users, ideas, tags, relations, sessions, billing events — lives in **Cloudflare D1**, a serverless SQLite database at the edge. Includes an FTS5 virtual table for full-text search.
**Vectorize**
Semantic search and duplicate detection are powered by **Cloudflare Vectorize** (index `neuralrepo-ideas`), storing 1024-dimension embeddings alongside metadata. Uses cosine similarity for nearest-neighbor queries.
**Queues**
Asynchronous processing — embedding generation, duplicate detection, auto-tagging — is handled by **Cloudflare Queues**, in batches of up to 10 with three retries and a dead-letter queue. Ideas are processed in the background so API responses return immediately.
**KV**
**Cloudflare KV** holds short-lived counters and registrations: daily rate-limit counters, the free plan's monthly semantic-search counter, the stale-nudge cooldown, and dynamically registered MCP OAuth clients. Every key is TTL'd.
**R2**
Object storage via **Cloudflare R2**. Today the `ATTACHMENTS` bucket serves exactly one thing — the demo video on the marketing page. User file attachments are not implemented.
**Workers AI**
Used for embeddings (`@cf/baai/bge-m3`), search reranking (`@cf/baai/bge-reranker-base`), the two keyless agent models (GPT-OSS 120B, Qwen 30B), and the agent's sub-agents (IBM Granite 4.0 H Micro for utility work, Qwen 30B for creative work) — none of which need a user API key.
**AI Gateway**
All AI requests — BYOK Anthropic calls and Workers AI calls alike — are routed through **Cloudflare AI Gateway**, providing logging, caching, and rate limiting.
**Durable Objects**
The **AI agent** (`NeuralRepoAgent`) runs as a Cloudflare **Durable Object**, giving each user a persistent, stateful WebSocket session. Conversation history is stored in the Durable Object's SQLite storage.
**Worker Loaders**
Code mode — both the MCP `code_mode` tool and the agent's — executes user-authored JavaScript in a dynamically loaded, isolated Worker via the `LOADER` binding.
**Email Routing**
Mail to `idea@neuralrepo.com`, including every personal `idea+@` address (subaddressing), is routed to the Worker's `email` handler. It parses the message into an idea, or rejects it at SMTP time. The confirmation for a captured idea is a reply to the incoming message (`message.reply()`); refusals get no reply. The `send_email` binding is configured but not used.
**Cron Triggers**
Four schedules: the Sunday weekly digest, Monday stale-idea nudges, a daily 04:00 UTC run of the trial-expiry and data-retention sweeps, and an hourly job that re-queues ideas whose background processing never ran.
## Backend Dependencies
The Worker itself has a deliberately short dependency list:
| Package | Purpose |
|---|---|
| `hono` | HTTP routing and middleware |
| `arctic` | GitHub, Google, and Apple OAuth flows |
| `zod` | Request validation, shared with the CLI and frontend |
| `ai` + `@ai-sdk/anthropic` + `workers-ai-provider` | Agent model calls across Anthropic and Workers AI |
| `agents` | The `AIChatAgent` base class behind the Durable Object |
| `@cloudflare/codemode` | Sandboxed code-mode execution |
| `stripe` | Billing and webhook signature verification |
| `fflate` | Zipping multi-file exports |
## Frontend
The web dashboard is a **React 19 SPA** built with **Vite**, with hand-rolled routing and plain CSS — no router library, no CSS framework, and no data-fetching library:
| Technology | Purpose |
|-----------|---------|
| React 19 | UI framework |
| Vite | Build tool and dev server |
| `3d-force-graph` + `three` | 3D mind map visualization |
| `d3` | 2D force-directed mind map |
| `@dnd-kit/*` | Drag and drop for the kanban board and the mind map |
| `react-markdown` + `remark-gfm` | Rendering idea bodies and agent replies |
| `rehype-highlight` + `rehype-sanitize` + `dompurify` | Syntax highlighting and sanitizing rendered Markdown |
| `mermaid` | Diagram rendering inside idea bodies and agent output |
The SPA is served via Cloudflare Workers static assets and communicates with the Workers API.
## CLI
The `@neuralconfig/nrepo` CLI is built with:
| Technology | Purpose |
|-----------|---------|
| Commander.js | Command parsing and help generation |
| Chalk | Terminal styling |
| Ora | Spinner animations |
| Zod | Response validation |
Configuration is a plain JSON file at `~/.config/neuralrepo/config.json`, written with `node:fs` —
there is no config library and no OS keychain integration. The CLI is published to npm and
communicates with the same REST API.
## Shared Library
The `@neuralrepo/shared` package contains code shared between the API, frontend, and CLI:
- **Zod schemas** for request and settings validation
- **TypeScript types** inferred from those schemas
- **Constants** — the status, source, link-type, and relation-type enums
- **`LIMITS`**, the single source of truth for field limits enforced across API, MCP, and CLI
This ensures type safety and validation consistency across all three surfaces.
## Why Cloudflare
NeuralRepo chose the Cloudflare platform for three reasons:
1. **Edge latency.** Every API call runs at the nearest Cloudflare edge location. Whether you are in Tokyo or Toronto, response times are consistently low.
2. **Unified billing.** All services (compute, storage, vectors, AI, queues) are under one account with predictable pricing. No cross-provider networking costs.
3. **Integrated tooling.** D1, Vectorize, Queues, and Workers AI are first-class bindings in the Workers runtime. No SDKs, no connection strings — just direct bindings with TypeScript types.
**Note:**
The entire NeuralRepo backend is a single Workers project with bindings to all services. There are no microservices, no containers, and no servers to manage.
## Architecture Diagram
Three clients — the React web app, the Node.js CLI, and the remote Claude MCP
connector — all reach the same Workers (Hono) API, which binds D1, KV, R2, the agent
Durable Object, and the Queue directly. Background queue jobs call Workers AI and
Vectorize; Workers AI and BYOK Anthropic calls both go through AI Gateway.
```mermaid
flowchart TD
web["Web App (React)"] --> workers["Workers (Hono)"]
cli["CLI (Node.js)"] --> workers
mcp["Claude MCP (Remote)"] --> workers
workers --> d1[("D1 SQLite")]
workers --> kv[("KV Counters")]
workers --> r2[("R2 Demo asset")]
workers --> agent["Durable Object NeuralRepoAgent"]
workers --> queues["Queues (Background)"]
agent --> gateway["AI Gateway"]
queues --> ai["Workers AI"]
ai --> gateway
queues --> vec[("Vectorize")]
```
## Next Steps
- [Data Model](https://docs.neuralconfig.com/neuralrepo/architecture/data-model/): Every table in D1, with the columns that matter.
- [Queue Processing](https://docs.neuralconfig.com/neuralrepo/architecture/queue-processing/): What the background pipeline does with each message type.
- [Semantic Search](https://docs.neuralconfig.com/neuralrepo/architecture/semantic-search/): The embedding model, Vectorize configuration, and search path.
- [Security & Auth](https://docs.neuralconfig.com/neuralrepo/architecture/security/): Sign-in methods, credential storage, CORS, and rate limiting.
---
# REST API
Source: https://docs.neuralconfig.com/neuralrepo/capture/api/
The NeuralRepo REST API gives you full programmatic access to your ideas. Build custom integrations, automate idea capture from other tools, or create your own front-end — the API supports everything the web dashboard does.
## Base URL
```
https://neuralrepo.com/api/v1
```
All endpoints are served over HTTPS. HTTP requests are rejected.
## Authentication
Authenticate every request with your API key in the `X-API-Key` header:
```
X-API-Key: nrp_your_api_key_here
```
Generate an API key in **Settings > API Keys** on the web dashboard. Keys use the format `nrp_` followed by 64 hexadecimal characters.
**Caution:**
Keep your API key secret. It grants full read-write access to your NeuralRepo account. Do not commit it to version control or share it publicly.
## Quick Example: Create an Idea
**curl**
```bash
curl -X POST https://neuralrepo.com/api/v1/ideas \
-H "Content-Type: application/json" \
-H "X-API-Key: nrp_your_api_key_here" \
-d '{
"title": "Build a personal finance dashboard",
"body": "Track spending, budgets, and investments in one place. Use Plaid for bank integration.",
"tags": ["fintech", "side-project"],
"status": "exploring"
}'
```
**JavaScript (fetch)**
```javascript
const response = await fetch("https://neuralrepo.com/api/v1/ideas", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": "nrp_your_api_key_here",
},
body: JSON.stringify({
title: "Build a personal finance dashboard",
body: "Track spending, budgets, and investments in one place. Use Plaid for bank integration.",
tags: ["fintech", "side-project"],
status: "exploring",
}),
});
const idea = await response.json();
console.log(idea);
```
**Python (requests)**
```python
import requests
response = requests.post(
"https://neuralrepo.com/api/v1/ideas",
headers={
"Content-Type": "application/json",
"X-API-Key": "nrp_your_api_key_here",
},
json={
"title": "Build a personal finance dashboard",
"body": "Track spending, budgets, and investments in one place. Use Plaid for bank integration.",
"tags": ["fintech", "side-project"],
"status": "exploring",
},
)
idea = response.json()
print(idea)
```
### Request Body
| Field | Type | Required | Description |
|-------|------|:--------:|-------------|
| `title` | string | Yes | Idea title (1-200 characters) |
| `body` | string | No | Idea body in plain text or Markdown |
| `tags` | string[] | No | Array of tags (max 20) |
| `status` | string | No | One of: `captured`, `exploring`, `building`, `shipped`, `shelved`. Defaults to `captured` |
| `source` | string | No | Capture channel to record: `web`, `cli`, `claude-mcp`, `siri`, `email`, `api`, `shortcut`, `ios` (reserved for the iOS app, which is not yet released). Defaults to `web`, so send `"source": "api"` explicitly if you want API captures labelled |
| `source_url` | string | No | URL where the idea originated (max 2,000 characters) |
| `parent_id` | number | No | Parent idea ID. Background processing turns this into a `parent` relation |
Bodies are capped at 50,000 characters and titles at 200.
### Response
A successful request returns `201 Created` with the full idea object:
```json
{
"id": 42,
"title": "Build a personal finance dashboard",
"body": "Track spending, budgets, and investments in one place. Use Plaid for bank integration.",
"status": "exploring",
"source": "api",
"tags": ["fintech", "side-project"],
"processing": true,
"created_at": "2026-03-24T14:22:00Z",
"updated_at": "2026-03-24T14:22:00Z"
}
```
**Tip:**
The `processing: true` field indicates that background tasks are still running — embedding generation for semantic search and duplicate detection. The idea is fully saved and accessible immediately; processing happens asynchronously.
## Error Responses
The API returns standard HTTP status codes with a JSON error body. Validation failures put
the offending fields in `error`, one array of messages per field:
```json
{
"error": {
"title": ["String must contain at most 200 character(s)"]
}
}
```
| Status Code | Meaning |
|:-----------:|---------|
| `400` | Bad request — invalid or missing parameters |
| `401` | Unauthorized — missing or invalid API key |
| `403` | Forbidden — action not allowed (e.g., free tier limit reached) |
| `404` | Not found — idea or resource does not exist |
| `429` | Rate limited — daily request quota exceeded |
| `500` | Server error — try again or contact support |
Hitting the free plan's idea limit returns a `403` with a machine-readable shape you can
branch on:
```json
{
"error": "idea_limit_reached",
"message": "You've captured 50 ideas on the free plan. Upgrade to Pro for unlimited ideas.",
"currentCount": 50,
"limit": 50,
"upgradeUrl": "https://neuralrepo.com/upgrade",
"featureRequestUrl": "https://support.neuralrepo.com/feature-requests"
}
```
Pro-only endpoints return `403` with `pro_required: true` and the feature name.
## Rate Limits
API endpoints are rate-limited per user per day. Free plans allow **100 requests per day**, and Pro plans allow **10,000 requests per day**, counted per UTC day and reset at midnight UTC. If you exceed this, the API returns `429 Too Many Requests`. Rate limits apply to API key and Bearer token requests only — web UI sessions are exempt. Multiple keys share one quota, and MCP calls count against it too.
## Next Steps
- [API Reference](https://docs.neuralconfig.com/neuralrepo/api/overview/): Complete documentation for all API endpoints — ideas, relations, links, tags, search, and more.
- [Authentication](https://docs.neuralconfig.com/neuralrepo/api/authentication/): Key formats, Bearer sessions, and how to rotate a key.
- [Rate Limits](https://docs.neuralconfig.com/neuralrepo/api/rate-limits/): Quotas per plan, what counts, and how to read a 429.
- [Search](https://docs.neuralconfig.com/neuralrepo/organize/search/): The search endpoint's modes, thresholds, and fallback behaviour.
---
# CLI
Source: https://docs.neuralconfig.com/neuralrepo/capture/cli/
The NeuralRepo CLI (`nrepo`) lets you capture and manage ideas without leaving your terminal. It is fast, scriptable, and works seamlessly with developer workflows — including Claude Code.
## Why Use the CLI?
- **Fast capture** — push an idea in a single command, no context switching
- **Scriptable** — pipe output, use in shell scripts, integrate with CI/CD
- **Works with Claude Code** — Claude Code can invoke `nrepo` commands directly in your development workflow
- **JSON output** — parse results programmatically for automations
## Installation
Install the CLI globally via npm:
```bash
npm install -g @neuralconfig/nrepo
```
Verify the installation:
```bash
nrepo --version
```
**Caution:**
The CLI requires Node.js 18 or later. If you encounter permission errors, see [npm's guide to fixing global install permissions](https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally).
## Authentication
**OAuth (recommended)**
Log in using your browser — this is the easiest method:
```bash
nrepo login
```
Your browser opens **GitHub** sign-in, and the server hands a freshly minted API key back
to the CLI. It is stored in `~/.config/neuralrepo/config.json`.
This flow works on **every plan**, so the CLI is usable without Pro.
**API Key**
For headless environments, CI/CD, scripting — or if you sign in to NeuralRepo with Google
or a magic link rather than GitHub — paste a key instead:
```bash
nrepo login --api-key
# Paste your API key when prompted: nrp_your_api_key_here
```
`--api-key` is a bare flag: it prompts, it does not take the key as an argument. Generate
the key in **Settings ▸ API Keys** on the web dashboard (a Pro feature). Keys use the
format `nrp_` followed by 64 hexadecimal characters.
## Quick Push
Capture an idea in a single command:
```bash
nrepo push "Build a habit tracker app" --body "Mobile-first, with streak tracking and reminders" --tag side-project --status exploring
```
All flags are optional except the title (the first positional argument):
| Flag | Description | Default |
|------|-------------|---------|
| `--body ` | Idea body text (Markdown) | *(empty)* |
| `--tag ` | Add a tag (repeatable) | *(none)* |
| `--status ` | Set initial status | `captured` |
| `--json` | JSON output | *(human-readable)* |
| `--human` | Human-readable output — wins over `--json` | *(default)* |
The body is passed as a flag value, so quote it. Multi-line bodies work with a quoted
string or your shell's line continuation; there is no stdin mode.
### Example with multiple tags
```bash
nrepo push "AI-powered code review tool" \
--tag ai \
--tag devtools \
--tag saas \
--status exploring
```
## Output Modes
Every command prints **human-readable** output by default, on any authentication method.
Pass `--json` for machine-parseable output — including errors, which become a single JSON
object on stderr with an `error`, a `code`, and (for API failures) a `status`.
```bash
# JSON output (useful for scripting)
nrepo push "My idea" --json
# --human wins if both are passed
nrepo search "tracker" --json --human
```
### Human-readable output
Ideas print as one row each: number, status, a glyph for the capture source, title, tags,
and — for search results — the relevance score.
```
✓ Idea captured
#42 exploring ⬡ AI-powered code review tool [ai, devtools, saas]
Processing: embeddings, dedup, and auto-tagging queued
```
### JSON output
```json
{
"id": 42,
"title": "AI-powered code review tool",
"status": "exploring",
"tags": ["ai", "devtools", "saas"],
"source": "cli",
"processing": true,
"created_at": "2026-03-24T10:30:00Z"
}
```
**Tip:**
The `processing: true` field indicates that background tasks (embedding generation, duplicate detection) are still running. The idea is saved and available immediately.
## Common Commands
```bash
# Search ideas
nrepo search "habit tracker"
# List recent ideas
nrepo log
# View a specific idea
nrepo show 42
# Change an idea's status
nrepo move 42 building
# Delete an idea
nrepo rm 42
```
## Configuration
The CLI stores its configuration at:
```
~/.config/neuralrepo/config.json
```
This file contains your authentication credentials (OAuth token or API key). Do not share it or commit it to version control.
## Full Reference
- [CLI Command Reference](https://docs.neuralconfig.com/neuralrepo/cli/installation/): Complete documentation for every nrepo command, flag, and option.
## Next Steps
- [Search](https://docs.neuralconfig.com/neuralrepo/organize/search/): What nrepo search actually runs, and when it returns nothing rather than falling back.
- [Tags](https://docs.neuralconfig.com/neuralrepo/organize/tags/): Tag one idea or many, and what auto-classification does with an untagged capture.
- [Duplicate Detection](https://docs.neuralconfig.com/neuralrepo/organize/duplicates/): Review and merge duplicates from the terminal with nrepo duplicate.
- [REST API](https://docs.neuralconfig.com/neuralrepo/capture/api/): The same surface the CLI drives — useful for scripts in other languages.
---
# Email Capture
Source: https://docs.neuralconfig.com/neuralrepo/capture/email/
NeuralRepo lets you capture ideas by sending a simple email. No app to open, no login required — just compose and send.
## Where do I send ideas?
Send them to your **personal capture address**, which looks like this:
```
idea+<24 hex characters>@neuralrepo.com
```
Find it in the web app under **Settings ▸ Integrations ▸ Email capture**. The address is created
the first time you open that card, and it stays the same until you replace it.
The address itself is the credential. Any mail that reaches it becomes an idea in your repository,
**whoever sent it and from whatever mailbox**. So you can send from a work alias or a second
address, or forward mail through a service that rewrites the sender, and it still lands. It also
means you should keep the address private, as the Settings card says: anyone who has it can add
ideas to your repo.
| Email Field | Idea Field |
|-------------|------------|
| **Subject** | Title — or the first line of the body if the subject is empty |
| **Body** | Body (plain text, signature stripped) |
| **To address** | Picks the account — the token after `idea+` |
The idea is created with:
- **Status**: `captured`
- **Source**: `email`
- **Tags**: none at first — auto-tagging fills them in a few seconds later
**Tip:**
Email capture is designed for quick, low-friction idea saving. Write a clear subject line — it becomes your idea title. Put details, context, or notes in the body.
## What happens to the mail I send?
A message is either **captured** or **rejected while it is being delivered**. NeuralRepo does
not send a reply explaining a refusal. The rejection is an SMTP-time reject, so your own mail
provider sends you a bounce (a non-delivery notice), which usually quotes NeuralRepo's reason.
| You sent to | Result |
|-------------|--------|
| Your personal address, on a Pro account | **Captured.** A confirmation is sent back to the sender, with the title, the idea's `#number`, and a link to open it |
| Your personal address, but the mail was empty | Still captured; the title falls back to the first line of the body, or `Untitled idea (from email)` |
| Your personal address, on a Free account | Rejected: `Email capture is a NeuralRepo Pro feature` |
| An `idea+…` address that has been replaced, or never existed | Rejected: `Unknown NeuralRepo capture address` |
| The shared `idea@neuralrepo.com` | Rejected, unless you opted in during the transition (see below): `Send ideas to your personal capture address, shown in NeuralRepo Settings (idea+...@neuralrepo.com)` |
**Note:**
The confirmation is best-effort. Cloudflare refuses to send a reply to a message that has no
valid DMARC result, so some senders get the idea but no confirmation. Check the web app or
`nrepo log` if a confirmation doesn't arrive.
If you heard nothing at all, neither a confirmation nor a bounce, the mail most likely never
reached NeuralRepo. Check your sent folder, and check the address against the one in Settings.
If you get the confirmation and see the idea but can't find its tags yet, give it a few seconds:
tagging runs in the background.
## How do I set it up?
1. **Open the Email capture card**
In the web app, go to **Settings ▸ Integrations** and find the **Email capture** card. On a
Free plan the card is titled **Email capture — Pro** and shows an upgrade link instead of an
address.
2. **Copy your address**
Click **Copy** next to your `idea+…@neuralrepo.com` address, and save it as a contact in your
mail app so you don't have to type it.
3. **Send an email**
- **To**: your personal capture address
- **Subject**: Your idea title (e.g., "Gamified onboarding flow for new users")
- **Body**: Any additional details or context
4. **Check your ideas**
The idea appears in your NeuralRepo dashboard within a few seconds, ready for triage.
## How do I get a new address?
Click **New address** on the Email capture card and confirm. The old address **stops working
immediately**: mail sent to it afterwards is rejected with `Unknown NeuralRepo capture address`.
Update the saved contact in your mail app. Replace the address whenever you think someone else
has it.
Deleting your account clears the address as well.
## What about the old idea@neuralrepo.com address?
Until **October 24, 2026** (00:00 UTC), you can opt in to keep using the shared address during the
move to personal addresses. After that date the shared address rejects everything.
- **The opt-in is a checkbox on the Email capture card:** *Also accept mail from my account email
to the shared idea@neuralrepo.com until…*. It is shown only until the cut-off, and it is off
unless you tick it.
- **It works only from your account email.** The shared address can't tell who a message is
for, so it matches the **From** address, lowercased, against your account email. Mail from any
other address is rejected.
- **It needs a Pro plan** at the moment the mail arrives.
- **It sends no confirmation.** The shared address can't verify who sent a message, so it never
replies. The idea simply appears.
**Caution:**
If you opt in, don't send the same message to both addresses. Mail addressed to your personal
address **and** `idea@neuralrepo.com` is delivered twice, and each copy creates an idea.
## Requirements
- A NeuralRepo **Pro** plan, checked when each message arrives
- Mail sent to your personal capture address from **Settings ▸ Integrations ▸ Email capture**
## How the Body Is Cleaned Up
The handler keeps the readable text and throws away the mail plumbing:
- **Multipart messages** use the `text/plain` part when there is one, and convert the HTML
part when there isn't — paragraphs, line breaks, and list items become newlines, and HTML
entities are decoded.
- **Signatures and quoted replies are cut**, along with everything after them. The cut
triggers on a `--` delimiter line, `Sent from my …`, `Get Outlook for …`, a long line of
underscores, or an `On … wrote:` quote header.
**Caution:**
That last rule is the one that surprises people: **forwarding an email keeps only the text
above the quoted original**. If you forward an article or a colleague's mail, write what
you want to keep at the top — everything from the `On … wrote:` line down is discarded.
## Use Cases
- **Capture ideas from your phone** without installing anything — use your default email app
- **Capture from any device** — any device that can send email can capture ideas
- **Comment on something you were sent** — forward it and put your thinking at the top, above the quoted text
## Tips
- **Write descriptive subjects** — the subject line is your idea title, so make it clear and specific
- **Lead with your own words** — anything below a quoted reply or signature is stripped
- **One idea per email** — each email creates one idea. For multiple ideas, send multiple emails
- **Add context later** — email capture is intentionally minimal. Add tags, relations, and detail from the web or CLI
## Limitations
- Only text is kept. Images and file attachments are not processed.
- There is no way to set tags or status via email — every email-captured idea starts as
`captured`, and is auto-tagged afterwards like any other capture.
- Non-ASCII subjects are decoded from RFC 2047 encoded words; anything the decoder cannot
read arrives verbatim in the title.
## Next Steps
- [Tags](https://docs.neuralconfig.com/neuralrepo/organize/tags/): What auto-classification does with an email that arrives with no tags.
- [Review Queue](https://docs.neuralconfig.com/neuralrepo/organize/review/): Triage what you emailed in — quiet ideas, new connections, possible duplicates.
- [Weekly Digest](https://docs.neuralconfig.com/neuralrepo/integrations/weekly-digest/): The other side of email: what NeuralRepo sends you, and when.
- [Plans & Pricing](https://docs.neuralconfig.com/neuralrepo/getting-started/pricing/): Email capture is Pro — what else the plan includes.
---
# iOS App
Source: https://docs.neuralconfig.com/neuralrepo/capture/ios/
The NeuralRepo iOS app is a native SwiftUI client for the two things you do on a phone: capture an
idea the moment it arrives, and find one when you need it. It talks to the same `/api/v1` surface
as the [CLI](https://docs.neuralconfig.com/neuralrepo/capture/cli/), authenticated with the same `nrp_` key, so everything you
capture appears everywhere else immediately.
**Not yet released:**
**The NeuralRepo iOS app is not released, and you can't install it today.** There is no App
Store or TestFlight listing, and its source is not public, so the build steps below only work
for the NeuralRepo team. The rest of this page describes the app as it is built for testing,
and it may change before release.
**On iPhone today**, use [Siri Shortcuts](https://docs.neuralconfig.com/neuralrepo/capture/siri/) for voice capture,
[email capture](https://docs.neuralconfig.com/neuralrepo/capture/email/) from any mail app, or the
[web app](https://docs.neuralconfig.com/neuralrepo/capture/web/) in Safari.
## Installing
1. Install the prerequisites:
```bash
brew install xcodegen # Xcode 16 or later is also required
```
2. Generate the Xcode project from the checked-in definition:
```bash
cd ios
xcodegen generate
open NeuralRepo.xcodeproj
```
The `.xcodeproj` is generated and git-ignored — edit `project.yml` and re-run `xcodegen
generate` rather than the project file.
3. In Xcode, select your Apple Developer team under **Signing & Capabilities** for all three
targets: **NeuralRepo**, **ShareExtension**, and **NeuralRepoWidget**. They share an App Group
and a Keychain access group, both already declared in `project.yml`.
4. Build to a device with ⌘R.
**Danger:**
**Debug builds talk to staging.** The backend origin is chosen at compile time: ⌘R from Xcode
points the app at `staging.neuralrepo.com`, while a Release build (Archive, TestFlight, App
Store) points at `neuralrepo.com`. Ideas captured from a Debug build land in your staging
account, not your real one. Archive the app rather than running it if you want your own data.
## Signing in
The sign-in screen offers two paths, and both end with an `nrp_` API key in the shared Keychain:
- **Sign in with Apple** — native, on-device. The identity token is posted to
`/auth/apple/native` and exchanged for a key.
- **Sign in** (web) — opens `/auth/cli` in a `ASWebAuthenticationSession`, the same endpoint
`nrepo login` uses, and returns to the app at `neuralrepo://auth`.
If your account hasn't accepted the current Terms of Service and Privacy Policy, **Sign in with
Apple** first shows a sheet linking the Terms, the Privacy Policy and the NeuralRepo privacy
addendum, an agreement toggle, and **Accept and continue** and **Cancel**. **Accept and
continue** stays disabled until the toggle is on. Cancelling leaves you signed out. The web
path shows the same documents on the website instead.
Use the same account you use on the web or in Claude — the app is a client, not a separate space.
The key is stored once and read by the app, the share extension, the widget, and the Siri intent
through a shared Keychain group, so you sign in a single time.
Signing out clears the key **and discards anything still waiting in the offline queue**, so that
captures made under one account can never post under the next.
## The five tabs
| Tab | What it does |
|---|---|
| **Capture** | Title, notes, a starting status, and tags with suggestions from your existing tag list |
| **Search** | Semantic search across everything you have captured |
| **Ideas** | The full list, filtered by status, with swipe actions and a context menu |
| **Review** | Pending duplicates, new connections, and ideas that have gone quiet — badged with the count |
| **Settings** | Account and plan, theme, offline queue, and links out to the web app |
### Capture
The capture form offers `captured`, `exploring`, and `building` as starting statuses — the same
three the web app's capture modal offers — and a tag field that suggests tags you already use.
Ideas post with `source: ios`, shown as `⬢` everywhere else in NeuralRepo.
Two conveniences worth knowing:
- **Drafts survive.** Backgrounding the app with a part-written idea saves it; you come back to
what you were typing.
- **The form clears itself.** After a successful capture the fields reset, the keyboard returns to
the title, and a "Captured" toast appears for about a second and a half.
### The offline queue
If a capture cannot reach the server, the app keeps it locally and syncs it later.
| Behaviour | Detail |
|---|---|
| What gets queued | Only requests that demonstrably never reached the server — a network failure, not a rejection |
| Where | One file per capture in the App Group container, so the app and the share extension can queue at the same time |
| Order | Oldest first, so capture order and idea numbering are preserved |
| When it drains | On the next successful capture, when you open the app, or from **Sync now** on the banner |
| When an item is deleted | Only after the server confirms the create |
| What stops a drain | Being offline, or a `401` — everything stays queued |
| Other errors | The item stays queued and the drain moves on, so a capture blocked by the free-plan cap retries after you upgrade |
A banner on the Capture tab and a row in Settings both show the pending count.
**Note:**
Nothing is ever dropped, but nothing is deduplicated either — the API has no idempotency key, so
the app deliberately only queues failures where the request certainly never arrived.
### The duplicate banner
After a capture, the app polls for a duplicate detection three times over the next couple of
seconds and, if the idea it just saved was flagged, raises a banner offering **View duplicate**,
**Dismiss**, or a close button. Dismissing resolves the detection server-side so it will not come
back.
This is the one surface in NeuralRepo with a duplicate banner attached to capture, and it appears
**after** the idea is saved — detection is asynchronous, so it cannot happen before. It is Pro-only,
because duplicate detections are.
### Search, Ideas, and Review
**Search** runs the same semantic search as `nrepo search`, 30 results at a time. When the server
falls back to keyword results — on the free plan past 10 semantic searches a month — the list is
labelled **Keyword results** and the match percentages disappear.
**Ideas** filters by status with a chip bar, pages as you scroll, and supports swipe-to-archive,
swipe-to-ship, a leading swipe to move to `building`, and a context menu for copying the title or
setting any status. Opening an idea shows its body, tags, relations, and — at the bottom —
`id 391 · #42`, which is the easiest way to read an idea's real ID off your phone for a CLI or API
call.
**Review** is the mobile version of the [Review Queue](https://docs.neuralconfig.com/neuralrepo/organize/review/): possible
duplicates (Merge / Not a duplicate), new connections (Open / Seen it), and ideas gone quiet past
your stale threshold (Keep / Develop / Archive). The tab badge shows how many cards are waiting.
## Capturing without opening the app
### Share sheet
Share a page from Safari, or selected text from anywhere, into NeuralRepo. The extension pulls the
page title, text, and URL into a pre-filled capture form with the same status and tag controls as
the app. It posts with `source: ios` and uses the same offline queue, so a share on the Underground
still lands.
### Siri and Shortcuts
The app registers a native App Intent, so these all work without opening it:
- "Capture to NeuralRepo"
- "Add an idea to NeuralRepo"
- "New idea in NeuralRepo"
Siri asks "What's the idea?", and the dictated text is saved with `source: siri`. Text longer than
200 characters is split: the first 200 become the title and the whole text becomes the body. If
you are not signed in, the intent replies "Open the NeuralRepo app and sign in first"; at the
free-plan cap it says so and points at Pro.
This replaces the older iCloud shortcut described in
[Siri Shortcuts](https://docs.neuralconfig.com/neuralrepo/capture/siri/) — the intent needs no download and no API key pasted
into a shortcut.
### Widget
A home and lock screen widget in small and medium sizes: a **New idea** button that deep-links
straight into the capture screen, your active idea count, and — at medium size — your four most
recent ideas.
**Note:**
The widget reads a snapshot the app writes, and asks iOS to refresh at most every 30 minutes. Its
counts update when you open the app or capture something, not continuously.
## What the app does not do
Organising work stays on the web: the mind map, the kanban board, the AI agent, tag management,
bulk actions, and export are all web-app features. The iOS app is deliberately a capture-and-lookup
client, and **Open web app** in Settings takes you to the rest.
## Next Steps
- [Web App](https://docs.neuralconfig.com/neuralrepo/capture/web/): The full workspace the iOS app deliberately leaves out.
- [Siri Shortcuts](https://docs.neuralconfig.com/neuralrepo/capture/siri/): The shortcut-based capture path, for devices without the app.
- [Review Queue](https://docs.neuralconfig.com/neuralrepo/organize/review/): What the Review tab is showing you, and what each action does.
- [Plans & Pricing](https://docs.neuralconfig.com/neuralrepo/getting-started/pricing/): Which of the app's features — duplicates, unlimited search — need Pro.
---
# Claude AI (MCP)
Source: https://docs.neuralconfig.com/neuralrepo/capture/mcp/
NeuralRepo integrates with Claude AI through the **Model Context Protocol (MCP)**, allowing Claude to read, create, and manage your ideas directly during a conversation. No copy-pasting, no switching tabs.
## What Is MCP?
The [Model Context Protocol](https://modelcontextprotocol.io) is an open standard that lets AI assistants interact with external tools and data sources. When NeuralRepo is connected as an MCP server, Claude gains access to a set of tools for managing your ideas — saving new ones, searching existing ones, linking ideas together, and more.
## Setup
**Claude.ai**
1. Open **Claude.ai** and go to your account settings.
2. Navigate to the **Integrations** or **MCP Servers** section.
3. Click **Add Remote MCP Server** and enter the following URL:
```
https://neuralrepo.com/mcp/v2
```
4. Save the configuration. On your next conversation, Claude will have access to NeuralRepo tools.
5. The first time Claude attempts to use a NeuralRepo tool, you will be prompted to **authorize via OAuth**. Sign in with your NeuralRepo account to grant access.
**Claude Code**
Add the NeuralRepo MCP server from your terminal:
```bash
claude mcp add --transport http neuralrepo https://neuralrepo.com/mcp/v2
```
On the next Claude Code session, NeuralRepo tools will be available. The OAuth authorization flow triggers automatically on first use.
**Note:**
MCP works on **all plans** (Free and Pro). The `link_ideas` and `unlink_ideas` tools require a Pro plan. Semantic search is limited to 10/month on Free. The OAuth flow ensures Claude only accesses your ideas with your explicit permission.
## Available Tools
**Tip:**
The setup above uses `https://neuralrepo.com/mcp/v2`, which has 22 discrete tools (no `code_mode`);
they're listed on [Claude Connector (MCP v2)](https://docs.neuralconfig.com/neuralrepo/integrations/claude-connector/). The table
below is for the original `https://neuralrepo.com/mcp/` connector, which keeps working with no end
date. Use it if you rely on `code_mode`.
Connected to the original `/mcp/` address, Claude has access to the following tools:
| Tool | Description |
|------|-------------|
| `save_idea` | Create a new idea with title, body, tags, status, source URL, or a parent idea |
| `search_ideas` | Search your ideas semantically, or browse them by recency |
| `get_idea` | Retrieve a specific idea by ID |
| `update_idea` | Update an idea's title, body, status, or tags — one idea, or many at once |
| `delete_idea` | Archive an idea by ID (a soft delete) |
| `link_ideas` | Create typed relations between ideas — one pair, or a batch (Pro) |
| `unlink_ideas` | Remove a relation between two ideas (Pro) |
| `list_tags` | List all tags with their idea counts |
| `code_mode` | Run JavaScript against **24** NeuralRepo functions in one call |
`update_idea` and `link_ideas` each take a bulk form — an `ids` array and a `links` array
respectively — so Claude can retag or connect a batch of ideas without a call per idea.
`code_mode` is the one to reach for when a request needs several steps: it exposes a
`neuralrepo` object with functions the eight named tools don't cover — merging ideas,
managing tag colours, adding links, resolving duplicates, and repository stats. See the
[MCP integration guide](https://docs.neuralconfig.com/neuralrepo/integrations/mcp/).
## Usage Examples
Once NeuralRepo is connected, you can interact with your ideas naturally in conversation:
### Saving an idea
> **You:** I just thought of something — we should build a browser extension that highlights text and saves it as an idea. Save this to NeuralRepo.
>
> **Claude:** I have saved that as a new idea in NeuralRepo. *(calls `save_idea`)*
### Searching ideas
> **You:** Do I have any ideas related to browser extensions?
>
> **Claude:** Let me check your ideas. *(calls `search_ideas`)* You have two related ideas: "Browser extension for bookmarking" and "Chrome plugin for code snippets."
### Linking ideas
> **You:** Those two browser extension ideas are related. Link them.
>
> **Claude:** Done — I have linked them with a "related" relation. *(calls `link_ideas`)*
### Updating status
> **You:** Move the habit tracker idea to "building" status.
>
> **Claude:** Updated. "Build a habit tracker app" is now in the `building` status. *(calls `update_idea`)*
## OAuth Authorization Flow
NeuralRepo's MCP integration uses **OAuth 2.0** for secure authorization:
1. When Claude first attempts to use a NeuralRepo tool, you see an authorization prompt
2. Clicking **Authorize** opens the NeuralRepo OAuth consent screen
3. Sign in with your NeuralRepo account (GitHub, Google, Apple, or magic link)
4. Grant Claude permission to access your ideas
5. Authorization is persisted — you do not need to re-authorize for future sessions
Two scopes exist: `ideas:read` and `ideas:write`. Anything that changes data —
`save_idea`, `update_idea`, `delete_idea`, `link_ideas`, `unlink_ideas`, and `code_mode` —
is refused without `ideas:write`.
**Tip:**
You can revoke MCP access at any time from **Settings > Connected Apps** in the NeuralRepo web dashboard.
## Limitations
- MCP access is **read-write** — Claude can create, update, and archive ideas on your behalf. Review Claude's actions if you want to be cautious.
- The `link_ideas` and `unlink_ideas` tools require a **Pro** plan; everything else works on Free.
- Semantic search is limited to 10/month on Free, after which `search_ideas` returns keyword results with a notice.
- On Free, `save_idea` refuses once you hold 50 active ideas.
- Rate limits apply: Free allows 100 requests/day, Pro allows 10,000 requests/day, reset at midnight UTC. Every MCP call counts.
## Next Steps
- [MCP Integration Guide](https://docs.neuralconfig.com/neuralrepo/integrations/mcp/): Advanced MCP configuration, the full code_mode surface, and troubleshooting.
- [Search](https://docs.neuralconfig.com/neuralrepo/organize/search/): How search_ideas resolves semantic versus keyword, and what the thresholds do.
- [Idea Relations](https://docs.neuralconfig.com/neuralrepo/organize/relations/): The relation types link_ideas can create, and the cycles it refuses.
- [Plans & Pricing](https://docs.neuralconfig.com/neuralrepo/getting-started/pricing/): Which MCP tools need Pro, and what the daily request limits are.
---
# Siri Shortcuts
Source: https://docs.neuralconfig.com/neuralrepo/capture/siri/
NeuralRepo integrates with **Siri Shortcuts** for hands-free idea capture. Speak your idea, and it lands in your NeuralRepo inbox — no typing, no unlocking, no app switching.
## Quick Capture
Once set up, capturing an idea is as simple as:
> **"Hey Siri, NeuralRepo"**
Siri prompts you for the idea title, sends it to NeuralRepo via the API, and confirms the capture. The idea is created with:
- **Status**: `captured`
- **Source**: `siri`
- **Tags**: none (add them later from the web or CLI)
## Requirements
- An active NeuralRepo **Pro** plan — the Shortcuts card in Settings is Pro-gated
- An **API key**, generated from the same card
- Apple device running iOS 16+, iPadOS 16+, macOS 13+, or watchOS 9+
## Setup
1. **Open Settings ▸ Integrations**
In the NeuralRepo web dashboard, scroll to the **Apple Shortcuts** card. On a free plan
the card shows an upgrade link instead of the setup UI.
2. **Generate an API key**
If you have no key yet, the card offers **Generate API Key** directly. Copy it — it
starts with `nrp_` and is 68 characters long.
**Caution:**
Store your API key securely. It grants full access to your NeuralRepo account and cannot be viewed again after generation. If you lose it, generate a new one.
3. **Add the Quick Capture shortcut**
Click **Add to Shortcuts** on the card. It opens the NeuralRepo Quick Capture shortcut in
Apple's Shortcuts app — an iCloud shortcut, so it syncs to every device signed in to the
same Apple account.
4. **Configure the API key**
When you first run the shortcut, it prompts you to paste your API key. This is stored securely in the Shortcuts app and used for all subsequent captures.
5. **Test it**
Say **"Hey Siri, NeuralRepo"** or tap the shortcut in the Shortcuts app. Speak or type your idea when prompted. You should see a confirmation that the idea was saved.
**Tip:**
There is one shortcut — Quick Capture. Expand **"What can I say to Siri?"** on the same
card for the phrase list and the places you can put the shortcut: Home Screen, a widget,
or the Action Button.
## Supported Devices
**iPhone & iPad**
Full voice capture via Siri. Works from the lock screen, home screen, or while using other apps.
**Mac**
Use Siri from the menu bar or keyboard shortcut. Ideal for capturing ideas while working at your desk.
**Apple Watch**
Raise your wrist and say "Hey Siri, NeuralRepo" for the fastest possible capture. Dictation handles the transcription.
## Tips
- **Keep titles short and clear** — Siri's dictation works best with concise phrases. You can always add detail later.
- **Use a distinctive shortcut name** — if "NeuralRepo" is hard for Siri to recognize, rename the shortcut to something like "Save Idea" or "Idea Capture."
- **Add to your Home Screen** — add the shortcut as a home screen icon for one-tap capture without voice.
**Tip:**
Ideas captured via Siri land in your inbox with `captured` status. Triage them on the web or CLI later — add tags, write a body, and move them to `exploring`.
## Next Steps
- [Siri Shortcuts Setup Guide](https://docs.neuralconfig.com/neuralrepo/integrations/siri-shortcuts/): Step-by-step instructions for creating custom shortcuts, adding parameters, and troubleshooting.
- [Apple Watch](https://docs.neuralconfig.com/neuralrepo/integrations/apple-watch/): Capture from the wrist, and what the watch can and cannot do.
- [Tags](https://docs.neuralconfig.com/neuralrepo/organize/tags/): Dictated ideas arrive untagged — here is what happens next.
- [Review Queue](https://docs.neuralconfig.com/neuralrepo/organize/review/): Triage the pile of quick captures without reading the whole list.
---
# Web App
Source: https://docs.neuralconfig.com/neuralrepo/capture/web/
The web dashboard at [neuralrepo.com](https://neuralrepo.com) is NeuralRepo's primary interface. It provides the full feature set — idea creation, kanban workflow, search, mind map visualization, and settings management.
## Dashboard Overview
The left sidebar is the dashboard's spine: a **+ Capture** button at the top, then the six
views, with **Settings** at the bottom.
| View | What it is |
|------|-----------|
| **Ideas** | The filterable list of everything you have captured |
| **Board** | The kanban board — one column per status, drag to move |
| **Mind Map** | Your idea graph as an interactive force-directed map |
| **Agent** | The AI agent: a chat panel beside an artifact canvas (Pro) |
| **Duplicates** | Pending duplicate detections to merge or dismiss (Pro) |
| **Review** | The resurfacing queue — quiet ideas, new connections, duplicates |
Each view has its own URL (`/app`, `/board`, `/map`, `/agent`, `/duplicates`, `/review`), so
you can bookmark or link to one directly.
From the **Ideas** list you can:
- **Filter by status** — narrow to `captured`, `exploring`, `building`, `shipped`, or `shelved`
- **Filter by tags** — click any tag in the sidebar; click more to narrow further
- **Filter by source** — see ideas from a specific capture channel
- **Search** — the sidebar search box runs keyword or semantic search
- **Select and act in bulk** — tick several ideas and use the bulk toolbar
## Creating Ideas
1. Click **+ Capture** at the top of the sidebar (it is on every view).
2. Fill in the idea details:
| Field | Required | Description |
|-------|:--------:|-------------|
| **Title** | Yes | A concise name for the idea |
| **Notes** | No | Longer description — supports full Markdown |
| **Status** | No | `captured` (default), `exploring`, or `building` |
| **Tags** | No | Type a name and press Enter, or click one of your existing tags |
3. Click **Capture**.
**Tip:**
The notes field supports full Markdown including headings, lists, code blocks, and links. Use it to capture context, requirements, or early notes.
The capture form only offers the first three statuses — an idea you are capturing has not
shipped or been shelved yet. Set either from the idea's detail panel or the board afterwards.
If you leave the tag field empty, NeuralRepo tags the idea for you a few seconds later. See
[Tags](https://docs.neuralconfig.com/neuralrepo/organize/tags/#auto-classification).
## Duplicates Surface After the Save, Not Before
Duplicate detection is asynchronous — the idea is saved first, then embedded and compared in
the background. There is no warning banner in the capture form, and nothing blocks a save.
A few seconds later, any pair above your dedup threshold appears in the **Duplicates** view
(a Pro feature) and in the duplicates section of **Review**, where you merge or dismiss it.
On the free plan the detections are still recorded — they are simply not displayed, and they
are all there waiting if you upgrade later. See
[Duplicate Detection](https://docs.neuralconfig.com/neuralrepo/organize/duplicates/).
## Bulk Actions
Tick the checkbox on any idea in the list and a bulk toolbar appears. With a selection you
can:
- **Move** every selected idea to a status
- **Add** or **remove** a tag across the selection
- **Archive** the selection (after a confirmation)
Bulk edits are applied **per idea, not as a transaction** — the API takes up to 50 ideas per
request (the web app splits larger selections into batches), and reports success or failure
for each one. A bad ID fails alone; the rest still change.
## Kanban Board
Open the **Board** view from the sidebar. The kanban board displays ideas as cards in columns:
| Column | Status |
|--------|--------|
| Captured | New, unevaluated ideas |
| Exploring | Ideas under research or consideration |
| Building | Ideas actively being implemented |
| Shipped | Completed and delivered ideas |
| Shelved | Parked ideas — not active, not deleted |
**Drag and drop** cards between columns to change their status. This is the fastest way to move ideas through your workflow.
**Note:**
The kanban board respects your current tag and source filters. If you have a tag filter active, only matching ideas appear on the board.
## Search
The sidebar search box supports two modes, and both are available on every plan:
**Semantic**
Vector-powered search that finds ideas by **meaning**. A search for "daily routine app" will match ideas titled "habit tracker" or "morning checklist tool" even if the exact words do not appear.
```
app for tracking daily routines
```
The free plan includes 10 semantic searches a month. After that, searches return keyword
results with a notice above them until the month rolls over.
**Keyword**
Full-text matching across idea titles and bodies, with three matching styles — **any
word**, **all words**, or **exact** phrase. Unlimited on every plan.
```
habit tracker mobile
```
Results are ranked by relevance and can be further narrowed by status, tags, and source. The
result header tells you which mode answered — `semantic`, or `keyword` with the matching
style in brackets. See [Search](https://docs.neuralconfig.com/neuralrepo/organize/search/) for thresholds and the API.
## Mind Map
The **Mind Map** view renders your idea graph as an interactive network visualization. Each idea is a node, and relations are edges connecting them. Free accounts get it **view-only** — pan, zoom, filter, and open ideas; editing the graph requires Pro.
- **Zoom and pan** to navigate the graph
- **Click a node** to open the idea detail view
- **Color coding** reflects idea status
- **Edge labels** show the relation type (related, blocks, inspires, etc.)
The mind map is especially useful for spotting clusters of related ideas, identifying blocking dependencies, and understanding how your ideas connect across projects.
## Settings
**Settings** sits at the bottom of the sidebar. From here you can:
- **Generate API keys** for the CLI, Shortcuts, and the REST API (Pro)
- **Manage billing** — upgrade, downgrade, or cancel your plan
- **Configure BYOK AI** — add your own Anthropic, OpenAI, or OpenRouter key (Pro)
- **Set up integrations** — the Claude MCP connector, the CLI, and the Siri shortcut
- **Tune notifications** — the weekly digest, stale-idea nudges, and the staleness threshold
- **Export data** — download every idea as JSON, CSV, or a Markdown archive
- **Manage account** — display name, theme, and connected sign-in providers
**Tip:**
The Markdown export is Obsidian-ready: one note per idea with YAML frontmatter, and
relations written as `[[wiki-links]]` so the vault reproduces your idea graph.
## Next Steps
- [Review Queue](https://docs.neuralconfig.com/neuralrepo/organize/review/): Work the resurfacing queue — quiet ideas, new connections, and possible duplicates.
- [Mind Map](https://docs.neuralconfig.com/neuralrepo/organize/mind-map/): Read the graph: node anatomy, edge types, filters, 3D, and the agent panel.
- [Search](https://docs.neuralconfig.com/neuralrepo/organize/search/): Semantic versus keyword, thresholds, and what each surface falls back to.
- [Status Workflow](https://docs.neuralconfig.com/neuralrepo/organize/status-workflow/): Move ideas through captured, exploring, building, shipped, and shelved.
---
# nrepo login / logout / whoami
Source: https://docs.neuralconfig.com/neuralrepo/cli/auth-commands/
## nrepo login
```bash
nrepo login [--api-key]
```
Authenticates the CLI and writes an `nrp_` key to `~/.config/neuralrepo/config.json`. Without
flags it runs the browser flow through GitHub; with `--api-key` it prompts for a key you already
have. [Authentication](https://docs.neuralconfig.com/neuralrepo/cli/authentication/) covers both flows end to end.
| Flag | Type | Default | Description |
|---|---|---|---|
| `--api-key` | boolean | `false` | Prompt for an API key instead of opening the browser. |
```bash
# Browser login (any plan)
nrepo login
# API key login — prompts, does not take the key as an argument
nrepo login --api-key
# Same thing, non-interactive
echo "$NEURALREPO_API_KEY" | nrepo login --api-key
```
Both paths end with the same line, which includes your plan:
```
✔ Logged in as Alice (pro)
```
**Note:**
Logging in again simply overwrites the stored key. The previous key stays valid on the server —
browser login mints a new one every time, so repeated logins accumulate keys labelled
`CLI (auto-generated)` in `nrepo key list`.
## nrepo logout
```bash
nrepo logout
# Logged out.
```
Overwrites the local config file with `{}`. Every other command then fails with
`Not logged in. Run \`nrepo login\` to authenticate.` until you log in again.
**Caution:**
Logging out is local only — the key is not revoked and keeps working anywhere else it is stored.
Use [`nrepo key revoke`](https://docs.neuralconfig.com/neuralrepo/cli/key/) when a key may have leaked.
## nrepo whoami
```bash
nrepo whoami [--json|--human]
```
Fetches the current account from `GET /user/me`. It is the cheapest way to check whether the CLI
is authenticated at all.
| Flag | Type | Default | Description |
|---|---|---|---|
| `--json` | boolean | `false` | Output as JSON. |
| `--human` | boolean | — | Force human-readable output. Wins over `--json`. |
```bash
nrepo whoami
```
```
Alice
Email: alice@example.com
Plan: pro
ID: usr_abc123
```
The plan prints as the raw value — `free` or `pro`. Trial accounts report `pro`.
```bash
nrepo whoami --json
```
```json
{
"id": "usr_abc123",
"email": "alice@example.com",
"display_name": "Alice",
"avatar_url": "https://avatars.githubusercontent.com/u/1?v=4",
"plan": "pro",
"settings_json": "{}",
"has_anthropic_key": true,
"has_openai_key": false,
"has_openrouter_key": false,
"active_idea_count": 137,
"is_trialing": false,
"trial_days_remaining": null,
"created_at": "2026-01-04 09:12:33",
"updated_at": "2026-07-21 18:40:02"
}
```
`active_idea_count` counts unarchived ideas, so it is the field to watch against the free plan's
50-idea cap:
```bash
nrepo whoami --json | jq 'select(.plan == "free") | .active_idea_count'
```
`is_trialing` is true only while a Pro trial is running with no Stripe subscription behind it;
`trial_days_remaining` is `null` otherwise.
## Next Steps
- [Authentication](https://docs.neuralconfig.com/neuralrepo/cli/authentication/): Which identity provider the browser flow uses, and how to authenticate without one.
- [nrepo key](https://docs.neuralconfig.com/neuralrepo/cli/key/): List, create, and revoke the keys these commands authenticate with.
- [nrepo status](https://docs.neuralconfig.com/neuralrepo/cli/status/): The same account summary plus a breakdown of every idea by status.
- [Plans & Pricing](https://docs.neuralconfig.com/neuralrepo/getting-started/pricing/): What the free plan caps and what Pro unlocks across every surface.
---
# Authentication
Source: https://docs.neuralconfig.com/neuralrepo/cli/authentication/
Both login methods end in the same place: an `nrp_` API key in
`~/.config/neuralrepo/config.json`, sent as an `X-API-Key` header on every request.
| Method | Best for | Command | Plan required |
|---|---|---|---|
| **Browser** | Interactive use on a workstation | `nrepo login` | Any — including free |
| **API key** | CI, scripts, headless machines | `nrepo login --api-key` | Pro, to create the key |
## Browser login
```bash
nrepo login
```
1. The CLI starts a one-shot HTTP server on a **random local port** between 49152 and 65535 and
prints the sign-in URL:
```
Starting local auth server...
Open this URL to log in:
https://neuralrepo.com/auth/cli?callback=http%3A%2F%2Flocalhost%3A51234%2Fcallback
Waiting for authentication...
```
2. It also tries to open that URL for you (`open` on macOS, `start` on Windows, `xdg-open`
elsewhere). If nothing opens, paste the printed URL yourself.
3. You sign in with **GitHub**. NeuralRepo matches the account by your GitHub **verified primary
email**, so this works whether or not you originally signed up through GitHub, as long as that
email is the one your account uses. If it isn't, the login creates a **separate new account**.
4. The server redirects back to the local callback with a freshly minted key. The CLI saves it
and prints:
```
✔ Logged in as Alice (pro)
```
```mermaid
sequenceDiagram
participant C as nrepo
participant L as localhost:PORT
participant B as Your browser
participant N as neuralrepo.com
participant G as GitHub
C->>L: Start one-shot server on a random port
C->>B: Open /auth/cli?callback=http://localhost:PORT/callback
B->>N: GET /auth/cli
N->>B: Redirect to /auth/github with the callback stored
B->>G: Sign in and approve
G->>N: OAuth code
N->>N: Match account by verified primary email
N->>N: Mint an nrp_ key labelled "CLI (auto-generated)"
N->>B: Redirect to localhost:PORT/callback?api_key=nrp_...
B->>L: GET /callback with the key
L->>C: Hand over the key, close the server
C->>N: GET /user/me to verify
C->>C: Write ~/.config/neuralrepo/config.json
```
**Caution:**
The local server gives up after **120 seconds**. If the browser sign-in takes longer than that,
the CLI fails with `Login timed out after 120 seconds` — re-run `nrepo login` rather than
finishing the browser flow, because the callback has nowhere left to land.
Two consequences of that flow are worth knowing:
- **`nrepo login` mints a real API key**, not a session token. It is labelled
`CLI (auto-generated)` and appears in `nrepo key list` alongside keys you made yourself.
- **No plan check runs on that path.** Browser login works on the free plan even though creating
a key from the dashboard or with `nrepo key create` is a Pro feature.
`nrepo login` always authenticates through GitHub — it sends no provider preference, and the
endpoint's default is GitHub. Google sign-in is not part of this flow at all.
## API key login
`--api-key` is a bare flag. It prompts; it does not take the key as an argument.
**Interactive**
```bash
nrepo login --api-key
# Enter your API key: nrp_...
# ✔ Logged in as Alice (pro)
```
**Piped**
```bash
echo "$NEURALREPO_API_KEY" | nrepo login --api-key
```
The prompt reads a single line from stdin, so piping and here-strings both work.
The key is verified against `GET /user/me` before it is saved. A bad key fails with
`Invalid API key. Generate one at https://neuralrepo.com/settings` and exit status 1.
Keys use the format `nrp_` followed by 64 hexadecimal characters. Create one with
[`nrepo key create`](https://docs.neuralconfig.com/neuralrepo/cli/key/) or in **Settings ▸ API Keys** on the web dashboard;
both require Pro.
**Tip:**
Signed in with Google or a magic link, on the free plan? Browser login still works — GitHub
matches you by verified primary email, so your GitHub primary email must be the one you signed
up with; otherwise you land in a separate new account. If you have no GitHub account at all, you
need Pro to mint a key, because that is the only other way in.
## Configuration file
```
~/.config/neuralrepo/config.json
```
| Field | Description |
|---|---|
| `api_url` | API base URL. Defaults to `https://neuralrepo.com/api/v1` when absent |
| `api_key` | The `nrp_` key, in plain text |
| `user_id` | Your account identifier, recorded at login |
| `auth_method` | `browser` or `api-key` — recorded at login and **never read again** |
**Caution:**
The file is plain text with no special permissions. Treat it like an SSH private key: never
commit it, never bake it into an image.
## Output format
Every command prints human-readable output by default, on **both** login methods. Pass `--json`
for machine-parseable output, and `--human` to force text back on if something upstream in your
tooling sets `--json`. `--human` wins when both are given.
**Caution:**
`nrepo login --api-key` finishes by printing "Output defaults to JSON. Use `--human` for
human-readable output." That line is wrong, and has been since the flag was added: `auth_method`
is written to the config file and never read, so output is human-readable until you pass
`--json`. Script accordingly.
## Headless and CI
```bash
export NREPO_NO_UPDATE_CHECK=1
echo "$NEURALREPO_API_KEY" | nrepo login --api-key
nrepo push "Nightly build failed on $(git rev-parse --short HEAD)" \
--body "$(tail -50 build.log)" --tag ci --json
```
Give CI its own key so you can revoke it independently, and check the exit status: every failure
exits 1, and with `--json` the error is a single JSON object on stderr.
## Logging out
```bash
nrepo logout
# Logged out.
```
**Caution:**
`nrepo logout` overwrites the local config file with `{}`. It does **not** revoke the key on the
server — anyone holding a copy can still use it. To invalidate it, run
[`nrepo key revoke`](https://docs.neuralconfig.com/neuralrepo/cli/key/) first, or delete the key in the web dashboard.
## Next Steps
- [nrepo login / logout / whoami](https://docs.neuralconfig.com/neuralrepo/cli/auth-commands/): The full flag and output reference for the three authentication commands.
- [nrepo key](https://docs.neuralconfig.com/neuralrepo/cli/key/): Create, list, and revoke the API keys the CLI authenticates with.
- [Troubleshooting](https://docs.neuralconfig.com/neuralrepo/cli/troubleshooting/): What an expired session, a Pro gate, and a network failure look like on the terminal.
- [API authentication](https://docs.neuralconfig.com/neuralrepo/api/authentication/): The same key, used directly against the REST API from any language.
---
# nrepo branch
Source: https://docs.neuralconfig.com/neuralrepo/cli/branch/
```bash
nrepo branch [options]
```
Creates a **new** idea from an existing one, recording the original as its parent. Use it to
explore an alternative without editing — or losing — the version you already have.
## Options
| Flag | Type | Default | Description |
|---|---|---|---|
| `--title ` | string | The original's title | Title for the variant. |
| `--body ` | string | The original's body | Body for the variant. |
| `--json` | boolean | `false` | Output the new idea as JSON. |
| `--human` | boolean | — | Force human-readable output. Wins over `--json`. |
## What the new idea inherits
| Field | Value on the branch |
|---|---|
| Title | The original's, unless `--title` is given |
| Body | The original's, unless `--body` is given |
| Tags | Copied from the original |
| Status | Always `captured` — never the original's status |
| Source | `cli`, whatever the original's source was |
| Parent | The original, recorded in `parent_id` |
Nothing is copied back the other way, and the original is not modified.
## Output
```bash
nrepo branch 391 --title "Dark mode — Tailwind approach"
```
```
✓ Branched from #391 as #55
#55 captured ⬡ Dark mode — Tailwind approach [ui, frontend]
Processing: embeddings, dedup, and auto-tagging queued
Compare with: nrepo diff 391 508
```
**Note:**
The first line mixes two kinds of number: `#391` is the **ID you passed**, `#55` is the new
idea's display number. The `nrepo diff` hint below it uses IDs for both, so it is safe to copy
and run as printed.
## The parent relation is created a moment later
Setting `parent_id` is synchronous, but the visible `parent` relation between the two ideas is
created by the background queue that also handles embedding and dedup. For a second or two after
branching, `nrepo links` on the new idea shows nothing:
```bash
nrepo branch 391 --json | jq -r .id # 508
nrepo links 508 # "No links" — the queue has not run yet
sleep 3
nrepo links 508 # Child of ← #42
```
The relation is recorded as system-created, so it appears in the mind map styled differently from
relations you draw by hand. [`nrepo diff 508`](https://docs.neuralconfig.com/neuralrepo/cli/diff/) works immediately either
way, because it reads `parent_id` directly.
## Next Steps
- [nrepo diff](https://docs.neuralconfig.com/neuralrepo/cli/diff/): Compare the variant against the idea it came from.
- [nrepo merge](https://docs.neuralconfig.com/neuralrepo/cli/merge/): Fold the winning variant back into the original.
- [nrepo link](https://docs.neuralconfig.com/neuralrepo/cli/link/): Add relations by hand when parent is not the right shape.
- [Idea Relations](https://docs.neuralconfig.com/neuralrepo/organize/relations/): What parent means alongside the other relation types.
---
# nrepo diff
Source: https://docs.neuralconfig.com/neuralrepo/cli/diff/
```bash
nrepo diff [id2] [options]
```
Compares two ideas across five fields — title, body, status, source, and tags — and prints only
the ones that differ.
## Options
| Flag | Type | Default | Description |
|---|---|---|---|
| `--json` | boolean | `false` | Output both ideas plus a field-by-field diff as JSON. |
| `--human` | boolean | — | Force human-readable output. Wins over `--json`. |
## Output
```bash
nrepo diff 391 402
```
```
diff #42 → #43
────────────────────────────────────────────────────────────
Title
- Add dark mode to dashboard
+ Add dark mode and a theme picker
Body
Switch the dashboard to support both schemes.
- Use CSS custom properties.
+ Use CSS custom properties and a picker in Settings.
Tags
- ui, frontend
+ ui, frontend, themes
```
Identical ideas print `No differences found.` An empty field renders as `(empty)`.
**Note:**
The body comparison is line-by-line **by position**, not a real diff. Insert a line near the top
of one body and every line after it is reported as changed, because line 5 is now being compared
against line 4. It is a readable summary, not a patch.
## Comparing against a related idea
With one ID, the CLI picks the comparison target itself, in this order:
1. The idea's `parent_id` — set when it was created by [`nrepo branch`](https://docs.neuralconfig.com/neuralrepo/cli/branch/).
2. A `parent` relation.
3. A `duplicate` relation.
4. The highest-scoring relation of any type.
```bash
nrepo diff 402
# Compares #43 against the idea it was branched from
```
An idea with none of those has nothing to compare against:
```
No parent or related idea to diff against.
Usage: nrepo diff 402
```
That case exits 0 in human mode. With `--json` it exits 1 and prints
`{"error":"...","code":"no_diff_target"}`.
## JSON output
```bash
nrepo diff 391 402 --json
```
```json
{
"a": { "id": 391, "number": 42, "title": "Add dark mode to dashboard" },
"b": { "id": 402, "number": 43, "title": "Add dark mode and a theme picker" },
"diff": [
{ "field": "Title", "a": "Add dark mode to dashboard", "b": "Add dark mode and a theme picker", "changed": true },
{ "field": "Body", "a": "...", "b": "...", "changed": true },
{ "field": "Status", "a": "exploring", "b": "exploring", "changed": false },
{ "field": "Source", "a": "cli", "b": "cli", "changed": false },
{ "field": "Tags", "a": "ui, frontend", "b": "ui, frontend, themes", "changed": true }
]
}
```
`a` and `b` are the full idea objects; `diff` carries every field, including unchanged ones.
## Next Steps
- [nrepo branch](https://docs.neuralconfig.com/neuralrepo/cli/branch/): Create the variant that nrepo diff compares against its parent.
- [nrepo merge](https://docs.neuralconfig.com/neuralrepo/cli/merge/): Fold one of the two ideas into the other once you have decided.
- [nrepo duplicate](https://docs.neuralconfig.com/neuralrepo/cli/duplicate/): Review pairs the system flagged as near-identical.
- [nrepo show](https://docs.neuralconfig.com/neuralrepo/cli/show/): Read either idea in full instead of a field summary.
---
# nrepo duplicate
Source: https://docs.neuralconfig.com/neuralrepo/cli/duplicate/
```bash
nrepo duplicate list [options]
nrepo duplicate dismiss [options]
nrepo duplicate merge [options]
```
NeuralRepo compares every new idea against your existing ones a few seconds after capture and
records a **detection** for each near-identical pair. These three subcommands work through that
queue.
| Subcommand | What it does |
|---|---|
| `list` | Show unresolved detections |
| `dismiss ` | Mark a detection resolved without changing either idea |
| `merge ` | Fold the pair together and resolve the detection |
All three take `--json` and `--human`.
**Caution:**
**Duplicate detection is a Pro feature.** On the free plan the endpoint returns an empty list, so
`nrepo duplicate list` always prints `No pending duplicates.` Detections are still being recorded
in the background, and the backlog appears in full the moment you upgrade.
## Listing detections
```bash
nrepo duplicate list
```
```
2 pending duplicates
#101 Auth flow redesign ≈ #42 Rewrite auth flow 91%
#103 Dark mode support ≈ #40 Add dark mode to dashboard 85%
Use `nrepo duplicate dismiss ` or `nrepo duplicate merge ` to resolve.
```
**Caution:**
The `` those two commands want is the **detection's** ID, and the human output does not print
it — `#101` and `#42` are idea display numbers. Use `--json` to get it:
```bash
nrepo duplicate list --json | jq -r '.duplicates[] | "\(.id) #\(.idea_number) ≈ #\(.duplicate_number) \(.similarity_score)"'
```
The JSON form carries both ideas in full — numbers, titles, statuses, sources, creation dates, and
the first 280 characters of each body — which is enough to decide without a second lookup:
```json
{
"duplicates": [
{
"id": 17,
"idea_id": 512,
"duplicate_of_id": 391,
"similarity_score": 0.91,
"status": "pending",
"idea_number": 101,
"idea_title": "Auth flow redesign",
"duplicate_number": 42,
"duplicate_title": "Rewrite auth flow"
}
]
}
```
The human list shows only `pending` detections; `--json` returns what the API sent, which is also
pending-only.
## Dismissing
```bash
nrepo duplicate dismiss 17
# ✓ Dismissed duplicate #17
```
Marks the detection resolved. Neither idea changes, and the pair is not flagged again. A detection
that does not exist, or has already been resolved, returns
`API error (404): Duplicate not found or already resolved`.
## Merging
```bash
nrepo duplicate merge 17
# ✓ Merged duplicate #17 into primary idea
```
**Caution:**
This is **not** the same operation as [`nrepo merge`](https://docs.neuralconfig.com/neuralrepo/cli/merge/), and the difference
decides which idea survives.
| | `nrepo duplicate merge` | `nrepo merge ` |
|---|---|---|
| Idea kept | The **newer** one — the capture that triggered the detection | Whichever you name first |
| Idea archived | The older one it matched | The one you name second |
| Body | Newer body, `---`, older body | Kept body, `---`, absorbed body |
| Tags | Union | Union |
| Relations | **Not transferred** — they stay on the archived idea and become invisible | Transferred to the kept idea |
| Status of archived idea | Unchanged, but archived | Set to `shelved`, and archived |
If the older idea is the one carrying your relations, merge it explicitly with `nrepo merge`
instead, then dismiss the detection.
Both paths keep the earlier of the two creation dates on the surviving idea, and neither can be
undone.
## Next Steps
- [Duplicate Detection](https://docs.neuralconfig.com/neuralrepo/organize/duplicates/): What the detector compares, the score threshold, and what it does with the pair.
- [nrepo merge](https://docs.neuralconfig.com/neuralrepo/cli/merge/): Merge two ideas you chose yourself, with relation transfer.
- [Review Queue](https://docs.neuralconfig.com/neuralrepo/organize/review/): The web app's version of this queue, with connections and stale ideas too.
- [nrepo diff](https://docs.neuralconfig.com/neuralrepo/cli/diff/): Compare the two ideas in a detection before you resolve it.
---
# nrepo edit
Source: https://docs.neuralconfig.com/neuralrepo/cli/edit/
```bash
nrepo edit [options]
```
Updates the title, the body, or both. Anything you do not pass is left alone.
## Options
| Flag | Type | Default | Description |
|---|---|---|---|
| `--title ` | string | _(unchanged)_ | New title. Max 200 characters. |
| `--body ` | string | _(unchanged)_ | New body. Max 50,000 characters. |
| `--json` | boolean | `false` | Output the updated idea as JSON. |
| `--human` | boolean | — | Force human-readable output. Wins over `--json`. |
`nrepo edit` only touches those two fields. Status is
[`nrepo move`](https://docs.neuralconfig.com/neuralrepo/cli/move/), tags are [`nrepo tag`](https://docs.neuralconfig.com/neuralrepo/cli/tag/).
## Examples
```bash
# Retitle
nrepo edit 391 --title "Add dark mode and a theme picker"
# Replace the body from a file
nrepo edit 391 --body "$(cat notes.md)"
# Both at once
nrepo edit 391 \
--title "Theme system v2" \
--body "Complete redesign of the theming layer with dark mode support."
```
**Caution:**
`--body` **replaces** the body; it does not append. To add to what is there, read it first:
`nrepo edit 391 --body "$(nrepo show 391 --json | jq -r .body)$(printf '\n\n')New paragraph."`
Passing neither flag is an error, and nothing is sent:
```
Nothing to update. Use --title or --body.
```
With `--json` the same case prints `{"error":"Nothing to update. Use --title or --body.","code":"no_input"}`.
Both exit with status 1.
## Output
```
✓ Updated #42
Title: Theme system v2
Body updated (312 chars)
```
Only the fields you changed are echoed. `--json` prints the full updated idea object.
## What happens afterwards
Editing a title or body re-queues the idea for embedding, so its semantic search results and
duplicate detections refresh a few seconds later. Until that finishes, `nrepo search` still
matches the old wording.
## Next Steps
- [nrepo show](https://docs.neuralconfig.com/neuralrepo/cli/show/): Read the current title and body before you overwrite them.
- [nrepo branch](https://docs.neuralconfig.com/neuralrepo/cli/branch/): Keep the original and explore a variant instead of editing in place.
- [nrepo move](https://docs.neuralconfig.com/neuralrepo/cli/move/): Change status — the field nrepo edit deliberately does not touch.
- [Queue Processing](https://docs.neuralconfig.com/neuralrepo/architecture/queue-processing/): What the background pass does with an idea after you change its text.
---
# nrepo graph
Source: https://docs.neuralconfig.com/neuralrepo/cli/graph/
```bash
nrepo graph [options]
```
Walks outward from one idea through its relations and prints what it finds as a tree. Relations
are followed in **both** directions, so an idea that only ever appears as a target still shows up.
## Options
| Flag | Type | Default | Description |
|---|---|---|---|
| `--depth ` | number | `1` | Hops to traverse. Clamped to a maximum of `5`. |
| `--type ` | string | _(all)_ | Comma-separated relation types to follow: `related`, `blocks`, `inspires`, `supersedes`, `parent`, `duplicate`. |
| `--json` | boolean | `false` | Output nodes and edges as JSON. |
| `--human` | boolean | — | Force human-readable output. Wins over `--json`. |
## Output
```bash
nrepo graph 391 --depth 2
```
```
#42 Rewrite auth flow [exploring]
├── blocks → #43 Add dark mode and a theme picker [exploring]
│ └── related → #61 Design tokens audit [captured]
└── related → #18 Session cookie cleanup [captured]
```
Each branch is labelled with the relation type that produced it. An idea with no relations prints
just itself:
```
#42 Rewrite auth flow [exploring]
No connections found
```
**Caution:**
The tree renders **three levels** — the root, its neighbours, and their neighbours. `--depth 3`,
`4`, and `5` do fetch the deeper nodes, and they appear in `--json`, but the text tree stops
drawing at the second hop. Use `--json` when you ask for more than two.
**Note:**
Traversal makes one API call per node visited, so a wide graph at depth 3 is a lot of round
trips. Narrow it with `--type` when you only care about one kind of edge.
## Filtering by type
```bash
# Just the dependency chain
nrepo graph 391 --depth 3 --type blocks --json
# Structural relations only
nrepo graph 391 --type parent,supersedes
```
`--type` matches exact type names. An unknown name is not an error — it simply matches nothing,
and you get the root alone.
## JSON output
```bash
nrepo graph 391 --depth 2 --json
```
```json
{
"root": 391,
"depth": 2,
"types": null,
"nodes": [
{ "id": 391, "number": 42, "title": "Rewrite auth flow", "status": "exploring", "depth": 0 },
{ "id": 402, "number": 43, "title": "Add dark mode and a theme picker", "status": "exploring", "depth": 1 }
],
"edges": [
{ "source": 391, "target": 402, "type": "blocks" }
]
}
```
`depth` echoes the clamped value you asked for, not how far the walk actually reached. `types` is
`null` when `--type` was not passed. Edges keep their original direction regardless of which way
the traversal crossed them, so the output can be fed straight into a graph layout tool.
Each idea appears once: the first time the walk reaches it. In a diamond-shaped graph only the
first edge into a node is drawn in the tree, though every edge is present in `edges`.
## Next Steps
- [nrepo links](https://docs.neuralconfig.com/neuralrepo/cli/link/): One idea's relations in full, grouped by direction and type.
- [Mind Map](https://docs.neuralconfig.com/neuralrepo/organize/mind-map/): The same graph rendered visually, with filters and time windows.
- [The Idea Graph](https://docs.neuralconfig.com/neuralrepo/guides/idea-graph/): How connections accumulate and what to do with the clusters they form.
- [nrepo link](https://docs.neuralconfig.com/neuralrepo/cli/link/): Add the relations this command traverses.
---
# Installation
Source: https://docs.neuralconfig.com/neuralrepo/cli/installation/
The CLI ships as a single npm package, `@neuralconfig/nrepo`, and installs one binary: `nrepo`.
## Requirements
- **Node.js 18** or later (the package declares `engines.node` as `>=18`)
- npm (included with Node.js)
## Install
1. Install the package globally:
```bash
npm install -g @neuralconfig/nrepo
```
2. Confirm the binary is on your `PATH`:
```bash
nrepo --version
# 0.0.9
```
3. Authenticate:
```bash
nrepo login
```
Until you have logged in once, `nrepo --help` ends with a short getting-started box. It disappears
as soon as `~/.config/neuralrepo/config.json` exists — the CLI prints it there because npm
suppresses the package's own post-install message on most systems.
## Update
```bash
npm install -g @neuralconfig/nrepo@latest
```
`npm update -g @neuralconfig/nrepo` also works. Nothing in the CLI updates itself — the version
check below only tells you that an update exists.
## The version check
Every invocation runs a non-blocking version check:
| Behaviour | Detail |
|---|---|
| When it runs | Before the command executes, so the notice prints **above** the command's own output |
| Where it prints | **stderr**, so it never contaminates `--json` output on stdout |
| How often it calls npm | At most **once a week**, cached in `~/.config/neuralrepo/update-check.json` |
| Network timeout | 5 seconds, in the background; the command never waits for it |
| On failure | Silent — a failed check never fails the command |
| Disable it | Set `NREPO_NO_UPDATE_CHECK=1` |
The notice looks like this:
```
nrepo 0.1.0 available (current: 0.0.9). Run npm i -g @neuralconfig/nrepo to update.
```
**Tip:**
Because the notice goes to stderr, `nrepo log --json | jq .` is safe as-is. Set
`NREPO_NO_UPDATE_CHECK=1` in CI anyway, to avoid a weekly outbound call to the npm registry.
## The bundled Claude Code skill
The package bundles a Claude Code skill. It is **not** installed by `npm install` — you opt in:
```bash
nrepo install-skill
# Claude Code skill installed.
# /Users/you/.claude/skills/neuralrepo/SKILL.md
```
If `~/.claude` does not exist, the command reports that Claude Code is not installed and exits
with status 1 without writing anything.
Two things then happen on their own:
- When the weekly version check finds a **newer** release, it refreshes that `SKILL.md` in the
background, so the skill stays in step with the CLI between upgrades.
- `npm uninstall -g @neuralconfig/nrepo` deletes `~/.claude/skills/neuralrepo/` entirely.
## Troubleshooting
If `nrepo` is not found after installing, your npm global bin directory is probably not on your
`PATH`:
```bash
npm config get prefix
# Add /bin to your PATH
export PATH="$(npm config get prefix)/bin:$PATH"
```
## Next Steps
- [Authentication](https://docs.neuralconfig.com/neuralrepo/cli/authentication/): What nrepo login actually does, which identity provider it uses, and how to authenticate on a headless machine.
- [nrepo push](https://docs.neuralconfig.com/neuralrepo/cli/push/): Create your first idea from the terminal, with tags and an initial status.
- [Troubleshooting](https://docs.neuralconfig.com/neuralrepo/cli/troubleshooting/): Which argument is the idea ID, what the error output means, and which commands need Pro.
- [Claude Code](https://docs.neuralconfig.com/neuralrepo/integrations/claude-code/): What the bundled skill teaches Claude Code, and the workflow it enables.
---
# nrepo key
Source: https://docs.neuralconfig.com/neuralrepo/cli/key/
```bash
nrepo key list [options]
nrepo key create