πŸ“˜ manggaleh documentation Β· Dashboard Β· Raw markdown for AI agents (llms.txt)

Using manggaleh β€” guide for AI agents

A practical reference for another Claude Code session to build on / integrate with a manggaleh backend. manggaleh is a multitenant backend-as-a-service: each project gets an isolated Postgres database with end-user auth, an auto-generated REST Data API, file storage, server-side functions, scheduled jobs, realtime, webhooks and email β€” all behind one SDK (@manggaleh/sdk) and one CLI (mg / @manggaleh/cli).

If you (the agent) just need to use an existing manggaleh service, you mostly need: the API origin (e.g. https://api.manggaleh.com), the project slug (the tenant), the environment (dev/staging/prod/…), and an API key. Get the rest from this doc.


0. Capabilities β€” what manggaleh can do, and how to reach it

Capability What you can do SDK (@manggaleh/sdk) CLI (mg)
End-user auth sign up / in / out, sessions, optional signup codes; per-environment client.auth.signUp/signIn/signOut/getSession (app-side; CLI is owner-side)
Data API (CRUD) create / read / update / delete rows, typed client.data.from<T>(c).insert/get/update/remove/list β€”
Query filter (eq + operators), sort, cursor pagination, count, column projection, embed FK list/page({ filters, order, limit, cursor, select, count, embed }) β€”
Transactions atomic multi-op (insert/update/delete/get) β€” all or nothing client.tx([ … ]) β€”
Storage upload / list / download / remove files; owner/ABAC-scoped client.storage.upload/list/download/remove β€”
Functions server-side JS (trusted logic); invoke from app/server client.functions.invoke(name, input) mg functions push/list/delete
Scheduled jobs (cron) run a function on a schedule (admin mode) β€” (dashboard) β€”
Realtime subscribe to insert/update/delete over WebSocket client.realtime.subscribe(c, handler) β€”
Live data (optimistic) self-syncing store: optimistic insert/update/remove + rollback + realtime reconcile; useSyncExternalStore-ready client.data.from<T>(c).live(opts) β€”
Webhooks (outbound) HMAC-signed POST to your URL when data changes β€” (dashboard) β€”
Email transactional email (needs a service key) client.notifications.email.send / ctx.email.send β€”
Projects & envs create projects, environments, clone/reset, production flag β€” mg projects … / mg env …
Collections (schema) define tables/columns/relations, set RLS owner/ABAC β€” (dashboard) mg collections create --columns … --owner-column …
API keys mint publishable / service / function-scoped, revoke β€” mg keys create/list/revoke
Types codegen generate TS interfaces from the live schema use the output with data.from<T>() mg types --out db.d.ts
RLS & ABAC per-owner or tag-based row security, enforced server-side automatic once configured --owner-column / --permission-column

Division of labour: the CLI is for the owner/admin (provision projects, schema, keys, push functions, gen types β€” see Β§2). The SDK is for the application you build (auth, data, storage, functions, realtime β€” see Β§3). End-users authenticate through the SDK, not the CLI.


1. Mental model (learn these 5 words)

Auto-managed system columns (don't define these, they're added for you): id (uuid), created_at, created_by, updated_at, updated_by.

Auth model for requests: a request usually carries the API key and (for end-user context) the signed-in user's Bearer token. A service key runs as admin (no user needed). RLS is keyed on the user, not the key.


2. Fast path: provision a backend from the CLI

npm install -g @manggaleh/cli

# 1. Account (or `mg login` if it exists). Session saved at ~/.manggaleh/config.json.
mg signup --url https://api.manggaleh.com --email you@acme.com
# (non-interactive: set MANGGALEH_EMAIL / MANGGALEH_PASSWORD, or pass --email/--password)

# 2. Project (also provisions a "dev" environment)
mg projects create --name "Acme" --slug acme

# 3. A collection. On a column type: "!" = NOT NULL, "^" = UNIQUE.
#    --owner-column turns on per-user RLS.
mg collections create --project acme --env dev --name todos \
  --columns "title:text!,code:text^,done:boolean,priority:integer" \
  --owner-column owner_id

# 4. API keys
mg keys create --project acme --env dev --type publishable            # mgpk_… (browser)
mg keys create --project acme --env dev --type service --name server   # mgsk_… (shown once)

# 5. Generate TS types for the SDK
mg types --project acme --env dev --out src/db.d.ts

Column types: text, integer, bigint, numeric, boolean, timestamptz, date, uuid, jsonb, plus reference (a FK β€” set its target collection). Identifiers are lowercase / digits / underscore (my_table); project slugs are kebab-case (my-app).

CLI reference

Account & projects
  mg signup --url <baseUrl> [--name <n>] [--email <e>] [--password <p>]
  mg login  --url <baseUrl> [--email <e>] [--password <p>]
  mg projects
  mg projects create --name <name> --slug <slug>
  mg env list   --project <slug>
  mg env create --project <slug> --name <env> [--production]
  mg env delete --project <slug> --name <env> --yes

Schema & keys
  mg collections list   --project <slug> --env <env>
  mg collections create --project <slug> --env <env> --name <name>
        [--columns "title:text,done:boolean!,code:text^"]   (! = NOT NULL, ^ = UNIQUE)
        [--owner-column <col>] [--permission-column <col>]
  mg collections delete --project <slug> --env <env> --name <name> --yes
  mg keys list   --project <slug> --env <env>
  mg keys create --project <slug> --env <env> --type <publishable|service|function> [--name <n>]
        [--functions "fnA,fnB"]   (required for --type function)
  mg keys revoke --project <slug> --env <env> --id <id> --yes

Functions & types
  mg functions list   --project <slug> --env <env>
  mg functions push   --project <slug> --env <env> --file <path> [--name <name>]
  mg functions delete --project <slug> --env <env> --name <name> --yes
  mg types            --project <slug> --env <env> [--out <file.d.ts>]

3. SDK (@manggaleh/sdk)

Framework-agnostic (browser or Node 18+; needs global fetch + WebSocket).

npm install @manggaleh/sdk

Create one client per project + environment

import { createClient } from "@manggaleh/sdk";

export const client = createClient({
  baseUrl: "https://api.manggaleh.com", // API origin (no trailing slash needed)
  tenant: "acme",                       // project slug
  env: "dev",                           // "dev" | "staging" | "prod" (default "prod")
  apiKey: "mgpk_xxx",                   // publishable for browser, service for server
  // storage: tokenStorage,             // persist the session across reloads (see Gotchas)
});

End-user auth

const { user } = await client.auth.signUp({ email, password, name, /* code? */ });
await client.auth.signIn({ email, password });
await client.auth.signOut();
const session = await client.auth.getSession(); // SessionResult | null

The session token is captured & attached automatically. Auth is per environment.

Data API (CRUD)

interface Todo { id: string; title: string; done: boolean; created_at: string }
const todos = client.data.from<Todo>("todos");

const created = await todos.insert({ title: "Buy milk", done: false }); // returns full row
const one     = await todos.get(created.id);   // null (not error) when missing
const updated = await todos.update(created.id, { done: true });          // partial patch
await todos.remove(created.id);                                           // -> void
const rows    = await todos.list();            // one page (default 50, server max 200)

Query: filter / sort / paginate / embed

const open = await todos.list({
  filters: { done: false, title: "ilike.%milk%", priority: "gte.3" }, // plain value = eq
  order: "created_at.desc",   // add ".desc" for descending
  limit: 50,                  // capped at 200
});

// Cursor pagination + optional total
const p1 = await todos.page({ limit: 20, count: true });   // { data, nextCursor, count }
if (p1.nextCursor) await todos.page({ limit: 20, cursor: p1.nextCursor });

// Projection + embed one-level FK relations
await client.data.from("orders").list({
  select: ["id", "total", "customer_id"],   // id always included
  embed: ["customer(name,phone)"],           // FK value replaced by the related row
});

Filter operators: plain value = eq; gt/gte/lt/lte; like/ilike (ilike.%x%); in.a,b,c / nin.x,y; is.null / is.notnull. Filtering & sorting run on the server over the whole dataset.

Transactions (ACID β€” all or nothing)

const results = await client.tx([
  { op: "update", collection: "accounts", id: a, patch: { balance: 75 } },
  { op: "update", collection: "accounts", id: b, patch: { balance: 25 } },
  { op: "insert", collection: "ledger",   values: { from: a, to: b, amount: 25 } },
  // ops: "insert" | "update" | "delete" | "get"
]);
// results[i].data (row) or results[i].deleted (for delete)

A tx is a fixed list β€” you can't read-then-branch mid-transaction. For that, use a Function.

Storage (files)

const obj  = await client.storage.upload(file, { name: "receipt.pdf" }); // file: Blob/File
const list = await client.storage.list();
const blob = await client.storage.download(obj.id);
await client.storage.remove(obj.id);

Owner/ABAC-scoped by RLS β€” safe from the browser with a publishable key. (permissions tags on upload only apply with a service key.)

Functions (call server-side logic)

const { top } = await client.functions.invoke("topProducts", { n: 10 });

Realtime

const unsub = client.realtime.subscribe("todos", async (e) => {
  // e = { type: "change", schema, collection, op, id }  β€” NO row data!
  if (e.op === "delete") { removeFromUI(e.id); return; }
  const row = await todos.get(String(e.id));   // refetch (also runs through RLS)
  if (row) upsertInUI(row);
});
// later: unsub();

Auto-reconnects. e.id may be a number β†’ String(e.id) before get.

Live data + optimistic updates (.live())

Don't want to hand-write the subscribe→refetch→merge loop above? from<T>(c).live() returns an opt-in store that does it for you and applies mutations optimistically (instant UI, automatic rollback on failure). subscribe/getSnapshot match React's built-in useSyncExternalStore — no extra dependency; Zustand is an optional wrapper.

const todos = client.data.from<Todo>("todos").live({ order: "created_at.desc" });

// React:
const list = useSyncExternalStore(todos.subscribe, todos.getSnapshot);
useEffect(() => () => todos.close(), []);     // stop realtime on unmount

await todos.insert({ title });        // appears instantly β†’ swapped for the server row
await todos.update(id, { done: true }); // toggles instantly β†’ reverts if the server rejects
await todos.remove(id);               // disappears instantly β†’ comes back on failure

live() is additive: it reuses list()/get()/realtime under the hood and changes nothing about the existing APIs. Also exported: createLiveCollection, LiveCollection, LiveOptions.

Email (server-side, service key required)

const admin = createClient({ baseUrl, tenant, env, apiKey: process.env.MG_SERVICE_KEY });
await admin.notifications.email.send({
  to: "customer@example.com", subject: "Receipt", html: "<p>Thanks!</p>",
});

Error handling

import { ManggalehError } from "@manggaleh/sdk";
try { await todos.insert({ title: "x" }); }
catch (err) {
  if (err instanceof ManggalehError) {
    // err.status: 401 (sign in), 403 (RLS / wrong key / scope), 404, 429 (quota), 400…
    // err.message, err.body
  } else throw err;
}

Note: get(id) on a missing row resolves to null; everything else throws.


4. Writing Functions (server-side JS, in the dashboard "Functions" tab)

// Function "topProducts"
module.exports = async (input, ctx) => {
  // ctx.db.list returns { data, nextCursor, count? } β€” note the .data
  const { data: orders } = await ctx.db.list("orders", { order: "total.desc", limit: input.n ?? 5 });
  ctx.log("rows", orders.length);          // appears in run logs (console.log works too)
  await ctx.email.send({ to: "ops@acme.com", subject: "Report", text: "..." });
  return { top: orders };                  // must be JSON-serializable
};

ctx: input (the invoke payload), db (list/get/insert/update/remove/tx, RLS-aware), email.send(...), log(...). Sandbox limits (important): no require/import, no fetch/network, no process/fs; JS stdlib only. ~5s timeout. Return value is JSON-serialized. Cannot read request headers or verify HMAC signatures inside a function.

Functions can be scheduled (cron) β€” those runs are admin (RLS bypassed), so guard sensitive logic inside the function body, not just with RLS.


5. Direct HTTP (no SDK β€” e.g. curl from an agent)

Base for tenant requests: {baseUrl}/api/t/{tenant}/{env}. Auth via header x-api-key: <key> or query ?apikey=<key>; end-user token via Authorization: Bearer <token>.

B=https://api.manggaleh.com/api/t/acme/dev

# sign in (returns a token; SDK manages this for you)
curl -s $B/auth/sign-in/email -H 'content-type: application/json' \
  -H 'x-api-key: mgpk_xxx' -d '{"email":"u@x.com","password":"secret"}'

# data
curl -s "$B/data/todos?done=false&order=created_at.desc&limit=20" -H 'x-api-key: mgpk_xxx' -H "authorization: Bearer $TOKEN"
curl -s -XPOST $B/data/todos -H 'content-type: application/json' -H 'x-api-key: mgsk_xxx' -d '{"title":"hi"}'
curl -s -XPATCH  $B/data/todos/$ID -H 'content-type: application/json' -H 'x-api-key: mgsk_xxx' -d '{"done":true}'
curl -s -XDELETE $B/data/todos/$ID -H 'x-api-key: mgsk_xxx'

# invoke a function (service or function-scoped key works without a user)
curl -s -XPOST "$B/functions/topProducts?apikey=mgsk_xxx" -H 'content-type: application/json' -d '{"n":10}'

Responses: data list = { data, nextCursor, count? }; get/insert/update = { data }; function = { result }. Realtime is a WebSocket at {wsBase}/api/t/{tenant}/{env}/realtime?collection=..&token=..&apikey=...


6. API keys & 3rd-party access

Type Prefix Use Power
publishable mgpk_ frontend / browser normal (RLS applies via the signed-in user)
service mgsk_ your own server full admin (bypasses RLS) β€” never ship to a browser
function mgfk_ give to a 3rd party / vendor scoped: only invokes its allowlisted functions; 403 on Data API / storage / email / other functions

To let an external system (payment gateway, vendor) call one of your functions, create a function-scoped key (mg keys create --type function --functions "paymentCallback") β€” never hand out a service key. The gateway POSTs its payload as the function input.


7. Gotchas (save yourself debugging)


8. Recipes (agent intent β†’ exact steps)

"Stand up a backend for a new app" β†’ use the CLI bootstrap in Β§2 (signup β†’ projects create β†’ collections create β†’ keys create β†’ types). Hand the publishable key + tenant + env to the frontend; keep the service key on the server.

"Read/write data as the logged-in user" (frontend) β†’

const c = createClient({ baseUrl, tenant, env, apiKey: "mgpk_…" });
await c.auth.signIn({ email, password });
await c.data.from("todos").insert({ title: "x", done: false });  // RLS owner set automatically
const mine = await c.data.from("todos").list({ order: "created_at.desc" });

"Run trusted multi-step logic the client must not bypass" β†’ write a Function (Β§4), then client.functions.invoke("placeOrder", { … }). Put the rules in the function; the client only sends intent.

"Let an external system (payment gateway / vendor) call one function" β†’ mint a function-scoped key and give them the invoke URL β€” never a service key:

mg keys create --project acme --env prod --type function --functions "paymentCallback"
# they POST their payload to: {baseUrl}/api/t/acme/prod/functions/paymentCallback?apikey=mgfk_…

"Live-update the UI when data changes" β†’ realtime.subscribe(collection, e => …) then refetch the row by e.id (the event has no row data).

"Do an admin/server task (cron, migration, bulk import)" β†’ server-side client with a service key (mgsk_…) β†’ runs as admin, bypasses RLS.

"Send a receipt / notification email" β†’ from a Function: ctx.email.send({ to, subject, html }), or server-side admin.notifications.email.send(...) with a service key.

"Give the SDK full type-safety" β†’ mg types --project … --env … --out src/db.d.ts, then client.data.from<Todo>("todos").

Decision shortcuts