Subscription Migration Plan: site.standard.graph.subscription #
Overview #
Migrate subscription handling to support both pub.leaflet.graph.subscription and site.standard.graph.subscription lexicons, following the same pattern as documents/publications.
Current State #
Schema Comparison #
Both formats are nearly identical:
// pub.leaflet.graph.subscription
interface Record {
$type: 'pub.leaflet.graph.subscription'
publication: string // AT-URI of the publication
}
// site.standard.graph.subscription
interface Record {
$type: 'site.standard.graph.subscription'
publication: string // AT-URI of the publication
}
Current Storage #
- Table:
publication_subscriptions - Columns:
uri,identity,publication,record(JSON),created_at - Both formats stored in same table (appview already handles this)
Current Usage #
- Write path (
subscribeToPublication.ts): Createspub.leaflet.graph.subscriptionrecords - Read path (
getSubscriptions.ts): Reads frompublication_subscriptions, normalizes the publication record (not subscription record) - Appview (
appview/index.ts): Already indexes both formats intopublication_subscriptions
Migration Structure #
1. Add Normalization Functions (lexicons/src/normalize.ts) #
import type * as PubLeafletGraphSubscription from "../api/types/pub/leaflet/graph/subscription";
import type * as SiteStandardGraphSubscription from "../api/types/site/standard/graph/subscription";
// Normalized type - use site.standard as canonical
export type NormalizedSubscription = SiteStandardGraphSubscription.Record;
/**
* Checks if the record is a pub.leaflet.graph.subscription
*/
export function isLeafletSubscription(
record: unknown
): record is PubLeafletGraphSubscription.Record {
if (!record || typeof record !== "object") return false;
const r = record as Record<string, unknown>;
return (
r.$type === "pub.leaflet.graph.subscription" ||
// Legacy: has publication but no $type
(typeof r.publication === "string" && !("$type" in r))
);
}
/**
* Checks if the record is a site.standard.graph.subscription
*/
export function isStandardSubscription(
record: unknown
): record is SiteStandardGraphSubscription.Record {
if (!record || typeof record !== "object") return false;
const r = record as Record<string, unknown>;
return r.$type === "site.standard.graph.subscription";
}
/**
* Normalizes a subscription record from either format.
* Since both formats are identical, this is mostly for type consistency.
*/
export function normalizeSubscription(
record: unknown
): NormalizedSubscription | null {
if (!record || typeof record !== "object") return null;
if (isStandardSubscription(record)) {
return record;
}
if (isLeafletSubscription(record)) {
return {
$type: "site.standard.graph.subscription",
publication: record.publication,
};
}
return null;
}
2. Add Database Helpers (src/utils/normalizeRecords.ts) #
import {
normalizeSubscription,
type NormalizedSubscription,
} from "lexicons/src/normalize";
export function normalizeSubscriptionRecord(
record: Json | unknown
): NormalizedSubscription | null {
return normalizeSubscription(record);
}
export function normalizeSubscriptionRow<T extends { record: Json | unknown }>(
row: T
): Omit<T, "record"> & { record: NormalizedSubscription | null } {
return {
...row,
record: normalizeSubscriptionRecord(row.record),
};
}
export function hasValidSubscription<
T extends { record: NormalizedSubscription | null }
>(row: T): row is T & { record: NormalizedSubscription } {
return row.record !== null;
}
// Re-export
export {
normalizeSubscription,
isLeafletSubscription,
isStandardSubscription,
type NormalizedSubscription,
} from "lexicons/src/normalize";
3. Files to Update #
Low Priority (subscription record itself rarely accessed) #
Since subscriptions are simple and mostly used for their existence (not their content), most files don't need changes. The record field is typically only used to verify the subscription exists.
Files that access subscription.record directly:
| File | Usage | Action |
|---|---|---|
getSubscriptions.ts |
Just passes through, doesn't read fields | No change needed |
subscribeToPublication.ts |
Creates new records (always pub.leaflet) | No change needed |
appview/index.ts |
Already handles both formats | Already done |
Optional: Type safety improvements
If we want strict typing, we could update PublicationSubscription type in getSubscriptions.ts:
export type PublicationSubscription = {
authorProfile?: { handle: string };
// publication record is normalized (existing)
record: NormalizedPublication;
uri: string;
// Could add subscription record if needed:
// subscriptionRecord: NormalizedSubscription;
documents_in_publications: { ... }[];
};
Key Differences from Document/Publication Migration #
| Aspect | Documents/Publications | Subscriptions |
|---|---|---|
| Schema complexity | Different fields | Identical fields |
| Read access | Frequent field access | Rarely access record fields |
| Normalization value | High (field mapping) | Low (just $type change) |
| Risk if skipped | Broken field access | None (fields identical) |
Recommendation #
Minimal migration approach:
- Add normalization functions for completeness and type safety
- Don't update existing read paths unless they start accessing subscription record fields
- Keep write path as pub.leaflet (or decide to switch to site.standard)
Full migration approach (if consistency is priority):
- Add all normalization functions
- Update types to use
NormalizedSubscription - Add normalization calls where subscription records are read
- Consider switching write path to site.standard
Write Path Decision #
Currently subscribeToPublication.ts creates pub.leaflet.graph.subscription records. Options:
- Keep as-is: Continue creating pub.leaflet records, normalization handles reading
- Switch to site.standard: Update to create site.standard records for new subscriptions
If switching to site.standard:
// In subscribeToPublication.ts
let record = await agent.site.standard.graph.subscription.create(
{ repo: credentialSession.did!, rkey: TID.nextStr() },
{ publication }
);