Dashboard routes
Base paths: /auth and /dashboard · Auth: session token
These back the bundled dashboard UI. They are not an integration surface, they are unversioned and may change without notice. Documented so you can script administration.
An API key does not work here, and a session token does not work on
/v1/*.
POST /auth/login
Section titled “POST /auth/login”Public.
{ "username": "admin", "password": "…" }{ "token": "ms_sess_1bbe…", "user": { "id": "61a8…", "username": "admin", "createdAt": "…", "updatedAt": "…" }}401 Invalid username or password for both an unknown user and a wrong
password. Sessions last 7 days. The response also carries
usingDefaultPassword: true while the account still uses the shipped
open-sesame.
POST /auth/logout
Section titled “POST /auth/logout”Session required. 204, and the token is revoked immediately.
GET /auth/me
Section titled “GET /auth/me”{ "user": { "userId": "61a8…", "username": "admin" } }POST /auth/password
Section titled “POST /auth/password”Session required.
{ "currentPassword": "open-sesame", "newPassword": "something-longer" }204 on success, 401 if the current password is wrong, 400 if the new one
is shorter than 6 characters. Sessions are not revoked.
GET /dashboard/users
Section titled “GET /dashboard/users”{ "users": [ { "id": "61a8…", "username": "admin", "createdAt": "…", "updatedAt": "…" } ]}Password hashes are never returned.
POST /dashboard/users
Section titled “POST /dashboard/users”{ "username": "alice", "password": "at-least-6-chars" }201 → { "user": { … } }
| Code | Cause |
|---|---|
409 |
Username already taken |
400 |
Username 1–100 chars, password 6–200 chars |
DELETE /dashboard/users/:id
Section titled “DELETE /dashboard/users/:id”204 on success.
| Code | Cause |
|---|---|
400 |
Deleting yourself |
400 |
Deleting the last remaining user |
404 |
No such user |
Both guards exist to prevent locking everyone out. The last-user check runs in a transaction with the rows locked, so concurrent deletes cannot both slip past it.
There is no password-change endpoint. Create a new user and delete the old one, or update the hash directly in SQL.
Projects
Section titled “Projects”GET /dashboard/projects
Section titled “GET /dashboard/projects”{ "projects": [ { "id": "ea43…", "name": "default", "description": "Auto-created default project", "createdAt": "…" } ]}POST /dashboard/projects
Section titled “POST /dashboard/projects”{ "name": "production", "description": "optional, max 500 chars" }201 → { "project": { … } }
PATCH /dashboard/projects/:id
Section titled “PATCH /dashboard/projects/:id”{ "name": "renamed", "description": "…" }name is required.
DELETE /dashboard/projects/:id
Section titled “DELETE /dashboard/projects/:id”204.
Does not cascade in the application layer. Threads, facts and entities are removed by database-level
ON DELETE CASCADE; API keys for the project are removed too. This is destructive and irreversible, everything the project remembered is gone.
GET /dashboard/projects/:id/settings
Section titled “GET /dashboard/projects/:id/settings”{ "settings": { "episodic": { "enabled": true, "autoEpisodeIntervalMs": 1800000, "maxMessages": 100, "maxRetries": 3, "contextEpisodes": 3, "similarityWeight": 0.7, "recencyWeight": 0.3 }, "semantic": { "enabled": true, "retrievalMinConfidence": 0.5, "factsInContext": 8, "entityResolutionThreshold": 0.88, "factDedupThreshold": 0.95, "contradictionBandMin": 0.8, "anchorVectorMin": 0.75, "anchorVectorTopK": 3 } }}Always merged with defaults, so every field is present.
PATCH /dashboard/projects/:id/settings
Section titled “PATCH /dashboard/projects/:id/settings”Partial, deep-merged:
{ "semantic": { "factsInContext": 12 } }Bounds are in Project settings.
episodic.autoEpisodeIntervalMsaccepts>= 1000ornull, same as thread-level overrides.
API keys
Section titled “API keys”GET /dashboard/api-keys
Section titled “GET /dashboard/api-keys”{ "apiKeys": [ { "id": "93fe…", "name": "production", "keyPreview": "ms_3f9a4c…0161", "projectId": "ea43…", "createdAt": "…", "lastUsedAt": "…", "revokedAt": null } ]}Full key values are never returned, only a preview.
POST /dashboard/api-keys
Section titled “POST /dashboard/api-keys”{ "name": "production", "projectId": "ea43…" }projectId is optional; it falls back to the default project.
201:
{ "key": "ms_3f9a…", "apiKey": { "id": "93fe…", "…": "…" } }key appears here and never again.
DELETE /dashboard/api-keys/:id
Section titled “DELETE /dashboard/api-keys/:id”204. Immediate and permanent, revokedAt is stamped and the row is kept.
Takes the key id, not the key value.
Threads
Section titled “Threads”GET /dashboard/projects/:projectId/browse/threads
Section titled “GET /dashboard/projects/:projectId/browse/threads”| Param | Required | Default |
|---|---|---|
dataset |
no | , |
limit |
no | 50 (1–100) |
offset |
no | 0 |
{ "threads": [ { "threadId": "f2cb…", "dataset": "user_42", "projectId": "ea43…", "tags": [], "messageCount": 2, "metadata": null, "createdAt": "…", "updatedAt": "…", "lastActivityAt": "…" } ], "total": 1}messageCount is derived live with a correlated subquery, not stored.
GET /dashboard/projects/:projectId/browse/threads/:threadId/messages
Section titled “GET /dashboard/projects/:projectId/browse/threads/:threadId/messages”{ "thread": { "…": "…" }, "messages": [{ "…": "…" }] }Every message in sequence order, including compacted ones.
GET /dashboard/projects/:projectId/browse/threads/:threadId/episodes
Section titled “GET /dashboard/projects/:projectId/browse/threads/:threadId/episodes”Every episode for the thread, newest first, regardless of status.
Datasets
Section titled “Datasets”GET /dashboard/projects/:projectId/browse/datasets
Section titled “GET /dashboard/projects/:projectId/browse/datasets”| Param | Required | Default |
|---|---|---|
q |
no | , (ILIKE filter on the dataset name) |
limit |
no | 50 (1–100) |
offset |
no | 0 |
{ "datasets": [ { "dataset": "user_42", "threadCount": 3, "factCount": 17, "lastActivityAt": "2026-08-16T09:14:02.114Z" } ], "total": 1}The list is derived from
threads, so a dataset with facts but no threads will not appear.
The memory routes, under session auth
Section titled “The memory routes, under session auth”Everything under /v1 is mounted a second time at
/dashboard/projects/:projectId/v1. Same router, same handlers, same
response shapes, the only difference is the credential and where the project
comes from.
/v1/… |
/dashboard/projects/:projectId/v1/… |
|
|---|---|---|
| Credential | API key | session token |
| Project | the key’s project | named in the path |
# Identical results, different callers.curl "$API/v1/memory/semantic/datasets/user_42/facts" \ -H "Authorization: Bearer ms_…"
curl "$API/dashboard/projects/$PROJECT/v1/memory/semantic/datasets/user_42/facts" \ -H "Authorization: Bearer ms_sess_…"See Semantic memory, Episodic
memory, Recall, Threads
and Working memory for the routes themselves; the same
paths are served under /dashboard/projects/:projectId with a session instead
of an API key.
Scripting administration
Section titled “Scripting administration”API=http://localhost:3004
TOKEN=$(curl -s -X POST $API/auth/login \ -H 'Content-Type: application/json' \ -d '{"username":"admin","password":"'"$ADMIN_PASSWORD"'"}' | jq -r .token)
PROJECT=$(curl -s $API/dashboard/projects \ -H "Authorization: Bearer $TOKEN" | jq -r '.projects[0].id')
# Issue a key for CIcurl -s -X POST $API/dashboard/api-keys \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d "{\"name\":\"ci\",\"projectId\":\"$PROJECT\"}" | jq -r .key
curl -s -X POST $API/auth/logout -H "Authorization: Bearer $TOKEN"Authorization caveat
Section titled “Authorization caveat”/dashboard/* checks that a valid session exists, not that the user owns
the projectId they passed. Any signed-in dashboard user can read and modify
any project.
Fine for the intended single-tenant self-hosted deployment. If you need per-user project isolation, it does not exist yet.