Docs
API
Client

Client API

There are currently three ways on how to use Secsync on the client:

  • useYjsSync - works with Yjs & React
  • useAutomergeSync - works with Automerge & React
  • createSyncMachine - agnostic to the CRDT implementation and rendering engine

The two hooks abstract away the logic how to map between the CRDT data-structures and Secsync's one.

Usage

The client implementation is built using xState (opens in a new tab). The hooks instantiate the machine right away.

Instantiate

useYjsSync

const yDocRef = useRef<Yjs.Doc>(new Yjs.Doc());
const [state, send] = useYjsSync({
  yDoc: yDocRef.current,
  ...moreRequiredArguments,
});

useAutomergeSync

const [initialDoc] = useState<Doc<YouDataStructure>>(() => Automerge.init());
const [currentDoc, syncDoc, state, send] = useAutomergeSync<Todos>({
  initialDoc,
  ...moreRequiredArguments,
});

createSyncMachine

const syncMachine = createMachine();
const syncActor = createActor(syncMachine, {
  input: {
    documentId: docId,
    ...moreRequiredArguments,
  },
});
 
syncActor.subscribe((state) => {
  console.log(state.value, state.context);
});

State

They all expose a state object representing the current state. The state contains the value which indicates the current state of state machine.

Most relevant to the user are if the client is connected or not and if the document synchronisation failed. It should not fail unless an error was caused the protocol can not recover from.

Examples of

Sending events

In addition you can hook into the state machine and trigger events. most of them are irrelevant from a developer perspective.

The most relevant ones are

send({ type: "DISCONNECT" }); // disconnect from the backend service
send({ type: "CONNECT" }); // connect to the backend service (when disconnected)

The full list can be found in the code here (opens in a new tab).

Parameters

useYjsSync, useAutomergeSync & createSyncMachine

documentId

Required: true

A unique ID as string to identifying the document.

signatureKeyPair

Required: true

The current client's signing keyPair matching the libsodium KeyPair, but with the type ed25519. Depending on the application this can be the keypair of a user or just the device of a user.

interface KeyPair {
  keyType: "ed25519";
  privateKey: Uint8Array;
  publicKey: Uint8Array;
}

websocketEndpoint

Required: true

The backend service to connect to as a string. Example:

const websocketEndpoint =
  process.env.NODE_ENV === "development"
    ? "ws://localhost:4000"
    : "wss://secsync.fly.dev";

websocketSessionKey

Required: true

Used to authenticate the client with the server. Can be any string.

getSnapshotKey

Required: true

Callback to return the key for the Snapshot. One argument is passed to the callback of the type:

type SnapshotProofInfo = {
  snapshotId: string;
  snapshotCiphertextHash: string;
  parentSnapshotProof: string;
  additionalPublicData: any; // additionalPublicData that was attached to the workshop
};

Expects the key to be returned as Promise<Uint8Array> | Uint8Array.

getNewSnapshotData

Required: true

Callback invoked to return the necessary values to create a Snapshot.

One argument is passed to the callback { id: string }. This is generated ID used for the snapshot.

The return type must be:

type NewSnapshotData = {
  // A Snapshot of the CRDT data. Can be a compressed and/or garbage collected version.
  readonly data: Uint8Array | string;
  // Encryption key for the snapshot
  readonly key: Uint8Array;
  // Custom data that will not be encrypted, but cryptographically attached
  // to the Snapshot as public data. In cryptography also referred to as
  // additional authenticated data (AAD).
  // If additional data is provided, also `additionalAuthenticationDataValidations`
  // must be setup correctly for parsing incoming messages to work.
  // In mose cases this will simply be an empty objects `{}`.
  readonly publicData: any;
  // Additional data that should be sent along to the server with the new snapshot
  // but is not part of the public data.
  readonly additionalServerData?: any;
};

isValidClient

Required: true

Callback invoked for ever snapshot, update, ephemeralMessage to validate if it was signed by a valid client for this document. While the encryption key is securing confidentiality this can be used to verify the author and possibly reject changes from clients that are not valid anymore e.g. a removed member.

One argument is passed to the callback signingPublicKey: string.

The return type must be a boolean. true if it's a valid client and false if not.

sodium

Required: true

Needs to be libsodium implementation. In most JavaScript environment it can be imported like:

import sodium from "libsodium-wrappers";

For React Native we created bindings to libsodium that can be imported as:

import sodium from "react-native-libsodium";

shouldSendSnapshot

Required: false

Callback which is invoked while processing the sending queue to determine if a snapshot or an update should be created next. By default only updates are sent.

shouldSendSnapshot: ({ activeSnapshotId, snapshotUpdatesCount }) => {
  // create a new snapshot if the active snapshot has more than 300 updates
  return snapshotUpdatesCount > 300;
},

Note: The backend can force a client to create a new snapshot by throwing a SecsyncNewSnapshotRequiredError error which results in requiresNewSnapshot set to true on the server to client message of type "update-save-failed".

onDocumentUpdated

Required: false

A callback that is invoked every time the document has been updated. It contains an object with the type of update which can be "snapshot-saved" | "snapshot-received" | "update-saved" | "update-received" and the related knownSnapshotInfo of type SnapshotInfoWithUpdateClocks.

type SnapshotInfoWithUpdateClocks = {
  snapshotId: string;
  snapshotCiphertextHash: string;
  parentSnapshotProof: string;
  additionalPublicData: any;
  updateClocks: SnapshotUpdateClocks;
};

The knownSnapshotInfo can be stored locally in order to provide it to loadDocumentParams when being disconnected. It will allow Secsync to perform various validations and checks to verify the correctness of the document.

(params: {
  type: OnDocumentUpdatedEventType;
  knownSnapshotInfo: SnapshotInfoWithUpdateClocks;
}) => void | Promise<void>

onPendingChangesUpdated

Required: false

A callback that is invoked every time the pending changes of a document are updated. It combines all unsynced changes, changes from a snapshot in flight and changes from updates in flight.

(pendingChanges: any[]) => void | Promise<void>

pendingChanges

Required: false

When initializing secsync pendingChanges can be passed in. These changes will be synced as soon as a connection is established. The pendingChanges can be received via the onPendingChangesUpdated callback.

loadDocumentParams

Required: false

loadDocumentParams are only relevant for the initial document loading. They allow secsync to do certain verifications and if activated also

While loadDocumentParams are not required, especially providing the knownSnapshotInfo is an important element to protect against server compromise. Then Secsync can verify if the known Snapshot is an ancestor of the newly received one.

type GetDocumentMode = "complete" | "delta";
 
type LoadDocumentParams = {
  knownSnapshotInfo: SnapshotInfoWithUpdateClocks;
  mode: GetDocumentMode;
};
knownSnapshotInfo

This should be the same object as provided by the last onDocumentUpdated-callback.

mode

Can be "complete" or "delta". The default is "complete"

Providing "complete" informs the backend that the entire document should be loaded. This means the last known snapshot and all connected updates.

Setting it to "delta" means the only the necessary updates should be provided. This means only the necessary data is retrieved e.g.

  1. Client is aware of the latest snapshot and three updates and only misses two updates then only two updates are sent.
  2. Client is aware of an older snapshot, then the latest snapshot and all it's updates are sent.

delta-mode only makes sense if the client locally stores the document and the loadDocumentParams are passed in. Otherwise it basically will work as complete-mode.

onCustomMessage

Required: false

Secsync allows to pass down custom messages. With this callback they can be handled.

(message: any) => Promise<void> | void;

additionalAuthenticationDataValidations

Required: false

If you plan to attach additional public data to a Snapshot, Update or EphemeralMessage this must be hard-coded using the additionalAuthenticationDataValidations argument.

Each of them is optional, but if not provided the additional public data will be rejected by other clients. Internally we are using Zod (opens in a new tab) and therefor the types have been declared like that. You could provide just an object as long as it has a parse function that accepts the full public data and returns the valid additional public data.

type AdditionalAuthenticationDataValidations = {
  snapshot?: z.SomeZodObject;
  update?: z.SomeZodObject;
  ephemeralMessage?: z.SomeZodObject;
};

logging

Required: false

Logging is off by default, but can be set to "errors" to log out errors to the console for debugging. For a more in-depth debugging logging can be set to "debug".

createSyncMachine (only)

applyChanges

Required: true

A callback receiving an array of changes that should be applied to the CRDT datastructure.

(changes: any[]) => void;

applyEphemeralMessage

Required: true

A callback receiving the ephemeralMessage and the author's public key.

(ephemeralMessages: any, authorPublicKey: string) => void;

serializeChanges

Required: true

A callback receiving an array of changes, which should be serialized to a string. This is the content that will be encrypted in an update.

(changes: any[]) => string;

deserializeChanges

Required: true

A callback receiving a string of serialized changes. These should be de-serialized to an array of changes.

(serializeChanges: string) => any[];