# Seige application integration Seige gives browser apps sign-in, a database, file storage and AI, kept in the user's encrypted Seige account. Apps need no backend and no registration. Removing an app's access keeps the user's data. ## Canonical sources - App directory: https://seige.ai/apps/ (curated apps, descriptions and launch links) - Developer guide: https://seige.ai/developers/ - Complete package reference: https://seige.ai/developers/sdk-reference.md - Testing your app (Playwright, CI): https://seige.ai/developers/testing/ - Before you launch (origins, registration, headers, error table, user data): https://seige.ai/developers/launch-checklist/ - Coming from Supabase: https://seige.ai/developers/from-supabase/ - SDK archive: https://seige.ai/sdk/seige_sdk-sdk-0.7.0.tgz - Coding-agent prompt: https://seige.ai/seige-integration-prompt.txt - Integration overview: https://seige.ai/vibe-coding-backend/ - Account encryption: https://seige.ai/security/ ## Environments These documents describe the production service. Apps use https://account.seige.ai/oauth by default; pass createSeige({ issuer }) to use another Seige environment, such as a local or test one, for example from a build-time variable (Vite: issuer: import.meta.env.VITE_SEIGE_ISSUER || undefined). The issuer's origin is the only Seige origin an app talks to. AI is available where that environment provides it. ## Minimal app import { createSeige } from "@seige_sdk/sdk"; const seige = createSeige(); await seige.from("trips").insert({ city: "Lisbon", nights: 3 }); const { data: trips } = await seige.from("trips").select().order("city"); await seige.files.put(`trips/${file.name}`, file); const { data: answer } = await seige.ai.generate({ prompt: "What should I pack?", input: trips }); ## Setup npm install @seige_sdk/sdk. No bundler configuration is needed with Vite 5-8, Next.js 16 (App Router; Turbopack or webpack) or webpack 5. In Next.js, use the SDK in "use client" components and make calls from event handlers or effects. In a workspace that does not hoist the SDK into the app's node_modules, Vite's dev server may report worker_unavailable; add optimizeDeps: { exclude: ["@seige_sdk/sdk"] }. Workers and Wasm load on the first data call (crypto Wasm about 3.1 MB, about 1 MB gzip; SQLite Wasm about 870 KB, about 400 KB gzip); the SDK's page JavaScript is about 85 KB gzip. HTTPS in production; http://localhost for development. createSeige(options?) takes no required options and does no work until the first call. Seige identifies an unregistered app by its website origin. Optional: appName, a display label (up to 80 characters) shown with the website on the approval screen and used to name a new home folder; clientId for a registered app (shows its registered name and allows several callback pages on one host; adding it to an app with users changes their ids); issuer; migrations; database (default "app.sqlite"). Headers: Cross-Origin-Opener-Policy same-origin-allow-popups or none (same-origin fails with opener_policy_blocks_sign_in). A Content-Security-Policy needs connect-src with the issuer origin, script-src 'wasm-unsafe-eval' and worker-src 'self'; no 'unsafe-inline' styles or data: images. Do not build login, token storage, encryption, a backend or a database server. Account passwords, master keys and provider credentials never belong in app code. ## Results Every call returns { data, error } and never throws; exceptions: seige.sync.status() and seige.sync.resources() return plain values, seige.auth.onChange() returns an unsubscribe function, seige.close() returns nothing. error is a SeigeError with code, message (the specific reason) and hint (a note for developers on what happened and what to do next); choose the text shown to users from code. error.status is the HTTP status when Seige answered; error.details carries retryAfter (RATE_LIMITED), worker and reason (worker_unavailable) or directive and blockedURI (blocked_by_csp). Codes (type SeigeErrorCode, runtime ERROR_CODES): - ACCESS_DENIED: user declined in the Seige window, including through seige.from(); message ai_access_denied when only AI was declined (app stays connected). - SIGN_IN_CANCELLED: user closed the Seige window or dismissed the page prompt. - POPUP_BLOCKED: browser blocked the window; call again from a click. - REAUTH_REQUIRED: the session ended; seige.auth.signIn(). - PERMISSION_REVOKED: user disconnected the app; the next call asks again. - PERMISSION_DENIED: the app lacks the access (for example write to a read-only folder); seige.request(). - NETWORK_UNAVAILABLE: Seige unreachable; the call did not complete. Saved writes publish later; sign-in and AI are not retried. - RATE_LIMITED: HTTP 429, including the hourly AI limits; details.retryAfter seconds when known; seige.ai.usage() has resetAt. - ALLOWANCE_EXCEEDED: storage or AI allowance used up (HTTP 402, or 413 quota_exceeded). - CONFLICT: a conflicting change; the hint names the resolving call. - IN_USE: another browser or tab kept writing the database; the write waited about 45 seconds and was not saved, so retrying it does not duplicate it. A write whose page closed during the wait is not reported: it is kept in the browser and runs on the newest version the next time the app opens there, and can surface through seige.sync as CONFLICT. - MIGRATION_FAILED: a migration failed or an applied migration was edited; queries fail until fixed. - CONSENT_REQUIRED: app storage not approved yet. - UNSUPPORTED: a supabase-js feature Seige lacks (textSearch, csv, explain, embedded relations). - OPERATION_FAILED: anything else; message names it, for example worker_unavailable (a worker or Wasm file did not load: bundler, CSP, MIME or 404), blocked_by_csp, opener_policy_blocks_sign_in, sign_in_timed_out (no result from the window in 10 minutes), folder_member_limit, file_not_found. - Query codes: Postgres SQLSTATE 23505 unique, 23502 not null, 23503 foreign key, 23514 check, 42P01 no table, 42703 no column, 42601 malformed query, 21000 update/delete without filter; PGRST116 single() did not match one row; 20 aborted. Codes renamed in 0.5.2: SEIGE_OPERATION_FAILED, SEIGE_CONSENT_REQUIRED, SEIGE_UNSUPPORTED are now unprefixed; error.legacyCode keeps the old value for one release. Handles returned by db.open(), files.open() and request({resource}) throw on errors, like drivers. ## Connecting the app The first call that needs the user's data opens a Seige window: the user signs in if needed and approves the website's access to its own folder; the call waits and continues. Without a recent click, for example a read on page load, the page first shows a "Continue with Seige" prompt, because browsers open windows only from clicks; its button, or the next click that calls the SDK, opens the window. The first seige.ai call asks for AI access the same way. Approval is remembered in the browser; reloads do not ask again. seige.auth.signIn() asks up front. seige.auth.user() returns { data: null } or { data: { id } } and never asks; id is a stable identifier for the account that is specific to the website; email and emailVerified are present only after signIn({ email: true }) was approved; no username or display name is exposed. seige.auth.onChange(({ user }) => ...) is called after this tab signs in or out, after the session ends (user null) and when another tab of the app signs in or out; not for the state at subscription. seige.auth.signOut() ends this browser's session and keeps data. If the app is disconnected or the session cannot be renewed, the next call asks again. The SDK takes Seige's DPoP nonce from the discovery document. When that nonce is older than about three minutes, the first pushed authorization request of a sign-in gets 400 use_dpop_nonce, which the browser logs as a failed request; the SDK retries with the fresh nonce. Each origin (scheme, host, port) is a separate app with its own user ids and approvals; give preview deployments a different appName. ## Service capabilities seige.capabilities({signal?}) returns {data:{familySharing,calendar},error} without sign-in, consent, private keys or browser storage. Each capability is null or {version:1}; service support does not grant permission. SDK 0.7.0 implements households and shared workspaces; deployments with that service report familySharing:{version:1}. Calendar stays null and no Calendar client is provided. @seige_sdk/sdk/sharing-contracts exports actual family API types, proposed Calendar types, SHARING_LIMITS and SHARING_ERRORS. Membership and app installation approval are separate. See https://seige.ai/guides/share-a-family-workspace/. Older services without discovery report null; network outages return errors, not unsupported results. ## Data seige.from(table): select(columns, {count, head}), insert, upsert(values, {onConflict, ignoreDuplicates}), update and delete (update/delete require a filter), with .select() to return affected rows. Filters: eq, neq, gt, gte, lt, lte, like, ilike, is, in, contains, containedBy, overlaps (JSON arrays), match, not, filter, or("a.eq.1,and(b.gt.2,c.is.null)"). Modifiers: order, limit, range, single, maybeSingle, abortSignal, throwOnError. Results are {data, error, count, status}. Each call is one transaction, saved encrypted in the browser and, when Seige is reachable, saved to Seige before it resolves; while Seige is unreachable the call resolves without an error and the change publishes after reconnection. Tables are created on first insert with id (UUID unless supplied) and created_at. New keys become columns on insert or update. Without migrations, reading a missing table returns no rows. Column types come from the first value: booleans read back as booleans, objects and arrays as JSON. A query reads one table; join in the app or use SQL via db.open(). No access policies exist: each user's database belongs to that user. Optional migrations: createSeige({ migrations }) as [{name, sql}] or a Vite import.meta.glob("./migrations/*.sql", {query: "?raw", import: "default", eager: true}) map. They run once each in name order in one transaction and are recorded with checksums in _seige_migrations. Changing an applied migration fails with MIGRATION_FAILED; add a new one. They cannot use PRAGMA, ATTACH, transaction control or triggers. With migrations, reading a missing table returns 42P01. An insert without id into a table whose primary key is one text column named id gets a UUID; integer primary keys keep SQLite numbering. Declared boolean and json columns are decoded. Foreign keys are enforced; ON DELETE CASCADE and SET NULL apply. Tables from migrations do not gain columns automatically. seige.db.open(name, {folder?, readonly?, migrations?}) returns { data: handle }; the handle has from(), batch, exec, run, get, all, export, flush, getConflict and close. readonly reads the copy stored in Seige; export() returns the SQLite file. Limits: 32 MiB per database; 10,000 rows and 8 MiB per query result; one writer per database. A browser hands the writer lease back after each published write and when its page closes; a crashed browser's lease lasts at most 30 seconds. Reads never wait. A write made while another browser holds the lease is stored in this browser and waits (resource reason "open_elsewhere"), then applies to the newest version or fails with IN_USE after about 45 seconds; if the page closes while waiting, it runs at the app's next call in that browser. ## Files seige.files.put(path, Blob|File|Uint8Array), get(path) (data is a Blob), list(prefix) and delete(path) (permanent; frees storage; no trash) use relative paths in the app's folder with encrypted names and metadata. The app's database file is not listed and cannot be replaced or deleted through files. Updates check the observed version. Over the storage allowance, put() still resolves, the upload stays queued, sync.resources() reports reason "allowance_exceeded" and sync.flush() returns ALLOWANCE_EXCEEDED. seige.request({resource: "folder", id, access: ["read"]}) asks for another folder when a feature needs it; seige.request({access: "read"|"write"}) lets the user choose folders. ## Sync Writes are encrypted in the browser and sent with a signed receipt. seige.files.put returns once the file is encrypted and stored in the browser; the SDK publishes file changes in the background, in order, sending only the newest of several waiting saves of one file. Database writes publish before they return when Seige is reachable. seige.sync.flush() waits until every change saved in the browser is published and returns NETWORK_UNAVAILABLE while Seige is unreachable, CONFLICT for a conflict, IN_USE while another browser keeps writing, ALLOWANCE_EXCEEDED while an upload is refused for quota and REAUTH_REQUIRED when sign-in is needed; call it before signing out and before a test checks data, not after each write. During an outage writes stay in an encrypted queue and retry on reconnect, page visibility and backoff. seige.sync.status() returns one of the strings synced, syncing, offline, needs_attention or reauthentication_required (not { data, error }); it reports synced until the first other call and has no change event, so read it again after writes. seige.sync.resources() lists {resource, state, reason}. A change that conflicts with a newer remote version stops retrying and reports reason "conflict"; the local change is kept, reads return the remote version and writes fail with CONFLICT until it is resolved. seige.files.getConflict(path) returns a file conflict and seige.db.getConflict(name?) the conflict of a database (default: the seige.from() database); resolve("discard-local") or resolve(async ({remote, pending}) => value). A database resolver gets remote (a writable copy of the remote database: exec, run, get, all) and pending (the local database: get, all); copy the local rows to keep into remote, for example with INSERT OR IGNORE. There is no overwrite-remote option. Every retry checks current authorization. There are no change subscriptions: use BroadcastChannel for tabs in one browser and polling for other devices. seige.close() stops the client and its handles without signing out or affecting other tabs; use it at the end of tests and on teardown. ## AI seige.ai.generate({prompt, input?, maxOutputTokens?, signal?, schema?}) returns { data: { text, usage: {inputTokens, outputTokens}, value? } }. It is a standard model endpoint, not an agent: no tools and no account access; it sees only the input the app supplies. Prompt limit 16,000 bytes; input 100,000 bytes; output default 2,048 tokens, maximum 4,096. seige.ai.usage() reports the current window, requestsRemaining and resetAt. Limits: 20 requests/hour/app, 100/hour/account, one active request per account; over them RATE_LIMITED; allowance used up ALLOWANCE_EXCEEDED. Requests that fail because the model provider failed are not counted against the hourly limits. Pass an AbortSignal to cancel; do not retry automatically. Inference input is processed remotely in plaintext. ## Keys and boundaries Each browser is a separate installation of the app. The SDK generates its keys in the browser and sends Seige only public keys with proofs bound to the app and the DPoP key; Seige never generates or stores app private keys. Private keys are AES-256-GCM ciphertext in IndexedDB under a non-extractable wrapping key; code in the app's origin can use them. Separate untrusted apps by origin. Private windows and cleared site data create a new installation; unpublished local changes are lost with the storage. While the Seige window is open the SDK shows a card on the app page with the same connection badge and fingerprint as Seige's approval screen; seige.auth.pendingFingerprint() returns that fingerprint for apps that show their own comparison. Disconnecting an app retires its keys and replaces folder keys; data stays in the account, and copies an app already made cannot be erased. A replacement app needs the user's approval and must understand the stored format. Seige has no account deletion or whole-account export yet; apps offer export (db export(), files.list/get) and deletion themselves. ## Testing There is no test mode, mock client or sandbox issuer. Automated tests drive the real Seige window with Playwright against a Seige account: on seige.ai a dedicated account created by hand, or a throwaway account from /create-account on an environment with open signup. Sign-in form: #username, #password, button "Sign in". Approval screen: #consent-title; buttons "Cancel" and one approve button named "Create folder and connect", "Allow on this browser", "Allow connection" or "Trust application and allow access" depending on account state. These labels are not a versioned interface. Await writes and seige.sync.flush() before reloading or checking another profile; sync.status() reads synced before any write. Rate limits per IP: 120 sign-in and 6,000 data requests per minute. Details: https://seige.ai/developers/testing/ ## Node @seige_sdk/sdk/node exports createSeigeNode({storage, ...}) for supported Node 22.23.4+, 24.22.1+ or 26.12.1+ over explicit OAuth and storage connections. db.open({folder, object, path, migrations?}) returns a handle with the same from() queries; writes commit to a local plaintext file (keep it private) and flush() publishes them. Server-side SQL, hosted functions, queues and scheduled jobs are not SDK primitives. Do not invent them. ## Acceptance Activation is the first verified remote write from a fresh project using only the public docs, followed by read-back from Seige. The browser quickstart is shared verbatim with the package README and the installed-package browser test. ## Task guides - Call an AI model from a browser app without exposing an API key: https://seige.ai/guides/ai-without-an-api-key/ (Markdown: https://seige.ai/guides/ai-without-an-api-key/index.md) - Add a backend without API keys: instructions for coding agents: https://seige.ai/guides/backend-for-coding-agents/ (Markdown: https://seige.ai/guides/backend-for-coding-agents/index.md) - Check SDK service capabilities: https://seige.ai/guides/check-sdk-capabilities/ (Markdown: https://seige.ai/guides/check-sdk-capabilities/index.md) - A database for a frontend-only app, with no server to run: https://seige.ai/guides/database-for-frontend-only-apps/ (Markdown: https://seige.ai/guides/database-for-frontend-only-apps/index.md) - End-to-end encrypted storage for a web app: https://seige.ai/guides/end-to-end-encrypted-storage/ (Markdown: https://seige.ai/guides/end-to-end-encrypted-storage/index.md) - Store user files from a browser app without a server: https://seige.ai/guides/file-storage-for-browser-apps/ (Markdown: https://seige.ai/guides/file-storage-for-browser-apps/index.md) - Share a family workspace: https://seige.ai/guides/share-a-family-workspace/ (Markdown: https://seige.ai/guides/share-a-family-workspace/index.md) - Add sign-in to a browser app without a backend: https://seige.ai/guides/sign-in-without-a-backend/ (Markdown: https://seige.ai/guides/sign-in-without-a-backend/index.md)