Build with an AI assistant
If an AI coding assistant writes your app, paste the instructions below into its chat or context before you describe the app. They work for a new project, which the assistant starts with Vite, and for an app that already exists. The assistant then installs the SDK, connects sign-in, data and files, and checks the result with a real save.
A complete app
import { createSeige } from "@seige_sdk/sdk";
const seige = createSeige();
async function planTrip(file) {
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 });
return answer;
}
document.querySelector("#plan").addEventListener("click", () =>
planTrip(document.querySelector("#photo").files[0]),
);
That is a complete app. The first call that needs the user asks them to connect the app to their Seige account and then carries on. Tables are created the first time you insert into them. Your application supplies its interface and logic; the user’s account holds the data.
Each user signs in with a Seige account. Account creation is currently open to invited usernames.
Install the SDK
npm install @seige_sdk/sdk
Use HTTPS in production; http://localhost works for
development. The browser needs IndexedDB, Web Locks, WebCrypto,
module workers and BroadcastChannel. The package includes
TypeScript declarations and the
complete SDK reference.
No registration is needed. Seige identifies your app by the website
it runs on. To show your app's name on the approval screen, pass it
as appName:
createSeige({ appName: "Trips" }). Seige shows the
name, up to 80 characters, next to the website address and uses it
to name the app's new folder. The name is a label supplied by the
app; the website remains the app's identity.
Registering the app in
Connected apps
and passing its clientId to
createSeige shows the registered name instead and lets
the app use several callback pages on one host. Adding a
clientId to an app that already has users changes their
user identifiers; read
when to register
first. Pass issuer to use a Seige environment other
than https://account.seige.ai/oauth, such as a test
environment.
Use the SDK with Vite, Next.js or webpack
The SDK runs two module workers, one for encryption and one for
SQLite, and each loads a WebAssembly file. The package refers to
them with new URL(…, import.meta.url), which these
bundlers understand, so none of them needs SDK-specific
configuration:
| Framework | Tested with | What to do |
|---|---|---|
| Vite 5, 6, 7 and 8 | vite build and the development server |
Nothing. Import the SDK. |
| Next.js 16 (App Router), with Turbopack or webpack | next build, next start and next dev |
Use the SDK in client components ("use client") and call it from event handlers or effects. |
| webpack 5 | Production build | Nothing. Import the SDK. |
"use client";
import { useEffect, useState } from "react";
import { createSeige } from "@seige_sdk/sdk";
const seige = createSeige({ appName: "Trips" });
export default function Page() {
const [user, setUser] = useState(null);
useEffect(() => {
seige.auth.user().then(({ data }) => setUser(data));
return seige.auth.onChange(({ user }) => setUser(user));
}, []);
return user
? <p>Connected</p>
: <button onClick={() => seige.auth.signIn()}>Sign in with Seige</button>;
}
The examples on these pages call the SDK from functions, such as
event handlers. Vite’s default production build target does
not allow top-level await in modules, so
vite build fails on an await outside a
function. Call the SDK from a function, or set
build.target to "es2022" in
vite.config.js.
createSeige() does no work until the first call, so
creating the client at module level is safe. Make the calls
themselves in the browser: in effects and event handlers, not during
server rendering.
In a workspace that does not hoist the SDK into the app's own
node_modules, Vite's development server can fail to
load the workers, and calls return OPERATION_FAILED
with message worker_unavailable. Add
optimizeDeps: { exclude: ["@seige_sdk/sdk"] } to
vite.config.js in that case. The page that loads the
SDK should declare <meta charset="utf-8">, as
the Vite and Next.js templates do.
The crypto module (about 3.1 MB, about 1 MB with gzip)
loads when a call needs it, or, while no user is signed in, once
the page is idle, because signing in needs it first. SQLite (about
870 KB, about 400 KB with gzip) loads with the first call
that reads or writes data. seige.auth.user() itself
loads neither. The SDK's JavaScript in the page is about
85 KB with gzip. Serve the WebAssembly files compressed and
with long-lived caching; bundlers give them content-hashed names.
Use the primitives
Every call returns { data, error } and never throws.
The exceptions are seige.sync.status() and
seige.sync.resources(), which return plain values,
seige.auth.onChange(), which returns a function that
unsubscribes, and seige.close().
seige.from(table)-
Queries the app's encrypted SQLite database:
select,insert,update,upsertanddeletewith filters and ordering. Each call is one transaction. When Seige is reachable, the call resolves after the change is saved to Seige. While Seige is unreachable, the change is kept encrypted in this browser, the call resolves without an error, and the SDK publishes the change after reconnection. seige.files.put(path, file)-
Saves a
Blob,FileorUint8Arrayin the app's folder. It returns once the file is encrypted and stored in this browser, and the SDK publishes it to Seige in the background.getreturns aBlob;listanddeletetake relative paths.deleteis permanent and frees the file's storage. Names, content and pending writes are encrypted. seige.ai.generate({ prompt, input })- Generates an answer from the input your app supplies. The first call asks the user for AI access. Provider credentials stay on Seige's servers.
seige.auth.user()-
Returns
{ data: user }, or{ data: null }when no one is signed in. It never asks the user anything. The user is{ id }: a stable identifier for the account that is specific to your website. It includesemailandemailVerifiedonly after the user approvedsignIn({ email: true }). Seige does not give apps a username or display name.signIn()asks to connect now, for example from a sign-in button;signOut()ends this browser's session and keeps the data. seige.auth.onChange(callback)-
Calls back with
{ user }after this tab signs in or out, after its session ends (userisnull), and when another tab of the same app signs in or out. It is not called for the state at the time of subscribing; read that withseige.auth.user(). It returns a function that unsubscribes. seige.sync.status()-
Returns one of the strings
synced,syncing,offline,needs_attentionorreauthentication_required, not a{ data, error }result. It reportssynceduntil the app makes its first other call, and it has no change event: read it again after writes. The SDK retries encrypted pending writes on its own.seige.sync.flush()waits until every change saved in this browser has been published. seige.close()- Stops this client and its handles, for example at the end of a test or when a component that owns the client is removed. It does not sign out and does not affect other tabs.
Handle errors by code
error is a SeigeError with a stable
code, a message that names the specific
reason, and a hint. The hint is written for you as the
developer: it says what happened and what the app can do next. Log
all three and choose the text your users see from
code.
const { data, error } = await seige.from("trips").select();
if (error) console.warn(error.code, error.message, error.hint);
code is one of ACCESS_DENIED,
SIGN_IN_CANCELLED, POPUP_BLOCKED,
REAUTH_REQUIRED, PERMISSION_REVOKED,
PERMISSION_DENIED, NETWORK_UNAVAILABLE,
RATE_LIMITED, ALLOWANCE_EXCEEDED,
CONFLICT, IN_USE,
MIGRATION_FAILED, CONSENT_REQUIRED,
UNSUPPORTED and OPERATION_FAILED,
typed as SeigeErrorCode and exported at run time as
ERROR_CODES. Queries also return Postgres SQLSTATE
codes, such as 23505 for a unique value that already
exists, PGRST116 when single() does not
match exactly one row, and 20 when a query was
aborted. These query codes are typed as
QueryErrorCode and exported as
QUERY_ERROR_CODES, so comparing
error.code with a misspelled code is a type error. A
missing file has no separate code: files.get()
returns OPERATION_FAILED with message
file_not_found. The
error table
gives the cause of each code and a message to show users.
How users connect your app
The first call that needs the user's data opens Seige in a small
window. The user signs in if needed and approves your website's
access to its own folder in their account. The call waits and
continues when they approve. If the call did not come from a click,
for example a read when the page loads, the page first shows a small
“Continue with Seige” prompt, because browsers open
windows only in response to a click. Its button, or the next click
in your app that calls the SDK, opens the window. To avoid the
prompt, call seige.auth.user() when the page loads and,
if its data is null, make the first data
call from a click. Approval is remembered in this browser, so later
calls and reloads never ask again.
If the user closes the window or dismisses the page prompt, the call
returns SIGN_IN_CANCELLED; if they select Cancel in the
window, ACCESS_DENIED. Calls waiting for the same
window receive the same result. Seige sends the sign-in result only
to a window on your website, so another site cannot receive it. User
identifiers are specific to your website.
The sign-in result returns to your page through
window.opener. Send
Cross-Origin-Opener-Policy: same-origin-allow-popups,
or no COOP header; with same-origin, sign-in returns
OPERATION_FAILED with message
opener_policy_blocks_sign_in. A Seige window that
returns no result within 10 minutes ends the call with message
sign_in_timed_out. The
launch checklist
has a Content-Security-Policy that works with the SDK.
Sign-in uses OpenID Connect with pushed authorization requests, PKCE and DPoP-bound tokens. Each browser is its own installation of the app. The SDK generates the installation's keys in that browser and sends Seige only the public keys, with proofs bound to the app and to the browser's DPoP key. Seige never generates or stores app private keys. The account's password and master keys are never given to the app.
Seige requires a server-issued DPoP nonce in every proof sent to its
OAuth endpoints. Seige's discovery document carries a current nonce,
which the SDK uses for the first pushed authorization request of a
sign-in. A nonce stays valid for about three minutes. When the page
loaded the discovery document longer ago than that, Seige refuses the
request with 400 use_dpop_nonce and a fresh
DPoP-Nonce header, and the SDK repeats the request with
that nonce. Browsers log that answer as a failed request; it is
expected and does not mean sign-in failed.
While the Seige window is open, the SDK shows a card on your page
with a connection badge, an emoji picture, and the fingerprint of
the keys it sent. Seige's approval screen shows the badge and
fingerprint of the keys it received. When they match, the keys
reached Seige unchanged; if they differ, the user should select
Cancel. Do not cover that card.
seige.auth.pendingFingerprint() returns the same value
for apps that show their own comparison. This detects keys replaced
between your app and the approval screen. It does not protect
against a compromised app origin or modified approval code.
Keys and tokens are protected in app-origin IndexedDB with a local non-extractable wrapping key. Code running in that origin can still use them. Host mutually untrusted apps on separate origins; different URL paths are not isolation boundaries.
Make your first verified remote write
This example saves a row and a file and reads both back from the copy stored in Seige. It runs as published in Seige's own tests.
import { createSeige } from "@seige_sdk/sdk";
// Saves a row and a file, then reads both back from Seige.
export async function firstWrite(options) {
const seige = createSeige(options);
try {
const saved = await seige.from("messages").insert({ text: "Hello" }).select().single();
if (saved.error) throw saved.error;
const path = `notes/${crypto.randomUUID()}.txt`;
const file = await seige.files.put(path, new Blob(["Hello"]));
if (file.error) throw file.error;
// A file write returns once it is saved on this device; wait for Seige.
const synced = await seige.sync.flush();
if (synced.error) throw synced.error;
const { data: blob } = await seige.files.get(path);
// Read the row from the copy saved in Seige, not this browser's cache.
const { data: remote } = await seige.db.open("app.sqlite", { readonly: true });
const { data: rows } = await remote.from("messages").select("text").eq("id", saved.data.id);
await remote.close();
const { data: user } = await seige.auth.user();
return { user, rows, fileHead: file.data.head, text: await blob.text() };
} finally {
seige.close();
}
}
Call firstWrite() from your app. Pass
{ issuer } for another Seige environment or
{ clientId } for a registered app. Each run adds one
row and one file; use it as a setup check.
The success condition is a verified remote write followed by authenticated read-back. A local result or a queued upload does not prove that data reached Seige. To check this in automated tests, read Testing your app.
Query data with seige.from()
const { data: open, error } = await seige
.from("todos")
.select("id, title, done")
.eq("done", false)
.order("created_at", { ascending: false });
await seige.from("todos").insert({ title: "Pack camera" });
await seige.from("todos").update({ done: true }).eq("id", id);
The first insert into a table creates it, with an
id and a created_at timestamp. A new key
becomes a new column. Reading a table that does not exist yet
returns no rows. true/false values read
back as booleans, and objects and arrays as JSON. A misspelled
column in a filter returns an error.
Filters include eq, neq,
gt, lt, in,
ilike, is and
or(); order, range,
single() and counts are supported. A query reads one
table. update() and delete() require a
filter. There are no access policies to write: each user's database
belongs to that user. Coming from supabase-js? Read
Coming from Supabase.
For constraints such as unique or required columns, define tables in
SQL migration files and pass them as
createSeige({ migrations }). They run once each, in
order, in one transaction, and their names and checksums are
recorded in the user's database so a replacement app can check the
schema version. With migrations, reading or writing a table that
does not exist returns 42P01: a read does not return
no rows, and an insert does not create the table. An insert that omits
id into a table whose primary key is a single text
column named id gets a UUID, as automatic tables do.
Foreign keys are enforced, and ON DELETE CASCADE and
ON DELETE SET NULL apply when a row is deleted. A
migration whose SQL changed after it was applied fails with
MIGRATION_FAILED; add a new migration instead. The
SDK reference lists
every method.
Save files and handle conflicts
await seige.files.put("reports/summary.txt", new Blob(["Hello"]));
const { data: saved } = await seige.files.get("reports/summary.txt");
console.log(await saved.text());
// Before signing out, wait until Seige has every saved change.
const { error } = await seige.sync.flush();
A file write returns as soon as the file is encrypted and stored in
this browser, and reads in this browser return it at once. The SDK
publishes changes to Seige in the background, in order; when several
saves of one file are waiting, only the newest is sent.
seige.sync.flush() waits until everything saved in this
browser has been published, and returns
NETWORK_UNAVAILABLE while Seige is unreachable and
ALLOWANCE_EXCEEDED while an upload is refused because
the account is out of storage.
File names and metadata are encrypted. Updates check the remote version they started from, so a stale write becomes a conflict instead of replacing newer data. Temporary network failures keep encrypted pending writes for automatic retry.
When a pending change conflicts with a newer remote version, the
SDK stops retrying that file or database and keeps the local change
in its encrypted queue. Other resources continue to synchronize.
seige.sync.resources() reports the resource as
needs_attention with reason conflict, and
reads return the remote version.
seige.sync.flush() returns CONFLICT. Read
a file conflict with seige.files.getConflict(path) and
a database conflict with seige.db.getConflict(), which
defaults to the database behind seige.from(). Resolve
with "discard-local", or with a function whose result
is written on top of the remote version. A database resolver
receives a writable copy of the remote database and a read-only view
of the local one, and copies the local rows to keep. There is no
option to overwrite the remote version.
const { data: conflict } = await seige.files.getConflict("reports/summary.txt");
if (conflict)
await conflict.resolve(async ({ remote, pending }) =>
merge(remote.content, pending.content),
);
const { data: dbConflict } = await seige.db.getConflict();
if (dbConflict)
await dbConflict.resolve(async ({ remote, pending }) => {
for (const row of await pending.all("SELECT * FROM todos"))
await remote.run(
"INSERT OR IGNORE INTO todos (id, created_at, title) VALUES (?, ?, ?)",
row.id, row.created_at, row.title,
);
});
How data is stored and synchronized
Queries run in memory. Each change is encrypted in the browser and
sent to Seige with a signed receipt before the call resolves. During
an outage, the SDK keeps encrypted pending work and retries after
reconnection. A database has one writer at a time. A browser takes
the writer lease when it writes and hands it back once the write is
published, and a page that closes asks Seige to release it. Reading
never waits for another browser. A write made while another browser
holds the lease is stored in this browser and waits for it; if the
other browser stopped without handing it back, for example after a
crash, the lease lasts at most 30 seconds after its last write. The
write then applies to the newest version, or fails with
IN_USE after about 45 seconds without saving. If the
page closes during the wait, the write runs the next time the app
calls the SDK in this browser.
Databases are limited to 32 MiB. Query results are limited to
10,000 rows and 8 MiB.
seige.db.open(name, { readonly: true }) reads the copy
stored in Seige without taking the writer; its handle offers
from(), SQL reads through all,
get and batch, and
export(), which returns the database file. A write
through it returns PERMISSION_DENIED with message
read_only.
Node applications can use createSeigeNode from
@seige_sdk/sdk/node with Node 24.22.1 or later. It takes
explicit OAuth and storage connections instead of a browser session
and offers the same queries. Its database is a local file that
contains plaintext; keep it in a private directory. See the
Node connection and database reference.
Add AI generation
const { data, error } = await seige.ai.generate({
prompt: "Summarize this record in one sentence.",
input: { title: "Weekend trip", destination: "Lisbon" },
maxOutputTokens: 256
});
if (data) console.log(data.text);
The first call asks the user for AI access. If they decline, the
call returns ACCESS_DENIED with message
ai_access_denied, and the app stays connected for data
and files. This is a standard model endpoint, not an agent: it has
no tools and no access to the account. It sees only the input your
application supplies, and your app has only the data the user gave
it. Pass an AbortSignal as signal to
cancel. Generation is not retried automatically. Prompts and input
are processed remotely in plaintext; storage encryption does not
make inference end-to-end encrypted.
App AI permits 20 requests per app per hour and 100 across apps per
account, with one active request per account. Over a limit, the call
returns RATE_LIMITED; when a request exceeds the user's
available AI credit, ALLOWANCE_EXCEEDED. Pending requests
reserve credit until settlement. AI credit has no automatic hourly
reset. Output defaults to 2,048
tokens and can be set from 1 to 4,096.
seige.ai.usage() reports request counts, the requests
remaining and when the window resets, without storing prompt or
answer content. It never opens the Seige window: before the user
approved AI access it returns CONSENT_REQUIRED with
message ai_consent_required.
Keep access scoped to approved data
Files and database changes are encrypted on the client before upload. Independent folder keys and owner-signed grants restrict an app to the folders the user approved. The SDK verifies encrypted records and server commit receipts before acknowledging writes.
A token alone does not provide decryption keys. An approved app can read and keep copies of data it receives. Read the encryption and protection documentation for the account's security model.
Disconnect apps without deleting their data
Users disconnect apps in Connected apps, which lists each app with its website. This stops further access and replaces the affected folder keys. Files and records stay in the account. The app's next call asks the user to connect it again. Disconnecting cannot erase copies an app already obtained.
A replacement app can read retained data after the user approves the folder. It must understand the stored format, so use portable formats for attachments and keep your table structure stable or documented in migrations.
Reference and integration guide
- Complete SDK reference, error codes and Node setup
- Testing your app with Playwright
- Before you launch: origins, headers, errors and user data
- Coming from Supabase
- Installable SDK archive with types and Wasm assets
- Instructions for AI coding assistants
- Machine-readable integration guide
Server-side SQL, hosted functions and background jobs are not part of the SDK. Use the documented SDK exports rather than Seige's account endpoints.