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

@deverjak/tenantkit-adapter-supabase

v0.5.0

Published

Supabase reference adapter for @deverjak/tenantkit-kernel — Database, Identity, Session, Authz, Storage. Drop-in: createSupabaseRuntime() -> CoreRuntime.

Readme

@deverjak/tenantkit-adapter-supabase

The Supabase reference adapter for the reservation-core / tenantkit kernel ports. One call — createSupabaseRuntime() — gives you a fully‑wired CoreRuntime (Database, Identity, Session, Authz, Storage) that withRoute() consumes. Bring your own email (Resend/SMTP) and, if you sell, payments (Stripe); Supabase covers your DB + auth + storage.

Why Supabase is the reference adapter: it implements every flow the kernel needs natively — password, magic link, OTP, OAuth, admin user creation, and RLS that reads the caller's JWT — so the mapping is thin and complete. Other adapters (@deverjak/tenantkit-adapter-postgres + @deverjak/tenantkit-adapter-authjs) implement the same ports; nothing in your app changes when you swap.

Install

pnpm add @deverjak/tenantkit-kernel @deverjak/tenantkit-adapter-supabase @deverjak/tenantkit-email-resend
# .env — modern Supabase key names (legacy SUPABASE_ANON_KEY / SUPABASE_SERVICE_ROLE_KEY are also accepted)
NEXT_PUBLIC_SUPABASE_URL=https://YOUR-PROJECT.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_...   # browser-safe; RLS-enforced
SUPABASE_SECRET_KEY=sb_secret_...                         # server-only — bypasses RLS, never ship to the browser
RESEND_API_KEY=re_...                                     # only if you use @deverjak/tenantkit-email-resend

Which keys do you need?

The adapter uses two Supabase API keys and SUPABASE_URL. It accepts both the new key names (*_PUBLISHABLE_KEY / SUPABASE_SECRET_KEY) and the legacy ones (SUPABASE_ANON_KEY / SUPABASE_SERVICE_ROLE_KEY); the NEXT_PUBLIC_* variants are read too (required by the Edge proxy/middleware).

| You want to… | Key needed | Env var | |---|---|---| | Auth — sign in / sign up / magic link / OAuth / session refresh | publishable (anon) | NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY | | Tables as the signed-in user — RLS-scoped reads/writes (ctx.db.user()) | publishable (anon) | (same — the user's JWT rides the cookie) | | Public / anon table reads (ctx.db.anon()) | publishable (anon) | (same) | | The tenant layerresolveClaims (memberships, profiles), provisionTenant, plugin activation, tenant tier | secret (service-role) | SUPABASE_SECRET_KEY | | Storage — logos / exports (StorageProvider) | secret (service-role) | SUPABASE_SECRET_KEY | | Admin — invite staff (createUser), mint magic links (createMagicLink) | secret (service-role) | SUPABASE_SECRET_KEY | | Rate limits / webhooks / cron (Database.service()) | secret (service-role) | SUPABASE_SECRET_KEY |

Rule of thumb: the publishable key alone runs all user-facing auth + RLS-scoped data. Add the secret key the moment you touch the tenant/authz layer, storage, admin user creation, or any service-role work — it bypasses RLS, so keep it server-only (never in a NEXT_PUBLIC_* var or the browser bundle).

The project's JWT secret is not used by this adapter — it authenticates with the API keys above, it does not verify JWTs itself. You only need the JWT secret if you verify Supabase tokens manually elsewhere.

Wire it (≈12 lines)

// app/server/runtime.ts
import { cookies } from 'next/headers'
import { createSupabaseRuntime } from '@deverjak/tenantkit-adapter-supabase'
import { createResendEmail } from '@deverjak/tenantkit-email-resend'

export const runtime = createSupabaseRuntime({
  email: createResendEmail({ from: 'Acme <[email protected]>' }),
  // next/headers → the CookieAdapter the SSR client needs
  cookies: async () => {
    const store = await cookies()
    return {
      getAll: () => store.getAll(),
      setAll: (cs) => cs.forEach((c) => store.set(c.name, c.value, c.options)),
    }
  },
})
// app/api/courses/route.ts — a real route, RLS-enforced
import { withRoute, jsonOk } from '@deverjak/tenantkit-kernel'
import { runtime } from '@/server/runtime'

export const POST = withRoute(
  { runtime, audience: 'staff', minRole: 'coach', can: 'courses:create', tenantFrom: 'cookie', body: CreateCourseSchema },
  async (ctx) => {
    // ctx.db.user() is the caller's RLS-scoped Supabase handle; the INSERT can't escape their tenant.
    const course = await ctx.db.user().rpc('create_course', ctx.input.body)
    return jsonOk({ course })
  },
)

That's it. The session cookie carries the user's JWT, PostgREST sets request.jwt.claims, and the kernel's core.current_user_id() resolves automatically — no SET LOCAL, no service key in the request path.

What each kernel port maps to

| Kernel port | Supabase mapping | |---|---| | Database.forRequest(req).user() | cookie‑bound SSR client — RLS as the caller | | Database.forRequest(req).anon() | anon client — public catalogue reads | | Database.forRequest(req).service() / Database.service() | service‑role client — bypasses RLS (webhooks/cron) | | ScopedDb.rpc(fn, args) | supabase.rpc(...) — your SECURITY DEFINER functions (e.g. redeem_credit_into_session) | | ScopedDb.client (escape hatch) | the raw SupabaseClient for idiomatic .from() on your own tables | | IdentityProvider.* | supabase.auth.* — password / magic link (via admin.generateLink) / OTP / OAuth / admin.createUser | | SessionStore.refresh() | the updateSession cookie‑rotation pattern (call it in middleware) | | AuthzStore.* | service‑role reads of core.{profiles,memberships,participant_accounts,plugin_activations,tenants} keyed by the verified userId | | StorageProvider.* | supabase.storage.* |

Database setup (one migration)

The kernel ships its schema + RLS. Apply, in order: (1) the kernel core migration (creates core.*, the RLS predicates, core.current_user_id()), (2) your app/domain migrations, (3) optionally ./supabase/0000_current_user_id_supabase.sql if you want to alias current_user_id() to Supabase's native auth.uid() (Option B in that file). The portable default needs no Supabase‑specific SQL at all.

Supabase project settings:

  • Project → API → Exposed schemas: add core so the adapter's .schema('core') reads work — or keep core private and expose only RPCs (more locked‑down; the adapter then reads via rpc()).
  • Auth → Email: if you mint magic links with admin.generateLink() and send them via Resend (recommended, so you own the template), disable Supabase's built‑in magic‑link email; or point Supabase SMTP at Resend.

Honest limitations (so nothing surprises you)

  • No client‑side transactions. PostgREST can't BEGIN…COMMIT from the client, so ScopedDb.tx() runs inline. For real atomicity (overbooking guard, credit redeem) call a SECURITY DEFINER RPC via rpc() — which is exactly what the spec does (redeem_credit_into_session).
  • No raw query() on user/anon scopes (PostgREST, not SQL). Use .from() via ScopedDb.client, or an RPC. Driver adapters (@deverjak/tenantkit-adapter-postgres) do implement raw query().
  • Cookie writing is the one Next.js‑shaped seam; @deverjak/tenantkit-next supplies the cookies() factory for you. A non‑Next host passes its own CookieAdapter.
  • Service role bypasses RLS — the adapter fences it to AuthzStore/webhooks/cron; your code using Database.service() must re‑check authorization.

Use it à la carte

Don't want the whole runtime? Import a single factory — e.g. keep Supabase for the DB but bring a different IdentityProvider:

import { createSupabaseDatabase, createSupabaseAuthzStore } from '@deverjak/tenantkit-adapter-supabase'
const runtime = { db: createSupabaseDatabase(), authz: createSupabaseAuthzStore(), identity: myAuthjsIdentity, /* … */ }

MIT. Part of the tenantkit kernel family — see the platform spec, docs/14-portability-and-providers.md.