@manggaleh/sdk
v0.3.0
Published
Client SDK for manggaleh: end-user auth, Data API, transactions, storage, functions, realtime, and email.
Maintainers
Readme
@manggaleh/sdk
Official client SDK for apps built on manggaleh — a
multitenant backend-as-a-service. Framework-agnostic: it only needs global
fetch & WebSocket (browser / Node 18+).
npm install @manggaleh/sdkimport { createClient } from "@manggaleh/sdk";
const client = createClient({
baseUrl: "https://api.manggaleh.com", // manggaleh API origin
tenant: "acme", // project slug (organization)
env: "prod", // dev | staging | prod (default: prod)
apiKey: "mgpk_…", // environment publishable key (required)
// storage: { get, set } // optional: persist the token (e.g. localStorage)
});Get an API key from the dashboard (the API Keys tab). Use a publishable
key (mgpk_…) in the frontend; a service key (mgsk_…) only on the server.
End-user auth
await client.auth.signUp({ email, password, name });
await client.auth.signIn({ email, password });
const session = await client.auth.getSession(); // { user, session } | null
await client.auth.signOut();The bearer token is managed automatically (in-memory by default). Access it
manually via client.getToken() / client.setToken().
Data API
CRUD over the collections a tenant exposes. Owner-scoped collections are automatically scoped to the signed-in end-user.
const note = await client.data.from("notes").insert({ title: "Hi", body: "…" });
const all = await client.data.from("notes").list({
filters: { title: "eq.Hi" }, // PostgREST-style: eq/neq/gt/gte/lt/lte/like/ilike/in/is
order: "created_at.desc",
limit: 50,
});
const one = await client.data.from("notes").get(note.id); // null if missing
await client.data.from("notes").update(note.id, { body: "edited" });
await client.data.from("notes").remove(note.id);
// Keyset pagination: follow nextCursor until null (count: true adds the total).
let cursor: string | undefined;
do {
const { data, nextCursor } = await client.data.from("notes").page({ limit: 50, cursor });
// ...process data...
cursor = nextCursor ?? undefined;
} while (cursor);Typed via TypeScript generics (generate them with mg types):
interface Note { id: string; title: string; body: string; created_at: string }
const notes = await client.data.from<Note>("notes").list();Embed relations (belongs-to → object, one-to-many → array, nested via dotted paths) and aggregate — both RLS-respected:
await client.data.from("orders").list({ embed: ["customer(name)", "items"] });
await client.data.from("orders").aggregate({ groupBy: "status", count: true, sum: ["amount"] });Transactions, storage, functions, email
// ACID transaction — all succeed or all roll back (≤50 ops, RLS applies per op).
// ops: "insert" | "update" | "delete" | "get". For read-then-branch / upsert
// logic, do it inside a Function with ctx.db.tx.
await client.tx([
{ op: "update", collection: "accounts", id: a, patch: { balance: 75 } },
{ op: "update", collection: "accounts", id: b, patch: { balance: 25 } },
]);
// Images: transform on the fly (resize/blur/format) — great for thumbnails/LQIP.
const thumb = await client.storage.download(id, { width: 400, format: "webp" });
// Signed URL — works directly in <img src> with no api-key/token, until it expires:
const url = await client.storage.getSignedUrl(id, { expiresIn: 3600, width: 400, format: "webp" });
const obj = await client.storage.upload(file, { name: "receipt.pdf" });
const { top } = await client.functions.invoke("topProducts", { n: 10 });
// Email needs a service key (server-side)
await admin.notifications.email.send({ to, subject, html });Server-side (act-as-user)
On a trusted server, a service key can run calls as a specific end-user so row-level security still applies (instead of the admin bypass) — ideal for Next.js RSC / server actions:
const db = createClient({
baseUrl, tenant, env,
apiKey: process.env.MANGGALEH_SERVICE_KEY,
actAsUser: session.userId, // sends x-act-as-user
});
await db.data.from<Order>("orders").list(); // only this user's rows; typed via <Order>Realtime
const unsub = client.realtime.subscribe("notes", (e) => {
// e.op: 'insert' | 'update' | 'delete', e.id, e.collection
});
unsub(); // stopAuto-reconnects with backoff (disable via { reconnect: false }).
Live data & optimistic updates
from<T>(c).live() returns an opt-in store that seeds itself from list(),
applies insert/update/remove optimistically (the UI changes instantly,
and rolls back automatically if the server rejects), and reconciles with realtime
in the background. Its subscribe/getSnapshot pair is exactly React's built-in
useSyncExternalStore contract — no extra dependency, and Zustand is an
optional thin wrapper.
const todos = client.data.from<Todo>("todos").live({ order: "created_at.desc" });
// React — no extra library needed:
const list = useSyncExternalStore(todos.subscribe, todos.getSnapshot);
useEffect(() => () => todos.close(), []); // stop realtime on unmount
await todos.insert({ title: "Buy milk" }); // appears instantly, then gets its server id
await todos.update(id, { done: true }); // toggles instantly; reverts on failure
await todos.remove(id); // disappears instantly; comes back on failureBecause realtime events carry only { op, id }, the store refetches changed rows
by id (respecting RLS) — you don't write that boilerplate. Existing
list() / realtime.subscribe() usage is unchanged; live() is purely additive.
Documentation
Full guide: https://manggaleh.com · License: MIT
AI agents: machine-readable docs live at https://manggaleh.com/llms.txt.
