Skip to main content

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

BackendSHIFT_STORAGE ValueDescription
Convexconvex (default)Cloud or self-hosted Convex deployment
FilefileJSON files in dotfile directories, git-native
GatewaygatewayRoutes 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:

FilePurpose
repository.tsStore interface definition
store/file.tsFileStore wrapping file-based functions
store/convex.tsConvexStore calling Convex HTTP API
store/factory.tscreateStore() 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

ServicePrefix
Yellow Pagesyp_
Passportpassport_
Pulsepulse_
Ledgerledger_
Palettepalette_
Stagestage_
Billingbilling_

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"