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

covara

v0.11.2

Published

Real-time resource API framework built on Hono — runs standalone (Node) and on Cloudflare Workers, with a TypeScript client

Downloads

2,818

Readme

Your Drizzle schema is already a backend. Covara turns it into a complete, production-ready API — REST endpoints, real-time subscriptions, auth, file uploads, billing, email, and background jobs — with a type-safe, offline-first TypeScript client on the other end. Built on Hono, it runs standalone on Node or at the edge on Cloudflare Workers.

The Goal

Every product backend is the same 80%: CRUD endpoints, filtering, pagination, auth, sessions, password reset, file uploads, webhooks for payments, transactional email, a job queue, and a client that talks to all of it. You rewrite this plumbing for every project, and the pieces never quite fit together — your realtime layer doesn't know about your auth scopes, your client types drift from your API, your offline cache fights your subscriptions.

Covara's goal is to make that 80% one coherent system derived from a single source of truth: your Drizzle schema.

  • Define a table → get a full REST API with filtering, pagination, aggregations, batch ops, and OpenAPI docs.
  • Add an auth scope → it's enforced everywhere: queries, mutations, subscriptions, search.
  • Mutate data anywhere — generated endpoint, custom route, RPC — and every subscribed client updates in real time.
  • Use the client → full TypeScript inference, optimistic updates, offline queue, automatic reconnect. In React, React Native, or plain TS.

The remaining 20% — your business logic — goes in lifecycle hooks, RPC procedures, and ordinary Hono routes, with the framework's tracking and typing intact.

// server: a table becomes an API
const app = createCovara({ cors: true })
  .resource("/todos", todosTable, {
    id: todosTable.id,
    db,
    auth: { update: async (user) => rsql`userId==${user.id}` },
  });

// client: the API becomes live UI
function TodoList() {
  const { items, mutate } = useLiveList<Todo>("/api/todos", { orderBy: "position" });
  return items.map((todo) => (
    <Todo key={todo.id} {...todo} onDelete={() => mutate.delete(todo.id)} />
  ));
  // creates/updates/deletes apply optimistically, sync offline,
  // and stream to every other connected client over SSE
}

Features

Core API

  • Automatic REST API - Full CRUD endpoints from your Drizzle schema
  • Real-time Subscriptions - SSE with changelog-based updates, sequence numbers, and seamless reconnection
  • Relations & Joins - belongsTo, hasOne, hasMany, manyToMany with efficient batch loading, optional foreign-key auto-discovery (autoRelations), and scope-enforced includes (a relation never reveals rows the user couldn't read directly)
  • RSQL Filtering - Comprehensive query language (30+ operators) plus custom operators
  • Cursor Pagination - Keyset pagination with multi-field ordering
  • Aggregations - Group by, count, sum, avg, min, max, with HAVING filtering on aggregate output — available as a one-shot query or a live subscription (useLiveAggregate) that recomputes on every change
  • Nested Write-Through - Create belongsTo parents and hasMany/hasOne children in one atomic POST
  • Soft Delete - Mark rows deleted instead of removing them; reads hide them unless ?withDeleted=true
  • Batch Operations - Bulk create, update, delete with limits, plus bulk upsert (POST /batch/upsert)
  • Writable Enforcement - fields.writable is an enforced allowlist (mass-assignment protection); strictInput rejects unknown fields
  • Computed Fields - Virtual computed fields added to every response and subscription event
  • Optimistic Locking - ETags, If-Match preconditions with compare-and-swap, auto-incrementing version fields
  • Full-Text Search - Built-in SQLite FTS5 / Postgres tsvector / OpenSearch / in-memory adapters, with an opt-in transactional outbox for at-least-once index convergence
  • RPC Procedures & Lifecycle Hooks - Custom Zod-validated endpoints and before/after hooks on every operation
  • Mutation Tracking - Wrap your Drizzle db so custom routes feed subscriptions and cache invalidation automatically

Runs Everywhere

  • Standalone Node - startServer(app) via covara/node
  • Cloudflare Workers - export default app, D1/Postgres, nodejs_compat
  • Durable Object KV - Cross-isolate subscriptions, rate limits, and sessions on Workers without Redis
  • SQLite & PostgreSQL - libsql, better-sqlite3, D1, postgres-js, Neon, PGlite via Drizzle

Authentication & Security

  • OIDC Provider - Built-in OpenID Connect server with PKCE, token revocation (RFC 7009) and introspection (RFC 7662)
  • OIDC Hardening - Component-wise redirect-URI validation, PKCE-required public clients (plain rejected), federated id_token verification, endpoint rate limiting, KV-backed stores by default
  • Dynamic Client Registration & Consent Revocation - Opt-in POST /register, plus POST /consent/revoke and consent TTL
  • Federated Login - Google, Microsoft, Okta, Auth0, Keycloak, custom (OIDC); plus backends.passport to use any Passport.js OAuth2 strategy (GitHub, Discord, …) as an upstream under your own OIDC provider
  • Social Login - Drop in any Passport.js OAuth2 strategy (GitHub, Discord, Google, …) via fromPassport; runs on Node and Workers, with one-call loginWithSocial / signInWith on the client
  • JWT Auth - JWT bearer adapter on the server, JWTClient + useJWTAuth hook on the client with pluggable token storage (localStorage, memory, AsyncStorage)
  • Session Auth - Auth.js, Passport.js, and session adapters with session rotation on login
  • Multi-Factor Auth - Opt-in TOTP MFA with backup codes
  • Magic Links - Opt-in passwordless email login
  • API Keys - Standalone helpers to create, verify, rotate, and revoke hashed API keys
  • Password Hashing & Policy - Built-in scrypt hashPassword/verifyPassword/needsRehash (Workers-safe) plus an enforceable password policy
  • Account Security - Opt-in CSRF protection, login throttling, email verification, password reset
  • Security Headers - HSTS, X-Frame-Options, MIME-sniffing protection, and more, auto-mounted by createCovara; opt-in CSP (so it never blocks your frontend)
  • Authorization Scopes - Row-level security with RSQL expressions, enforced across reads, writes, subscriptions, and search
  • Field-level Read Masking - fields.readable allowlist strips non-readable columns from every response and subscription event (cannot be bypassed via ?select=)

File Storage

  • Storage Adapters - Local disk, S3, Cloudflare R2 (native binding or S3-compat), and in-memory behind one StorageAdapter interface
  • File Resources - First-class resources with an upload/download layer: app.fileResource(...) chains like any resource and inherits the full CRUD/hooks/procedures/relations/subscriptions/scopes surface, plus MIME/size validation, per-user keys, and storage cleanup on delete
  • Zero-config local serving - createCovara auto-serves a local adapter's baseUrl; admin data explorer gets a per-row Download action
  • Presigned URLs - Optional direct-to-bucket uploads/downloads with configurable expiry
  • React Hooks - useFileUpload (with progress), useFile, useFiles; getDownloadUrl() for React Native

Background Processing

  • Task Queue - Distributed background jobs with Redis, Durable Objects, or in-memory backends
  • Cloudflare Queues - Producer/consumer adapter for running tasks on Workers without a poller
  • Retry Strategies - Exponential, linear, or fixed backoff
  • Scheduling - Delayed execution, cron expressions, recurring tasks with a missed-occurrence catchup policy
  • Progress & Result TTL - ctx.reportProgress, heartbeats, and resultTtlMs result expiry
  • Idempotency & Concurrency - Per-task idempotency keys and enforced maxConcurrency
  • Graceful Drain - worker.drain() / worker.stop({ drain: true })
  • Dead Letter Queue - Failed task management with replay lineage and an onDlqEnqueue alerting hook

Email

  • Unified Adapters - covara/email with Resend and Cloudflare Email Service adapters behind one EmailAdapter interface
  • Template Builder - Fluent createEmail().heading().button().code().build() rendering responsive, escaped HTML + a plaintext fallback, with theming
  • Batch Sending - sendEmailBatch for bulk delivery

Billing

  • One Interface, Four Providers - covara/billing over Stripe, Lemon Squeezy, Paddle, and Polar.sh (fetch-based, no SDK deps, Workers-safe)
  • Plans, One-Time & Usage - Define subscription/one-time/usage plans by key; checkout, subscription management, reportUsage, hosted portal
  • Credits Ledger - KV-backed atomic grant/consume/balance/history
  • Webhooks - Per-provider signature verification, idempotent delivery dedupe, and automatic credit granting on payment.succeeded
  • Router & Client - createBillingRouter plus client.billing.* and useCredits/useSubscription/useCheckout hooks

Client Library

  • Type-safe Client - Full TypeScript inference, select projections that narrow return types, typed filter builder, generated types from your API
  • React Hooks - useLiveList, useInfiniteList, useLiveAggregate, useMutation, useSearch, useAuth, useJWTAuth, useFileUpload/useFile/useFiles, useCredits/useSubscription/useCheckout, usePublicEnv, plus query invalidation and prefetch
  • React Native Support - No DOM assumptions: pluggable TokenStorage (AsyncStorage-compatible), environment-aware transport and offline backends, getDownloadUrl() for native file handling
  • Resilient Transport - Per-request AbortSignal + timeout, automatic 401 refresh-and-retry, SSE reconnect with jitter
  • Offline Support - Optimistic updates, mutation queue, field-level merge, multi-tab coherence, IndexedDB backend
  • Auth Strategies - OIDC (PKCE flow, token refresh), JWT, bearer, API key, or cookie sessions — selected per client or auto-detected
  • HMR-safe - getOrCreateClient for development

Server-rendered htmx (Beta)

  • One JSX page → full app - app.page(path, Component) server-renders a page whose <Live> regions auto-generate the htmx endpoints (list/create/update/delete/subscribe). Covara generates wiring, not UI components
  • Live by default - each region streams server-rendered fragments over SSE (reusing the existing subscription engine); rows update in place as anyone mutates the resource
  • Optimistic & offline - new rows insert once via the live SSE (no duplicate/phantom), deletes are optimistic, and mutations queue offline and replay on reconnect
  • No client framework - vendored htmx core + a tiny runtime, served and injected automatically
  • Beta - newer than the JSON API + TypeScript client; the API may still change

Environment Variables

  • Type-safe Configuration - Define and validate env vars with Zod via createEnv / envVariable
  • Public and Private Vars - PUBLIC_-prefixed or explicitly-marked vars served to clients via usePublicEnv (with ETag)
  • Client Access - Typed fetchPublicEnv / createEnvClient and a usePublicEnv React hook

Developer Experience

  • Project Scaffolding - npx covara create my-app (Node/Workers templates, SQLite/Postgres), optional live React SPA with --frontend react, plus covara generate resource|migration
  • covara dev Loop - Dev watcher: streams schema changes to the DB (additive auto-applied, destructive gated), regenerates the typed client, runs the server — no manual push. Plus covara db connection profiles (local/remote/Turso/Postgres), push/migrate/studio, and data/import/export/run/types
  • Deploy-Ready Output - Generated Dockerfile, docker-compose, complete wrangler.toml, GitHub Actions CI, .env.example
  • Framework Migrations - covara/db ships canonical internal-table schemas, an idempotent autoMigrate/migrateInternal, a generic seeder, and pool-sizing helpers
  • Shapeable Internal Tables - bring your own auth tables (custom names + column remapping) with defineInternalSchema; every internal/system table and its required columns is documented
  • Pluggable Observability Storage - the admin audit log, request/error logs, and metrics persist to KV (or your own adapter) when configured, in-memory otherwise
  • App Factory - createCovara() wires errors, auth, security headers, health, OpenAPI, admin UI
  • Graceful Shutdown - SIGTERM/SIGINT draining with /readyz 503 and clean SSE close
  • Admin UI - Built-in dashboard at /__covara/ui
  • OpenAPI Generation - Auto-generated specs from resources (filters, procedures, subscriptions, ETags)
  • Structured Logging - Pluggable JSON logger with COVARA_LOG_LEVEL and traceparent propagation
  • Middleware - Observability, versioning, idempotency, rate limiting
  • RFC 7807 Errors - Problem+JSON everywhere, even without custom error handling

Quick Start

Scaffold a project

npx covara create my-app                          # Node + SQLite
npx covara create my-app --frontend react         # + a live React SPA on the same server
npx covara create my-app --db postgres            # Node + PostgreSQL
npx covara create my-app --template cloudflare    # Cloudflare Workers + D1

Or add to an existing app

npm install covara hono drizzle-orm zod @libsql/client

Define your schema:

import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";

export const usersTable = sqliteTable("users", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  name: text("name").notNull(),
  email: text("email").notNull(),
  role: text("role").default("user"),
});

Create your API:

import { drizzle } from "drizzle-orm/libsql";
import { createClient } from "@libsql/client";
import { createCovara } from "covara";
import { startServer } from "covara/node";
import { usersTable } from "./schema";

const client = createClient({ url: "file:./data.db" });
const db = drizzle(client);

const app = createCovara({ cors: true })
  .resource("/users", usersTable, { id: usersTable.id, db });

await startServer(app, { port: 3000 });

Health endpoints (/healthz, /readyz), OpenAPI (/__covara/openapi.json), the admin UI (/__covara/ui), and RFC 7807 error handling are wired automatically. The path is optional — .resource(usersTable, config) mounts at the table name.

Cloudflare Workers

import { drizzle } from "drizzle-orm/d1";
import { createCovara, CovaraApp } from "covara";
import { usersTable } from "./schema";

let app: CovaraApp | undefined;

export default {
  fetch(request: Request, env: { DB: D1Database }, ctx: ExecutionContext) {
    app ??= createCovara().resource("/users", usersTable, {
      id: usersTable.id,
      db: drizzle(env.DB),
    });
    return app.fetch(request, env, ctx);
  },
};
# wrangler.toml
compatibility_flags = ["nodejs_compat"]

[[d1_databases]]
binding = "DB"
database_name = "my-app"

Workers bill CPU time, not wall-clock time — long-lived idle SSE subscriptions cost almost nothing, since heartbeats and event pushes use negligible CPU.

For production Workers deployments, bind the bundled CovaraKVDurableObject as Covara's KV store so subscriptions, rate limits, and sessions are shared across isolates — see the Durable Object KV guide. Projects scaffolded with covara create --template cloudflare have it wired up already.

Using a plain Hono app

useResource returns a regular Hono router — compose it however you like:

import { Hono } from "hono";
import { useResource, errorHandler, notFoundHandler } from "covara";

const app = new Hono();
app.onError(errorHandler);
app.notFound(notFoundHandler);
app.route("/api/users", useResource(usersTable, { id: usersTable.id, db }));

export default app;

Generated Endpoints

| Method | Path | Description | |--------|------|-------------| | GET | /api/users | List with filtering, pagination | | GET | /api/users/:id | Get single resource | | POST | /api/users | Create resource | | PATCH | /api/users/:id | Update resource (partial) | | PUT | /api/users/:id | Replace resource | | DELETE | /api/users/:id | Delete resource | | GET | /api/users/count | Count with filtering | | GET | /api/users/aggregate | Aggregations | | GET | /api/users/aggregate/subscribe | Live aggregation (SSE, recomputed on change) | | GET | /api/users/subscribe | SSE subscription | | GET | /api/users/search | Full-text search (when configured) | | POST | /api/users/batch | Batch create | | PATCH | /api/users/batch | Batch update | | DELETE | /api/users/batch | Batch delete | | POST | /api/users/batch/upsert | Bulk insert-or-update by primary key | | POST | /api/users/rpc/:name | RPC procedures |

Resource Configuration

Everything is opt-in per resource:

app.resource("/posts", postsTable, {
  id: postsTable.id,
  db,

  // Batch operation limits
  batch: { create: 100, update: 100, delete: 100 },

  // Pagination settings
  pagination: { defaultLimit: 20, maxLimit: 100 },

  // Rate limiting
  rateLimit: { windowMs: 60000, maxRequests: 100 },

  // Optimistic locking (ETag / If-Match)
  etag: { versionField: "version" },

  // Authorization scopes (row-level security via RSQL)
  auth: {
    public: { read: true },
    update: async (user) => rsql`authorId==${user.id}`,
    delete: async (user) => rsql`authorId==${user.id}`,
  },

  // Relations
  relations: {
    author: {
      resource: "users",
      schema: usersTable,
      type: "belongsTo",
      foreignKey: postsTable.authorId,
      references: usersTable.id,
    },
    comments: {
      resource: "comments",
      schema: commentsTable,
      type: "hasMany",
      foreignKey: commentsTable.postId,
      references: postsTable.id,
    },
  },

  // Lifecycle hooks
  hooks: {
    onBeforeCreate: async (ctx, data) => ({ ...data, createdAt: new Date() }),
  },

  // RPC procedures
  procedures: {
    publish: defineProcedure({
      input: z.object({ id: z.string() }),
      output: z.object({ success: z.boolean() }),
      handler: async (ctx, input) => {
        await db.update(postsTable).set({ published: true }).where(eq(postsTable.id, input.id));
        return { success: true };
      },
    }),
  },
});

See the Resources reference for the full option reference (soft delete, computed fields, field allowlists, search, custom filter operators, and more).

Client Library

import { getOrCreateClient } from "covara/client";
import { useLiveList, useAuth } from "covara/client/react";

const client = getOrCreateClient({
  baseUrl: "https://api.myapp.com",
  credentials: "include",
  offline: true, // optimistic updates + mutation queue + persistence
});

function TodoApp() {
  const { user, isAuthenticated } = useAuth();
  const { items, status, mutate } = useLiveList<Todo>("/api/todos", {
    orderBy: "position",
  });

  return (
    <div>
      <p>Welcome, {user?.name}! ({status})</p>
      <ul>
        {items.map((todo) => (
          <li key={todo.id}>
            {todo.title}
            <button onClick={() => mutate.delete(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
      <button onClick={() => mutate.create({ title: "New todo" })}>Add</button>
    </div>
  );
}

mutate.create/update/delete apply optimistically, queue while offline, reconcile on sync, and every other subscribed client sees the change over SSE.

Authentication options

The client supports OIDC (PKCE), JWT, bearer tokens, API keys, and cookie sessions. useAuth({ strategy }) selects one explicitly, or auto-detects.

JWT (works in React Native — bring your own token storage):

import { initJWTClient } from "covara/client/react";
import { useJWTAuth } from "covara/client/react";

initJWTClient({
  baseUrl: "https://api.myapp.com",
  // storage: AsyncStorage-backed TokenStorage for React Native;
  // defaults to localStorage in the browser
});

function LoginGate() {
  const { user, isAuthenticated, login, signup, logout } = useJWTAuth<User>();

  if (!isAuthenticated) {
    return <button onClick={() => login(email, password)}>Sign In</button>;
  }
  return <button onClick={logout}>Sign out, {user?.name}</button>;
}

OIDC (PKCE flow, token refresh, automatic 401 retry):

const client = getOrCreateClient({
  baseUrl: "https://api.myapp.com",
  auth: {
    issuer: "https://auth.myapp.com/oidc",
    clientId: "web-app",
    redirectUri: window.location.origin + "/callback",
  },
});

client.auth.login(); // redirects to the provider

Low-level API

const users = client.resource<User>("/users");

// CRUD operations
const allUsers = await users.list({ filter: 'role=="admin"', limit: 10 });
const user = await users.get("123");
const newUser = await users.create({ name: "Alice", email: "[email protected]" });
await users.update("123", { name: "Alice Smith" });
await users.delete("123");

// Real-time subscriptions
const subscription = users.subscribe(
  { filter: 'role=="admin"' },
  {
    onAdded: (user) => console.log("New admin:", user),
    onChanged: (user) => console.log("Updated:", user),
    onRemoved: (id) => console.log("Removed:", id),
    onInvalidate: () => console.log("Out-of-band change, refetching"),
    onConnected: (seq) => console.log("Live from sequence", seq),
    onError: (err) => console.error(err),
  }
);

React Native

The client has no hard DOM dependencies: pass an AsyncStorage-backed TokenStorage for JWT auth, offline persistence picks an environment-appropriate backend, and the file hooks expose getDownloadUrl() for use with Linking instead of browser downloads.

Server-rendered htmx (Beta)

Beta. This is newer than the JSON API and TypeScript client and may still change between releases. The JSON API it builds on is stable.

Prefer hypermedia over a client framework? Register a single JSX page; <Live> is the only special element, and Covara generates the htmx endpoints, the live SSE stream, and the optimistic/offline client runtime for you.

// tsconfig: { "jsx": "react-jsx", "jsxImportSource": "hono/jsx" }
import { createCovara } from "covara/server";
import { Live } from "covara/htmx";
import { todos } from "./schema";

const app = createCovara()
  .resource("/todos", todos, { id: todos.id, db })
  .page("/todos", () => (
    <Live
      resource={todos}
      query={{ orderBy: "position" }}
      create={(c) => (
        <form {...c.create()}>
          <input name="title" />
          <button>Add</button>
        </form>
      )}
      container={(rows, c) => <ul {...c.container()}>{rows}</ul>}
      render={(t, c) => (
        <li {...c.row(t.id)}>
          {t.title}
          <button {...c.delete(t.id)}>Delete</button>
        </li>
      )}
    />
  ));

GET /todos is now a full server-rendered page. Creating a todo in one browser tab streams the new row into every other tab over SSE; updates and deletes patch rows in place. The helpers (c.create(), c.row(id), c.update(id), c.delete(id), c.container()) emit plain htmx attributes pointing at auto-generated endpoints under /__covara/live. See the htmx docs for optimistic/offline behavior, live aggregates, and scoping.

File Storage

Configure a storage backend once, then chain a file resource like any other. Local uploads are auto-served at baseUrl:

import { createCovara, initializeStorage } from "covara";

initializeStorage({
  type: "local", // or "s3" | "r2" | "memory"
  local: { basePath: "./uploads", baseUrl: "/uploads" },
});

const app = createCovara({ cors: true })
  .resource("/todos", todosTable, { id: todosTable.id, db })
  .fileResource("/files", filesTable, {
    db,
    id: filesTable.id,
    allowedMimeTypes: ["image/jpeg", "image/png"],
    maxFileSize: 5 * 1024 * 1024,
    auth: {
      read: async (user) => rsql`userId==${user?.id}`,
      delete: async (user) => rsql`userId==${user?.id}`,
    },
    usePresignedUrls: true, // direct-to-bucket on S3/R2
    // ...plus any resource option: hooks, relations, procedures, subscriptions
  });

Upload from React with progress tracking:

import { useFileUpload, useFiles } from "covara/client/react";

function Uploader() {
  const { upload, isUploading, progress } = useFileUpload({
    resourcePath: "/api/files",
    onSuccess: (file) => console.log("Uploaded", file.id),
  });

  return (
    <input
      type="file"
      disabled={isUploading}
      onChange={(e) => e.target.files?.[0] && upload(e.target.files[0])}
    />
  );
}

See Storage.

Server-side Authentication

Standard auth routes

import { useAuth } from "covara/auth";

const { router, middleware } = useAuth({
  adapter: authAdapter, // JWT, Auth.js, Passport.js, or OIDC adapter
  login: { validateCredentials: async (email, password) => user },
  signup: { createUser: async ({ email, password, name }) => user },
});

app.route("/api/auth", router); // /me, /login, /signup, /logout
app.use("*", middleware);       // populates c.get("user")

Opt-in extras: TOTP MFA with backup codes, magic links, email verification, password reset, login throttling, CSRF protection, and API key management. See Authentication.

OIDC Provider

A complete OpenID Connect server, in your app:

import { createOIDCProvider } from "covara";

const { router, middleware } = createOIDCProvider({
  issuer: "https://auth.myapp.com",
  keys: { algorithm: "RS256" },
  tokens: {
    accessToken: { ttlSeconds: 3600 },
    refreshToken: { ttlSeconds: 30 * 24 * 3600, rotateOnUse: true },
  },
  clients: [{
    id: "web-app",
    name: "My Web App",
    redirectUris: ["https://myapp.com/callback"],
    grantTypes: ["authorization_code", "refresh_token"],
    tokenEndpointAuthMethod: "none", // public client, PKCE required
  }],
  backends: {
    emailPassword: {
      enabled: true,
      validateUser: async (email, password) => { /* ... */ },
      findUserById: async (id) => { /* ... */ },
    },
    federated: [
      oidcProviders.google({ clientId: "...", clientSecret: "..." }),
    ],
  },
});

app.route("/oidc", router);
app.use("/api/*", middleware);

The provider exposes discovery, JWKS, /authorize, /token, /userinfo, /logout, plus RFC 7009 revocation (/revoke) and RFC 7662 introspection (/introspect). Confidential client secrets may be stored hashed (scrypt$...) and are verified with the built-in hashPassword/verifyPassword helpers.

Background Tasks

Distributed task queue with retries and scheduling:

import { defineTask, initializeTasks, getTaskScheduler, getTaskRegistry, startTaskWorkers } from "covara/tasks";
import { createKV } from "covara/kv";

const kv = await createKV({ type: "redis", redis: { url: "redis://localhost" } });
initializeTasks(kv);

const sendEmailTask = defineTask({
  name: "send-email",
  input: z.object({ to: z.string().email(), subject: z.string(), body: z.string() }),
  retry: { maxAttempts: 3, backoff: "exponential" },
  handler: async (ctx, input) => {
    await sendEmail(input.to, input.subject, input.body);
  },
});

getTaskRegistry().register(sendEmailTask);
await startTaskWorkers(kv, getTaskRegistry(), 3);

// Enqueue
await getTaskScheduler().enqueue(sendEmailTask, {
  to: "[email protected]",
  subject: "Welcome!",
  body: "Thanks for signing up.",
});

// Recurring
await getTaskScheduler().scheduleRecurring(dailyReportTask, {}, {
  cron: "0 6 * * *",
  timezone: "UTC",
});

On Workers, swap the poller for the Cloudflare Queues adapter. See Background tasks.

Email

import { setGlobalEmail, createResendAdapter, createEmail, sendEmail } from "covara/email";

setGlobalEmail(createResendAdapter({ apiKey: process.env.RESEND_API_KEY }));

const { html, text } = createEmail({ brandColor: "#4f46e5" })
  .heading("Verify your email")
  .text("Tap the button below to verify your account.")
  .button("Verify email", `https://acme.com/verify?token=${token}`)
  .divider()
  .code("123456")
  .build();

await sendEmail({ from: "[email protected]", to: email, subject: "Verify your email", html, text });

The builder renders responsive, escaped HTML plus a plaintext fallback. A Cloudflare Email Service adapter is included for Workers. See Email.

Billing

One interface over Stripe, Lemon Squeezy, Paddle, and Polar.sh — fetch-based, no provider SDKs, Workers-safe:

import { createBilling, createBillingRouter, createStripeAdapter } from "covara/billing";

const billing = createBilling({
  adapter: createStripeAdapter({ apiKey: env.STRIPE_SECRET_KEY }),
  webhookSecret: env.STRIPE_WEBHOOK_SECRET,
  plans: [
    { key: "pro_monthly", priceId: "price_123", type: "subscription", credits: 10_000 },
  ],
});

app.route("/api/billing", createBillingRouter(billing, {
  getAccount: (c) => c.get("user")?.id,
  getCustomerEmail: (c) => c.get("user")?.email,
}));
import { useCredits, useSubscription, useCheckout } from "covara/client/react";

function Account() {
  const { balance } = useCredits();
  const { activeSubscription } = useSubscription();
  const { redirectToCheckout, loading } = useCheckout();

  return (
    <div>
      <p>Credits: {balance}</p>
      <button onClick={() => redirectToCheckout({ plan: "pro_monthly" })} disabled={loading}>
        Upgrade
      </button>
    </div>
  );
}

Webhooks are signature-verified, deduplicated, and grant credits automatically on payment.succeeded. See Billing.

Mutation Tracking

Custom routes participate in the realtime system by wrapping your db once:

import { drizzle } from "drizzle-orm/libsql";
import { trackMutations, readJsonBody, requireUser } from "covara";
import * as schema from "./schema";

const baseDb = drizzle(/* config */);

export const db = trackMutations(baseDb, {
  todos: { table: schema.todosTable, id: schema.todosTable.id },
  users: { table: schema.usersTable, id: schema.usersTable.id },
});

app.post("/api/custom-action", async (c) => {
  const body = await readJsonBody(c) as { title: string };
  const user = requireUser(c);

  const [todo] = await db
    .insert(schema.todosTable)
    .values({ title: body.title, userId: user.id })
    .returning();
  // recorded in the changelog — subscribers are notified

  return c.json(todo);
});

Optional query caching with automatic invalidation:

const db = trackMutations(baseDb, tables, {
  cache: { enabled: true, ttl: 60000 },
});

For writers outside the tracked db — cron jobs, other services, manual edits — recordExternalMutation appends a changelog entry, invalidates the cache, and tells live subscribers to refetch. It's the portable alternative to database-specific CDC:

import { recordExternalMutation } from "covara";

await recordExternalMutation("todos", "update", { objectId: "todo-1" });

Typed Environment Variables

import { createEnv, envVariable, usePublicEnv } from "covara";
import { z } from "zod";

const env = createEnv({
  PUBLIC_API_URL: z.string().url(),                       // PUBLIC_ prefix → exposed to clients
  SECRET_KEY: envVariable(process.env.SECRET, z.string()), // explicit source
  PORT: z.string().default("3000").transform(Number),
});

app.route("/api/env", usePublicEnv(env)); // serves public vars (with ETag)

Clients read public vars with fetchPublicEnv/createEnvClient or the usePublicEnv React hook. See Environment variables.

Query Parameters

| Parameter | Example | Description | |-----------|---------|-------------| | filter | age>=18;role=="admin" | RSQL filter expression | | select | id,name,email | Field projection | | include | author,comments(limit:5) | Related data to load | | cursor | eyJpZCI6MTB9 | Pagination cursor | | limit | 20 | Page size | | orderBy | name:asc,age:desc | Sort order | | totalCount | true | Include total count | | having | count>=5;sum_amount>100 | Filter aggregate groups (/aggregate only) | | withDeleted | true | Include soft-deleted rows (when softDelete is configured) |

Filter Syntax

# Comparison
name=="John"              # Equals
age>=18                   # Greater than or equal
status!="deleted"         # Not equals

# Logical operators
age>=18;role=="admin"     # AND (semicolon)
role=="admin",role=="mod" # OR (comma)
(age>=18;verified==true),role=="admin"  # Grouping

# String operations
name=icontains="john"     # Case-insensitive contains
email=iendswith="@company.com"
title=istartswith="how to"

# Set and range
role=in=("admin","mod")   # In list
age=between=[18,65]       # Range (inclusive)

# Null and empty
deletedAt=isnull=true     # Is null
bio=isempty=false         # Has non-empty value

# See https://kahveciderin.github.io/covara/core/filtering for all 30+ operators

The same expression filters database queries, subscription scopes, and auth scopes — parsed once, executed as SQL or in-memory as needed.

Error Handling

All errors follow RFC 7807 Problem Details format:

{
  "type": "/__covara/problems/not-found",
  "title": "Not found",
  "status": 404,
  "detail": "users with id '123' not found",
  "code": "NOT_FOUND",
  "resource": "users",
  "id": "123"
}

Error types include:

  • not-found (404) - Resource not found
  • validation-error (400) - Invalid input data
  • unauthorized (401) - Authentication required
  • forbidden (403) - Insufficient permissions
  • rate-limit-exceeded (429) - Too many requests
  • batch-limit-exceeded (400) - Batch size exceeded
  • filter-parse-error (400) - Invalid filter syntax
  • cursor-invalid (400) - Malformed pagination cursor
  • precondition-failed (412) - ETag mismatch

Errors extend Hono's HTTPException and self-render — resources mounted in any Hono app return proper problem+json without extra setup.

Testing

npm test                                           # all tests
npm test -- tests/integration/useResource.test.ts  # one file
npm test -- --coverage

Testing your own app needs no HTTP server:

const res = await app.request("/api/users", {
  method: "POST",
  body: JSON.stringify({ name: "Alice", email: "[email protected]" }),
  headers: { "content-type": "application/json" },
});
expect(res.status).toBe(201);

Documentation

📚 Full documentation: kahveciderin.github.io/covara

Highlights:

Requirements

  • Node.js 18+ or Cloudflare Workers (nodejs_compat)
  • TypeScript 5+
  • Drizzle ORM
  • Hono 4+