4.2 — CRUD endpoints
On this page
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:
| Method | Path | Action |
|---|---|---|
GET | /api/crud/[entity] | List entities (with query param filters) |
GET | /api/crud/[entity]?id=UUID | Get 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=UUID | Delete 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
| Entity | Path Segment | Parent |
|---|---|---|
| Projects | projects | -- |
| Features | features | Project |
| Personas | personas | Project |
| User Stories | user-stories | Feature |
| Acceptance Criteria | acceptance-criteria | User Story |
| Pages | pages | Project |
| Schema | schema | Project |
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:
| Field | Meaning |
|---|---|
total | Exact number of rows matching your filters, ignoring the page window |
limit | Page size in effect — from ?limit=, default 100 |
offset | Row the page started at — from ?offset=, default 0 |
hasMore | true 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:
| Parameter | Applies to | Description |
|---|---|---|
project_id | features, personas, pages, user-stories, acceptance-criteria | Filter by parent project |
feature_id | user-stories, acceptance-criteria | Filter by parent feature |
story_id | acceptance-criteria | Filter criteria by user story |
status | projects, features, pages, user-stories, acceptance-criteria | Filter by status |
priority | features, user-stories | Filter by priority level |
category, complexity | features | Filter by category or complexity |
scenario_category | acceptance-criteria | Filter by BDD scenario category |
search | All list endpoints | Case-insensitive match on name/title and description |
limit, offset | All list endpoints | Page window — see Pagination |
includeRelationships | features, pages, user-stories | Single fetch only — nest related entities under relationships |
includeAnalysis, includeArchitecture | projects | Nest 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.