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 differentidfor 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.
| Code | Cause | Suggested 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_FAILEDwith messagefolder_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 reasonallowance_exceeded, andsync.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 whoseexport()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, andseige.files.get(path)returns each file as aBlob. 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 withseige.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.