SDK — excalibase/sdk¶
Pre-release
@excalibase/sdk v0.1 is in active development. Install from GitHub for
now; npm publish is scheduled with the next platform release.
Official TypeScript client for Excalibase. Single package covers auth, GraphQL, REST, and Functions (typed RPC) surfaces with helpers and a persistent refresh-token session.
Why a first-party SDK¶
- Auth done right — password login, refresh-before-expiry, OAuth2
/tokenflow, API keys. No manual JWT wrangling. - One client for every protocol — pick GraphQL, REST, or typed Functions (RPC) per call; same auth, same headers, same error types.
- Typed search / vector —
search,webSearch,vectorSearchare first-class; autocomplete on the operator shape. - Works in the browser and Node (≥18). ESM + CJS. Sub-10 KB gzipped core.
- Codegen —
excalibase-codegenpoints at your running server and emits TypeScript types for every table. Your client code becomes schema-aware.
Install¶
Create a client¶
import { createClient, localStorageAdapter } from "@excalibase/sdk";
const db = createClient({
url: "https://api.example.com",
projectId: "acme/prod",
publishableKey: "esk_pub_...",
storage: localStorageAdapter(), // or memoryStorageAdapter() for SSR
autoRefreshToken: true,
});
Browser origins
In the browser the request's Origin is set by the browser, not the SDK.
The project must list the origins your app is served from (including
http://localhost:<port> for local dev) or the call is blocked by CORS —
see CORS (per-project origins).
Auth¶
await db.auth.signUp({ email, password, fullName });
await db.auth.signInWithPassword({ email, password });
await db.auth.signOut();
db.auth.onAuthStateChange((event, session) => { /* ... */ });
Supports API key exchange, OAuth2-style /token, and automatic refresh
before JWT expiry. See the SDK README for the full surface.
GraphQL¶
const issues = await db.graphql.query<{ kanbanIssues: Issue[] }>(`
{
kanbanIssues(
where: { status: { eq: "open" }, search_vec: { search: "stripe" } },
limit: 10
) { id title priority }
}
`);
await db.graphql.mutation<{ createKanbanIssue: Issue }>(`
mutation ($input: CreateKanbanIssueInput!) {
createKanbanIssue(input: $input) { id }
}
`, { input: { title: "..." } });
Vector k-NN:
const nearby = await db.graphql.query(`
{
kanbanIssues(
vector: { column: "embedding", near: [0.1, 0.2, 0.3], distance: "COSINE", limit: 5 }
) { id title }
}
`);
REST¶
// GET /api/v1/rest/customer?status=eq.active&limit=10
const customers = await db.rest.get("customer", {
query: { status: "eq.active", limit: 10 },
});
// POST
await db.rest.post("customer", { body: { name: "Vu", status: "active" } });
Functions (typed RPC)¶
Deployed functions are called as db.functions.<module>.<name>(args).
Functions hold logic; the data stays behind the project's REST and GraphQL
API shown above. A function has no ctx.db: when it needs data it calls that
API with a project key kept as a function secret.
// deployed with id "users.welcome" (server-side)
import { z } from "zod";
import { mutation } from "@excalibase/server";
export default mutation({
args: z.object({ email: z.string() }),
handler: async (ctx, { email }) => {
await ctx.scheduler.runAfter(0, { moduleName: "mail", exportName: "default" }, { to: email });
return { queued: true };
},
});
Then from the client:
Codegen¶
Point excalibase-codegen at a running server and regenerate types whenever
your schema changes:
Your generated Database type is the single source of truth for what
operators and fields are available on your specific DB. The SDK's
createClient<Database> picks up the generics and every query / filter is
type-checked at compile time.
Errors¶
import { AuthError, NetworkError, ExcalibaseError } from "@excalibase/sdk";
try {
await db.graphql.query("{ bogus }");
} catch (e) {
if (e instanceof AuthError) { /* 401/403 */ }
else if (e instanceof NetworkError) { /* transport failure */ }
else if (e instanceof ExcalibaseError) { /* server error payload */ }
}
Error codes¶
| Code | Where | Meaning |
|---|---|---|
permission_check_failed |
GraphQL errors[].extensions.code; REST 403 body code |
A written row fails the role's permission check; the whole write rolled back. The message never contains database text. Reads are filtered silently and never raise. |
permission_denied |
REST 403 body code |
The role holds no permission for this method's operation on a table it can see (or for Prefer: count=). |
missing_session_variable / invalid_session_variable |
GraphQL errors[].extensions.code; REST 400 body code |
A permission needs a session variable the request lacks, or one its column type cannot hold. |
permissions_unavailable |
503, GraphQL-shaped body |
The project's permissions cannot be read right now. |
See the error contract.
Realtime¶
WebSocket-based subscriptions land in a future SDK release — today the
server exposes /api/v1/realtime and GraphQL subscriptions, and any
graphql-ws or browser WebSocket client works. SDK-level
db.realtime.channel(...).on(...) helpers are on the roadmap.
Roadmap¶
-
db.realtimehelpers - Python SDK
- Dart SDK (Flutter-first)
- Typed filter builder variants per DB (PostgreSQL-only ops gated by
Databasegeneric)
Links¶
- GitHub: excalibase/excalibase-sdk-js
- npm (pending):
@excalibase/sdk - Issues / feedback: GitHub Issues