Skip to content

JavaScript: Create the Storage transfer

createSupabaseTransfer(options) from kizunasync/supabase builds the byte mover the attachment queue drives. It uploads through Supabase Storage, downloads through a short-lived signed URL, and records each object's hash so a peer can verify what it downloaded.

The createSupabaseKizunaSync call in src/kizunasync.ts, shown on Initializing, builds this adapter by itself once it receives a fileStore, so an app calls this factory only to change one of the values below, and passes the result as transfer in that same file. Read this page to see how bytes move, how long each request may take, and which errors an attachment entry records.

Examples#

Basic#

// src/kizunasync.ts (excerpt)
import { createSupabaseTransfer } from 'kizunasync/supabase'
import { createWebFileStore } from 'kizunasync/web'
import { supabase } from './supabase-client'

const fileStore = createWebFileStore()
const transfer = createSupabaseTransfer({ client: supabase, fileStore })

Pass it to the client#

The whole module, here giving large files on slow links more time:

// src/kizunasync.ts
import { attachment, byOwner, defineConfig } from 'kizunasync'
import { createSupabaseKizunaSync, createSupabaseTransfer } from 'kizunasync/supabase'
import { createWebFileStore, createWebWorkerDriver } from 'kizunasync/web'
import { supabase } from './supabase-client'

const config = defineConfig({
  tables: {
    todos: {
      sync: 'read-write',
      bucket: byOwner('user_id'),
      attachments: { image_path: attachment('todo-images') },
    },
  },
})

const fileStore = createWebFileStore()

export const kizunasync = createSupabaseKizunaSync({
  supabase,
  driver: createWebWorkerDriver('todos.db'),
  config,
  fileStore,
  transfer: createSupabaseTransfer({ client: supabase, fileStore, bytesTimeoutMs: 300_000 }),
})

Parameters#

NameTypeRequiredDescription
options.clientSupabaseClientYesThe supabase-js client. Its Storage slice moves the bytes, its kizunasync schema records the metadata, and its session authorizes both. A resumable upload started with no session falls back to the client's publishable key as the Bearer.
options.fileStoreIFileStoreYesThe local sandbox the bytes are read from and written to. Browser file store provides one for the web.
options.controlTimeoutMsnumberNoDeadline for the short requests: the session lookup, the resumable create and offset probe, the signed URL, the confirm call, the metadata lookup, and the two removals. A value at or below 0 disables it. Default: DEFAULT_TRANSFER_CONTROL_TIMEOUT_MS (30000) from kizunasync, shared with Expo: Native download.
options.bytesTimeoutMsnumberNoDeadline for the requests that carry bytes: the single-shot upload, each resumable chunk, and the download. A value at or below 0 disables it. Kept separate because a 6 MiB body on a mobile link legitimately takes longer than a control request. Default: DEFAULT_TRANSFER_BYTES_TIMEOUT_MS (120000) from kizunasync, shared with Expo: Native download.
options.loggerILoggerNoSink for the adapter's byte counts and outcomes. Default: silent.
options.singleShotMaxBytesnumberNoThe size at which the upload switches to the resumable protocol. Intended for tests; the value the protocol requires is the default. Default: 6291456, which is 6 MiB.
options.tusEndpointstringNoOverrides the resumable endpoint. Intended for tests. Default: the project URL followed by /storage/v1/upload/resumable, or, when that URL is a *.supabase.co host, https://<ref>.storage.supabase.co/storage/v1/upload/resumable.
options.setTimer / options.clearTimer(callback: () => void, delayMs: number) => unknown / (handle: unknown) => voidNoTimer pair threaded into every deadline this adapter arms. Default: the platform setTimeout and clearTimeout.

Returns#

ITransfer, the value Initializing takes as its transfer argument.

NameTypeRequiredDescription
createUpload(localPath, target, options) => Promise<IUploadHandle>—Sends a file at or under the threshold as one standard upload with overwrite enabled, and anything larger as a resumable upload in 6 MiB chunks. The session URL is announced before the first chunk moves, so the queue persists it in case the transfer is interrupted.
download(target, toLocalPath, options?) => Promise<void>—Signs the object for 60 seconds, fetches it, verifies the bytes against the expected hash, and writes them to the sandbox only then. The signed URL is used rather than the SDK download because React Native cannot build a blob the SDK way.
confirm(target, meta) => Promise<void>—Records the object's hash, size, and content type through the attachment_confirm function, which is what lets a peer verify its download.
metadata(target) => Promise<{ sha256: string } | null>—Reads the recorded hash for one object through the definer RPC kizunasync.attachment_metadata, or null when the server has none. The RPC checks that the caller can read the owning row's table through Row Level Security before it answers, which is what lets a peer verify a download it did not upload; the table itself grants no SELECT to a client role.
remove(target) => Promise<void>—Deletes the Storage object and then clears its metadata through the attachment_vacuum function.

Errors#

This adapter throws every failure to the queue, which records it on the attachment entry and retries on a later run.

CodeCondition
ATTACHMENT_TRANSFER_TIMEOUTA request outlived controlTimeoutMs or bytesTimeoutMs.
AUTH_SESSION_TIMEOUTThe session lookup before a resumable upload outlived controlTimeoutMs.
ATTACHMENT_NOT_YET_AVAILABLEThe object answered 404, which Supabase documents under error codes. The download entry stays queued rather than failing, because the writing device has probably not uploaded it, and the attempt still counts against the budget.
ATTACHMENT_UPLOAD_EXPIREDThe resumable session answered 404 or 410, so it cannot be resumed. The port documents the session as valid for up to 24 hours; the client detects the end of it from the status rather than from a clock. The stored session URL is dropped and the next attempt starts from zero.
ATTACHMENT_HASH_MISMATCHThe downloaded bytes hashed to something other than the expected value, checked before anything is written to the sandbox.
(untagged)A refused upload, a signed URL that failed for any other status, a rejected confirm or vacuum call, or a resumable upload started with no session and no publishable key configured either.

Notes#

A single-shot upload cannot be aborted, because the Storage client exposes no signal for it. The deadline therefore bounds only how long this adapter waits, and a request that times out may land on the server regardless. That is harmless: the retry re-uploads with overwrite enabled.

Nothing here decides who may read or write an object. The bucket's own policies on storage.objects do, and Storage policies shows the rules the attachment path needs.

On iOS and Android the platform fetch differs, so Expo: Native download replaces the download half. It supplies that one method, and the other four stay as they are.

The queue drives this adapter only from a full Sync, after the outbox has drained, so the row already carries the reference when the object appears. Downloads are lazy and run from Resolve a download instead.

Found a problem? Edit this page on GitHub