Skip to main content

SID Bridging

When using the Convex storage backend, there is an inherent mismatch between the IDs used by The Shift Platform and the IDs used internally by Convex. SID bridging solves this transparently.

The Problem

The Shift Platform assigns every resource an 8-character nanoid as its id. These IDs are:

  • Portable -- they work the same way regardless of which storage backend is active.
  • Stable -- they never change, even if the resource is moved between backends.
  • Short -- 8 characters are easy to type, copy, and pass as CLI arguments.

Convex, on the other hand, uses its own internal _id field for every document. These IDs are opaque, backend-specific, and not suitable for use in CLI commands or API URLs.

The Solution: sid Field

Every Convex table in The Shift Platform includes a sid field (short for "shift ID") that stores the nanoid. Each table has an index on sid for efficient lookups.

// In convex/schema.ts
export default defineSchema({
yp_services: defineTable({
sid: v.string(), // The platform nanoid (e.g., "abc12345")
name: v.string(),
owner: v.string(),
system: v.string(),
language: v.string(),
// ...other fields
}).index("by_sid", ["sid"]),
});

How ConvexStore Translates

The ConvexStore implementation handles the translation in both directions:

Reading (Convex to App)

When fetching records from Convex, the store maps sid back to id:

// ConvexStore.getById(id: string)
// 1. Query Convex using the sid index
const doc = await convexQuery("yp_services:getBySid", { sid: id });

// 2. Map Convex document to app model
return {
id: doc.sid, // sid -> id
name: doc.name,
owner: doc.owner,
// ...
};

Writing (App to Convex)

When saving records, the store maps id to sid:

// ConvexStore.save(record: T)
// 1. Check if a document with this sid already exists
const existing = await convexQuery("yp_services:getBySid", { sid: record.id });

// 2. Create or update with sid field
await convexMutation("yp_services:upsert", {
sid: record.id, // id -> sid
name: record.name,
owner: record.owner,
// ...
});

The Translation Is Transparent

Application code never sees Convex's _id field. The store interface uses id everywhere:

// Application code -- same regardless of backend
const service = await store.getById("abc12345");
console.log(service.id); // "abc12345"
console.log(service.name); // "auth-service"

Whether store is a FileStore, ConvexStore, or GatewayStore, the id field always contains the platform nanoid.

Why Not Just Use Convex IDs?

  1. Backend portability. The same IDs work with file storage, Convex, and gateway storage. Switching backends does not invalidate existing references.
  2. Human usability. An 8-character nanoid like abc12345 is easy to type in a terminal. Convex internal IDs are longer and opaque.
  3. Cross-service references. Services reference each other by id (e.g., a Pulse event references a Yellow Pages service ID). These references must survive backend migrations.
  4. CLI and API consistency. The ID-or-Name resolution pattern works uniformly because IDs are always nanoids, regardless of storage.

Index Design

Every Convex table follows the same pattern:

FieldPurpose
_idConvex internal ID (never exposed to application code)
sidPlatform nanoid, indexed via by_sid
nameHuman-readable name, indexed via by_name

The by_sid index ensures that lookups by platform ID are fast (O(1) rather than a full table scan).