# Storage layout

> Default `.rifts` sibling storage, custom `--into` paths, trash relocation naming, and central SQLite database location.

- 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`
- `README.md`
- `crates/core/src/lib.rs`
- `crates/core/src/registry.rs`
- `crates/core/src/marker.rs`

---

---
title: "Storage layout"
description: "Default `.rifts` sibling storage, custom `--into` paths, trash relocation naming, and central SQLite database location."
---

Rift separates three on-disk concerns: the source workspace at its original path, created child workspaces under a storage parent (default sibling `.rifts/<basename>/` or a custom `--into` directory), and a single central SQLite registry at `<user-data>/rift/rift.sqlite` that tracks active paths, trash paths, and parent-child provenance.

## On-disk layout

Every managed workspace carries a `.rift` marker at its root containing a ULID. The registry stores that same ULID plus the workspace’s current filesystem path. Created workspaces are never stored inside the workspace being copied, because an exact copy would recursively include existing rifts.

```text
~/code/                              parent of source root
├── app/                             registered source workspace
│   ├── .rift                        ULID identity marker
│   └── src/                         project files
└── .rifts/
    └── app/                         default storage parent for app’s children
        ├── parser-fix/              active created rift (--name parser-fix)
        ├── amber-brook/             active rift (random name when --name omitted)
        └── .trash/                  removed rifts awaiting gc
            ├── 01HXYZ...-parser-fix
            └── 01HABC...-amber-brook
```

<Note>
Storage parent resolution always walks up to the **registered source root**, not the immediate parent rift. Creating from a child rift still stores new workspaces under the root’s `.rifts/<basename>/`, not beside the child.
</Note>

### Default storage path

When `into` is omitted, Rift computes the storage parent as:

```text
<parent-of-root>/.rifts/<basename-of-root>/
```

For a source root at `/projects/app/`, the default storage parent is `/projects/.rifts/app/`. Each `rift create` adds a single path segment for the rift name under that directory.

| Component | Resolved from | Example |
| --- | --- | --- |
| Storage parent | Registered root’s parent + `.rifts` + root basename | `/projects/.rifts/app/` |
| Created rift path | Storage parent + rift name | `/projects/.rifts/app/parser-fix/` |
| Trash directory | Storage parent + `.trash/` | `/projects/.rifts/app/.trash/` |

Rift creates the storage parent directory if it does not exist, then canonicalizes it before joining the rift name.

### Identity markers

Each workspace root contains a `.rift` file with one ULID line (no JSON wrapper). On create, the copied marker is replaced with the new workspace’s ULID. The registry and marker must agree; mismatches block removal and other managed operations.

## Custom storage with `--into`

Override the default storage parent by passing `--into` on the CLI or `into` through the FFI/JavaScript API.

<ParamField body="into" type="AbsolutePath">
Parent directory for the new rift. Relative paths resolve from the current working directory. The final destination is `<canonical-into>/<name>/`.
</ParamField>

<RequestExample>

```bash
rift create --name hotfix --into /fast/rifts
```

</RequestExample>

<ResponseExample>

```text
/fast/rifts/hotfix
```

</ResponseExample>

Custom storage is fully supported: trash relocation uses the rift’s actual storage parent, so removed rifts from custom `--into` paths land in `<into>/.trash/<id>-<name>` on the same filesystem.

### Constraints

| Constraint | Behavior |
| --- | --- |
| Same filesystem | Copy-on-write requires source and destination on the same filesystem. Cross-device `--into` fails with `copy-on-write cloning unavailable`. |
| Not inside source | Destination paths that fall inside the source tree are rejected with `cannot copy a workspace into itself`. |
| Single path segment name | Names must be one path component: no `/`, no `.`, no `..`. |
| No collision | If `<into>/<name>` already exists, create fails with `rift directory already exists`. |
| Mount-root sources | When the registered root has no parent (filesystem mount root), the default sibling `.rifts/` path may not share a CoW-capable filesystem with the source. Pass `--into` on the same filesystem. |

## Trash relocation

`rift remove` does not delete created rift directories immediately. It renames each rift in the removed subtree from its active path into adjacent trash storage, deepest-first, then updates the registry.

### Trash path format

```text
<storage-parent>/.trash/<ulid>-<original-basename>
```

| Field | Source |
| --- | --- |
| `storage-parent` | Parent directory of the rift’s active path (default `.rifts/<basename>/` or custom `--into`) |
| `ulid` | The rift’s registry ID from its `.rift` marker |
| `original-basename` | Final path segment of the active rift directory |

Example: removing `/projects/.rifts/app/parser-fix/` moves it to `/projects/.rifts/app/.trash/01HXYZABCDEF-parser-fix/`.

### Registry transition on remove

After successful filesystem renames:

1. Active `rift` table rows for the removed subtree are deleted.
2. Corresponding `trash` table rows are inserted with the **new trash path** (not the original active path).
3. `removed_at` is recorded as a millisecond Unix timestamp.

If a trash target already exists at the computed path, removal fails before any registry change. On partial failure during rename, earlier moves in the batch are rolled back.

### Physical deletion with `rift gc`

`rift gc` walks trash registry entries, physically deletes each trash directory using the platform copy strategy, then deletes the trash record. It also prunes active registry entries whose directories were deleted outside Rift when no recorded descendant still exists on disk.

```text
active rift  --remove-->  .trash/<id>-<name>  --gc-->  (deleted)
```

<Warning>
Unregistering a source root with `rift remove -f` preserves the source directory and removes its `.rift` marker, but moves all existing descendants into trash using the same `<parent>/.trash/<id>-<name>` scheme relative to each rift’s storage parent.
</Warning>

## Central SQLite database

Metadata lives in one shared SQLite database per machine (or per explicit override), not inside individual workspaces. Multiple processes and agents can create, inspect, and remove rifts concurrently; the registry uses WAL journaling, a 2-second busy timeout, and foreign keys.

### Default location

`Manager::open_default()` resolves the database path via `dirs::data_local_dir()`:

```text
<platform-user-data-local>/rift/rift.sqlite
```

| Platform | Typical path |
| --- | --- |
| Linux | `$XDG_DATA_HOME/rift/rift.sqlite` or `~/.local/share/rift/rift.sqlite` |
| macOS | `~/Library/Application Support/rift/rift.sqlite` |
| Windows | `%LOCALAPPDATA%\rift\rift.sqlite` |

The parent `rift/` directory is created automatically on first open if missing.

### Custom database path

Override the default for testing or multi-registry isolation:

| Surface | Parameter | Visibility |
| --- | --- | --- |
| CLI | `--database <path>` | Hidden global flag |
| FFI / JavaScript | `database?: string` | Per-request option |

Integration tests pass `--database` with an isolated file while setting `XDG_DATA_HOME` to verify the default path convention separately.

### 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
);
```

| Table | Holds | Path column meaning |
| --- | --- | --- |
| `rift` | Active managed workspaces | Current on-disk location |
| `trash` | Logically removed workspaces | Current trash directory location |

`path` is the workspace’s current filesystem location, not its identity. Identity is the ULID in `id` and the `.rift` marker. `parent_id` links created rifts to their immediate source; the registered source root has `parent_id = NULL`.

## How storage, registry, and markers interact

```mermaid
flowchart LR
  subgraph filesystem [Filesystem]
    SRC["Source root\napp/ + .rift"]
    STORE[".rifts/app/"]
    CHILD["Created rift\nparser-fix/ + .rift"]
    TRASH[".trash/id-name"]
  end

  subgraph registry [SQLite rift.sqlite]
    ACTIVE["rift table"]
    TRASHTBL["trash table"]
  end

  SRC -->|"rift create"| CHILD
  CHILD -->|"stored in"| STORE
  CHILD -->|"ULID + path"| ACTIVE
  CHILD -->|"rift remove"| TRASH
  TRASH -->|"path updated"| TRASHTBL
  TRASH -->|"rift gc"| DELETE["Physical delete"]
```

On `rift create`:

1. Resolve the managed source and its registered root.
2. Choose storage parent (default `.rifts/<basename>/` or `--into`).
3. Copy the workspace with the platform CoW strategy.
4. Write a new `.rift` marker with a fresh ULID.
5. Insert a `rift` row with `parent_id` pointing at the source rift.

On `rift remove` of a created rift:

1. Collect the full active subtree (deepest first).
2. Verify each existing directory’s `.rift` marker matches the registry.
3. Rename each directory into `.trash/<id>-<name>` under its storage parent.
4. Move registry rows from `rift` to `trash`.

## Verification signals

| Operation | Expected filesystem signal | Expected registry signal |
| --- | --- | --- |
| `rift init` | `.rift` appears at source root | One `rift` row with `parent_id = NULL` |
| `rift create` | New directory under storage parent with its own `.rift` | New `rift` row with `parent_id` set |
| `rift list` | (no filesystem change) | Returns direct children by `parent_id` |
| `rift remove` | Active path gone; `.trash/<id>-<name>` exists | Row moved from `rift` to `trash` |
| `rift gc` | Trash directory deleted | `trash` row removed |

## Related pages

<CardGroup>
  <Card title="Workspaces and registry" href="/workspaces-and-registry">
    ULID identity, `.rift` markers, SQLite schema details, and parent-child provenance resolution.
  </Card>
  <Card title="Create workspaces" href="/create-workspaces">
    `--name`, `--into`, filtered vs `--copy-all` copy, and post-create hook execution.
  </Card>
  <Card title="Manage and remove workspaces" href="/manage-workspaces">
    Trash removal, source-root unregister, `--children` mode, and `rift gc` physical deletion.
  </Card>
  <Card title="Copy strategies and platforms" href="/copy-strategies">
    Platform CoW backends and why destination must share the source filesystem.
  </Card>
</CardGroup>
