Ada AI

Knowledge base

Projects are org-scoped collections of inter-connected markdown documents. Bodies live in object storage; [[slug]] wikilinks are resolved client-side.

A knowledge base is a set of inter-connected markdown documents, grouped into projects. Each project belongs to one owner — your personal account, or an organization — and every document in it is markdown you can link together with Obsidian-style [[slug]] wikilinks.

The server stores document bodies in object storage (not Postgres), keeps only ownership and metadata in the database, and does not parse links or compute backlinks. Connectivity lives in the documents' own content and is rebuilt in the browser from the document list — matching how an Obsidian vault is a folder of notes, not a graph store.

Requires a Stytch session JWT

Every route on this page is under /me/* and authenticates with a Stytch session JWT (Authorization: Bearer $JWT), held by the dashboard in a cookie. A proxy key (sk-rc-…) or an admin token returns 401 — the same rule as every other /me/* route. See API keys.

Owners and scope

Projects are org-scoped. "Personal" projects are simply the projects of your auto-provisioned personal organization; org projects live under the org they belong to. There is no per-user ownership column threaded through the API — exactly as invokers and upstreams work after the organizations migration.

  • Personal/me/projects. You are always your personal org's admin, so no role check applies: read and write your own projects freely.
  • Organization/me/organizations/{id}/projects. Any member can list projects and documents and fetch a single one; only admins can create, update, or delete. A non-member and a nonexistent org both return 404 organization not found — the response is identical on purpose, so it can't be used to probe which org IDs exist. See Organizations for roles and membership.

Projects

GET /me/projects

List your personal projects.

curl https://api.ada.ai/me/projects \
  -H "Authorization: Bearer $JWT"
{
  "projects": [
    {
      "id": 42,
      "organization_id": 7,
      "name": "Research notes",
      "description": "Context for ongoing experiments.",
      "created_at": "2026-08-20T23:02:02Z",
      "updated_at": "2026-08-21T08:46:10Z"
    }
  ]
}

POST /me/projects

Create a project. name is required; description is optional (defaults to the empty string). Returns 201 Created with the new project.

curl -X POST https://api.ada.ai/me/projects \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{"name": "Research notes", "description": "Context for ongoing experiments."}'

A name that already exists for the same owner returns 409 name_taken. Names are unique within an owner but free to repeat across owners.

GET /me/projects/{id} / PUT /me/projects/{id} / DELETE /me/projects/{id}

Fetch, partially update, or delete a single project. PUT takes {name, description} with both fields optional — omit a field to leave it unchanged. DELETE returns 204 No Content and cascades to every document in the project (their bodies are deleted best-effort from object storage).

curl -X PUT https://api.ada.ai/me/projects/42 \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{"description": "Updated scope."}'

A {id} that doesn't exist or belongs to another owner returns 404 not found — ownership checks resolve to "no row," so a foreign id is indistinguishable from a nonexistent one (same as invokers and upstreams).

Documents

Every document has a title, a slug, and a markdown body. The slug is the stable, URL-safe link key that [[slug]] in any document's content resolves to, and the body is the markdown text itself.

List endpoints return metadata onlyid, project_id, title, slug, timestamps. The markdown body is fetched separately and returned only by the single-document GET. The internal object pointer is never serialized.

GET /me/projects/{id}/documents

List the documents in a project (metadata only).

curl https://api.ada.ai/me/projects/42/documents \
  -H "Authorization: Bearer $JWT"
{
  "documents": [
    {
      "id": 100,
      "project_id": 42,
      "title": "Welcome",
      "slug": "welcome",
      "created_at": "2026-08-20T23:10:00Z",
      "updated_at": "2026-08-21T08:46:10Z"
    }
  ]
}

POST /me/projects/{id}/documents

Create a document. title and slug are required; content is optional (defaults to the empty string, which stores an empty note). Returns 201 Created with the new document including its content.

curl -X POST https://api.ada.ai/me/projects/42/documents \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Welcome",
    "slug": "welcome",
    "content": "# Welcome\n\nSee [[getting-started]] to begin."
  }'

A slug that already exists within the same project returns 409 slug_taken. Slugs are unique within a project but free to repeat across projects — a [[welcome]] link only ever resolves within the project that contains it.

GET /me/projects/{id}/documents/{document_id}

Fetch a single document with its content.

curl https://api.ada.ai/me/projects/42/documents/100 \
  -H "Authorization: Bearer $JWT"
{
  "id": 100,
  "project_id": 42,
  "title": "Welcome",
  "slug": "welcome",
  "created_at": "2026-08-20T23:10:00Z",
  "updated_at": "2026-08-21T08:46:10Z",
  "content": "# Welcome\n\nSee [[getting-started]] to begin."
}

PUT /me/projects/{id}/documents/{document_id}

Partially update a document. title, slug, and content are all optional — omit a field to leave it unchanged. Returns 200 OK with the full document (including content).

curl -X PUT https://api.ada.ai/me/projects/42/documents/100 \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{"content": "# Welcome\n\nUpdated. See [[getting-started]]."}'

Renaming a document only rewrites metadata

The body is stored under a key derived from the document's immutable id, not its slug. Changing slug is a pure metadata update — the body is never copied or deleted — and is the only thing that changes how [[old-slug]] links resolve across the rest of the project. Sending a non-nil content replaces the body at its existing key.

DELETE /me/projects/{id}/documents/{document_id}

Delete a document. Returns 204 No Content; the body is removed from object storage best-effort.

A [[slug]] token embedded in any document's content links to the document with that slug within the same project. The server treats content as opaque text: it does not parse [[…]], does not validate that a target exists, and does not compute backlinks. A dangling [[slug]] (one whose target was never created, renamed, or deleted) is a client rendering concern, not a write-time error — link resolution and any "missing link" treatment happen entirely in the browser from the document list.

Because slugs are unique only within a project, the same slug can appear in different projects and resolve independently. There is no cross-project linking.

Organization projects

The same surface is available for an organization at /me/organizations/{id}/projects. Replace the personal base with the org base and use {project_id} for the project path segment:

MethodPath
GET/me/organizations/{id}/projects
POST/me/organizations/{id}/projects
GET / PUT / DELETE/me/organizations/{id}/projects/{project_id}
GET / POST/me/organizations/{id}/projects/{project_id}/documents
GET / PUT / DELETE/me/organizations/{id}/projects/{project_id}/documents/{document_id}

The request bodies, response shapes, validation, and conflict codes are identical to the personal endpoints above. The only differences are access control:

  • Any member can list projects/documents and fetch a single one.
  • Only admins can create, update, or delete projects and documents; a non-admin gets 403 only admins can ….
  • A non-member (or a nonexistent org) gets 404 organization not found, identical to every other org-scoped route.
curl -X POST https://api.ada.ai/me/organizations/7/projects \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{"name": "Team knowledge base"}'

Validation

FieldRuleError type
Project nameRequired (trimmed).invalid_name
Document titleRequired (trimmed).invalid_title
Document slugRequired. 1–80 lowercase alphanumeric or -, and cannot start or end with -.invalid_slug
descriptionOptional. Defaults to "".
contentOptional. Defaults to "".

A malformed JSON body returns 400 invalid JSON: …. The entire request body is capped at 1 MB, so title + slug + content together must fit — a larger body fails to decode.

Errors

StatusMeaningtype
400Bad path id, invalid JSON, or a field failed validation.invalid_name / invalid_title / invalid_slug
401Missing or invalid Stytch session JWT (a proxy key is not accepted here).
403An org mutation attempted by a non-admin.
404Project or document not found, or you don't own it. On org routes, organization not found also covers non-members.
409Name or slug already exists for the owner.name_taken / slug_taken
500Unexpected store or object-storage failure.

Limits (v1)

  • Documents are plain markdown. The server does not parse links, compute backlinks, or validate [[slug]] targets — all of that is client-side.
  • Slugs are unique within a project and may repeat across projects. There is no cross-project linking.
  • Request bodies are capped at 1 MB. There is no separate streaming upload for document content yet.
  • There is no search, versioning, or attachments in v1 — a document is one markdown string under a slug.

On this page