@mortar-ai/client
v0.10.0
Published
TypeScript client for Mortar — Backend-as-a-Service for mainland-China-friendly mobile + web apps
Maintainers
Readme
@mortar-ai/client
TypeScript / JavaScript client for Mortar — a mainland-China-friendly Backend-as-a-Service.
Works in Node 20+, modern browsers, Cloudflare Workers, Deno, Bun,
React Native — anywhere fetch exists.
Install
npm install @mortar-ai/client
# or pnpm / yarn / bun addProject-scope client
Use this from your app code (mobile / web / server) to call Mortar's
data APIs. Authenticated by your project's API key (created via
the Mortar dashboard or POST /v1/{tenant}/_admin/keys).
The SDK sends that project key in X-Mortar-API-Key. After end-user sign-in,
it sends the User access JWT separately as Authorization: Bearer <jwt>.
The surface is supabase-js compatible: data / auth / storage calls
resolve a { data, error } envelope and never throw — always
destructure { data, error } and check error. Rows come back flat
(your fields + id / created_at / updated_at at the top level), so
read row.title, not row.data.title.
import { createClient } from '@mortar-ai/client';
const mortar = createClient({
// The tenant lives in the host subdomain (Supabase's `<ref>.supabase.co`
// shape) — there is no separate `tenant` option.
url: 'https://project-uuid.api.mortar.appunvs.com',
apiKey: process.env.MORTAR_API_KEY!,
});
// Database — chainable PostgREST query, resolves { data, error }.
const { data: todos } = await mortar.from('todos')
.select('*')
.order('created_at', { ascending: false })
.limit(50);
const { data: row } = await mortar.from('todos').insert({ title: 'buy milk' }).select().single();
await mortar.from('todos').update({ title: 'buy milk + eggs' }).eq('id', row.id);
await mortar.from('todos').delete().eq('id', row.id);
// Arrays are sent as one all-or-nothing PostgreSQL transaction.
await mortar.from('todos').insert([{ title: 'a' }, { title: 'b' }]).select();
// End-user auth — { data: { user, session }, error }; the session is
// persisted + auto-refreshed and attached to every later call.
const { data, error } = await mortar.auth.signInWithPassword({
email: '[email protected]',
password: '...',
});
// Storage — supabase-js bucket handle.
await mortar.storage.from('avatars').upload('alice.jpg', file); // file: Blob/ArrayBuffer/…
const { data: signed } = await mortar.storage.from('avatars').createSignedUrl('alice.jpg', 3600);
image.src = signed!.signedUrl;
// Realtime — supabase channel over SSE.
const channel = mortar
.channel('todos-room')
.on('postgres_changes', { event: '*', table: 'todos' }, (payload) => {
console.log(payload.eventType, payload.new); // 'INSERT'|'UPDATE'|'DELETE'
})
.subscribe();
// later: mortar.removeChannel(channel);
// Usage / credit dashboard
const usage = await mortar.usage.me();
console.log(`balance: ¥${usage.credit.balance_yuan}`);
// Analytics + error capture — events batch client-side and flush to your
// tenant's event plane (shows up in the Fabric Cloud "分析" tab).
mortar.analytics.track('pageview', { path: '/' });
mortar.analytics.identify(user.id, { plan: 'pro' }); // bind an anonymous visitor
try {
await checkout();
} catch (err) {
mortar.analytics.captureError(err, { where: 'checkout' });
}Trusted server code holding an app or admin key can also atomically combine
cross-table row writes with a durable PostgreSQL queue enqueue. This class is
not attached to the browser-oriented MortarClient, so it is harder to expose
the privileged operation accidentally:
import { DatabaseTransactions } from '@mortar-ai/client';
const transactions = new DatabaseTransactions({
url: 'https://project-uuid.api.mortar.appunvs.com',
apiKey: process.env.MORTAR_APP_KEY!,
});
await transactions.execute([
{ op: 'insert', table: 'orders', data: { customer_id, total } },
{ op: 'enqueue', type: 'send_receipt', payload: { customer_id } },
]);This ACID boundary is PostgreSQL only. Object storage uses immutable physical versions plus durable retryable cleanup; it does not claim two-phase commit with PostgreSQL.
For browser media, render the signed URL directly with <img src>, <video
src>, or <audio src>. A media element does not need CORS merely to display
the object. Signed-URL minting is outside the project's API-concurrency budget
and does not itself create an object-egress charge. Lazy-load visible media and
reuse each URL until it nears expiry instead of signing again on every render.
Bound the signing call independently from larger uploads or function calls when
needed:
const controller = new AbortController();
const { data, error } = await mortar.storage.from('avatars').createSignedUrl(
'alice.jpg',
900,
{ signal: controller.signal, timeoutMs: 10_000 },
);storage.download() instead uses JavaScript fetch() against the signed
object-store URL. Browser use therefore requires the backing bucket to allow
the page origin through CORS. Mortar-managed OSS buckets allow credential-free
GET / HEAD from arbitrary app origins; self-hosted backends must configure
an equivalent rule. The SDK never forwards Mortar authorization or browser
cookies to the object store.
Account-scope client
Use this from your control-plane code (CI scripts, admin tools,
the mortar CLI itself) to manage your own Mortar account + projects.
import { createAccountClient } from '@mortar-ai/client/account';
const account = createAccountClient({ url: 'https://api.mortar.appunvs.com' });
// First-time
await account.signUp({ email: '[email protected]', password: 'hunter2' });
// or returning user
const { token } = await account.signIn({ email: '[email protected]', password: '...' });
// Projects
const projects = await account.projects.list();
const created = await account.projects.create({
name: 'my-app-prod',
tier: 'small',
credit_mode: 'hard_budget',
});
await account.projects.update(created.id, { tier: 'medium' });
await account.projects.delete(created.id);WeChat Mini Program (微信小程序)
The core is runtime-neutral — zero dependencies, and every platform touch point
(fetch, storage, streamTransport) is injectable. The Mini Program runtime
has none of the web globals those default to, so @mortar-ai/client/wechat
supplies the wx.* equivalents:
import { createWeChatClient } from '@mortar-ai/client/wechat';
export const mortar = createWeChatClient({
url: 'https://<tenant>.mortar.appunvs.com',
apiKey: 'mtr_live_...',
});Everything else is identical to web/RN: mortar.from(...), mortar.auth,
mortar.storage, mortar.realtime. Under the hood it wires wxFetch()
(wx.request), wxStorage() (wx.*StorageSync, so a session survives restarts)
and wxStreamTransport() (wx.request({ enableChunked: true }) for realtime's
SSE, since the MP has no ReadableStream or EventSource). Each is exported
separately if you'd rather pass them to createClient yourself.
Three things are on you, not the SDK:
- 构建 npm. After
npm install, run 微信开发者工具 → 工具 → 构建 npm. The package ships aminiprogramfield pointing at its CommonJS build, so DevTools copies ready-to-run code rather than bundling the ESM tree. - 服务器域名. Register your Mortar host under 微信公众平台 → 开发管理 → 服务器域名 → request合法域名. An unregistered host fails before any SDK code runs.
- Base library 2.20.2+ for realtime (
wx.request'sonChunkReceived). On anything older realtime reports a clear error and REST keeps working.
One gap: storage.download().blob() needs Blob, which the MP runtime lacks —
use storage.downloadAsBytes() (ArrayBuffer) instead.
Errors
Supabase-compatible project calls resolve { data, error }; other methods
throw MortarApiError. Both carry the same stable mortarCode/code,
category, requestId, retryable, and retryAfterMs metadata. The envelope's
legacy error.code remains the HTTP status string. These fields describe the
failure; they do not prescribe UI, navigation, localization, reporting, or a
recovery flow. Each application decides how the failure affects its own state
and interaction design:
const { data, error } = await mortar.from('todos').insert({ title: 'x' }).select();
if (error) {
handleTodoCreateFailure(error); // Defined by this application and feature.
return;
}Throwing calls expose the same facts on MortarApiError. The exported
normalizeMortarError helper can normalize an unknown caught value without
turning it into a UI policy. compute.invoke is special: Mortar platform
failures throw, while the tenant Function's own 4xx/5xx remains a raw Response
whose business-error body the application must handle.
import { normalizeMortarError } from '@mortar-ai/client';
try {
await account.projects.create({ name: 'prod', tier: 'small', credit_mode: 'hard_budget' });
} catch (cause) {
const error = normalizeMortarError(cause, 'create project');
handleProjectCreateFailure(error); // Application-defined, not SDK-defined.
}License
Apache-2.0 — see Mortar's main LICENSE.
