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

@lapub974/pgbase

v0.6.0

Published

SDK TypeScript sans dépendance pour PgBase et PgHost : PostgreSQL, Auth, Realtime, Storage et RPC

Readme

pgbase — SDK TypeScript officiel

Client typé pour PgBase et PgHost. Les requêtes et réponses sont typées ; en combinant avec les types générés depuis votre schéma, pb.collection('posts') renvoie des enregistrements Post complets (autocomplétion + vérification).

Installation

npm install @lapub974/pgbase

Aucune dépendance runtime. Le paquet est ESM et nécessite fetch (Node ≥ 18 ou navigateur). Pour le temps réel sous Node, injectez un WebSocket (voir plus bas).

Pour contribuer au SDK depuis le monorepo sans passer par npm :

npm install ../pocketpostgres/sdk/typescript

Projet hébergé : démarrage rapide

Le client d'un projet se crée comme avec Supabase : l'URL dédiée et la clé publishable affichées dans Studio → Clés API. Cette clé peut être placée dans une application web ou mobile ; elle identifie le projet mais ne contourne jamais le RLS. Les autorisations viennent ensuite de la session utilisateur et des politiques PostgreSQL.

import { createClient } from '@lapub974/pgbase'
import type { Schema, RPC } from './pgbase-types'

export const pg = createClient<Schema, RPC>(
  'https://ma-boutique.pg.monappli.re',
  import.meta.env.VITE_PGBASE_PUBLISHABLE_KEY,
)

L'ancienne forme reste compatible pendant la migration :

const pg = createClient('https://pg.monappli.re', 'ma-boutique')

Cette forme historique sans clé reste disponible uniquement pour les projets qui n'ont encore jamais créé de clé. Dès qu'une première clé existe, PgBase refuse tout appel applicatif sans clé, même si toutes les clés ont ensuite expiré ou été révoquées.

Authentification des utilisateurs du projet

Chaque projet possède sa propre collection users, ses propres refresh tokens et un espace de signature JWT distinct. Un compte ou un token d'un projet ne peut pas être utilisé dans un autre projet.

// Inscription avec métadonnées de profil
const { data, error } = await pg.auth.signUp({
  email: '[email protected]',
  password: 'mot-de-passe-solide',
  options: { data: { name: 'Alice' } },
})

// Connexion
const login = await pg.auth.signInWithPassword({
  email: '[email protected]',
  password: 'mot-de-passe-solide',
})

// Connexion sans mot de passe : le lien reçu contient token_hash
await pg.auth.signInWithOtp({
  email: '[email protected]',
  options: {
    shouldCreateUser: false,
    emailRedirectTo: 'https://app.example.com/auth/callback',
  },
})
const magicSession = await pg.auth.verifyOtp({
  token_hash: new URLSearchParams(location.search).get('token_hash')!,
  type: 'magiclink',
})

// Session locale et utilisateur vérifié côté API
const { data: sessionData } = await pg.auth.getSession()
const { data: userData } = await pg.auth.getUser()

// Événements : INITIAL_SESSION, SIGNED_IN, TOKEN_REFRESHED, SIGNED_OUT
const { data: listener } = pg.auth.onAuthStateChange((event, session) => {
  console.log(event, session?.user)
})

await pg.auth.signOut()
listener.subscription.unsubscribe()

Connexion Google et Meta/Facebook

Dans Studio → Auth & utilisateurs → Fournisseurs sociaux OAuth, renseignez le Client ID/Secret Google ou l'App ID/Secret Meta, puis copiez l'unique URI de callback affichée vers la console du fournisseur. Les identifiants sont propres à la base du projet et les secrets sont chiffrés dans PostgreSQL.

L'usage côté application reprend supabase-js :

// Google
const { data, error } = await pg.auth.signInWithOAuth({
  provider: 'google',
  options: { redirectTo: 'https://shop.example.com/auth/callback' },
})

// Meta Login conserve le nom de provider compatible Supabase : "facebook"
await pg.auth.signInWithOAuth({
  provider: 'facebook',
  options: { redirectTo: 'https://shop.example.com/auth/callback' },
})

redirectTo doit correspondre exactement à l'URL principale ou à une entrée de la liste autorisée du projet. Au retour, le SDK détecte automatiquement ?pgbase_oauth=1&code=…, échange ce code à usage unique et émet SIGNED_IN. Pour gérer la callback vous-même :

const pg = createClient(projectUrl, publishableKey, { detectSessionInUrl: false })
const code = new URL(location.href).searchParams.get('code')!
const { data, error } = await pg.auth.exchangeCodeForSession(code)

Google doit autoriser les scopes openid email profile. Dans Meta Login, activez public_profile et email ; sans adresse confirmée renvoyée par Meta, PgBase refuse la création du compte. PgBase ne conserve jamais le token du fournisseur, seulement le lien entre son identifiant stable et l'utilisateur. Guides de configuration : Google et Meta/Facebook.

Chaque connexion est une session serveur révocable avec son appareil, sa date d'expiration et sa dernière activité. L'adresse IP n'est conservée que sous forme de HMAC :

const { data: sessions } = await pg.auth.getSessions()
await pg.auth.revokeSession(sessions![0].id)

await pg.auth.signOut({ scope: 'local' })  // cette session
await pg.auth.signOut({ scope: 'others' }) // toutes les autres
await pg.auth.signOut({ scope: 'global' }) // toutes les sessions

Le SDK conserve la session dans un espace localStorage propre à l'origine du projet : une API locale et une API de production ne peuvent donc jamais partager leurs jetons. Il renouvelle automatiquement le JWT avant expiration. Le refresh token est à usage unique et tourne à chaque renouvellement.

Les méthodes et redirections disponibles dépendent de la politique réglée dans Studio → Auth & utilisateurs. Une redirection de lien magique doit correspondre exactement à l'URL principale ou à une URL explicitement autorisée ; la même liste stricte protège les retours OAuth.

Les parcours de récupération et de sécurité sont également disponibles :

await pg.auth.resetPasswordForEmail('[email protected]')
await pg.auth.confirmPasswordReset(token, 'nouveau-mot-de-passe')
await pg.auth.requestEmailVerification('[email protected]')
await pg.auth.confirmEmailVerification(token)
await pg.auth.changePassword('mot-de-passe-actuel', 'nouveau-mot-de-passe')
await pg.auth.changeEmail('[email protected]', 'mot-de-passe-actuel')

Un visiteur peut commencer sans e-mail puis conserver le même identifiant et ses données lorsqu'il crée son compte définitif :

await pg.auth.signInAnonymously()
await pg.auth.convertAnonymous({
  email: '[email protected]',
  password: 'mot-de-passe-solide',
})

Le MFA TOTP reprend l'ergonomie Supabase et élève la session en aal2. Le secret TOTP est chiffré côté serveur et les codes de récupération ne sont affichés qu'à la première validation :

const { data: enrollment } = await pg.auth.mfa.enroll({
  factorType: 'totp', friendlyName: 'Téléphone',
})
const { data: challenge } = await pg.auth.mfa.challenge({ factorId: enrollment!.factor.id })
const { data: verified } = await pg.auth.mfa.verify({
  factorId: enrollment!.factor.id,
  challengeId: challenge!.id,
  code: codeTOTP,
})
console.log(verified?.aal) // aal2

Requêtes façon Supabase

const { data: posts, error, count } = await pg
  .from('posts')
  .select('id,title,status,created')
  .eq('status', 'published')
  .order('created', { ascending: false })
  .limit(20)

const { data: matches } = await pg
  .from('posts')
  .select('id,title,status')
  .ilike('title', '%postgres%')
  .in('status', ['draft', 'published'])
  .not('archived', 'eq', true)
  .or([
    { column: 'featured', operator: 'is', value: true },
    { column: 'views', operator: 'gte', value: 1000 },
  ])
  .search('optimisation')
  .order('created', { ascending: false })
  .range(0, 19) // positions inclusives, comme Supabase

const { data: post } = await pg
  .from('posts')
  .select()
  .eq('id', postId)
  .single()

const { data: created } = await pg
  .from('posts')
  .insert({ title: 'Bonjour', status: 'draft' })
  .select('id,title,status')
  .single()

await pg.from('posts').update({ status: 'published' }).eq('id', postId)
await pg.from('posts').delete().eq('id', postId)

const controller = new AbortController()
const pending = pg.from('posts').select().abortSignal(controller.signal)
controller.abort()
await pending

Les projets récents utilisent le RLS PostgreSQL natif configuré dans Studio : politiques permissives (OR), restrictives (AND), rôles anon et authenticated, expressions USING/WITH CHECK, et droits par colonne. Les helpers auth.uid(), auth.role() et auth.jwt() sont disponibles dans les expressions. Les anciennes règles list/view/create/update/delete restent le mode de compatibilité tant que le RLS natif d'une collection n'est pas activé. Une erreur est renvoyée dans error; ajoutez .throwOnError() si vous préférez les exceptions. Les filtres du builder sont transmis sous forme d'arbre JSON puis validés et paramétrés côté serveur : aucune valeur utilisateur n'est interprétée comme du SQL. or() accepte des descripteurs typés plutôt que la chaîne PostgREST brute de Supabase.

Les insertions en lot, upserts et mutations filtrées sont exécutés côté PostgreSQL dans une transaction, sans boucle de requêtes dans le navigateur :

await pg.from('products').insert([{ sku: 'A' }, { sku: 'B' }]).select()
await pg.from('products').upsert({ sku: 'A', stock: 12 }, { onConflict: 'sku' }).select()
await pg.from('products').update({ published: true }).eq('category', 'books').select()
await pg.from('products').delete().lt('stock', 1)

Une mutation sans filtre est refusée pour prévenir une modification accidentelle de toute la table. L'upsert utilise INSERT … ON CONFLICT et la cible onConflict doit correspondre à une contrainte unique réelle.

Storage par buckets (local ou S3)

Les buckets se configurent dans Studio → Storage. Un bucket privé applique par défaut la propriété utilisateur (owner_id = @request.auth.id) à la lecture, à l'écrasement et à la suppression ; l'upload nécessite une session. Le Studio permet de régler séparément les quatre règles, la limite par fichier, les types MIME et le caractère public.

const avatars = pg.storage.from('avatars')
const objectPath = `users/${user.id}/avatar.webp`

const { data: uploaded, error } = await avatars.upload(objectPath, file, {
  contentType: 'image/webp',
  cacheControl: 3600,
  upsert: true,
  metadata: { variant: 'profile' },
})

const { data: objects } = await avatars.list('users', {
  search: 'avatar',
  sortBy: 'updated',
  order: 'desc',
})

const { data: signed } = await avatars.createSignedUrl(objectPath, 300)
image.src = signed!.signedUrl // accès privé exact, valable 5 minutes

const { data: uploadGrant } = await avatars.createSignedUploadUrl(objectPath)
await avatars.uploadToSignedUrl(objectPath, uploadGrant!.token, file)

await avatars.copy(objectPath, 'archive/avatar.webp')
await avatars.move('archive/avatar.webp', 'archive/avatar-final.webp')

let resume
await avatars.uploadResumable('videos/demo.mp4', file, {
  onUploadCreated: (session) => { resume = session }, // à persister si nécessaire
  onProgress: (uploaded, total) => console.log(uploaded / total),
  // resume: { uploadId: resume.uploadId, token: resume.token },
})

const { data: blob } = await avatars.download(objectPath)
await avatars.remove([objectPath])

Pour un bucket public, l'URL ne nécessite aucun aller-retour :

const { data } = avatars.getPublicUrl(objectPath)
image.src = data!.publicUrl

Le backend physique est transparent pour l'application : disque local par défaut, ou AWS S3/MinIO/Cloudflare R2/Wasabi/Spaces avec STORAGE_DRIVER=s3. Les clés S3 restent uniquement sur le serveur et ne sont jamais envoyées au SDK.

Queues PostgreSQL

Les producteurs et consommateurs partagent une file durable sans Redis. Une lecture crée un lease et un receiptToken; il faut ensuite acquitter, remettre en file ou archiver le message :

const emails = pg.queues.from<{ userId: string }>('emails')
await emails.send({ userId }, { delaySeconds: 5 })

const { data: messages } = await emails.read({ limit: 10 })
for (const message of messages ?? []) {
  try {
    await sendWelcomeEmail(message.payload.userId)
    await emails.ack(message)
  } catch {
    await emails.nack(message, { delaySeconds: 30 })
  }
}

La création/configuration d'une file est réservée à un client backend initialisé avec une clé pgb_secret_… possédant le scope queues :

await serverPg.queues.create('emails', {
  visibilityTimeoutSeconds: 60,
  maxAttempts: 5,
  allowAuthEnqueue: true,
  allowAuthDequeue: false,
})

Fonctions PostgreSQL avec rpc()

Créez ou adoptez une fonction dans Studio → Fonctions RPC, testez-la, puis activez explicitement son endpoint. Aucune fonction n'est exposée simplement parce qu'elle existe dans PostgreSQL.

const { data, error, count, durationMs } = await pg.rpc('search_products', {
  term: 'café',
  max_results: 20,
})

if (error) console.error(error.message, error.details)
else console.log(data, count, durationMs)

Une RPC peut être publique ou réservée aux utilisateurs connectés. Dans une fonction, l'identité courante est disponible via les paramètres transactionnels request.jwt.claims, request.jwt.sub, request.jwt.role et request.jwt.collection. Les résultats SETOF/RETURNS TABLE sont renvoyés comme tableaux (1 000 lignes maximum par appel), les autres comme une valeur.

Générer les types de vos collections

Téléchargez les interfaces générées depuis votre instance (admin ou clé API) :

curl https://api.exemple.re/api/types/typescript \
  -H "X-API-Key: pgb_VOTRE_CLE" > pgbase-types.ts

Cela produit une interface par collection, une map Schema et une map RPC :

export interface PostsRecord {
  id: string; created: string; updated: string
  title: string
  status: "draft" | "published"
  views: number | null
}
export type Schema = { posts: PostsRecord; users: UsersRecord }
export type RPC = {
  search_products: {
    Args: { term: string | null; max_results?: number | null }
    Returns: unknown
  }
}

Utilisation

import { PgBase } from '@lapub974/pgbase'
import type { Schema } from './pgbase-types'

const pb = new PgBase<Schema>('https://api.exemple.re')

// Auth utilisateur (collection auth « users »)
const { record } = await pb.collection('users').authWithPassword('[email protected]', 'motdepasse')
// le token est mémorisé dans pb.authStore et joint automatiquement

// Records typés
const { items } = await pb.collection('posts').getList({ page: 1, filter: 'status="published"', sort: '-created' })
const post = await pb.collection('posts').getOne(id, { expand: 'author' })
const created = await pb.collection('posts').create({ title: 'Bonjour', status: 'draft' })
await pb.collection('posts').update(id, { status: 'published' })
await pb.collection('posts').delete(id)

// Tout récupérer, agréger, importer
const all = await pb.collection('posts').getFullList({ filter: 'views>=100' })
const stats = await pb.collection('posts').aggregate({ op: 'sum', field: 'views', groupBy: ['status'] })
const res = await pb.collection('posts').import(rows, { atomic: false }) // { inserted, failed, errors }

Côté serveur (secret = service_role)

const pgAdmin = createClient<Schema>(
  'https://ma-boutique.pg.monappli.re',
  process.env.PGBASE_SECRET_KEY!,
)
// ⚠️ uniquement côté serveur : pgb_secret_… contourne le RLS du projet.

Les clés ont des scopes (auth, data.read, data.write, storage, realtime, rpc, queues), une expiration facultative, une rotation atomique et une révocation. Leur valeur complète n'est affichée qu'à la création/rotation.

Admin (console)

await pb.admins.login('[email protected]', 'motdepasse')
const me = await pb.admins.me()

Fichiers attachés aux enregistrements

// upload via FormData sur create/update
const fd = new FormData()
fd.append('title', 'Avec image')
fd.append('cover', fileInput.files[0])
const rec = await pb.collection('posts').create(fd)

// URL de téléchargement (token court pour <img>)
const { token } = await pb.files.getToken()
const url = pb.files.getUrl('posts', rec.id, rec.cover[0], { token })

Cette API historique lie le fichier au champ file d'un enregistrement. Pour des chemins libres, des buckets et des URLs signées, utilisez pb.storage.

Temps réel

const unsub = await pb.collection('posts').subscribe((e) => {
  // e.action: 'insert' | 'update' | 'delete', e.record typé
  console.log(e.action, e.record)
})
// plus tard : unsub()

Broadcast, Presence et changements PostgreSQL peuvent aussi être multiplexés dans un canal façon Supabase :

const room = pb.realtime.channel('private:checkout', { config: { private: true } })
  .on('broadcast', { event: 'cart-updated' }, ({ payload }) => console.log(payload))
  .on('presence', { event: 'sync' }, () => console.log(room.presenceState()))
  .on('postgres_changes', { event: '*', schema: 'public', table: 'orders' }, console.log)
  .subscribe()

await room.track({ online: true })
await room.send({ type: 'broadcast', event: 'cart-updated', payload: { count: 3 } })
await room.unsubscribe()

La connexion se reconnecte automatiquement (backoff) après une coupure et ré-émet l'abonnement avec le token courant (y compris après login). Le temps réel s'authentifie avec la clé projet pendant le handshake et par token JWT pour l'identité utilisateur. Une clé publishable sans login reste anonyme et est filtrée par RLS ; une clé secret est service_role et doit donc rester côté serveur. Le SDK transporte la clé dans un sous-protocole WebSocket, jamais dans l'URL, afin qu'elle ne soit pas enregistrée par les journaux d'accès usuels.

Sous Node (pas de WebSocket global) :

import WebSocket from 'ws'
const pb = new PgBase<Schema>(url, { webSocket: WebSocket })

À la déconnexion, utilisez pb.logout() (vide la session et ferme le temps réel).

Gestion des erreurs

Toute réponse non-2xx lève une PgBaseError portant l'enveloppe d'erreur PgBase :

import { PgBaseError } from '@lapub974/pgbase'
try {
  await pb.collection('posts').getOne('inexistant')
} catch (e) {
  if (e instanceof PgBaseError) console.log(e.status, e.message, e.details)
}

Auth & jetons

pb.authStore mémorise token, refreshToken, sessionId et le modèle. En navigateur il persiste dans un localStorage isolé par endpoint et projet ; passez votre propre AuthStore ou storage au constructeur pour personnaliser. auth.refreshSession() fait tourner le refresh token et met à jour la session de façon atomique.

Build

npm ci
npm test        # build TypeScript + tests du SDK
npm run build   # tsc -> dist/ (ESM + .d.ts)
npm pack --dry-run

Les versions publiques suivent Semantic Versioning. Les changements sont consignés dans CHANGELOG.md.