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

mdbase-client

v0.3.0

Published

JavaScript client for mdbase — per-project auth, database and storage. supabase-js compatible API surface.

Readme

mdbase-client

Official JavaScript client for mdbase — per-project auth, database and storage. If you know @supabase/supabase-js, you already know this API.

Zero runtime dependencies. Ships ESM + CommonJS with full TypeScript types (JSDoc examples included). Runs in browsers (evergreen) and Node ≥ 18.

The library mirrors the mdbase public API planes (the tabs of the API reference):

| Tab | Namespace | Base | | --- | --- | --- | | Auth API | client.auth | <projectUrl>/auth/v1 | | Data API (REST) | client.from('table') / client.rest | <projectUrl>/rest/v1 | | Storage API | client.storage | <projectUrl>/storage/v1 |

A project URL is https://<host>/<ref> — e.g. https://api.mdbase.example.com/my-project. The gateway also answers GET <projectUrl>/health (client.health()).

Two key roles exist: the public key (safe in browsers, sent as apikey on every request) and the base key (service role — bucket administration and unrestricted reads; pass it as the secretKey option, server-side only).

Install

npm install mdbase-client

Quick start

import { createClient } from 'mdbase-client'

const mdbase = createClient(
  'https://api.mdbase.example.com/my-project', // project URL
  '<public key>'                               // apikey for every request
)

// Sign up (queues a confirmation email when confirmation is on)
const { data, error } = await mdbase.auth.signUp({
  email: '[email protected]',
  password: 'a-long-password',
  options: { data: { plan: 'pro' } }, // → raw_user_meta_data
})

// Sign in
await mdbase.auth.signInWithPassword({ email: '[email protected]', password: 'a-long-password' })

// Data API — RLS applies to every query as the signed-in user
const { data: tasks, count } = await mdbase
  .from('tasks')
  .select('id,title,teams(name)', { count: 'exact' })
  .eq('done', false)
  .order('created_at', { ascending: false })
  .limit(50)

Coming from Supabase

Method names and the { data, error } result pattern were deliberately kept compatible. The mapping below is the complete list of differences that matter.

Auth

| supabase-js | mdbase-client | Difference | | --- | --- | --- | | auth.signUp({ email, password, options: { data } }) | same | adds needsEmailConfirmation in data; session is null until the email is confirmed | | auth.signInWithPassword | same | — | | auth.signOut | same | — | | auth.getUser(jwt?) | same | — | | auth.updateUser({ data, password }) | auth.updateUser({ user_metadata, password }) | metadata key is user_metadata, not data | | auth.verifyOtp({ email, token, type: 'signup' }) | auth.verify({ type: 'signup', token }) | token only — no email argument | | auth.verifyOtp({ type: 'recovery' }) | auth.verify({ type: 'recovery', token }) | recovery signs in; then set the new password with updateUser({ password }) | | auth.resetPasswordForEmail(email, { redirectTo }) | auth.resetPasswordForEmail(email) | no redirectTo option | | auth.resend({ email, type }) | same | — | | auth.refreshSession / setSession / getSession / signOut | same | — | | auth.onAuthStateChange | same | same event names (SIGNED_IN, TOKEN_REFRESHED, …) | | auth.signInWithOAuth({ provider, options }) | same | provider is google for now | | auth.getMfaFactor/MFA, auth.admin | not supported | — | | — | auth.listSessions(), auth.revokeSession(id), auth.settings(), auth.reauthenticate() | mdbase extras |

Data

| supabase-js | mdbase-client | | --- | --- | | .select(columns, { count }) | same — one-level FK embeds use the same teams(name) syntax | | .eq .neq .gt .gte .lt .lte .like .ilike .in .is .contains .not .or .filter | identical names and semantics | | .order .limit .offset | same | | .range(from, to) | same — built from limit/offset | | .single() / .maybeSingle() | same | | .insert().select() / .update().select() / .delete().select() | same — mutations return rows only with .select() | | .upsert() | not supported on rows (object storage has upsert uploads) | | .rpc() | not supported | | .textSearch() | not supported |

Storage

| supabase-js | mdbase-client | Difference | | --- | --- | --- | | storage.from(bucket) | same | — | | .upload(path, body, { contentType }) | same | without upsert, an existing path returns 409 conflict | | .upload(path, body, { upsert: true }) | same | sent as PUT — RLS must allow insert and delete | | .download(path) → { data: { blob } } | same | returns a Blob in browsers and Node ≥ 18 | | .list(path, { limit, offset, search }) | .list(prefix, { limit, offset, search }) | no sortBy (results are ordered by path) | | .remove(paths) | same | returns the server's per-path { name, message? } results | | .createSignedUrl(path, expiresIn) → data.signedUrl | → data.signed_url | snake_case, plus expires_at | | .createSignedUrls(paths, expiresIn) | same | per-path { path, signed_url, error } | | .createSignedUploadUrl(path) + .uploadToSignedUrl(signed, body) | same | signed uploads are headerless and overwrite | | .getPublicUrl(path) → data.publicUrl | same | URL points at /object/public/… | | .copy(from, to) / .move(from, to) | same | references use bucketId (uuid) + path | | .updateBucket(id, options) / .deleteBucket(id) | same | buckets are addressed by id (uuid), not name | | — | .emptyBucket(id) | removes every object; the bucket stays | | — | .getInfo(path) | object metadata without downloading | | — | getRenderUrl(path, transform, variant) | /render/image/… resize/reformat endpoints | | storage.from(bucket).createBucket etc. | client.storage.createBucket… | bucket admin lives on client.storage; needs the base key (secretKey) |

Not supported (by design)

The mdbase gateway does not expose these endpoints yet, so the client does not pretend they exist — calling them fails fast instead of failing silently:

| Missing method | Why | What to do instead | | --- | --- | --- | | .upsert(values) on rows | the Data API has no upsert resolution | insert and handle 409/23505, or filter-update then insert what failed | | .rpc(name) | no stored-procedure endpoint | use the dashboard's SQL runner for server-side logic, or a Data API table as a queue | | .textSearch(col, q) | no endpoint support | like('col', '*q*') or ilike | | auth.signInWithOtp | no magic-link user endpoint yet | password + resetPasswordForEmail | | realtime channels, edge functions | no endpoints | poll with select + order/limit, or run server-side code in your app | | Control plane (projects, keys, SQL runner) | dashboard-only — removed from the public API reference | drive it from the dashboard |

When a new endpoint ships, the client gains the matching method in a minor release.

Auth API (client.auth)

| Method | Endpoint | | --- | --- | | auth.signUp({ email, password, options: { data } }) | POST /auth/v1/signup | | auth.verify({ type, token }) | POST /auth/v1/verify | | auth.resend({ email, type? }) | POST /auth/v1/resend | | auth.signInWithPassword({ email, password }) | POST /auth/v1/token?grant_type=password | | auth.refreshSession(refreshToken?) | POST /auth/v1/token?grant_type=refresh_token | | auth.signOut() | POST /auth/v1/logout | | auth.getUser(jwt?) | GET /auth/v1/user | | auth.updateUser({ password?, user_metadata? }) | PUT /auth/v1/user | | auth.resetPasswordForEmail(email) | POST /auth/v1/recover | | auth.reauthenticate() | GET /auth/v1/reauthenticate | | auth.settings() | GET /auth/v1/settings | | auth.listSessions() | GET /auth/v1/sessions | | auth.revokeSession(id) | DELETE /auth/v1/sessions/:id | | auth.signInWithOAuth({ provider, options }) | GET /auth/v1/authorize?provider=… | | auth.getSessionFromUrl() | consumes the OAuth/verify callback fragment |

signUp resolves { user, session, needsEmailConfirmation }: when the project requires email confirmation, session is null and needsEmailConfirmation is true; with confirmation disabled, a session comes back immediately. Signing up again for an email that already belongs to a confirmed user returns the user_already_exists error (unconfirmed duplicates simply re-queue the email).

Confirmation tokens are valid for 24 hours, recovery tokens for 1 hour. The recovery flow is three steps: resetPasswordForEmail(email) → verify({ type: 'recovery', token }) (revokes all other sessions and signs in) → updateUser({ password }). GET /auth/v1/verify?type=…&token=…&redirect_to=… is the browser-link variant — it redirects to your app with the session in the URL fragment, which the client picks up automatically (detectSessionInUrl).

Refresh tokens are single-use — replaying one triggers token_reuse_detected, revokes the whole session, and the client signs out locally with a SIGNED_OUT event. Session management is automatic: persistence (localStorage in browsers, memory in Node, pluggable adapter), refresh shortly before expiry, cross-tab sync, and state events via auth.onAuthStateChange(cb). auth.settings() exposes the project's public auth configuration (email_confirmation_required, cookie_mode, external providers) and auth.reauthenticate() confirms the session is live before sensitive operations.

Data API — REST (client.from, tab "Data API (REST)")

PostgREST-style queries over the project's public tables. Every statement runs as the caller's role — anon without a session, the signed-in user via the bearer token — so Row Level Security decides every row.

Reading

// Column picks + one-level FK embeds (parent → object, child → array)
await mdbase.from('tasks').select('id,title,teams(name),owner(*)')

// Filters — col=op.value syntax is generated for you
  .eq('done', false)                    // done=eq.false
  .neq('priority', 9).gt('priority', 0) // also gte lt lte
  .like('title', '*ship*')              // * wildcards; ilike for case-insensitive
  .in('owner_id', ['u1', 'u2'])         // owner_id=in.(u1,u2)
  .is('due', null)                      // due=is.null|true|false
  .contains('tags', ['a'])              // jsonb/array containment
  .not('done', 'eq', true)              // done=not.eq.true
  .or('done.eq.true,priority.in.(1,2)') // or=(…) groups

// Ordering & paging (limit is clamped to the project's max_rows server-side)
  .order('created_at', { ascending: false, nullsFirst: false })
  .order('title')                       // multi-order joins into one param
  .range(20, 29)                        // or .limit(n).offset(n)

// Modifiers
  .single()      // exactly one row as an object (406 PGRST116 otherwise)
  .maybeSingle() // first row or null (adds limit 1)

const { data, count, error, status } = await result

count: 'exact' (also 'planned' | 'estimated') on select adds Prefer: count=exact and parses the Content-Range total:

const { data, count } = await mdbase.from('tasks').select('*', { count: 'exact' }).limit(1)

Writing

// Insert — one row or a batch. 201; rows come back only with .select()
await mdbase.from('tasks').insert({ title: 'ship it', done: false })
const { data } = await mdbase.from('tasks').insert([{ title: 'a' }, { title: 'b' }]).select()

// Update — filters are mandatory (the API rejects full-table updates)
await mdbase.from('tasks').update({ done: true }).eq('id', id).select()

// Delete — filters are mandatory
await mdbase.from('tasks').delete().eq('done', true).select()

The builder is a thenable — await it directly. Results are { data, error, count, status, statusText }; API errors resolve into error (ApiError with .code like PGRST105, 42501, 23505) and never throw.

Storage API (client.storage, tab "Storage API")

Objects run under the caller's gateway role (public buckets are world-readable via headerless /object/public/… URLs, users own their uploads). Bucket administration requires the base key — pass it server-side only:

const mdbase = createClient(projectUrl, publicKey, { secretKey: baseKey }) // server-side!
// Buckets — created once, then addressed by their uuid `id`
const bucket = await mdbase.storage.createBucket('media', {
  public: true,
  fileSizeLimit: 1024 * 1024,
  allowedMimeTypes: ['image/*', 'application/pdf'],
})
await mdbase.storage.listBuckets()        // signed-in users or base key
await mdbase.storage.getBucket(bucket.data.id)
await mdbase.storage.updateBucket(bucket.data.id, { public: false })
await mdbase.storage.emptyBucket(bucket.data.id)   // remove every object
await mdbase.storage.deleteBucket(bucket.data.id)  // only when empty

// Objects
const media = mdbase.storage.from('media')

await media.upload('img/cat.png', fileBlob, { contentType: 'image/png' })
await media.upload('img/cat.png', fileBlob, { contentType: 'image/png', upsert: true }) // PUT overwrite
// (multipart: pass a FormData with a `file` part)

const { data } = await media.download('img/cat.png')     // { blob } — Blob in browsers & Node
await media.getInfo('img/cat.png')                        // metadata without downloading
await media.list('img/2026/', { limit: 50, search: 'cat' }) // prefix + substring search
await media.remove(['img/cat.png', 'img/old.png'])        // one bulk call; per-path { name, message? }
await media.createSignedUrl('docs/report.pdf', 3600)      // standalone URL, no headers needed
await media.createSignedUrls(['a.txt', 'b.txt'], 3600)    // bulk — per-path results
const signed = await media.createSignedUploadUrl('up/report.txt')
await media.uploadToSignedUrl(signed.data, blob)          // headerless PUT, overwrites
media.getPublicUrl('img/cat.png')                         // sync URL for public buckets

// Copy / move (references use the bucket id + path)
await mdbase.storage.copy(
  { bucketId: bucket.data.id, path: 'docs/a.pdf' },
  { path: 'docs/a-copy.pdf' }                             // destination bucketId defaults to source
)
await mdbase.storage.move(
  { bucketId: bucket.data.id, path: 'docs/a.pdf' },
  { bucketId: otherId, path: 'renamed.pdf' }
)

// Image transforms (/render/image/… — resize, reformat, quality)
const { data: url } = media.getRenderUrl('img/cat.png', { width: 300, format: 'webp' })             // public
const { data: authed } = media.getRenderUrl('p/x.png', { width: 300 }, { variant: 'authenticated' }) // private
media.getRenderUrl('p/x.png', { width: 300 }, { sign: signed.signed_url })                          // via signed URL

Errors keep the documented codes: validation_error, conflict, limit_exceeded (402/413), not_found (RLS-hidden objects 404 like the gateway), rate_limited (20 uploads/min per IP+project).

Cookie mode

When your project is configured for cookie sessions, the server keeps tokens in httpOnly cookies (mdb_at_<ref> / mdb_rt_<ref>) and refreshes them itself. The browser needs no JS-visible tokens — and auth, data and storage requests all switch to credentials: include:

const mdbase = createClient(projectUrl, apiKey, { cookieMode: true })

await mdbase.auth.getUser()          // authenticated via cookies
await mdbase.from('tasks').select()  // cookies ride along; RLS as the user

In cookie mode the client sends no Authorization header, stores nothing, and getSession() always resolves null.

Deploying behind an API subdomain (CORS)

In development everything is same-origin and CORS never appears. In production, apps commonly live on app.example.com while the gateway serves api.mdbase.example.com — that makes every client request cross-origin, so the gateway must:

  1. Answer OPTIONS preflights for GET/POST/PATCH/PUT/DELETE with Access-Control-Allow-Origin: <exact app origin> (never * when cookies are involved), Access-Control-Allow-Credentials: true (only needed for cookie mode), Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS, and Access-Control-Allow-Headers: apikey, authorization, content-type (apikey and authorization are non-simple headers, so every request triggers a preflight).
  2. Send Access-Control-Expose-Headers: Content-Range, ETag on real responses — without this, { count: 'exact' } silently resolves null in browsers, because JavaScript cannot read the Content-Range header cross-origin unless it is exposed.

Serving the API on the same host as the app (a path prefix like /my-project) avoids CORS entirely.

Options

createClient(projectUrl, apiKey, {
  secretKey,                   // server-side only: the base key (bucket admin, unrestricted reads)
  autoRefreshToken: true,      // rotate the refresh token before expiry
  persistSession: true,        // store the session in `storage`
  detectSessionInUrl: true,    // consume OAuth/verify fragments on load
  refreshMarginSeconds: 60,    // refresh this long before expiry
  cookieMode: false,           // httpOnly-cookie sessions
  storage,                     // { getItem, setItem, removeItem } adapter
  storageKey,                  // default `mdbase.auth.<projectRef>`
  credentials: 'same-origin',  // fetch credentials policy
  headers: {},                 // extra headers on every request
  fetch,                       // custom fetch implementation
})

Error codes

Methods never throw for API errors — they resolve { data, error }. error is an ApiError with .status and a machine-readable .code. The full set, by plane:

Client-side: network_error (status 0) — request never reached the gateway (offline, TLS, CORS block).

Gateway / Auth

| code | status | meaning | | --- | --- | --- | | missing_api_key / invalid_api_key | 401 | apikey header absent or wrong | | project_not_found | 404 | project ref does not exist | | project_paused / project_deleting | 503 | project temporarily unavailable | | validation_error | 400 | bad email, short password, bad input | | user_already_exists | 400 | signup for an email that already belongs to a confirmed user | | limit_exceeded | 402 | project plan limit (max users, storage quota) | | rate_limited | 429 | signup/verify/resend/recover 5/min, sign-in 30/min — per IP+project | | invalid_grant | 400 | wrong credentials, or revoked/expired refresh token | | email_not_confirmed | 400 | sign-in before confirming the email | | token_reuse_detected | 400 | a used refresh token was replayed — session revoked | | invalid_token | 400/401 | unknown, already-used or expired token, or revoked session | | unauthorized | 401 | no bearer/cookie where one is required | | oauth_not_configured / unsupported provider / invalid_oauth_state / oauth_provider_error | 400 | OAuth setup/callback problems |

Data API (Postgres codes pass through)

| code | status | meaning | | --- | --- | --- | | PGRST105 | 400 | update/delete without a filter | | PGRST116 | 406 | .single() matched 0 or >1 rows | | PGRST100 / PGRST102 / PGRST200 | 400 | bad filter syntax / bad body / bad embed | | 42P01 / 42703 | 404 | unknown table / unknown column | | 42501 | 403 | RLS denied the operation for this caller | | 22P02 / 23514 / 42883 | 400 | bad value cast / check violation / wrong operator type | | 23505 / 23503 | 409 | unique violation / foreign-key violation |

Storage

| code | status | meaning | | --- | --- | --- | | validation_error | 400/422 | invalid path, missing body, MIME not allowed | | not_found | 404 | object missing or hidden from this caller by RLS | | forbidden | 403 | non-public bucket on a public route / RLS denial | | conflict | 409 | path already exists / bucket not empty | | limit_exceeded | 402 / 413 | project storage quota / object size cap | | rate_limited | 429 | 20 uploads/min per IP+project | | invalid_token | 401 | expired or tampered signed URL |

Node / SSR

Sessions live in memory by default on the server; persist them yourself (e.g. in a cookie) and restore per request:

const mdbase = createClient(projectUrl, apiKey)
await mdbase.auth.setSession({ access_token, refresh_token }) // from your cookie
const { data: { user } } = await mdbase.auth.getUser()
const rows = await mdbase.from('tasks').select() // now acts as that user

Project layout

src/
  MdbaseClient.ts      client shell: namespaces, URL resolution, health()
  auth/                Auth API tab
  data/                Data API tab — PostgREST query builder
  storage/             Storage API tab — buckets + objects + renders
  lib/                 shared HTTP transport, storage adapters, helpers, errors
docs/
  api-reference-data-storage-control.md  transcribed API reference
llms.txt               index for AI assistants
CHANGELOG.md           release notes

Development

npm install
npm run build     # dist/esm + dist/cjs (+ type declarations)
npm test          # build, then node:test suites against an in-memory mock server

The test suites (test/) spin up an in-memory mock of the mdbase API — auth confirmation + recovery flows, single-use refresh tokens with reuse detection, PostgREST filtering/paging, the supabase-style storage surface (bucket ids, bulk deletes, signed uploads, copy/move, render params) — and assert both generated requests and end-to-end behavior.

License

MIT