| Title: | 'Automerge' Sync Server and Client |
|---|---|
| Description: | A WebSocket-based implementation of the 'automerge-repo' synchronization protocol used by 'sync.automerge.org'. Acts as a sync server, enabling 'R' to serve as a synchronization hub for 'Automerge' clients in 'JavaScript', 'Rust', and other languages, and as a client for fetching, editing, and synchronizing documents hosted on remote servers. |
| Authors: | Charlie Gao [aut, cre] (ORCID: <https://orcid.org/0000-0002-0750-061X>), Posit Software, PBC [cph, fnd] (ROR: <https://ror.org/03wc8by49>) |
| Maintainer: | Charlie Gao <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.0 |
| Built: | 2026-07-06 16:25:54 UTC |
| Source: | https://github.com/cran/autosync |
A WebSocket-based implementation of the 'automerge-repo' synchronization protocol used by 'sync.automerge.org'. Acts as a sync server, enabling 'R' to serve as a synchronization hub for 'Automerge' clients in 'JavaScript', 'Rust', and other languages, and as a client for fetching, editing, and synchronizing documents hosted on remote servers.
sync_server()Create a new sync server with $start() and
$close() methods
create_document()Create a new document
get_document()Retrieve a document by ID
list_documents()List all document IDs
generate_document_id()Generate a new document ID
The server implements the automerge-repo sync protocol over WebSockets. Messages are CBOR-encoded and include:
Handshake messages for connection establishment
Document synchronization messages
Transient messages forwarded without persistence
Error notifications
# Create and start a server server <- sync_server() server$start() server$url # Stop when done server$close()
Maintainer: Charlie Gao [email protected] (ORCID)
Authors:
Charlie Gao [email protected] (ORCID)
Other contributors:
Posit Software, PBC (ROR) [copyright holder, funder]
Useful links:
Report bugs at https://github.com/posit-dev/autosync/issues
Creates a configuration object for enabling OIDC JWT authentication on an autosync server. When enabled, clients must include a valid JWT (ID token) as a Bearer token in the Authorization header of the WebSocket upgrade request. Connections without valid credentials are rejected immediately at connection time.
auth_config( client_id = Sys.getenv("OIDC_CLIENT_ID"), issuer = oidc_issuer(), allowed_emails = NULL, allowed_domains = NULL, custom_validator = NULL )auth_config( client_id = Sys.getenv("OIDC_CLIENT_ID"), issuer = oidc_issuer(), allowed_emails = NULL, allowed_domains = NULL, custom_validator = NULL )
client_id |
The OIDC client ID (application ID). Validated against the
|
issuer |
The OIDC issuer URL. This is used to discover the provider's
public keys via the |
allowed_emails |
Character vector of allowed email addresses. When set,
a token is rejected unless it carries an |
allowed_domains |
Character vector of allowed email domains
(e.g., "mycompany.com"). Subject to the same verified-email requirement as
|
custom_validator |
Function(claims) returning TRUE/FALSE for custom validation logic. Receives the decoded JWT claims as a list. |
Works with any OIDC-compliant identity provider: Google, Microsoft Entra, Okta, Auth0, etc.
An object of class "autosync_auth_config".
# Google (default issuer) auth_config( client_id = "123456789.apps.googleusercontent.com", allowed_domains = "mycompany.com" ) # Microsoft Entra auth_config( client_id = "abcdef-1234-5678", issuer = "https://login.microsoftonline.com/common/v2.0", allowed_emails = "[email protected]" ) # Custom validator auth_config( client_id = "0oaXXXXXXXX", issuer = "https://dev-123456.okta.com/oauth2/default", custom_validator = function(claims) "editors" %in% claims$groups )# Google (default issuer) auth_config( client_id = "123456789.apps.googleusercontent.com", allowed_domains = "mycompany.com" ) # Microsoft Entra auth_config( client_id = "abcdef-1234-5678", issuer = "https://login.microsoftonline.com/common/v2.0", allowed_emails = "[email protected]" ) # Custom validator auth_config( client_id = "0oaXXXXXXXX", issuer = "https://dev-123456.okta.com/oauth2/default", custom_validator = function(claims) "editors" %in% claims$groups )
Creates a new empty Automerge document and registers it with the server.
create_document(server, doc_id = NULL)create_document(server, doc_id = NULL)
server |
An autosync_server object. |
doc_id |
Optional document ID. If NULL, generates a new ID. |
Document ID string.
server <- sync_server() doc_id <- create_document(server) server$close()server <- sync_server() doc_id <- create_document(server) server$close()
Creates a new unique document ID compatible with automerge-repo. The ID is a 16-byte random value encoded with Base58Check.
generate_document_id()generate_document_id()
Character string (Base58Check encoded).
generate_document_id()generate_document_id()
Retrieves an Automerge document by its ID.
get_document(server, doc_id)get_document(server, doc_id)
server |
An autosync_server object. |
doc_id |
Document ID string. |
Automerge document object, or NULL if not found.
server <- sync_server() doc_id <- create_document(server) get_document(server, doc_id) server$close()server <- sync_server() doc_id <- create_document(server) get_document(server, doc_id) server$close()
Returns the IDs of all documents currently loaded in the server.
list_documents(server)list_documents(server)
server |
An autosync_server object. |
Character vector of document IDs.
server <- sync_server() create_document(server) list_documents(server) server$close()server <- sync_server() create_document(server) list_documents(server) server$close()
Connects to an automerge-repo sync server and maintains a persistent
WebSocket connection. The connection performs the protocol handshake but
holds no documents on its own; open one or more live documents over it with
the $open_doc() method. Each document stays synced — receiving real-time
updates from other peers and flushing local changes — for as long as the
connection is open. Unlike sync_fetch(), which performs a one-off
retrieval over a throwaway connection, several documents can share a single
connection here.
sync_client(url, timeout = 5000L, tls = NULL, token = NULL, interval = 1000L)sync_client(url, timeout = 5000L, tls = NULL, token = NULL, interval = 1000L)
url |
WebSocket URL of the sync server (e.g., "ws://localhost:3030/" or "wss://sync.automerge.org/"). Note: trailing slash may be required. |
timeout |
Timeout in milliseconds for each receive operation. Default 5000. |
tls |
(optional) for secure wss:// connections to servers with
self-signed or custom CA certificates, a TLS configuration object
created by |
token |
(optional) JWT (ID token) for authenticated servers. Sent as a Bearer token in the Authorization header of the WebSocket upgrade request. |
interval |
Interval in milliseconds for pushing local changes to
the server. Default 1000. Uses |
Opening the connection performs a synchronous handshake before returning.
$open_doc() then performs a synchronous initial sync, so the returned
handle's $doc has meaningful content immediately. After that, incoming
changes are received asynchronously via a self-chaining promise loop, and
local changes are flushed periodically via a later::later() timer.
Neither close() flushes pending local changes. Call $push() first if you
have unsynced edits — otherwise any changes made since the last
sync-interval tick may be lost.
An environment of class "autosync_client" with reference semantics,
representing the connection:
open_doc(doc_id, timeout)Open a live document over this
connection and return a autosync_doc handle for it (see below).
Repeated calls for the same doc_id reuse the document already open
on the connection rather than requesting it again.
close()Disconnect and stop syncing all open documents.
activeLogical, whether the connection is active.
A autosync_doc handle returned by $open_doc() is itself an environment
with:
docThe live automerge document, kept in sync with the server.
push()Push this document's local changes to the server immediately.
close()Stop syncing this one document (detach it from the connection); the connection and its other documents are unaffected.
activeLogical, whether the document is still open on an active connection.
server <- sync_server() server$start() doc_id <- create_document(server) conn <- sync_client(server$url) doc <- conn$open_doc(doc_id) automerge::am_keys(doc$doc) # Make local changes and push automerge::am_put(doc$doc, automerge::AM_ROOT, "key", "value") doc$push() # Open another document over the same connection other <- conn$open_doc(create_document(server)) # Disconnect (closes every document on the connection) conn$close() server$close()server <- sync_server() server$start() doc_id <- create_document(server) conn <- sync_client(server$url) doc <- conn$open_doc(doc_id) automerge::am_keys(doc$doc) # Make local changes and push automerge::am_put(doc$doc, automerge::AM_ROOT, "key", "value") doc$push() # Open another document over the same connection other <- conn$open_doc(create_document(server)) # Disconnect (closes every document on the connection) conn$close() server$close()
Connects to an automerge-repo sync server and retrieves a document by ID. This is useful for debugging sync issues or fetching documents from remote servers like sync.automerge.org.
sync_fetch( url, doc_id, timeout = 5000L, tls = NULL, token = NULL, verbose = FALSE )sync_fetch( url, doc_id, timeout = 5000L, tls = NULL, token = NULL, verbose = FALSE )
url |
WebSocket URL of the sync server (e.g., "ws://localhost:3030/" or "wss://sync.automerge.org/"). Note: trailing slash may be required. |
doc_id |
Document ID (base58check encoded string) |
timeout |
Timeout in milliseconds for each receive operation. Default 5000. |
tls |
(optional) for secure wss:// connections to servers with
self-signed or custom CA certificates, a TLS configuration object
created by |
token |
(optional) JWT (ID token) for authenticated servers. Sent as a Bearer token in the Authorization header of the WebSocket upgrade request. |
verbose |
Logical, print debug messages. Default FALSE. |
The function implements the automerge-repo sync protocol:
Connects via WebSocket
Sends join message with peer ID
Receives peer response from server
Sends sync request for the specified document
Receives and applies sync messages until complete
Sync is considered complete when no new messages arrive within the timeout after at least one sync round.
An automerge document object containing the fetched data.
# Fetch from public sync server doc <- sync_fetch("wss://sync.automerge.org", "4F63WJPDzbHkkfKa66h1Qrr1sC5U") # Fetch from local server with debug output doc <- sync_fetch(server$url, "myDocId", verbose = TRUE) # Fetch from server with self-signed certificate cert <- nanonext::write_cert() tls <- nanonext::tls_config(client = cert$client) doc <- sync_fetch(server$url, "myDocId", tls = tls) # Fetch from authenticated server doc <- sync_fetch( "wss://secure.example.com", "myDocId", token = "eyJhbGciOi..." ) # Inspect the document automerge::am_keys(doc)# Fetch from public sync server doc <- sync_fetch("wss://sync.automerge.org", "4F63WJPDzbHkkfKa66h1Qrr1sC5U") # Fetch from local server with debug output doc <- sync_fetch(server$url, "myDocId", verbose = TRUE) # Fetch from server with self-signed certificate cert <- nanonext::write_cert() tls <- nanonext::tls_config(client = cert$client) doc <- sync_fetch(server$url, "myDocId", tls = tls) # Fetch from authenticated server doc <- sync_fetch( "wss://secure.example.com", "myDocId", token = "eyJhbGciOi..." ) # Inspect the document automerge::am_keys(doc)
Creates a WebSocket server that implements the automerge-repo sync protocol, compatible with JavaScript, Rust, and other Automerge clients.
sync_server( port = 0L, host = "127.0.0.1", data_dir = tempfile("autosync"), auto_create_docs = TRUE, storage_id = NULL, tls = NULL, auth = NULL, share = NA )sync_server( port = 0L, host = "127.0.0.1", data_dir = tempfile("autosync"), auto_create_docs = TRUE, storage_id = NULL, tls = NULL, auth = NULL, share = NA )
port |
Port to listen on. Default 0 (binds to a random available port).
The actual URL is retrieved via |
host |
Host address to bind to. Default "127.0.0.1" (localhost). |
data_dir |
Directory for document storage. Defaults to a session-temporary directory, so documents do not persist across R sessions. Supply an explicit path to persist documents across sessions. |
auto_create_docs |
Logical, whether to auto-create documents when clients request unknown document IDs. Default TRUE. |
storage_id |
Optional storage ID for this server. If NULL (default), generates a new persistent identity. Set to NA for an ephemeral server (no persistence identity). |
tls |
(optional) for secure wss:// connections, a TLS configuration
object created by |
auth |
Optional authentication configuration created by |
share |
Controls document sharing policy for connected clients. This unified parameter governs both proactive document announcement (pushing documents to clients) and access control (allowing clients to request documents). Accepts one of:
|
The returned server inherits from nanonext's nanoServer class and provides
$start() and $close() methods for non-blocking operation.
An autosync_server object inheriting from 'nanoServer', with
$start() and $close() methods.
# Create and start a server server <- sync_server() server$start() # Server is now running in the background # ...do other work... # Stop when done server$close() # With TLS for secure connections cert <- nanonext::write_cert() tls <- nanonext::tls_config(server = cert$server) server <- sync_server(tls = tls) server$start() server$url server$close() # Server with OIDC authentication (requires TLS) cert <- nanonext::write_cert() tls <- nanonext::tls_config(server = cert$server) server <- sync_server( tls = tls, auth = auth_config( client_id = "123456789.apps.googleusercontent.com", allowed_domains = "mycompany.com" ) )# Create and start a server server <- sync_server() server$start() # Server is now running in the background # ...do other work... # Stop when done server$close() # With TLS for secure connections cert <- nanonext::write_cert() tls <- nanonext::tls_config(server = cert$server) server <- sync_server(tls = tls) server$start() server$url server$close() # Server with OIDC authentication (requires TLS) cert <- nanonext::write_cert() tls <- nanonext::tls_config(server = cert$server) server <- sync_server( tls = tls, auth = auth_config( client_id = "123456789.apps.googleusercontent.com", allowed_domains = "mycompany.com" ) )
Performs the OAuth 2.0 Authorization Code flow with PKCE to obtain a JWT
(ID token) from an OIDC provider, delegating the browser handshake and token
exchange to httr2. Endpoints are discovered from the issuer's
.well-known metadata via httr2::oauth_server_metadata(), and the flow is
run by httr2::oauth_flow_auth_code(): it opens the system browser, listens
on a loopback redirect for the callback, and returns the ID token for use
with sync_fetch().
sync_token( client_id = Sys.getenv("OIDC_CLIENT_ID"), client_secret = Sys.getenv("OIDC_CLIENT_SECRET"), issuer = oidc_issuer(), scopes = "openid email", redirect_uri = oauth_redirect_uri() )sync_token( client_id = Sys.getenv("OIDC_CLIENT_ID"), client_secret = Sys.getenv("OIDC_CLIENT_SECRET"), issuer = oidc_issuer(), scopes = "openid email", redirect_uri = oauth_redirect_uri() )
client_id |
The OIDC client ID (application ID). Defaults to the
|
client_secret |
The OIDC client secret. Required by Google (Desktop
app) and "Web application" client types; leave unset for native / public
clients, which authenticate via PKCE alone. Defaults to the
|
issuer |
The OIDC issuer URL. Defaults to the |
scopes |
Space-separated OAuth scopes to request. Default
|
redirect_uri |
Local redirect URI for the OAuth callback. Defaults to
|
For Google, register the OAuth client as a "Desktop app" and set both
OIDC_CLIENT_ID and OIDC_CLIENT_SECRET. Google's Desktop app secret is
required in the token exchange but, unlike a "Web application" secret, is
not treated as confidential: Google states that for installed apps "the
client secret is obviously not treated as a secret"
(https://developers.google.com/identity/protocols/oauth2#installed),
consistent with the OAuth 2.0 for Native Apps standard
(RFC 8252 section 8.5,
https://datatracker.ietf.org/doc/html/rfc8252#section-8.5).
Providers that support native / public clients (Microsoft Entra, Okta,
Auth0, etc.) need only client_id, authenticating via PKCE alone; leave
client_secret unset for these.
A JWT (ID token) as a character string.
# Uses OIDC_CLIENT_ID and OIDC_CLIENT_SECRET env vars by default token <- sync_token() # Or supply credentials directly token <- sync_token( client_id = "YOUR_CLIENT_ID.apps.googleusercontent.com", client_secret = "YOUR_CLIENT_SECRET" ) # Use with sync_fetch doc <- sync_fetch(server$url, "myDocId", token = token, tls = tls)# Uses OIDC_CLIENT_ID and OIDC_CLIENT_SECRET env vars by default token <- sync_token() # Or supply credentials directly token <- sync_token( client_id = "YOUR_CLIENT_ID.apps.googleusercontent.com", client_secret = "YOUR_CLIENT_SECRET" ) # Use with sync_fetch doc <- sync_fetch(server$url, "myDocId", token = token, tls = tls)