.upsert() method
The .upsert() method in Supastash is a local-first merge operation that inserts or updates rows based on their id. If a row with the given id exists, it is updated; if not, a new one is inserted.
This operation is ideal when you want to ensure data exists without checking beforehand โ combining the behavior of .insert() and .update().
๐ง How It Worksโ
Each row is checked for existence using the provided
onConflictKeys(defaults to["id"]). If a match is found, the row is updated. Otherwise, it is inserted.
When .upsert() is called:
- The table and payload are validated.
- Each row is inspected to check if it already exists (via
id). - If it exists โ
UPDATEit. - If not โ
INSERTit. - Auto-adds timestamps:
updated_at, and optionallysynced_at. - Returns the upserted records from local DB.
โก Like other write operations,
.upsert()uses a debounced version tracker to batch rapid changes and trigger a single UI refresh per table.
๐งพ Accepted Payloadsโ
You can pass a single object or an array of objects:
// Single row
await supastash.from("users").upsert<T>({ id: "u1", name: "John" }).run();
// Multiple rows
await supastash
.from("users")
.upsert<T>([
{ id: "u1", name: "John" },
{ id: "u2", name: "Doe" },
])
.run();
If .single() is chained, the payload must not be an array.
// With custom conflict keys
await supastash
.from("chats")
.upsert<T>(
{ chat_id: "abc", user_id: "u1", status: "open" },
{
onConflictKeys: ["chat_id", "user_id"],
}
)
.run();
๐ Sync Modesโ
| Mode | Behavior |
|---|---|
localOnly | Performs upsert only on SQLite |
remoteOnly | Sends entire upsert payload to Supabase, skips local |
localFirst | (Default) Performs locally, queues sync to Supabase |
remoteFirst | Sends to Supabase first, mirrors to local if successful |
You can set sync behavior using .syncMode("...").
โฑ๏ธ Timestamp Handlingโ
Supastash helps you keep data in sync by managing timestamps consistently:
-
When using
.update()or.upsert(), if your payload does not include anupdated_atfield, Supastash will automatically assignupdated_at = new Date().toISOString()before saving locally and syncing remotely. -
This ensures reliable sync conflict resolution and avoids stale data.
Want to preserve a custom
updated_atvalue (e.g., from an imported backup or pre-synced record)? Simply include it in your payload:
await supastash
.from("tasks")
.upsert({ id: "xyz", title: "Fix bug", updated_at: oldDate })
.run();
If updated_at is explicitly set to null or undefined, it will be replaced with the current timestamp unless preserveTimestamp is configured.
โ ๏ธ For full control, use
.preserveTimestamp(true).
โ Return Shapeโ
If successful:
{
data: [...or single object],
error: null,
success: true
}
If something fails:
{
data: null,
error: { message: "..." },
success: false
}
โ ๏ธ ID Requirementโ
Every item must include an id field. If id is missing, the operation will fail.
await supastash.from("products").upsert({ name: "Invalid" }); // โ no id
โ
When to Use .upsert()โ
- Inserting or updating without worrying about row existence
- Syncing external or merged data sources
- Avoiding manual existence checks before saving
- Supporting conflict-free offline modifications
Next: .run() / .execute() behaviors