4.2 — CRUD endpoints

4.2 — CRUD endpoints

CRUD is the primary data access API — reading and writing every entity type (projects, features, user stories, acceptance criteria, personas, pages, schema). All entities except schema follow the same shape, so once you've learned one, you've learned them all.

Everything is scoped to projects owned by the authenticated user: list reads join through projects.user_id, and writes verify ownership before touching a row. Another user's data simply isn't visible — a single fetch returns 404, a list omits it, and a write returns 404 "not found or access denied" (deleting a feature you don't own is the one case that answers 403). A project shared with you through a team is not reachable here; use the app or the MCP tools for that.

Route pattern

Every entity type shares the same URL structure:

MethodPathAction
GET/api/crud/[entity]List entities (with query param filters)
GET/api/crud/[entity]?id=UUIDGet a single entity by ID
POST/api/crud/[entity]Create a new entity
PUT/api/crud/[entity]Update an existing entity (id goes in the body)
DELETE/api/crud/[entity]?id=UUIDDelete an entity (?id= or { "id": … } in the body)

Note it is PUT, not PATCH — but the semantics are a patch: send only the fields you want changed, alongside the id.

Available entities

EntityPath SegmentParent
Projectsprojects--
FeaturesfeaturesProject
PersonaspersonasProject
User Storiesuser-storiesFeature
Acceptance Criteriaacceptance-criteriaUser Story
PagespagesProject
SchemaschemaProject

schema is the odd one out: it accepts POST only, takes the whole data model in one camelCase body ({ projectId, tables, relationships? }), and reconciles it against what's already there rather than replacing it — so re-posting is safe. It returns 201 with { tables: [...], meta: { tables, relationships } }.

Sections have no route of their own. They come back nested inside a single page fetch: GET /api/crud/pages?id=UUID&includeRelationships=true returns the page with a relationships.sections array (plus the user stories and page links attached to it).

Examples

List features for a project

GET /api/crud/features?project_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890

Response 200 OK — rows under a key named after the entity, plus the meta pagination block:

{
  "features": [
    {
      "id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
      "name": "User Authentication",
      "description": "OAuth2 login with Google and GitHub providers",
      "priority": "high",
      "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "created_at": "2026-03-15T10:30:00Z"
    },
    {
      "id": "f2b3c4d5-e6f7-8901-bcde-f12345678901",
      "name": "Dashboard Analytics",
      "description": "Real-time usage metrics and charts",
      "priority": "medium",
      "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "created_at": "2026-03-15T10:32:00Z"
    }
  ],
  "meta": { "total": 2, "limit": 100, "offset": 0, "hasMore": false }
}

The list key matches the table, not the path segment: projects, features, personas, pages, user_stories, acceptance_criteria.

Get a single feature

GET /api/crud/features?id=f1a2b3c4-d5e6-7890-abcd-ef1234567890

Response 200 OK — the bare row, no wrapper:

{
  "id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
  "name": "User Authentication",
  "description": "OAuth2 login with Google and GitHub providers",
  "priority": "high",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "created_at": "2026-03-15T10:30:00Z"
}

Add &includeRelationships=true and the row gains a relationships object — for a feature, its user stories and the personas those stories point at.

Create a feature

POST /api/crud/features
Content-Type: application/json

{
  "name": "User Authentication",
  "description": "OAuth2 login with Google and GitHub providers",
  "priority": "high",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

Response 201 Created — the created row:

{
  "id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
  "name": "User Authentication",
  "description": "OAuth2 login with Google and GitHub providers",
  "priority": "high",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "created_at": "2026-03-15T10:30:00Z"
}

Update a feature

PUT /api/crud/features
Content-Type: application/json

{
  "id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
  "priority": "low",
  "description": "OAuth2 login with Google, GitHub, and Microsoft providers"
}

Response 200 OK — the updated row:

{
  "id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
  "name": "User Authentication",
  "description": "OAuth2 login with Google, GitHub, and Microsoft providers",
  "priority": "low",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "created_at": "2026-03-15T10:30:00Z"
}

Delete a feature

DELETE /api/crud/features?id=f1a2b3c4-d5e6-7890-abcd-ef1234567890

Response 200 OK — with a body, not an empty 204:

{
  "id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

The delete body differs by entity: features return { id, project_id }, user stories and acceptance criteria return { success, id }, pages and personas return the whole deleted row, and projects return { success, deleted, message }. Don't parse it — check the status code.

Pagination

Every list endpoint is paged, and every one of them answers with the same meta block:

FieldMeaning
totalExact number of rows matching your filters, ignoring the page window
limitPage size in effect — from ?limit=, default 100
offsetRow the page started at — from ?offset=, default 0
hasMoretrue when total is greater than offset + limit

Walk a project by incrementing offset by limit until hasMore is false:

GET /api/crud/user-stories?project_id=…&limit=50&offset=0
GET /api/crud/user-stories?project_id=…&limit=50&offset=50

Filtering

List endpoints accept query parameters for filtering. They are snake_case, matching the column:

ParameterApplies toDescription
project_idfeatures, personas, pages, user-stories, acceptance-criteriaFilter by parent project
feature_iduser-stories, acceptance-criteriaFilter by parent feature
story_idacceptance-criteriaFilter criteria by user story
statusprojects, features, pages, user-stories, acceptance-criteriaFilter by status
priorityfeatures, user-storiesFilter by priority level
category, complexityfeaturesFilter by category or complexity
scenario_categoryacceptance-criteriaFilter by BDD scenario category
searchAll list endpointsCase-insensitive match on name/title and description
limit, offsetAll list endpointsPage window — see Pagination
includeRelationshipsfeatures, pages, user-storiesSingle fetch only — nest related entities under relationships
includeAnalysis, includeArchitectureprojectsNest the full spec, or restore the large app_architecture blob that list responses omit

Error responses

{
  "error": "Feature not found"
}

Validation failures add a details object with the per-field problems. Common errors: 400 for missing or invalid fields, 401 for unauthenticated requests, 404 for a resource that doesn't exist or isn't yours, and 409 for an acceptance criterion whose Given/When/Then already exists on that story.

Where to go next

Generation endpoints