Nyora (JavaScript) - v2.0.3
    Preparing search index...

    Cloud sync (NyoraSync)

    NyoraSync is the account + library sync client. It signs in against the Nyora sync server (https://stream.hasanraza.tech) with an OAuth2 password grant and a JWT, then does a generic last-write-wins upsert/select over your per-user tables. Your favourites, history, and bookmarks follow you across every Nyora client — the JS SDK, the CLI/TUI, and the mobile apps.

    Sync is entirely optional and independent of the Nyora cloud client: you can read manga without ever signing in.

    import { NyoraSync } from "nyora-sdk";

    // Types and helpers, if needed:
    import type { SyncOptions } from "nyora-sdk";
    import { SYNC_BASE_URL, SyncNotSignedInError } from "nyora-sdk";
    const sync = new NyoraSync();
    

    Constructor options ( SyncOptions, all optional):

    new NyoraSync({
    baseUrl?: string, // sync server; defaults to SYNC_BASE_URL or $NYORA_SYNC_URL
    timeoutMs?: number, // per-request timeout (default 30_000)
    tokenPath?: string | null, // where to persist tokens; null disables persistence
    });

    The base URL resolves from, in order: the baseUrl option, the NYORA_SYNC_URL environment variable, then the public default (https://stream.hasanraza.tech, exported as SYNC_BASE_URL).

    On construction, NyoraSync loads any previously saved tokens, so a process can stay signed in across runs. Tokens are written to:

    ~/.config/nyora/sync.json
    

    (or $XDG_CONFIG_HOME/nyora/sync.json when XDG_CONFIG_HOME is set). Pass a custom tokenPath to change the location, or tokenPath: null to keep tokens in memory only. The file stores the access token, refresh token, and email.

    Register a new account and store the returned tokens. The server may have registration disabled, in which case this throws.

    await sync.register("you@example.com", "password");
    

    Sign in with the OAuth2 password grant and persist the tokens.

    await sync.signIn("you@example.com", "password");
    console.log(sync.isSignedIn); // true

    Forget the stored tokens and delete the token file.

    sync.signOut();
    

    Access tokens are refreshed automatically: when a sync request returns 401 and a refresh token is held, NyoraSync refreshes and retries once transparently.

    Both methods require a signed-in client and throw SyncNotSignedInError (exported as SyncNotSignedInError) otherwise.

    Last-write-wins upsert of rows into table. Returns the number of rows written (0 for an empty rows array).

    const now = new Date().toISOString();
    await sync.upsert("nyora_favourite", [
    { manga_id: "…", added_at: now, sort_key: 0, updated_at: now },
    ]);

    Fetch rows from table. Pass an ISO timestamp since to fetch only rows changed after that point (incremental pull).

    const favs = await sync.select("nyora_favourite");
    const changed = await sync.select("nyora_history", "2026-01-01T00:00:00.000Z");

    Sync operates over these per-user tables:

    Table Holds
    nyora_manga Manga metadata (title, url, cover, authors, description, source ref).
    nyora_favourite Favourited manga (references nyora_manga by manga_id).
    nyora_history Reading history / progress.
    nyora_bookmark Per-chapter bookmarks.

    Rows are plain JSON objects; carry an updated_at ISO timestamp so the last-write-wins merge resolves conflicts. Soft-deleted rows carry a deleted_at timestamp (filter these out when reading a live library).

    import { Nyora, NyoraSync } from "nyora-sdk";

    const client = new Nyora();
    const sync = new NyoraSync();
    await sync.signIn("you@example.com", "password");

    const source = await client.sources.find("mangadex");
    const results = await client.manga.search(source.id, "Frieren");
    const manga = results.entries[0];

    const now = new Date().toISOString();
    await sync.upsert("nyora_manga", [
    {
    id: manga.url,
    title: manga.title,
    url: manga.url,
    cover_url: manga.coverUrl ?? "",
    source_ref: JSON.stringify({ source: source.id }),
    updated_at: now,
    },
    ]);
    await sync.upsert("nyora_favourite", [
    { manga_id: manga.url, added_at: now, sort_key: 0, updated_at: now },
    ]);

    // Later, pull it back:
    const favs = (await sync.select("nyora_favourite")).filter((f) => !f.deleted_at);

    The terminal reader wraps this same client:

    • Type sync (or pick ⚙ account (sync)) to sign in or out.
    • After opening a manga's details while signed in, answer "Favourite to library?" to push it with upsert.
    • Type lib (or pick ★ library) to browse your synced favourites.