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

@mantlejs/supabase

v0.1.0

Published

Supabase adapter for Mantle JS — SupabaseRepository with full QueryParams support

Readme

@mantlejs/supabase

Supabase adapter for Mantle JS. Provides a supabase() plugin, an abstract SupabaseRepository<T> base class backed by Supabase PostgREST, a supabaseAdapter() for cross-instance event sync via Supabase Realtime Broadcast, and optional Postgres Changes subscriptions that translate direct DB mutations into Mantle service events.


Installation

npm install @mantlejs/supabase @supabase/supabase-js

Concepts

The supabase() plugin

supabase(config) is a Mantle plugin. It creates a SupabaseClient and stores it on the application at app.get("supabase"). Call it once during app configuration before registering any repositories.

SupabaseRepository<T>

SupabaseRepository is an abstract base class that implements all eight Repository<T> methods against a Supabase-hosted PostgreSQL table via the Supabase PostgREST API. Extend it, set tableName, and optionally override primaryKey and timestamps.

supabaseAdapter() — Realtime Broadcast sync transport

supabaseAdapter() returns a SyncAdapter compatible with @mantlejs/sync. It replaces Redis with Supabase Realtime Broadcast channels so teams already using Supabase get horizontal scaling with zero additional infrastructure.

listenToChanges — Postgres Changes subscription

When a SupabaseRepository<T> subclass sets readonly listenToChanges = true, it opens a Supabase Realtime Postgres Changes subscription. Direct database mutations from PostgREST, Supabase Studio, migrations, or any other source are automatically translated to Mantle service:event emissions and fan out through the app event bus. This works alongside @mantlejs/sync — both can be active simultaneously.

QueryParams support

All standard Mantle QueryParams operators are translated to PostgREST filter calls:

| Operator | PostgREST equivalent | | ---------------------------- | --------------------------------------------- | | { field: value } | .eq(field, value) | | { field: null } | .is(field, null) | | $lt, $lte, $gt, $gte | .lt(), .lte(), .gt(), .gte() | | $ne | .neq() / .not(field, "is", null) for null | | $in | .in(field, []) | | $nin | .not(field, "in", "(…)") | | $like, $ilike | .like(), .ilike() | | $notlike | .not(field, "like", pattern) | | $contains | .contains(field, value) — PostgREST cs (@> containment) | | $or | .or(filter) | | $and | chained calls |

Nested paths and $contains

Dot-path field names address JSON columns and are translated to PostgREST arrow syntax: "metadata.owner.name" becomes metadata->owner->>name when compared against a string (text comparison) and metadata->owner->name otherwise (jsonb comparison).

$contains uses jsonb @> semantics: an array operand requires the field array to contain every element, a scalar operand is treated as a single element, and an object operand requires the field object to be a recursive superset. Scalar operands are wrapped in an array before being passed to .contains(). $contains is not supported inside $or — move it to the top level of the where clause (top-level conditions are ANDed).

await repo.findAll({
  where: {
    "metadata.owner.name": "alice",          // metadata->owner->>name = 'alice'
    "metadata.level": { $gt: 4 },            // metadata->level > 4 (jsonb)
    tags: { $contains: ["red", "blue"] },    // tags @> '["red","blue"]'
    metadata: { $contains: { owner: { name: "alice" } } },
  },
});

Quick start

import { mantle } from "@mantlejs/mantle";
import { express } from "@mantlejs/express";
import { supabase, SupabaseRepository } from "@mantlejs/supabase";

// 1. Configure the plugin
const app = mantle()
  .configure(express())
  .configure(
    supabase({
      url: process.env.SUPABASE_URL!,
      key: process.env.SUPABASE_KEY!,
    }),
  );

// 2. Define your entity
interface User {
  id: string;
  name: string;
  email: string;
  created_at?: string;
  updated_at?: string;
}

// 3. Create a repository
class UserRepository extends SupabaseRepository<User> {
  readonly tableName = "users";
}

// 4. Wire it up
app.use("/users", new UserService(new UserRepository(app)));

app.listen(3030);

Custom queries

Access the raw PostgREST query builder via this.db inside your repository:

class UserRepository extends SupabaseRepository<User> {
  readonly tableName = "users";

  async findByEmail(email: string): Promise<User | null> {
    const { data, error } = await this.db.select("*").eq("email", email).maybeSingle();
    if (error) throw this.wrapError(error);
    return data ?? null;
  }
}

API

supabase(config)

Returns a MantlePlugin. Call via app.configure(supabase(config)).

app.configure(
  supabase({
    url: process.env.SUPABASE_URL!, // required — Supabase project URL
    key: process.env.SUPABASE_KEY!, // required — anon or service_role key
    options: {
      // optional — SupabaseClientOptions
      auth: { autoRefreshToken: false },
    },
  }),
);

Side effects:

  • Stores the SupabaseClient at app.get("supabase")

SupabaseConfig

| Field | Type | Default | Description | | --------- | ----------------------- | ------- | ------------------------------------------------------------------------------------ | | url | string | — | Supabase project URL (required) | | key | string | — | Supabase API key — anon for client-side, service_role for server-side (required) | | options | SupabaseClientOptions | — | Additional Supabase client options (optional) |


SupabaseRepository<T, D>

Abstract base class. Extend with a concrete tableName.

abstract class SupabaseRepository<T, D = Partial<T>> implements Repository<T, D> {
  abstract readonly tableName: string;
  readonly primaryKey: string; // default: "id"
  readonly timestamps: boolean; // default: true
}

Instance properties

| Property | Type | Default | Description | | ------------ | --------- | ------- | ------------------------------------------------------- | | tableName | string | — | Supabase table name (abstract — must be set) | | primaryKey | string | "id" | Primary key column name | | timestamps | boolean | true | Auto-write created_at / updated_at ISO-8601 strings |

Repository<T> methods

| Method | Description | | ---------------------- | ------------------------------------------------------------------------ | | findAll(params?) | Fetch all rows matching QueryParams (where, sort, limit, skip, select) | | findById(id) | Fetch a single row by primary key; returns null if not found | | save(data) | Insert a new row and return it | | saveAll(data[]) | Insert multiple rows and return them | | updateById(id, data) | Replace all non-key columns; throws NotFound if missing | | patchById(id, data) | Merge supplied fields only; throws NotFound if missing | | deleteById(id) | Delete a row and return it; throws NotFound if missing | | count(params?) | Count rows matching optional where clause |

protected db

Returns a PostgREST PostgrestQueryBuilder scoped to tableName. Use it in subclass custom queries.

listenToChanges

Set readonly listenToChanges = true in a subclass to subscribe to Postgres Changes for that table. Requires Realtime to be enabled for the table in your Supabase project settings.

class PostRepository extends SupabaseRepository<Post> {
  readonly tableName = "posts";
  readonly listenToChanges = true; // subscribe to WAL-based DB events
}

Changes are emitted as service:event on the app event bus:

| Postgres event | Mantle event | | -------------- | ------------ | | INSERT | created | | UPDATE | patched | | DELETE | removed |

External events carry { external: true }

These re-emissions did not pass through the hook pipeline — no before/after hooks ran, no validation, no params.user. So their params argument is { external: true } (typed on ServiceParams), letting event consumers (socket.io broadcasts, @mantlejs/sync, custom listeners) distinguish them from hook-pipeline events and, for example, skip authorization-derived filtering that assumes hook context.

Why UPDATE maps to patched — never updated

In Mantle, updated means "a caller replaced the full record via update()" while patched means "some fields changed". A WAL-level UPDATE only tells us the row's new state — the intent (full replace vs. partial mutation) is unknowable from outside the service layer. patched is the honest, conservative claim: consumers reacting to patched must already tolerate partial change, whereas mislabeling a partial write as updated would let them assume every field was intentionally set.

protected wrapError(err)

Maps Supabase / PostgreSQL error codes to typed MantleError subclasses. Call it in custom query methods to keep error handling consistent.


supabaseAdapter(options?)

Returns a SyncAdapter for use with @mantlejs/sync. Credentials fall back to SUPABASE_URL and SUPABASE_SERVICE_KEY (or SUPABASE_KEY) environment variables.

import { sync } from "@mantlejs/sync";
import { supabase, supabaseAdapter } from "@mantlejs/supabase";

const app = mantle()
  .configure(express())
  .configure(supabase({ url: process.env.SUPABASE_URL!, key: process.env.SUPABASE_SERVICE_KEY! }))
  .configure(socketio())
  .configure(sync({ adapter: supabaseAdapter() }));

SupabaseAdapterOptions

| Field | Type | Default | Description | | ----- | -------- | -------------------------------- | ---------------------------- | | url | string | SUPABASE_URL env var | Supabase project URL | | key | string | SUPABASE_SERVICE_KEY env var | Service role key |


Types

import type { SupabaseConfig, SupabaseAdapterOptions, SyncAdapter, SyncMessage } from "@mantlejs/supabase";
import { SupabaseRepository, supabaseAdapter } from "@mantlejs/supabase";

| Export | Kind | Description | | ------------------------ | -------------- | --------------------------------------------- | | SupabaseConfig | interface | Options passed to supabase() | | SupabaseRepository | abstract class | Base repository class to extend | | SupabaseAdapterOptions | interface | Options passed to supabaseAdapter() | | SyncAdapter | interface | Pluggable transport interface for sync | | SyncMessage | interface | Cross-instance sync message shape | | supabaseAdapter | function | Factory that returns a Supabase SyncAdapter |


Error mapping

All errors are converted to typed MantleError subclasses:

| PostgreSQL / PostgREST code | Error thrown | Condition | | --------------------------- | -------------------- | ---------------------------------------- | | PGRST116 | NotFound (404) | Zero rows returned by a .single() call | | 23505 | Conflict (409) | Unique constraint violation | | 23503, 23514, 23502 | BadRequest (400) | FK, check, or NOT NULL violation | | 42501, 28000, 28P01 | Forbidden (403) | Insufficient privilege | | anything else | GeneralError (500) | Unexpected error |


Development

npx nx build supabase   # compile
npx nx test supabase    # run tests
npx nx lint supabase    # lint

Publishing

Build before publishing:

npx nx build supabase

First publish (scoped packages require --access public):

cd packages/supabase
npm publish --access public

Subsequent releases — bump version in packages/supabase/package.json, then:

cd packages/supabase
npm publish

Testing locally with Verdaccio

# Terminal 1 — start the local registry
npx nx run @mantle/source:local-registry

# Terminal 2 — publish to it
cd packages/supabase
npm publish --registry http://localhost:4873

# Install from it in another project
npm install @mantlejs/supabase --registry http://localhost:4873