# Workspaces and registry

> Managed workspace model: ULID identity, `.rift` markers, SQLite registry schema, parent-child provenance tree, and upward resolution.

- Repository: anomalyco/rift
- GitHub: https://github.com/anomalyco/rift
- Human docs: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662
- Complete Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/llms-full.txt

## Source Files

- `specs.md`
- `crates/core/src/lib.rs`
- `crates/core/src/marker.rs`
- `crates/core/src/registry.rs`
- `crates/core/src/id.rs`

---

---
title: "Workspaces and registry"
description: "Managed workspace model: ULID identity, `.rift` markers, SQLite registry schema, parent-child provenance tree, and upward resolution."
---

Rift tracks every managed workspace in a central SQLite registry and binds each directory to a stable ULID through a root-level `.rift` marker. The `Manager` in `crates/core` opens the registry, resolves a path to its nearest managed ancestor, and uses `parent_id` links to form a provenance tree from registered source roots down through created child workspaces.

## Managed workspace model

A **managed workspace** is any directory that Rift has registered: a **source root** registered by `init`, or a **created rift** registered by `create`. Management requires two coupled artifacts:

| Artifact | Role |
| --- | --- |
| `.rift` marker | Filesystem identity file at the workspace root; contains the workspace ULID |
| SQLite `rift` row | Authoritative record of `id`, `parent_id`, `path`, and `created_at` |

Operations such as `create`, `list`, `ancestors`, and `remove` resolve the caller's path upward to the nearest `.rift` marker, load the matching registry row, and enforce marker–registry consistency before mutating state.

```text
/projects/app/                    source root (parent_id = NULL)
/projects/.rifts/app/task-a/      created rift (parent_id = app's id)
/projects/.rifts/app/task-b/      created rift (parent_id = app's id)
```

<Note>
Default child storage lives under a sibling `.rifts/<workspace-name>/` directory associated with the **registered source root**, not beside each descendant. See [Storage layout](/storage-layout) for path rules and custom `--into` destinations.
</Note>

## ULID identity

Each workspace receives a **ULID** at first registration or creation. The `RiftId` type wraps `ulid::Ulid::new()` and stores the canonical string form.

| Property | Behavior |
| --- | --- |
| Generation | New ULID on `init` of an unregistered directory; new ULID on every `create` |
| Stability | Identity persists across renames only while the marker and registry stay consistent |
| Uniqueness | `id` is the primary key in SQLite; `path` is a separate, mutable location field |

Created workspaces always get a fresh ULID. The copied `.rift` marker from the source is replaced with the new workspace's identifier before registry insertion.

## `.rift` marker files

The marker is a single file named `.rift` at the workspace root.

```text
01HXXXXXXXXXXXXXXXXXXXXXXXXX\n
```

| Operation | Marker behavior |
| --- | --- |
| `init` (new) | Writes a new ULID after filesystem preparation |
| `init` (existing root, marker missing) | Restores the marker from the existing registry `id` |
| `init` (existing root, marker present) | Verifies marker matches registry before continuing |
| `create` | Writes a new ULID to the destination after copy |
| `remove` (source root) | Deletes the marker; directory remains on disk |
| `remove` (created rift) | Marker moves with the directory into trash |

Git repositories add `/.rift` to `.git/info/exclude` so the marker does not appear in local Git status. See [Git integration](/git-integration).

### Marker verification

Before destructive operations, Rift calls `marker::verify`, which reads `.rift` and compares it to the expected registry `id`. A mismatch surfaces as `marker_mismatch` (FFI) or the `rift marker does not match the registry` error (core).

## SQLite registry

The registry lives in a platform-specific user data directory, opened by `Manager::open_default()`:

```text
<data-local-dir>/rift/rift.sqlite
```

On Linux this resolves through `dirs::data_local_dir()` (typically `~/.local/share/rift/rift.sqlite`, or `$XDG_DATA_HOME/rift/rift.sqlite` when set). The CLI accepts a hidden `--database` flag to override the path for testing or multi-registry isolation.

### Connection settings

On open, the registry applies:

| PRAGMA | Value |
| --- | --- |
| `busy_timeout` | `2000` ms |
| `journal_mode` | `WAL` |
| `foreign_keys` | `ON` |

WAL mode and a busy timeout support concurrent access from multiple CLI processes, agents, or FFI callers without a custom locking protocol.

### Schema

```sql
CREATE TABLE rift (
  id TEXT PRIMARY KEY,
  parent_id TEXT REFERENCES rift(id) ON DELETE CASCADE,
  path TEXT NOT NULL UNIQUE,
  created_at INTEGER NOT NULL
);

CREATE INDEX rift_parent_id_idx ON rift(parent_id);

CREATE TABLE trash (
  id TEXT PRIMARY KEY,
  path TEXT NOT NULL UNIQUE,
  removed_at INTEGER NOT NULL
);
```

```mermaid
erDiagram
  rift ||--o{ rift : "parent_id"
  rift {
    TEXT id PK
    TEXT parent_id FK
    TEXT path UK
    INTEGER created_at
  }
  trash {
    TEXT id PK
    TEXT path UK
    INTEGER removed_at
  }
```

| Column | Semantics |
| --- | --- |
| `rift.id` | Stable ULID; matches `.rift` marker contents |
| `rift.parent_id` | `NULL` for source roots; set to immediate source `id` on `create` |
| `rift.path` | Current absolute filesystem path; not identity |
| `rift.created_at` | Unix epoch milliseconds at insertion |
| `trash.id` | ULID of the removed workspace |
| `trash.path` | Relocated trash directory path |
| `trash.removed_at` | Unix epoch milliseconds when moved to trash |

`ON DELETE CASCADE` on `parent_id` ensures deleting an active ancestor row cascades to descendant rows. Normal removal instead moves subtrees to `trash` first, so no surviving active record depends on deleted ancestry.

## Provenance tree

Provenance is a rooted tree keyed by `parent_id`:

| Workspace type | `parent_id` | Registered by |
| --- | --- | --- |
| Source root | `NULL` | `init` → `insert_root` |
| Created rift | Immediate source `id` | `create` → `insert_child` |

When `create` runs from a nested path or an existing child rift, Rift copies the **resolved source workspace** (nearest `.rift` ancestor), not an earlier root. The new row's `parent_id` is the immediate source's `id`.

```text
app (root, id=A, parent_id=NULL)
├── task-a (id=B, parent_id=A)
└── task-b (id=C, parent_id=A)

create from task-a → task-c (id=D, parent_id=B)
```

`Manager::root` walks `parent_id` upward until `NULL` to locate the registered source root. Default child storage (`default_storage`) is derived from that root, keeping all descendants under `.rifts/<root-name>/` regardless of which managed ancestor was the copy source.

### Tree queries

| API | Scope | Ordering |
| --- | --- | --- |
| `list` | Direct children (`parent_id = ?`) | `created_at`, then `id` |
| `ancestors` | All ancestors up to root | Immediate parent first, then root |
| `subtree` (internal) | Recursive descendants via CTE | Deepest first (`depth DESC, id`) for safe removal |

The recursive subtree query powers `remove`, `remove_all`, and `gc` orphan pruning.

## Upward resolution

`workspace_from_optional` resolves any directory path to the nearest managed workspace by walking `path.ancestors()` from the requested path toward the filesystem root.

```mermaid
flowchart TD
  Start["Requested path"] --> Walk["For each ancestor directory"]
  Walk --> HasMarker{".rift exists?"}
  HasMarker -->|yes| LookupId["Load registry row by marker ULID"]
  LookupId --> KnownId{"Row exists?"}
  KnownId -->|no| UnknownMarker["Error: unknown_marker"]
  KnownId -->|yes| PathMatch{"row.path == directory?"}
  PathMatch -->|no| MarkerMismatch["Error: marker_mismatch"]
  PathMatch -->|yes| Found["Return Record"]
  HasMarker -->|no| RegistryOnly{"Registry row at path?"}
  RegistryOnly -->|yes| MissingMarker["Error: missing_marker"]
  RegistryOnly -->|no| MoreAncestors{"More ancestors?"}
  MoreAncestors -->|yes| Walk
  MoreAncestors -->|no| NotInit["Return None → workspace_not_initialized"]
```

### Resolution outcomes

| Condition | Core error | CLI message |
| --- | --- | --- |
| No marker and no registry row in any ancestor | `WorkspaceNotInitialized` | `no initialized workspace found; run rift init from the root folder` |
| Registry row at path but `.rift` missing | `MissingMarker` | `this workspace is missing its .rift marker; run rift init to restore it` |
| Marker ULID not in registry | `UnknownMarker` | (default error text) |
| Marker ULID exists but `path` column differs | `MarkerMismatch` | (default error text) |
| Marker and registry agree | Success | — |

<Warning>
A marker whose ULID is registered to a **different** directory path is rejected. Copying `.rift` between directories without a matching registry update produces `marker_mismatch`.
</Warning>

### CLI path selection vs core resolution

The CLI defaults omitted path arguments to the current working directory. Core operations always resolve through upward marker search.

`rift init` is special: unless `--here` is passed, the CLI selects the nearest managed ancestor, nearest Git root, or the requested path before calling core `init` on that exact directory. Core `init` itself does not search parents.

## Registry lifecycle operations

| Operation | Active `rift` table | `trash` table | Marker |
| --- | --- | --- | --- |
| `init` (new) | `insert_root` | — | Write new ULID |
| `init` (restore) | Unchanged | — | Rewrite existing ULID |
| `create` | `insert_child` | — | Write new ULID at destination |
| `remove` (child) | Delete subtree rows | Insert trash rows | Moves with directory |
| `remove` (root, `-f`) | Delete root row | Trash existing descendants | Delete marker |
| `gc` | Prune missing paths (conditional) | Delete after filesystem removal | — |

`trash_moved` runs in a transaction: insert into `trash`, then delete from `rift` for each moved record. Filesystem moves to `<storage-parent>/.trash/<id>-<name>` happen before the transaction; failed transactions roll back directory renames.

## Consistency guarantees

Rift treats the marker and registry as a coupled pair:

1. **Identity** is the ULID in both `.rift` and `rift.id`.
2. **Location** is `rift.path`, which must equal the directory containing the marker.
3. **Provenance** is `parent_id`, forming a single rooted tree.
4. **Removal** verifies every existing directory in a subtree has a matching marker before moving to trash.

External deletion is handled by `gc`: active rows whose paths no longer exist are pruned only when no recorded descendant still exists on disk, preventing orphaned provenance links.

## FFI and JavaScript bindings

The same registry model backs the native library, CLI, and FFI packages. Registry-related failures map to stable error codes:

| Code | Meaning |
| --- | --- |
| `workspace_not_initialized` | No `.rift` ancestor found |
| `missing_marker` | Registry row without marker |
| `unknown_marker` | Marker ULID not in registry |
| `marker_mismatch` | Marker ULID does not match expected path |
| `not_managed` | Ancestor chain references a missing parent row |

Postcreate hooks receive `RIFT_ID` and `RIFT_PARENT_ID` from the destination and source registry records. See [Postcreate hooks](/postcreate-hooks) and [JavaScript API reference](/javascript-api).

## Related pages

<CardGroup>
  <Card title="Initialize a workspace" href="/initialize-workspace">
    Register a source root, restore deleted markers, and write the first registry row.
  </Card>
  <Card title="Create workspaces" href="/create-workspaces">
    Copy from a resolved source, assign a new ULID, and record parent provenance.
  </Card>
  <Card title="Storage layout" href="/storage-layout">
    Default `.rifts` paths, trash naming, and central database location.
  </Card>
  <Card title="Manage and remove workspaces" href="/manage-workspaces">
    List children, trace ancestors, trash subtrees, and run garbage collection.
  </Card>
  <Card title="Error codes" href="/error-codes">
    Full `RiftError` catalog including marker and registry variants.
  </Card>
</CardGroup>
