Milestones
Milestones are document snapshots that represent the document state at a specific point in time. They provide version history and can be used to restore documents to previous states.
Overview
Section titled “Overview”A Milestone is a document snapshot as the client saw the document at a point in time. Milestones are:
- Stored separately from the document as a sort of version history
- Can be pulled back over the network again
- An example of the RPC system, which adds custom methods & handlers for custom operations over the transport layer in-protocol
Milestone operations live on the provider’s rpc.milestones namespace, which you enable by registering the createMilestoneRpc extension when creating the provider.
Creating Milestones
Section titled “Creating Milestones”Manual Creation
Section titled “Manual Creation”Create a milestone from the current document state:
import { Provider } from "teleportal/providers";import { createMilestoneRpc } from "teleportal/protocols/milestone";import { createEncryptionKey } from "teleportal/encryption-key";
const provider = await Provider.create({ url: "wss://example.com", document: "my-document", encryptionKey: createEncryptionKey(), // encrypted by default rpc: { milestones: createMilestoneRpc, },});
// Create a milestone with a nameconst milestone = await provider.rpc.milestones.create("v1.0");
// Or let the server auto-generate a nameconst milestone2 = await provider.rpc.milestones.create();Automatic Creation
Section titled “Automatic Creation”Milestones can be created automatically based on triggers:
import { Server } from "teleportal/server";
const server = new Server({ // ... other options milestoneTriggerConfig: { defaultTriggers: [ { id: "hourly", type: "time-based", enabled: true, config: { interval: 3600000 }, // every hour }, { id: "every-1000", type: "update-count", enabled: true, config: { updateCount: 1000 }, // after 1000 updates }, ], },});Note: Time-based and update-count triggers are evaluated on the document-write path only — there is no background timer. An idle document never accumulates milestones; a time-based trigger fires on the next write after its interval has elapsed. Automatic milestones are attributed to
{ type: "system", id: "auto" }.
Listing Milestones
Section titled “Listing Milestones”List all milestones for a document:
// List all milestonesconst milestones = await provider.rpc.milestones.list();
// Include soft-deleted milestones, or send known ids for incremental updatesconst withDeleted = await provider.rpc.milestones.list({ includeDeleted: true });const incremental = await provider.rpc.milestones.list({ snapshotIds: existingMilestoneIds });Retrieving Milestones
Section titled “Retrieving Milestones”Get the snapshot content for a specific milestone:
// Get milestone snapshotconst snapshot = await provider.rpc.milestones.getSnapshot(milestoneId);
// The snapshot is a Uint8Array containing the Y.js document state (decrypted// client-side for encrypted documents). You can apply it to a new document:import * as Y from "yjs";
const newDoc = new Y.Doc();Y.applyUpdateV2(newDoc, snapshot);Note: Milestones work on encrypted documents. Since content E2EE is the default, the client decrypts the snapshot locally — the server stores and serves only the encrypted snapshot and never sees the document content.
Updating Milestones
Section titled “Updating Milestones”Update a milestone’s name:
await provider.rpc.milestones.updateName(milestoneId, "v1.0.1");Soft Delete and Restore
Section titled “Soft Delete and Restore”Milestones support soft delete and restore:
// Soft delete a milestoneawait provider.rpc.milestones.delete(milestoneId);
// Restore a deleted milestoneawait provider.rpc.milestones.restore(milestoneId);Milestone Metadata
Section titled “Milestone Metadata”Each milestone includes metadata:
interface Milestone { id: string; name: string; documentId: string; createdAt: number; deletedAt?: number; lifecycleState?: "active" | "deleted" | "archived" | "expired"; expiresAt?: number; createdBy: { type: "user" | "system"; id: string; };}The createdBy field indicates who or what created the milestone:
{ type: "user", id: userId }- Created by a user via themilestoneCreateRPC method{ type: "system", id: "auto" }- Created automatically by a trigger
Note: A freshly-created milestone has
lifecycleState === undefined, which everygetMilestonesfilter andMilestone.toString()treats as"active". The string"active"is not written to the wire in the common case — a milestone only carries an explicitlifecycleStateonce it has been deleted, archived, or expired.
Server Configuration
Section titled “Server Configuration”To enable milestone operations on the server:
import { Server } from "teleportal/server";import { getMilestoneRpcHandlers } from "teleportal/protocols/milestone";import { UnstorageDocumentStorage, UnstorageMilestoneStorage } from "teleportal/storage";
const milestoneStorage = new UnstorageMilestoneStorage(storage, { keyPrefix: "document-milestone",});
const server = new Server({ storage: async (ctx) => new UnstorageDocumentStorage(storage, { keyPrefix: "doc", encrypted: ctx.encrypted }), rpcHandlers: { ...getMilestoneRpcHandlers(milestoneStorage), },});Storage
Section titled “Storage”Milestones are stored separately from documents using MilestoneStorage:
import type { MilestoneStorage } from "teleportal/storage";
// MilestoneStorage interface (abridged)interface MilestoneStorage { readonly type: "milestone-storage";
createMilestone(ctx: { name: string; documentId: string; createdAt: number; snapshot: MilestoneSnapshot; createdBy: { type: "user" | "system"; id: string }; }): Promise<string>; // returns the created milestone id
getMilestone(documentId: string, id: string): Promise<Milestone | null>;
getMilestones( documentId: string, options?: { includeDeleted?: boolean; lifecycleState?: Milestone["lifecycleState"] }, ): Promise<Milestone[]>;
deleteMilestone(documentId: string, id: string | string[], deletedBy?: string): Promise<void>; restoreMilestone(documentId: string, id: string | string[]): Promise<void>; updateMilestoneName( documentId: string, id: string, name: string, createdBy?: { type: "user" | "system"; id: string }, ): Promise<void>;}Use Cases
Section titled “Use Cases”Version History
Section titled “Version History”Create milestones at important points in the document lifecycle:
// Create milestone when user clicks "Save"await provider.rpc.milestones.create("Save point 1");
// Create milestone when document is publishedawait provider.rpc.milestones.create("Published v1.0");Document Restoration
Section titled “Document Restoration”Restore a document to a previous milestone:
// Get milestone snapshotconst snapshot = await provider.rpc.milestones.getSnapshot(milestoneId);
// Apply to current documentimport * as Y from "yjs";Y.applyUpdateV2(provider.doc, snapshot);Automatic Backups
Section titled “Automatic Backups”Use automatic triggers to create regular backups:
const server = new Server({ milestoneTriggerConfig: { defaultTriggers: [ { id: "hourly-backup", type: "time-based", enabled: true, config: { interval: 3600000 }, // every hour (fires on the next write after the interval) }, ], },});Best Practices
Section titled “Best Practices”- Name milestones meaningfully: Use descriptive names like “v1.0”, “Published”, “Before refactor”
- Create milestones at important points: Save milestones at key moments in the document lifecycle
- Use automatic triggers: Set up automatic milestone creation for regular backups
- Clean up old milestones: Periodically delete or archive old milestones to save storage
- Track createdBy: Use the
createdByfield to distinguish user vs system milestones
Next Steps
Section titled “Next Steps”- Provider - Learn how to use milestones from the client
- Server - Configure milestone triggers on the server
- Custom Storage - Understand milestone storage