npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mortar-ai/client

v0.10.0

Published

TypeScript client for Mortar — Backend-as-a-Service for mainland-China-friendly mobile + web apps

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 add

Project-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 a miniprogram field 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's onChunkReceived). 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.