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

@parcae/backend

v0.7.1

Published

Parcae Backend — TypeScript backend framework with auto-CRUD, realtime, and jobs

Downloads

3,911

Readme

@parcae/backend

TypeScript backend framework. One function call bootstraps Postgres persistence, auto-CRUD routes, realtime subscriptions, background jobs, and authentication from your Model classes.

Install

npm install @parcae/backend @parcae/model

Quick Start

import { createApp } from "@parcae/backend";

const app = createApp({ models: "./models" });
await app.start();
// -> .parcae/ generated, tables created, CRUD routes live, WebSocket ready

.env files are auto-loaded at startup:

# .env
DATABASE_URL=postgresql://localhost:5432/myapp

createApp()

The main entry point. Accepts model classes directly or a directory path for auto-discovery.

import { createApp } from "@parcae/backend";
import { User, Post } from "./models";

const app = createApp({
  models: [User, Post],       // or "./models" for auto-discovery
  controllers: "./controllers", // optional — auto-import route files
  hooks: "./hooks",             // optional — auto-import hook files
  jobs: "./jobs",               // optional — auto-import job files
  auth: {                       // optional — omit to skip auth
    providers: ["email"],
  },
  version: "v1",                // API prefix (default: "v1")
  root: process.cwd(),          // project root (default: cwd)
});

await app.start({ port: 3000, dev: true });

Controllers, hooks, and jobs self-register on import — just put files in the directory and they're auto-loaded (like Next.js pages).

Startup Sequence

  1. Parse and validate env config (Zod), auto-load .env
  2. Discover models (array or directory scan)
  3. Generate .parcae/ type metadata (RTTIST)
  4. Connect Postgres (Knex, optional read replica)
  5. Connect Redis (PubSub + Queue, optional — falls back to in-process)
  6. Create BackendAdapter, call Model.use()
  7. Ensure tables exist (additive DDL migration)
  8. Create HTTP server (Polka) + WebSocket server (Socket.IO)
  9. Set up QuerySubscriptionManager for realtime
  10. Mount auth middleware + routes (if configured)
  11. Register auto-CRUD routes for scoped models
  12. Auto-discover and import controllers, hooks, jobs
  13. Start BullMQ workers + HTTP listener

ParcaeApp

interface ParcaeApp {
  start(options?: { dev?: boolean; port?: number }): Promise<void>;
  stop(): Promise<void>;
  schemas: Map<string, SchemaDefinition>;
  models: ModelConstructor[];
}

Auto-CRUD Routes

Any model with a scope gets full REST endpoints automatically:

GET    /v1/posts          list (paginated, sortable, filterable)
GET    /v1/posts/:id      get one
POST   /v1/posts          create
PUT    /v1/posts/:id      update
DELETE /v1/posts/:id      delete
PATCH  /v1/posts/:id      atomic JSON Patch (RFC 6902)

Scopes

Scopes define per-operation access control. They receive the request context and return a query modifier, a data object, or null to deny.

class Post extends Model {
  static type = "post" as const;

  static scope = {
    read: (ctx) => (qb) =>
      qb.where("published", true).orWhere("user", ctx.user?.id),
    create: (ctx) => (ctx.user ? { user: ctx.user.id } : null),
    update: (ctx) => (qb) => qb.where("user", ctx.user.id),
    delete: (ctx) => (qb) => qb.where("user", ctx.user.id),
  };

  user!: User;
  title: string = "";
  published: boolean = false;
}

Query Parameters

List endpoints support:

| Parameter | Example | Description | | --- | --- | --- | | limit | ?limit=25 | Page size (max 100) | | page | ?page=2 | Page number | | sort | ?sort=createdAt | Sort column | | direction | ?direction=desc | Sort direction | | where[field] | ?where[published]=true | Field filter | | select | ?select=title,views | Column selection |

Custom Routes

Express-compatible function API. Middleware works the same way.

import { route } from "@parcae/backend";

route.get("/v1/health", (req, res) => {
  res.end(JSON.stringify({ status: "ok" }));
});

route.post("/v1/upload", requireAuth, rateLimit(100), async (req, res) => {
  // req.session available if auth is configured
});

Methods: route.get, route.post, route.put, route.patch, route.delete, route.options, route.head, route.all

Route Options

route.get("/health", handler, { priority: 0 }); // lower = registered first

Response Helpers

Convenience functions for common response patterns:

import { json, ok, error, unauthorized, notFound, badRequest } from "@parcae/backend";

route.get("/v1/posts", async (req, res) => {
  const posts = await Post.where({ published: true }).find();
  ok(res, { posts });
});

route.get("/v1/posts/:id", async (req, res) => {
  const post = await Post.findById(req.params.id);
  if (!post) return notFound(res, "Post");
  ok(res, post.toJSON());
});

route.post("/v1/admin/action", async (req, res) => {
  if (!req.session?.user) return unauthorized(res);
  if (!req.body.name) return badRequest(res, "name is required");
  // ...
});

| Helper | Status | Body | | --- | --- | --- | | json(res, status, body) | any | raw JSON | | ok(res, result) | 200 | { result, success: true } | | error(res, status, message) | any | { result: null, success: false, error } | | unauthorized(res) | 401 | { error: "Unauthorized" } | | notFound(res, what?) | 404 | { error: "{what} not found" } | | badRequest(res, message) | 400 | { error: message } |

Hooks

Model lifecycle hooks. Run before or after persistence operations.

import { hook } from "@parcae/backend";

hook.after(Post, "save", async ({ model, lock, enqueue, user }) => {
  const unlock = await lock(`index:${model.id}`);
  try {
    await model.refresh();
    await enqueue("post:index", { postId: model.id });
  } finally {
    await unlock();
  }
});

hook.before(Post, "create", ({ model }) => {
  model.title = model.title.trim();
});

Actions

save, create, update, patch, remove

save fires on both create and update. create and update fire on their respective operations only.

Hook Context

interface HookContext {
  model: any;
  action: HookAction;
  data?: Record<string, any>;
  user?: { id: string; [key: string]: any } | null;
  lock(key, ttl?): Promise<() => Promise<void>>;
  enqueue(name, data, opts?): Promise<boolean>;
}

Hook Options

hook.after(Post, "patch", handler, {
  async: true,    // don't block the response (default: false)
  priority: 200,  // execution order — lower runs first (default: 100)
});

Jobs

Background job processing via BullMQ. Requires Redis.

import { job } from "@parcae/backend";

job("post:index", async ({ data, bullJob, attempt }) => {
  const post = await Post.findById(data.postId);
  if (!post) return { skipped: true };
  // ... index in search engine ...
  return { success: true };
});

Jobs retry 3 times with exponential backoff (5s base).

Standalone enqueue

You can enqueue jobs from anywhere — not just hook contexts:

import { enqueue } from "@parcae/backend";

await enqueue("post:index", { postId: post.id });
await enqueue("post:index", { postId: post.id }, { jobId: `post:index:${post.id}` }); // deduped

BackendAdapter

The server-side ModelAdapter implementation. Handles Knex/Postgres persistence, hooks, pub/sub, and the overflow column pattern.

import { BackendAdapter } from "@parcae/backend";

const adapter = new BackendAdapter({
  read: readDb,   // Knex instance (read replica or same as write)
  write: writeDb, // Knex instance
  pubsub,         // PubSub instance (optional)
  logger,         // Winston logger (optional)
});

Model.use(adapter);

Key Features

  • UpsertINSERT ... ON CONFLICT MERGE for save operations
  • Atomic JSON Patch — Generates jsonb_set_lax, jsonb_insert, #- SQL for JSONB columns; direct SET for scalar columns
  • Overflow column — Declared schema properties get typed columns; everything else goes into a data JSONB column automatically
  • Additive migrationensureAllTables() creates tables/columns/indexes if missing. Never drops.
  • Read/write splitting — Separate Knex instances for read and write queries
  • Hook execution — Runs registered before/after hooks during persistence operations

PubSub

Redis-backed cross-process events. Falls back to in-process EventEmitter when Redis is unavailable.

import { PubSub } from "@parcae/backend";

const pubsub = new PubSub({ url: "redis://localhost:6379" });
await pubsub.building;

pubsub.emit("post:updated", { id: "abc" });
pubsub.on("post:updated", (data) => { ... });

Includes distributed locking via Redlock:

const unlock = await pubsub.lock("resource:key", 10000);
try { /* critical section */ }
finally { await unlock(); }

Standalone lock

import { lock } from "@parcae/backend";

const unlock = await lock("resource:abc", 120000);
try { /* exclusive access */ }
finally { await unlock(); }

Queue

BullMQ queue management. Falls back gracefully when Redis is unavailable.

import { QueueService, addJobIfNotExists } from "@parcae/backend";

const queue = new QueueService({ url: "redis://localhost:6379" });
await queue.building;

await addJobIfNotExists(queue.get(), "post:index", { postId: "abc" });

QuerySubscriptionManager

Manages realtime query subscriptions for connected clients. When a model changes, affected queries are re-evaluated and surgical diff ops (add, remove, update) are pushed to subscribers.

Auth

Auth is pluggable via the AuthAdapter interface. The framework doesn't ship with any auth provider — install the one you need:

| Package | Provider | Users live... | | --- | --- | --- | | @parcae/auth-betterauth | Better Auth | In your Postgres (same table as your User model) | | @parcae/auth-clerk | Clerk | In Clerk's cloud (proxied to your User model) |

import { betterAuth } from "@parcae/auth-betterauth";

const app = createApp({
  models: [User, Post],
  auth: betterAuth({ providers: ["email", "google"] }),
});
import { clerk } from "@parcae/auth-clerk";

const app = createApp({
  models: [User, Post],
  auth: clerk({
    secretKey: process.env.CLERK_SECRET_KEY!,
    publishableKey: process.env.CLERK_PUBLISHABLE_KEY!,
  }),
});

The User Model is always a real, managed Parcae Model. Auth adapters resolve identity and sync user data into it.

  • req.session.user available in route handlers and scopes
  • Socket.IO auth via authenticate event
  • Implement AuthAdapter to bring your own provider

Schema Generation

At startup, createApp() generates type metadata into .parcae/ (gitignored, like .next/):

  1. Runs RTTIST typegen to extract TypeScript type metadata
  2. SchemaResolver maps types to column definitions
  3. Falls back to default-value inference if RTTIST is unavailable
  4. Caches resolved schemas to .parcae/schema.json

Configuration

Environment variables validated at startup via Zod. .env files are auto-loaded.

| Variable | Required | Default | Description | | --- | --- | --- | --- | | DATABASE_URL | Yes | -- | PostgreSQL connection string | | DATABASE_READ_URL | No | -- | Read replica connection string | | REDIS_URL | No | -- | Redis for PubSub + Queue | | PORT | No | 3000 | HTTP server port | | AUTH_SECRET | No | -- | Session signing secret (required if auth enabled) | | TRUSTED_ORIGINS | No | -- | Comma-separated CORS origins | | NODE_ENV | No | development | development / production / test | | SERVER | No | true | Run HTTP + WebSocket server | | DAEMON | No | false | Run background workers |

Exports

// App
import { createApp } from "@parcae/backend";
import type { ParcaeApp, AppConfig } from "@parcae/backend";

// Adapter
import { BackendAdapter, registerModelRoutes } from "@parcae/backend";
import type { BackendServices } from "@parcae/backend";

// Routing
import { route, Controller, hook, job } from "@parcae/backend";
import type {
  RouteHandler, Middleware, RouteOptions, RouteEntry,
  HookContext, HookOptions, HookEntry,
  JobHandler, JobContext, JobEntry,
} from "@parcae/backend";

// Response helpers
import { json, ok, error, unauthorized, notFound, badRequest } from "@parcae/backend";

// Services
import { PubSub, QueueService, addJobIfNotExists, QuerySubscriptionManager } from "@parcae/backend";
import { enqueue, lock, getQueue, getPubSub } from "@parcae/backend";
import type { PubSubConfig, QueueConfig, EnqueueOptions } from "@parcae/backend";

// Auth (interface only — implementations in separate packages)
import type { AuthAdapter, AuthSession, AuthSetupContext } from "@parcae/backend";

// Schema
import { SchemaResolver, resolveFallbackSchema, generateSchemas, loadCachedSchemas } from "@parcae/backend";

// Config
import { parseConfig, configSchema } from "@parcae/backend";
import type { Config } from "@parcae/backend";

// Registry utilities
import { getRoutes, clearRoutes, getHooks, getHooksFor, clearHooks, getJobs, getJob, clearJobs } from "@parcae/backend";

// Convenience re-export
import { Model } from "@parcae/backend";

License

MIT