Before you launch

A Seige app keeps its users’ data in their own accounts, so the decisions that matter at launch are about identity, headers and what users see when something fails. This page lists them in the order most apps meet them.

Each website origin is a separate app

Without a clientId, Seige identifies an app by its origin: scheme, host and port. https://trips.example, https://www.trips.example, a preview deployment such as https://trips-git-feature.vercel.app and http://localhost:5173 are four different apps to Seige. For each origin:

  • users approve the app again, on every browser they use;
  • seige.auth.user() returns a different id for the same person, so do not use the id to link records across origins;
  • Connected apps in the user’s Seige settings lists the app separately, with its website, and the user can disconnect each one.

What a second origin can reach depends on what the user chooses on the approval screen. The screen suggests a folder named after appName. When the user already has a folder of that name, for example the production app’s folder, the screen shows ““Trips” already contains data” with a warning that the application will be able to read, change and delete data in it, and two choices: “Trust application and allow access”, which gives the new origin the existing folder and its data, and “Create a separate folder”, which creates an empty folder such as “Trips (2)”. Seige does not link the two origins on its own; the user decides.

For preview and staging deployments, pass a different appName, for example from an environment variable: createSeige({ appName: "Trips (preview)" }). The approval screen then offers a new folder by default, and testers do not see the production folder warning.

Choose the production origin before launch. If the app later moves to a new domain, every user approves it again on the new domain, gets a new user id, and sees the existing-folder warning for their folder. Their data is available on the new domain only if they choose to trust it. Tell users in advance which folder name to expect.

Decide whether to register before launch

Registering the app in Connected apps and passing its clientId shows the registered name on the approval screen and lets the app use several callback pages. All callback URLs of one registration must be on the same host and port; registration rejects a list that mixes hosts, so a preview domain needs its own registration. Registrations cannot currently be edited or deleted in Connected apps.

A clientId is the app’s identity in place of its origin. Adding one to an app that already has users makes it a new app for every one of them: on their next visit they are signed out, they approve the app again, their auth.user() id changes, and the approval screen shows the existing-folder warning for their folder. Their data stays available if they choose “Trust application and allow access”. If you expect to register, register before launch and ship the first public build with the clientId.

Send headers that allow sign-in and the SDK workers

Send Cross-Origin-Opener-Policy: same-origin-allow-popups on the app’s pages, or no COOP header. The sign-in result returns from the Seige window through window.opener, and same-origin cuts that link: the SDK detects it and returns OPERATION_FAILED with message opener_policy_blocks_sign_in. The SDK reads the page’s own headers when it starts, so this error normally comes before any window opens. Pages that need cross-origin isolation, which requires same-origin, cannot sign in. For a registered app, the callback page is the app’s own page; with same-origin-allow-popups it hands the result back to the app’s tab over a BroadcastChannel and closes, so it must load the SDK and call it on load, as the quickstart does.

A Content-Security-Policy needs three things for the SDK: the Seige origin in connect-src, 'wasm-unsafe-eval' in script-src for the WebAssembly modules, and worker-src 'self' for the two module workers. The page prompt and connection badge use a constructed stylesheet and inline SVG, so they need neither 'unsafe-inline' styles nor data: images. This policy works with a bundled app on seige.ai:

Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self'; img-src 'self'; connect-src 'self' https://account.seige.ai; worker-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'
Cross-Origin-Opener-Policy: same-origin-allow-popups

Send the header on every response, including the worker scripts and .wasm files, as most hosts do when a header is set for the whole site. For another Seige environment, replace https://account.seige.ai with the origin of its issuer. Add whatever your own app needs, such as a nonce for a framework’s inline scripts. No frame-src entry is needed: Seige opens in a window, not a frame.

When the policy blocks a request to Seige, calls return OPERATION_FAILED with message blocked_by_csp and the blocked directive in error.details. When it blocks WebAssembly or a worker, the message is worker_unavailable and the hint names the file and the cause.

Vite, Next.js and common static hosts already serve .wasm files as application/wasm and .mjs files as JavaScript. Check this only if you run your own static server; the wrong type also produces worker_unavailable.

Show users a message for every error code

Choose the text users see from error.code, and log error.message and error.hint for yourself. The messages below are suggestions; adapt them to your app and name only buttons it actually has.

CodeCauseSuggested message
ACCESS_DENIED The user selected Cancel in the Seige window. With message ai_access_denied, they declined only AI access and the app stays connected. “Not connected to Seige. To try again, select Sign in with Seige.” For ai_access_denied: “AI features need your approval. Everything else keeps working.”
SIGN_IN_CANCELLED The user closed the Seige window or dismissed the page prompt. “Sign-in was not finished. To try again, select Sign in with Seige.”
POPUP_BLOCKED The browser blocked the Seige window. “Your browser blocked the Seige window. Allow pop-ups for this site and try again.”
REAUTH_REQUIRED The app’s Seige session in this browser ended. “Your Seige session ended. Sign in again to keep saving. Your changes on this device are kept.”
PERMISSION_REVOKED The user disconnected the app or removed its access. The next call asks them to connect again. “This app was disconnected from your Seige account. Connect it again to continue.”
PERMISSION_DENIED The app lacks the access a call needs, such as write access to a folder shared read-only. A write through a database handle opened with { readonly: true } has message read_only. “This app does not have access to that folder.”
NETWORK_UNAVAILABLE Seige could not be reached. Writes that already succeeded are published later; reads, sign-in and AI calls are not retried. “Seige is unreachable. Changes you saved stay on this device and are sent when the connection returns.”
RATE_LIMITED Seige is limiting requests, for example the hourly AI limit. error.details.retryAfter is the wait in seconds when known; for AI, seige.ai.usage() reports resetAt. “Too many requests right now. Try again in a few minutes.”
ALLOWANCE_EXCEEDED The account is over its storage or AI allowance. Files saved in this browser stay queued. “Your Seige account is out of storage or AI allowance. Files you added are kept on this device until there is space.”
CONFLICT Another browser replaced the version a pending change was based on. The local change is kept until the app resolves the conflict. “This changed on another device while you were offline. Your changes are kept on this device.” Add how to review them if the app offers it.
IN_USE Another browser or tab kept writing the database for about 45 seconds. The write was not saved, so retrying it does not create a duplicate. A write whose page closed during the wait is kept in that browser and runs the next time the app opens there. “This app is saving in another browser or tab. Try again in a moment.”
MIGRATION_FAILED A migration could not be applied, or an applied migration was edited. The database stays at the last good migration. “This version of the app could not update your data. Your data is unchanged.”
CONSENT_REQUIRED The user has not approved the app’s storage yet. “Connect this app to Seige to continue.”
UNSUPPORTED The app called a feature Seige does not support, such as textSearch(). A bug in the app, not a user problem. “That action is not available.”
OPERATION_FAILED Any other failure. error.message is the reason, for example worker_unavailable, blocked_by_csp, opener_policy_blocks_sign_in, sign_in_timed_out, folder_member_limit or file_not_found. “Something went wrong. Try again later.” For sign_in_timed_out: “The Seige window did not finish. Close it and try again.” For folder_member_limit: “Too many browsers are connected to this app. Disconnect one you no longer use in Seige, under Connected apps.”
SQLSTATE codes Query errors: 23505 unique value exists, 23502 required value missing, 23503 foreign key, 23514 check constraint, 42P01 no such table, 42703 no such column, 42501 statement not allowed (such as ATTACH), 21000 update or delete without a filter, PGRST116 single() did not match one row, 20 aborted. For 23505: “That name is already used.” Most others are app bugs: “Something went wrong.”

SDK 0.5.1 and earlier used SEIGE_OPERATION_FAILED, SEIGE_CONSENT_REQUIRED and SEIGE_UNSUPPORTED; they are now the unprefixed codes, and error.legacyCode keeps the old value for one release.

Open the Seige window from a click

Browsers open windows only in response to a click or key press. Put the first call that needs the user, usually seige.auth.signIn(), in a click handler. A call made without one, such as a read on page load or a call several seconds after the click, shows the “Continue with Seige” card on the page; its button opens the window. If the browser still blocks the window, the call returns POPUP_BLOCKED. On page load, call seige.auth.user(), which never opens anything, and show a sign-in button when its data is null.

Plan for private windows and cleared browser storage

The SDK keeps the app’s keys, session and pending changes in the browser’s IndexedDB for your origin. When that storage is gone, the browser is a new installation of the app:

  • A private window starts empty and discards its storage when it closes, so each private window asks the user to approve the app.
  • Clearing site data for your origin has the same effect. Safari can also delete a site’s script-written storage after seven days of use without a visit to that site.
  • Changes saved in that browser and not yet published are lost with the storage. Data already in Seige is not affected, and the next approval on that browser shows it again.
  • Each installation appears under Connected apps in the user’s Seige settings. A folder accepts a limited number of connected browsers; when it is reached, sign-in returns OPERATION_FAILED with message folder_member_limit, and the user can disconnect browsers they no longer use.

Call seige.sync.flush() before actions that end a session, such as signing out, and show seige.sync.status() so users can see when changes are still waiting to be sent.

Know the allowance and rate limits

  • Storage counts against the user’s Seige storage allowance, 500 MB by default. Over it, uploads are refused with ALLOWANCE_EXCEEDED: files.put() still resolves, the file stays queued in the browser, sync.resources() reports reason allowance_exceeded, and sync.flush() returns the error until there is space.
  • A database is limited to 32 MiB, and a query result to 10,000 rows and 8 MiB.
  • App AI allows 20 requests per app per hour and 100 per account across apps, with one active request per account, charged to the user’s AI allowance. A request that fails because the model provider failed is not counted against the hourly limits.
  • Seige limits each IP address to 120 sign-in requests and 6,000 data requests per minute. Over a limit, calls return RATE_LIMITED.

Offer export and deletion in your app

Users’ data is in their Seige accounts, not on your servers. Seige does not yet offer account deletion or a whole-account export, so give users these controls inside your app:

  • Export the database. seige.db.open("app.sqlite", { readonly: true }) returns a handle whose export() gives the SQLite file stored in Seige, which any SQLite tool can open. Offer it as a download, or convert rows to CSV or JSON in the app.
  • Export files. seige.files.list(prefix) lists the files and folders under a path, and seige.files.get(path) returns each file as a Blob. The app’s own database file is not listed.
  • Delete data. seige.files.delete(path) deletes a file permanently and frees its storage; it does not go to a trash. Delete rows with seige.from(table).delete() and a filter. The SDK has no call that deletes the app’s database file or whole folder.
  • Disconnecting is not deleting. When a user disconnects your app in Seige, its folder and data stay in their account. If you shut the app down, the data also stays; another app the user approves for that folder can read it if it understands the format, so keep your schema documented in migrations.