Skip to main content

๐Ÿ“š Sync Log Utilities

Supastash tracks created_at, updated_at, and deleted_at checkpoints per table and per filter key.

Starting from newer releases, Supastash uses updated_at as the primary sync cursor.
This shift was introduced to enhance performance, reduce redundant network calls, and simplify sync logic, while preserving backward compatibility.

๐Ÿ’ก updated_at is now the authoritative cursor used during sync operations.
created_at and deleted_at are retained for metadata, inspection, analytics, and legacy consumers.

Legacy helpers such as setLocalSyncLog / setLocalDeleteLog are deprecated.


Table of Contentsโ€‹


Sync Cursor Model (Important)โ€‹

Supastash follows this internal model:

updated_atโ€‹

  • The single authoritative sync cursor
  • Used to determine which rows are pulled from the server
  • Drives last_synced_at
  • Used together with the row primary key (id) as a compound cursor
  • Internally tracked as (last_synced_at, last_synced_at_pk) to ensure stable paging
  • Guaranteed to update on every mutation (create, update, soft-delete)

๐Ÿ’ก Supastash uses a compound cursor internally: (updated_at, id)

This guarantees correct pagination when multiple rows share the same updated_at timestamp.

created_atโ€‹

  • Preserved for metadata and legacy use
  • Not used to determine sync boundaries
  • Useful for analytics and audit trails

deleted_atโ€‹

  • Used to represent soft-deletes
  • Informational only
  • Does not advance the sync cursor independently

โš ๏ธ Important: Even though all three timestamps are tracked, only updated_at affects sync progression.


Functionsโ€‹

๐Ÿงน clearLocalSyncLog(tableName: string)โ€‹

Clears the stored sync log for a single table.

Usage:

await clearLocalSyncLog("users");

When to use:

  • When you want to force a full resync of a specific table
  • During development/testing
  • When recovering from sync errors

๐Ÿ”„ clearAllLocalSyncLog()โ€‹

Drops the entire sync-status table and recreates it.

Usage:

await clearAllLocalSyncLog();

โš ๏ธ Warning: This removes sync checkpoints for all tables. Use with caution in production environments.

When to use:

  • Complete application reset
  • Major schema migrations
  • Testing fresh sync scenarios

๐Ÿ“ฅ getSyncLog(tableName: string)โ€‹

Returns the stored sync status for one table.

Usage:

const log = await getSyncLog("users");

Returns:

{
table_name: "users",
last_synced_at: "2024-06-01T10:00:00.000Z",
last_synced_at_pk: "00000000-0000-0000-0000-000000000000"
last_created_at: "2024-06-01T10:00:00.000Z",
last_deleted_at: "2024-06-01T10:00:00.000Z",
filter_key: "abc123",
filter_json: "[...]",
updated_at: "2024-06-01T10:00:00.000Z"
}

Returns null if the table has no entry.

Use cases:

  • Debugging sync status
  • Building custom sync dashboards
  • Monitoring sync health

โœ๏ธ setSyncLog(table: string, filters: RealtimeFilter[] | undefined, opts)โ€‹

Writes (or updates) sync metadata for a table.

Usage:

await setSyncLog("users", undefined, {
lastSyncedAt: new Date().toISOString(),
lastCreatedAt: new Date().toISOString(),
lastDeletedAt: null, // optional
});

With filters:

await setSyncLog("posts", [{ column: "user_id", value: "123" }], {
lastSyncedAt: new Date().toISOString(),
lastCreatedAt: new Date().toISOString(),
filterNamespace: "user_posts",
});

Optionsโ€‹

FieldTypeRequiredDescription
lastSyncedAtstringYesHighest updated_at pulled for this table (authoritative).
last_synced_at_pkstringYesHighest id pulled for this table (authoritative).
lastCreatedAtstringYesHighest created_at observed (informational).
lastDeletedAtstring | nullNoHighest deleted_at observed for soft-deletes.
filterNamespacestringNoOptional namespace to separate different filter sets.

When filters are supplied, a filter key is computed and stored alongside the timestamps.


๐Ÿ” resetSyncLog(table: string, filters?: RealtimeFilter[], scope?: "all" | "last_synced_at" | "last_created_at" | "last_deleted_at")โ€‹

Resets one or more timestamps for a table.

Usage examples:

// Reset everything for "users"
await resetSyncLog("users", undefined, "all");

// Only reset the sync cursor
await resetSyncLog("users", undefined, "last_synced_at");

// Only reset deleted metadata
await resetSyncLog("users", undefined, "last_deleted_at");

// Reset with specific filters
await resetSyncLog("posts", [{ column: "user_id", value: "123" }], "all");

Scope valuesโ€‹

ScopeDescription
"all" (default)Resets created, updated, and deleted checkpoints.
"last_synced_at"Resets only the authoritative updated_at cursor.
"last_synced_at_pk"Resets only the authoritative id cursor.
"last_created_at"Resets only the created metadata.
"last_deleted_at"Resets only the deleted metadata.

When to use:

  • Forcing a partial resync
  • Testing specific sync scenarios
  • Recovering from corrupted sync state

๐Ÿ—‘ clearSyncLog(table: string, filters?: RealtimeFilter[])โ€‹

Removes the sync status row for a table and filter key.

Usage:

// Clear all sync logs for "users" table
await clearSyncLog("users");

// Clear sync log for specific filter
await clearSyncLog("posts", [{ column: "user_id", value: "123" }]);

Behavior:

  • If filters are provided, only that filter key is cleared.
  • If no filters are provided, all sync logs for the table are removed.

Difference from resetSyncLog:

  • clearSyncLog deletes the sync log entry entirely
  • resetSyncLog resets timestamps to null but keeps the entry

Usage Notesโ€‹

  • These helpers manage rows in the internal supastash_sync_marks table.
  • They are safe to call manually (for testing, resets, or debugging).
  • In normal production flows, Supastash updates these automatically after pull and push operations.
  • Although multiple timestamps are tracked, sync behavior is driven exclusively by updated_at.
  • Supastash resumes sync using both last_synced_at and last_synced_at_pk to avoid duplicate or skipped rows when timestamps collide.
  • Always use setSyncLog with ISO 8601 formatted timestamps.
  • Filter keys are automatically computed from filter arrays for consistency.

Migration Guideโ€‹

Upgrading from Older Versionsโ€‹

If you're upgrading from an older version of Supastash that used created_at as the primary cursor:

What Changedโ€‹

Before (v0.1.6):

  • Sync relied on separate created_at and deleted_at tracking
  • Newly created rows were often processed more than once
  • Multiple cursor paths increased complexity and edge cases

After (v0.1.6+):

  • Sync now uses a compound (updated_at, id) cursor internally for stable pagination
  • All changes (create, update, soft-delete) advance sync uniformly
  • Fewer network calls, simpler logic, and improved performance

Migration Stepsโ€‹

  1. No immediate action required - The new system is backward compatible.

  2. Deprecated functions - Replace these if you're using them:

    // โŒ Old (deprecated)
    await setLocalSyncLog("users", new Date().toISOString());
    await setLocalDeleteLog("users", new Date().toISOString());

    // โœ… New
    await setSyncLog("users", undefined, {
    lastSyncedAt: new Date().toISOString(),
    lastCreatedAt: new Date().toISOString(),
    });
  3. Optional: Force resync - To ensure clean state:

    await resetSyncLog("your_table", undefined, "all");

    โ„น๏ธ Supastash automatically updates updated_at for all mutations it performs (creates, updates, and soft-deletes).

    However, database-level enforcement is still recommended, especially if:

    • you write to the database outside of Supastash
    • you use SQL scripts, dashboards, or RPCs
    • multiple services interact with the same tables
  4. Update your database schema (recommended)

Ensure your tables have updated_at triggers at the database level to guarantee consistency across all writes.

This makes it impossible for any mutation to bypass updated_at, even outside Supastash.

-- Example trigger for PostgreSQL
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

Best Practicesโ€‹

Let Supastash manage sync logs automaticallyโ€‹

  • Don't manually call setSyncLog unless you have a specific reason
  • Trust the built-in sync mechanisms

Troubleshootingโ€‹

Sync appears stuckโ€‹

// Check current sync status
const status = await getSyncLog("users");
console.log("Last sync:", status?.last_synced_at);

// Force resync
await resetSyncLog("users", undefined, "last_synced_at");

Data not appearing after syncโ€‹

// Verify sync log exists
const log = await getSyncLog("users");
if (!log) {
console.log("No sync log found - first sync will be full");
}

// Check for filter mismatches
console.log("Current filter key:", log?.filter_key);

Duplicate data appearingโ€‹

// Clear and resync
await clearLocalSyncLog("users");
await pullData("users");


API Reference Summaryโ€‹

// Clear operations
clearLocalSyncLog(tableName: string): Promise<void>
clearAllLocalSyncLog(): Promise<void>
clearSyncLog(table: string, filters?: RealtimeFilter[]): Promise<void>

// Read operations
getSyncLog(tableName: string): Promise<SyncLog | null>

// Write operations
setSyncLog(
table: string,
filters: RealtimeFilter[] | undefined,
opts: {
lastSyncedAt: string;
lastCreatedAt: string;
lastDeletedAt?: string | null;
filterNamespace?: string;
}
): Promise<void>

// Reset operations
resetSyncLog(
table: string,
filters?: RealtimeFilter[],
scope?: "all" | "last_synced_at" | "last_created_at" | "last_deleted_at"
): Promise<void>

Last updated: January 2025
Version: 0.1.6+
Supastash Documentation