Skip to content

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.

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.

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 name
const milestone = await provider.rpc.milestones.create("v1.0");
// Or let the server auto-generate a name
const milestone2 = await provider.rpc.milestones.create();

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" }.

List all milestones for a document:

// List all milestones
const milestones = await provider.rpc.milestones.list();
// Include soft-deleted milestones, or send known ids for incremental updates
const withDeleted = await provider.rpc.milestones.list({ includeDeleted: true });
const incremental = await provider.rpc.milestones.list({ snapshotIds: existingMilestoneIds });

Get the snapshot content for a specific milestone:

// Get milestone snapshot
const 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.

Update a milestone’s name:

await provider.rpc.milestones.updateName(milestoneId, "v1.0.1");

Milestones support soft delete and restore:

// Soft delete a milestone
await provider.rpc.milestones.delete(milestoneId);
// Restore a deleted milestone
await provider.rpc.milestones.restore(milestoneId);

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 the milestoneCreate RPC method
  • { type: "system", id: "auto" } - Created automatically by a trigger

Note: A freshly-created milestone has lifecycleState === undefined, which every getMilestones filter and Milestone.toString() treats as "active". The string "active" is not written to the wire in the common case — a milestone only carries an explicit lifecycleState once it has been deleted, archived, or expired.

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),
},
});

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>;
}

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 published
await provider.rpc.milestones.create("Published v1.0");

Restore a document to a previous milestone:

// Get milestone snapshot
const snapshot = await provider.rpc.milestones.getSnapshot(milestoneId);
// Apply to current document
import * as Y from "yjs";
Y.applyUpdateV2(provider.doc, snapshot);

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)
},
],
},
});
  1. Name milestones meaningfully: Use descriptive names like “v1.0”, “Published”, “Before refactor”
  2. Create milestones at important points: Save milestones at key moments in the document lifecycle
  3. Use automatic triggers: Set up automatic milestone creation for regular backups
  4. Clean up old milestones: Periodically delete or archive old milestones to save storage
  5. Track createdBy: Use the createdBy field to distinguish user vs system milestones
  • Provider - Learn how to use milestones from the client
  • Server - Configure milestone triggers on the server
  • Custom Storage - Understand milestone storage