How NextChat Synchronizes Chat History with WebDAV and Upstash
NextChat synchronizes chat history by abstracting WebDAV and Upstash behind a unified SyncClient interface that retrieves remote state, merges it with local application data, and pushes updates back to the cloud provider.
NextChat (ChatGPTNextWeb/NextChat) stores complete application state—including conversation history—in a local AppState object. When users enable cloud synchronization through Settings → Sync, the application can back up and restore this state using either WebDAV or Upstash Redis. Both providers implement the same minimal interface, allowing the sync logic in app/store/sync.ts to remain provider-agnostic while handling authentication, chunking, and conflict resolution automatically.
Provider Abstraction Architecture
The synchronization system relies on a generic SyncClient interface defined in app/utils/cloud/index.ts. This abstraction ensures that the high-level sync logic never directly interacts with provider-specific APIs.
// app/utils/cloud/index.ts
export type SyncClient = {
get: (key: string) => Promise<string>;
set: (key: string, value: string) => Promise<void>;
check: () => Promise<boolean>;
};
A factory function createSyncClient instantiates the appropriate concrete implementation based on the user's selected ProviderType:
export function createSyncClient<T extends ProviderType>(
provider: T,
config: SyncClientConfig[T],
): SyncClient {
return SyncClients[provider](config as any) as any;
}
This design allows app/store/sync.ts to treat WebDAV and Upstash identically, calling only get(), set(), and check() regardless of the backend storage mechanism.
WebDAV Client Implementation
The WebDAV provider (app/utils/cloud/webdav.ts) stores the entire backup as a single JSON file using standard HTTP verbs.
Authentication and Headers
The client constructs a Basic Auth header by base64-encoding the username and password combination:
const auth = btoa(config.username + ":" + config.password);
// Used in headers() method
Storage Structure
- File Location:
backup.jsoninside a folder defined bySTORAGE_KEY - Operations:
MKCOL(verify/create directory),GET(download),PUT(upload) - Proxy Support: When
store.useProxyis enabled, requests route throughstore.proxyUrlto handle CORS restrictions
const res = await fetch(this.path(fileName, proxyUrl), {
method: "PUT",
headers: this.headers(),
body: value,
});
The check() method validates connectivity by issuing an MKCOL request to ensure the remote directory exists and is accessible.
Upstash Client Implementation
The Upstash provider (app/utils/cloud/upstash.ts) interacts with the Upstash Redis HTTP API, automatically handling payload size limitations through intelligent chunking.
Authentication
Unlike WebDAV's Basic Auth, Upstash uses Bearer token authentication:
return { Authorization: `Bearer ${config.apiKey}` };
Chunking Strategy
Because Upstash free tier limits request payloads to 1 MiB, the client splits large JSON states into chunks using the chunks() utility from app/utils/format:
// Writing state in chunks
for await (const chunk of chunks(value)) {
await this.redisSet(chunkIndexKey(index), chunk);
}
await this.redisSet(chunkCountKey, index.toString());
Key Structure
- Chunk Keys:
<storeKey>-chunk-0,<storeKey>-chunk-1, etc. - Metadata Key:
<storeKey>-chunk-countstores the total number of chunks - Reading: The client fetches all chunk keys in sequence and concatenates them (
chunks.join("")) to reconstruct the original JSON
Sync Workflow and State Management
The central orchestration lives in app/store/sync.ts within the useSyncStore Zustand store.
The Sync Process
The sync() method executes a deterministic four-step workflow:
- Capture Local State: Retrieves current application state via
getLocalAppState() - Fetch Remote: Calls
client.get(config.username)to retrieve the stored backup - Merge: If remote data exists, merges it with local state using
mergeAppState(); otherwise uses local state exclusively - Persist: Writes the merged state back to the cloud via
client.set(config.username, JSON.stringify(localState)) - Timestamp: Records completion time via
markSyncTime()
Client Instantiation
The getClient() method dynamically creates the correct implementation:
const client = this.getClient(); // Returns WebDAV or Upstash client based on store.provider
This provider-agnostic approach means the sync logic remains identical regardless of whether the user configured WebDAV or Upstash in their settings.
Practical Implementation Examples
Configure WebDAV Synchronization
import { useSyncStore, ProviderType } from "@/app/store/sync";
useSyncStore.setState({
provider: ProviderType.WebDAV,
useProxy: true,
proxyUrl: "/api/cors/",
webdav: {
endpoint: "https://mydav.example.com/remote.php/webdav",
username: "myuser",
password: "mypassword",
},
});
Configure Upstash Synchronization
useSyncStore.setState({
provider: ProviderType.UpStash,
upstash: {
endpoint: "https://mydb.upstash.io",
apiKey: "AXXX...",
},
});
Trigger Manual Synchronization
async function performSync() {
const sync = useSyncStore.getState();
try {
await sync.sync();
console.log("Synchronization completed successfully");
} catch (error) {
console.error("Synchronization failed:", error);
}
}
Verify Provider Connectivity
const isReachable = await useSyncStore.getState().check();
// Returns true if the provider responds correctly, false otherwise
Automated Periodic Sync
// Sync every 10 minutes
setInterval(() => {
useSyncStore.getState().sync().catch(console.error);
}, 10 * 60 * 1000);
Summary
- NextChat uses a unified
SyncClientinterface inapp/utils/cloud/index.tsto abstract provider-specific implementations. - WebDAV (
app/utils/cloud/webdav.ts) stores data as a singlebackup.jsonfile using Basic Auth and standard HTTP verbs, with optional CORS proxy support. - Upstash (
app/utils/cloud/upstash.ts) stores data as multiple Redis keys using Bearer token authentication, automatically chunking payloads into 1 MiB segments to respect API limits. - The sync workflow in
app/store/sync.tsmerges remote and local states bidirectionally, ensuring chat history remains consistent across devices. - Both providers expose identical
get,set, andcheckmethods, making the high-level synchronization logic completely provider-agnostic.
Frequently Asked Questions
What is the maximum backup size supported by Upstash in NextChat?
Upstash free tier limits individual HTTP request payloads to 1 MiB. To accommodate this, NextChat automatically splits large state objects into chunks under this limit, storing them as separate Redis keys (<storeKey>-chunk-<index>) and reassembling them during retrieval. This effectively allows backups of any practical size supported by your Upstash database limits.
How does NextChat authenticate with WebDAV servers?
NextChat uses HTTP Basic Authentication for WebDAV connections. The client base64-encodes the username and password combination (btoa(username + ":" + password)) and includes this in the Authorization header of every MKCOL, GET, and PUT request. The credentials are retrieved from the SyncStore configuration object where users save their WebDAV settings.
Why does WebDAV use a single file while Upstash uses multiple keys?
WebDAV natively supports file-based storage, making it efficient to store the entire application state as one JSON file (backup.json) that gets overwritten atomically on each sync. Upstash is a key-value store with request size limitations, so NextChat implements a chunking strategy that splits the JSON into manageable segments. This allows the app to respect Upstash's 1 MiB request limit while still supporting full state backups that may exceed this size when uncompressed.
What happens if a synchronization is interrupted halfway through?
NextChat's sync logic in app/store/sync.ts performs atomic operations at the provider level. For WebDAV, a PUT request either succeeds or fails as a single operation. For Upstash, if writing chunks fails partway through, the next successful sync will overwrite the previous partial state, as the chunk-count key only updates after all chunks are written successfully. However, the merge logic always happens locally before uploading, so local data remains safe even if the network upload fails.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →