# Projects **Projects** are organizational containers that group channels, knowledge bases, notes, and files. They follow the scope model (personal, team, global). All endpoints require authentication (`Authorization: Bearer `). Admin endpoints additionally require `role=admin`. ## Project Object ```json { "id": "uuid", "name": "Q3 Research", "description": "...", "color": "#3b82f6", "icon": "folder", "scope": "personal|team|global", "owner_id": "uuid", "team_id": "uuid|null", "workspace_id": "uuid|null", "is_archived": false, "settings": {}, "channel_count": 5, "kb_count": 2, "note_count": 3, "created_at": "2025-01-15T10:30:00Z", "updated_at": "2025-01-15T10:30:00Z" } ``` | Field | Type | Notes | |-------|------|-------| | `id` | uuid | PK, auto-generated | | `name` | string | Required, max 200 chars | | `description` | string | Optional | | `color` | string? | CSS hex color, max 7 chars | | `icon` | string? | Icon identifier, max 50 chars | | `scope` | enum | `personal`, `team`, `global` | | `owner_id` | uuid | Set from JWT, immutable | | `team_id` | uuid? | Set for team-scoped projects | | `workspace_id` | uuid? | Linked workspace | | `is_archived` | bool | Default `false` | | `settings` | object | JSONB, default `{}` | | `channel_count` | int | Computed, omitted when 0 | | `kb_count` | int | Computed, omitted when 0 | | `note_count` | int | Computed, omitted when 0 | | `created_at` | datetime | Auto-set | | `updated_at` | datetime | Auto-updated on change | ## Project CRUD ### List Projects ``` GET /projects → { "data": [...] } ``` Query params: `?include_archived=true` to include archived projects. Returns projects the user can access: personal (own), team (member), and global. Ordered by name. ### Create Project ``` POST /projects → 201, project object ``` ```json { "name": "My Project", "description": "Optional description", "color": "#3b82f6", "icon": "folder" } ``` | Field | Required | Notes | |-------|----------|-------| | `name` | yes | Max 200 chars | | `description` | no | | | `color` | no | CSS hex | | `icon` | no | Icon identifier | Scope is set to `personal` and `owner_id` is taken from the JWT. Returns the created project object (bare, no envelope). ### Get Project ``` GET /projects/:id → project object ``` Returns bare project object (no envelope). 404 if not found or not accessible. ### Update Project ``` PUT /projects/:id → updated project object ``` Partial update — only supplied fields are changed. ```json { "name": "New Name", "description": "Updated", "color": "#ef4444", "icon": "star", "is_archived": true, "workspace_id": "uuid|null", "settings": { "key": "value" } } ``` Settings are merged (overlay, not replace). Returns the refreshed project object after update. ### Delete Project ``` DELETE /projects/:id → { "message": "project deleted" } ``` Only the project owner or an admin can delete. Channels get `project_id` set to NULL; junction rows cascade. --- ## Channel Association ``` GET /projects/:id/channels → { "data": [...] } POST /projects/:id/channels ← { "channel_id", "position" } DELETE /projects/:id/channels/:channelId → { "message": "channel removed" } PUT /projects/:id/channels/reorder ← { "channel_ids": ["uuid1", "uuid2"] } ``` A channel can only belong to one project. `POST` performs an atomic move if the channel is already in a different project. The user must own the channel being added. **Channel association object:** ```json { "project_id": "uuid", "channel_id": "uuid", "position": 0, "folder": "", "added_at": "2025-01-15T10:30:00Z" } ``` **POST request:** | Field | Required | Notes | |-------|----------|-------| | `channel_id` | yes | UUID of channel to add | | `position` | no | Sort order (default 0) | --- ## KB Association ``` GET /projects/:id/knowledge-bases → { "data": [...] } POST /projects/:id/knowledge-bases ← { "kb_id", "auto_search" } DELETE /projects/:id/knowledge-bases/:kbId → { "message": "KB removed" } ``` KBs bound to a project are automatically available to every channel in that project (resolution chain injection at completion time). Upsert on conflict — re-posting updates `auto_search`. **KB association object:** ```json { "project_id": "uuid", "kb_id": "uuid", "auto_search": true, "added_at": "2025-01-15T10:30:00Z", "name": "KB Name" } ``` `name` is enriched via JOIN from `knowledge_bases.name`. **POST request:** | Field | Required | Notes | |-------|----------|-------| | `kb_id` | yes | UUID of knowledge base | | `auto_search` | no | Default `false` | --- ## Note Association ``` GET /projects/:id/notes → { "data": [...] } POST /projects/:id/notes ← { "note_id" } DELETE /projects/:id/notes/:noteId → { "message": "note removed" } ``` **Note association object:** ```json { "project_id": "uuid", "note_id": "uuid", "added_at": "2025-01-15T10:30:00Z", "title": "Note Title" } ``` `title` is enriched via JOIN from `notes.title`. Insert uses ON CONFLICT DO NOTHING (idempotent). --- ## Project Files (v0.37.17 — workspace-backed) Project files are stored in the project's workspace (auto-created on first upload). The response uses `"files"` key, not `"data"`. Files are `WorkspaceFile` objects with tree structure (directories + files). ### List Files ``` GET /projects/:id/files → { "files": [...], "count": N } ?path=/docs&recursive=true ``` | Param | Default | Notes | |-------|---------|-------| | `path` | `""` | Directory to list (empty = root) | | `recursive` | `true` | Include subdirectories | ### Upload File ``` POST /projects/:id/files ← multipart/form-data ?path=/docs ``` Multipart form field: `file`. Optional `path` query param to upload into a subdirectory. Auto-creates the project workspace on first upload. Returns `201` with `{ path, filename, content_type, size_bytes }`. **Errors:** `413` if file exceeds size limit or workspace quota. ### Download File ``` GET /projects/:id/files/download?path=/docs/readme.md ``` Streams raw file content with `Content-Disposition: attachment`. ### Delete File ``` DELETE /projects/:id/files?path=/docs/readme.md &recursive=false ``` `recursive=true` to delete directories with contents. ### Create Directory ``` POST /projects/:id/files/mkdir ← { "path": "/docs/images" } ``` Path also accepted via `?path=` query param. Returns `201`. ### Upload Archive ``` POST /projects/:id/archive/upload ← multipart/form-data ``` Multipart field: `file`. Accepts `.zip`, `.tar.gz`, `.tgz`. Extracts contents into the workspace root. Returns `{ ok: true, files_extracted: N }`. ### Download Archive ``` GET /projects/:id/archive/download?format=zip ``` Bundles all project files into a zip (or `tar.gz`). Streams with `Content-Disposition: attachment`. --- ## Admin Project Management ``` GET /admin/projects → { "data": [...] } DELETE /admin/projects/:id → { "message": "project deleted" } ``` Query params: `?include_archived=true`. Cross-instance visibility for platform admins. Admin list returns an enriched object with extra fields: ```json { "id": "uuid", "name": "...", "description": "...", "scope": "personal", "owner_id": "uuid", "team_id": null, "is_archived": false, "created_at": "...", "updated_at": "...", "channel_count": 5, "kb_count": 2, "note_count": 3, "owner_name": "jdoe" } ``` Note: admin list object omits `color`, `icon`, `workspace_id`, and `settings` (uses a local struct, not the full model). ---