Storage Backends
The Shift Platform uses a pluggable storage architecture. Services define what data operations they need via store interfaces, and backends provide how those operations are executed.
Available Backends
| Backend | SHIFT_STORAGE Value | Description |
|---|---|---|
| Convex | convex (default) | Cloud or self-hosted Convex deployment |
| File | file | JSON files in dotfile directories, git-native |
| Gateway | gateway | Routes storage calls through the platform gateway |
Set the backend via the SHIFT_STORAGE environment variable:
# Use Convex (default)
export SHIFT_STORAGE=convex
# Use local file storage
export SHIFT_STORAGE=file
# Use gateway-backed storage (CLI default)
export SHIFT_STORAGE=gateway
Architecture
Service (CLI / API / Web)
|
v
Store Interface (Repository<T>, EventRepository<T>)
|
+---> FileStore (reads/writes JSON files on disk)
+---> ConvexStore (calls Convex HTTP API)
+---> GatewayStore (calls the platform gateway API)
Each service has its own store implementation files:
| File | Purpose |
|---|---|
repository.ts | Store interface definition |
store/file.ts | FileStore wrapping file-based functions |
store/convex.ts | ConvexStore calling Convex HTTP API |
store/factory.ts | createStore() factory based on SHIFT_STORAGE |
The factory function reads the environment variable and returns the appropriate store implementation:
import { getStorageBackend } from "@shift/platform-core/storage";
export function createStore(): ServiceStore {
const backend = getStorageBackend();
switch (backend) {
case "file":
return new FileStore();
case "gateway":
return new GatewayStore();
case "convex":
default:
return new ConvexStore();
}
}
Convex Backend
The default storage backend. All services share a single Convex deployment with table prefixes to avoid collisions.
Table Prefixes
| Service | Prefix |
|---|---|
| Yellow Pages | yp_ |
| Passport | passport_ |
| Pulse | pulse_ |
| Ledger | ledger_ |
| Palette | palette_ |
| Stage | stage_ |
| Billing | billing_ |
Configuration
# Convex Cloud
export CONVEX_URL=https://your-deployment.convex.cloud
# Self-hosted Convex
export CONVEX_SELF_HOSTED_URL=http://127.0.0.1:3210
The schema is defined in convex/schema.ts at the platform root, with per-service functions in convex/<service>.ts.
For details on the ID mapping between nanoid and Convex internal IDs, see SID Bridging.
File Backend
Stores data as JSON files in dotfile directories within the project:
.yellowpages/
services/
abc12345.json
def67890.json
systems/
ghi24680.json
owners/
jkl13579.json
Characteristics
- Git-native: Files are plain JSON, designed to be committed, diffed, and reviewed in pull requests.
- No server required: Works entirely on the local filesystem.
- Transparent: You can inspect and manually edit records with any text editor.
- Cache files are gitignored: Search indexes and hash files (e.g.,
.search-index.json) are excluded from version control.
Finding the Store Root
File-based storage walks up from the current working directory to find the service's dotfile directory (e.g., .yellowpages/). If no directory is found, commands exit with code 3 (EXIT_NOT_INITIALIZED).
Gateway Backend
Routes all storage operations through the platform gateway API at app.the-shift.dev (or SHIFT_GATEWAY_URL). This is the default for the standalone CLI binary (shift-cli), which does not have direct access to Convex or local files.
Migration
To migrate data from file-based storage to Convex:
bun run scripts/migrate-to-convex.ts
# Migrate a specific service
bun run scripts/migrate-to-convex.ts --service yellowpages
# Preview without writing
bun run scripts/migrate-to-convex.ts --dry-run
Core Interfaces
The base interfaces are defined in @shift/platform-core/storage:
/** Generic CRUD repository for entities with id + name. */
interface Repository<T extends { id: string }> {
getAll(): Promise<T[]>;
getById(id: string): Promise<T | null>;
findByName(name: string): Promise<T | null>;
resolveId(idOrName: string): Promise<string>;
save(record: T): Promise<void>;
delete(id: string): Promise<boolean>;
}
/** Append-only event repository with query support. */
interface EventRepository<T extends { id: string }> {
append(event: T): Promise<void>;
appendBatch(events: T[]): Promise<void>;
query(filters: Record<string, unknown>): Promise<T[]>;
count(filters: Record<string, unknown>): Promise<number>;
getById(id: string): Promise<T | null>;
}
The getStorageBackend() function reads SHIFT_STORAGE and returns the backend type:
import { getStorageBackend } from "@shift/platform-core/storage";
const backend = getStorageBackend(); // "convex" | "file" | "gateway"