Projects and workspaces — model and API
Audience: developer working on projects/workspaces CRUD or integrating the .arka-deck/ tree.
Workspace (group — metadata only) │ ├── Project A (= disk folder + marker .arka-deck/project/marker.json) ├── Project B └── Project C- Workspace: pure metadata. Persisted in the global user index.
- Project: a disk folder on the machine. The disk marker (
<projectPath>/.arka-deck/project/marker.json) is the source of truth. - Global index:
<arkaHome>/index/projects.json+workspaces.json= reconstructible aggregated view.
Default path: ~/.arka-deck/ (override via env ARKA_DECK_HOME).
1. Models
Section titled “1. Models”Workspace
Section titled “Workspace”interface Workspace { id: string; // UUID v4 name: string; // unique in index, ≤ 80 chars description: string; createdAt: string; // ISO 8601}Persisted only in <arkaHome>/index/workspaces.json.
Project
Section titled “Project”interface Project { id: string; // UUID v4 name: string; // display label, ≤ 80 chars description: string; path: string; // absolute folder path, IMMUTABLE workspaceId: string; createdAt: string; lastUsedAt: string; // updated on each open}Persisted twice:
- Disk marker:
<projectPath>/.arka-deck/project/marker.json← source of truth - Global index:
<arkaHome>/index/projects.json← reconstructible from markers
On conflict (marker says X, index says Y) → marker wins (cf. findByCwd which auto-reimports the index if out-of-sync).
2. Architecture (hexagonal)
Section titled “2. Architecture (hexagonal)”Outbound ports
Section titled “Outbound ports”| Port | Role |
|---|---|
ProjectStore | Reads/writes the disk marker |
ProjectIndexStore | Reads/writes the global user index |
WorkspaceStore | Reads/writes the global workspaces index |
InstalledItemsStore | Reads installed items list (agents/hooks/skills) — used by purge |
ChatSessionStore | Reads/deletes linked chat sessions (used by forget/purge) |
Filesystem | FS wrapper (read/write/exists/rm/readdir/…) |
Inbound ports
Section titled “Inbound ports”ForProjects:
create({name, path, description, workspaceId})— creates marker + indexlist()— all projects,lastUsedAtdesc orderlistByWorkspace(workspaceId)— filtergetById(id)— index lookupforget(id)— removes from index, disk marker preservedupdate({id, name, description, workspaceId})— patch (pathimmutable)touchLastUsed(id)— updateslastUsedAtfindByCwd(cwd)— auto-detect at bootpurge(id)— forget + cleanup.arka-deck/+ installed agents + chat sessions
ForWorkspaces:
create({name, description})— name must be uniquelist()— all, newest firstgetById(id)update({id, name, description})— rechecks uniquenessdelete(id)— refuses if attached projects (WorkspaceNotEmptyError)
Production adapters
Section titled “Production adapters”| Adapter | File |
|---|---|
FsProjectStore | adapters/outbound/filesystem/fs-project-store.ts |
FsProjectIndexStore | adapters/outbound/filesystem/fs-project-index-store.ts |
FsWorkspaceStore | adapters/outbound/filesystem/fs-workspace-store.ts |
3. Disk tree
Section titled “3. Disk tree”~/.arka-deck/ ← arkaHome (override via ARKA_DECK_HOME) index/ workspaces.json projects.json preferences/ user.json security.json ← allowed-path allowlist cache/ catalogue/... debrief/... providers/ providers.db ← provider instances SQLite secrets/ secrets.key ← AES-256-GCM master key (mode 600) chat/ chat.db ← chat sessions/messages SQLite
<projectPath>/ .arka-deck/ project/ marker.json ← project source of truth agents/installed.json hooks/installed.json skills/installed.json memory/ ← cf. addons-firstparty/memory-local .claude/ agents/<slug>/ ← recruited agent (HYOS profile) settings.json ← Claude Code settings (never touched)4. Project lifecycle
Section titled “4. Project lifecycle”Creation
Section titled “Creation”ForProjects.create({ name, path, description, workspaceId }) ↓[Check path = disk directory] ↓[Check marker absent (anti-duplicate)] ↓[Check workspaceId exists] ↓[Filesystem.mkdir <path>/.arka-deck/project/, recursive] ↓[ProjectStore.save → marker.json] ↓[ProjectIndexStore.save → append to projects.json] ↓EventBus → 'project.created' (async)Boot detection — findByCwd
Section titled “Boot detection — findByCwd”ForProjects.findByCwd(cwd) ↓[ProjectStore.exists(cwd) ? = marker lookup] ↓ ├─ Marker absent → null (not an arka-deck project) │ └─ Marker present ↓ [Read marker → Project] ↓ [ProjectIndexStore.getById → present?] ↓ ├─ Yes → returns project └─ No → re-imports (index is reconstructible) + returns projectTypical case: a user git clones an existing project on another machine. arka-deck detects the marker at next boot and adds it to its index.
Forget vs Purge
Section titled “Forget vs Purge”| Mode | Index | Disk marker | .arka-deck/ | Agents .claude/agents/ | Chat sessions SQLite |
|---|---|---|---|---|---|
forget | removed | preserved | preserved | preserved | deleted |
purge | removed | deleted | deleted | deleted (only arka-deck installed) | deleted |
purge never touches non-arka-deck .claude/ files (user settings, non-arka agents). Only artifacts tracked in installed.json.
Code: core/use-cases/projects/build-for-projects.ts:purgeProjectUseCase.
Security allowlist
Section titled “Security allowlist”The web server checks a project creation path is in the user allowlist before writing the marker. Default allowlist: ~, ~/Documents, ~/Projects, etc. — configurable in ~/.arka-deck/preferences/security.json.
Refusal → HTTP 403 PATH_NOT_ALLOWED with a body listing allowed locations.
5. Workspace lifecycle
Section titled “5. Workspace lifecycle”Creation
Section titled “Creation”forWorkspaces.create({ name: 'Arkalabs', description: 'Main workspace' });namemust be globally unique (no per-user — single-user app)- Raises
WorkspaceNameAlreadyExistsErrorotherwise
Deletion — non-empty invariant
Section titled “Deletion — non-empty invariant”await forWorkspaces.delete(workspaceId);// → throws WorkspaceNotEmptyError if attached projectsNo automatic cascade. The user must explicitly move or delete their projects first.
The UI WorkspaceDeleteTunnel:
- Lists attached projects if non-empty
- Disables the “Delete” button while
attachedProjects.length > 0 - Offers a “Go to projects” shortcut
6. Server API
Section titled “6. Server API”Workspaces
Section titled “Workspaces”GET /api/workspaces → listGET /api/workspaces/:id → detailPOST /api/workspaces → createPUT /api/workspaces/:id → patchDELETE /api/workspaces/:id → delete (refuses if non-empty)GET /api/workspaces/:id/projects → projects of this workspaceProjects
Section titled “Projects”GET /api/projects → list all projectsGET /api/projects/:id → detailGET /api/projects/find-by-cwd?path=... → auto-detect by markerPOST /api/projects → create (checks allowlist + workspace exists)PUT /api/projects/:id → patch (path immutable)DELETE /api/projects/:id → forget (204)DELETE /api/projects/:id?purge=true → full purge, returns {deletedPaths}POST /api/projects/:id/touch → touchLastUsed (200 + DTO)Tunnels
Section titled “Tunnels”| Tunnel | Steps | Scope |
|---|---|---|
WorkspaceTunnel | 3 (name → description → confirm) | Creation |
WorkspaceEditTunnel | 3 | Edit |
WorkspaceDeleteTunnel | 2 (check non-empty → confirm) | Deletion blocked if attached projects |
ProjectTunnel | 5 (path → name → desc → workspace → confirm) | Creation with FolderPicker + auto-suggest server cwd + conflict alert |
ProjectEditTunnel | 4 (name → desc → workspace → confirm) | Edit (path immutable) |
ProjectDeleteTunnel | 3 (forget/purge mode → confirm → result) | Deletion with mode choice + deletedPaths recap |
All tunnels expose the blocking loader overlay during long mutations (busy={isSubmitting}).
- HomeView: project grid of the current workspace + workspace header banner (Edit / Delete directly accessible).
- ProjectView: project detail — installed agents, chat sessions, actions (Edit project / Launch options / Delete this project).
- ContextBand (top bar): workspace + project selector + Edit/Delete actions in the workspace dropdown footer.
8. Event bus
Section titled “8. Event bus”The projects/workspaces module publishes on the arka-deck bus:
| Event | Mode | Payload |
|---|---|---|
workspace.created | async | { workspaceId, name, createdAt } |
workspace.updated | async | { workspaceId, name, description } |
workspace.deleted | sync | { workspaceId } |
project.created | async | { projectId, projectPath, workspaceId, name } |
project.updated | async | { projectId, name, workspaceId } |
project.forgotten | sync | { projectId } |
project.purged | sync | { projectId, projectPath, deletedPaths } |
project.last-used | async | { projectId, lastUsedAt } |
project.detected-by-cwd | async | { projectId, projectPath, cwd } |
Expected subscribers: memory (session cleanup on purge), governance (creation audit), squad (recompose if project added).
9. How to add a cleanup to purge
Section titled “9. How to add a cleanup to purge”If your module places files in <projectPath> (other than .arka-deck/ which is cleaned by default), implement a cleanup on the purgeProjectUseCase side.
Option A — Subscribe to project.purged (recommended)
Section titled “Option A — Subscribe to project.purged (recommended)”bus.subscribe('project.purged', async (event) => { await myStore.cleanupProject(event.projectPath);});Option B — Extend purgeProjectUseCase
Section titled “Option B — Extend purgeProjectUseCase”Modify core/use-cases/projects/build-for-projects.ts to accept a new optional dep and call its cleanup. Prefer Option A to respect decoupling.
10. Security
Section titled “10. Security”| Guarantee | Mechanism |
|---|---|
| No path traversal | pathPolicy.isAllowed(path) on the HTTP route side before writing marker |
| User allowlist | ~/.arka-deck/preferences/security.json — validated glob patterns |
No write outside .arka-deck/ on create | Use-case only touches <projectPath>/.arka-deck/project/marker.json |
| Purge does not touch user files | Cleanup limited to items tracked in installed.json |
| Atomic marker + index write | tmp + rename POSIX |
| Corruption detection | MarkerCorruptedError HTTP 422 — marker is unusable, index remains source |
11. Anti-patterns to avoid
Section titled “11. Anti-patterns to avoid”Read the marker from the UI
Section titled “Read the marker from the UI”The disk marker is not exposed by HTTP routes. The UI consumes server DTOs only via /api/projects (the aggregated index).
Bypass findByCwd
Section titled “Bypass findByCwd”// ❌const proj = await projectIndexStore.list().find((p) => p.path === cwd);
// ✅const proj = await forProjects.findByCwd(cwd);Modify a project’s path
Section titled “Modify a project’s path”The path is immutable on Project.create. To change the path:
forgetthe old project (preserves the marker)createa new project at the new path- Manually migrate
.arka-deck/files if needed
Silently delete a workspace cascade
Section titled “Silently delete a workspace cascade”// ❌await forWorkspaces.delete(wid);forceDeleteAll(wid);
// ✅const projects = await forProjects.listByWorkspace(wid);await Promise.all(projects.map((p) => forProjects.forget(p.id)));await forWorkspaces.delete(wid);Trust the index without the marker
Section titled “Trust the index without the marker”For absolute truth on a project’s state, read the disk marker (projectStore.load(path)). The index may be out-of-sync.
12. Quick reference
Section titled “12. Quick reference”| Action | File |
|---|---|
| Domain Project | core/domain/project/project.ts |
| Domain Workspace | core/domain/workspace/workspace.ts |
| Projects use-cases | core/use-cases/projects/build-for-projects.ts |
| Workspaces use-cases | core/use-cases/workspaces/build-for-workspaces.ts |
| Marker store | adapters/outbound/filesystem/fs-project-store.ts |
| Index store | adapters/outbound/filesystem/fs-project-index-store.ts |
| Workspace store | adapters/outbound/filesystem/fs-workspace-store.ts |
| Path policy | adapters/outbound/system/preferences-path-policy.ts |
| Server routes | adapters/inbound/web/server/src/routes/{projects,workspaces}.ts |
| UI tunnels | adapters/inbound/web/ui/src/components/tunnel/*Tunnel.tsx |
| UI hooks | adapters/inbound/web/ui/src/hooks/{useProjects,useWorkspaces}.ts |
| Errors | core/domain/errors.ts |