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

@digelo/signal

v1.0.0

Published

Production-grade backend framework for serverless environments. Meteor-like DX, stateless & database-agnostic.

Readme

Signal - Production-Grade Backend Framework

A Meteor-like backend framework redesigned for stateless, serverless, database-agnostic environments.

🎯 Philosophy

Signal brings the best of Meteor (named queries/mutations, automatic data management, events) to modern serverless architectures. No long-lived connections, no implicit subscriptions, no magic—just explicit, deterministic backend code.

🏗️ Architecture

Signal Architecture

📦 Key Features

✅ Non-Negotiable Design Constraints

  • Framework-level, backend-only - Not a full-stack framework
  • Named queries only - No implicit queries, all explicit
  • Named mutations only - Mutations are the exclusive write path
  • Stateless events - At-least-once, unordered, deterministic
  • No server-side observers - No change streams, no DB triggers
  • Deterministic handlers - Same input = same output
  • Serverless-ready - Runs on Vercel, Fly, VPS, Edge

🏗️ Production Guarantees

  1. Runtime Immutability - Configuration frozen after startup
  2. Explicit Lifecycle - configure()register()start()
  3. Registry Integrity - Unique names, no overrides
  4. Context Safety - Immutable, request-scoped, serializable
  5. Access Control - Declarative, enforced before execution
  6. Error Model - Deterministic codes, safe serialization
  7. Input Validation - Lightweight, fail-fast
  8. Event Discipline - Stateless, stable naming, at-least-once

🚀 Quick Start

import { Signal, MemoryAdapter, InMemoryTransport } from "signal";

// 1. Create and configure
const signal = new Signal();
signal.configure({
  db: new MemoryAdapter(),
  transport: new InMemoryTransport(),
});

// 2. Register collection with access control
signal
  .collection("posts")
  .access({
    query: { public: "public" },
    mutation: { create: "auth" },
  })
  .query("public", async (params, ctx) => {
    return await ctx.db.find("posts", { published: true });
  })
  .mutation("create", async (params, ctx) => {
    const postId = await ctx.db.insert("posts", {
      title: params.title,
      authorId: ctx.auth.user?.id,
    });
    await ctx.emit("posts.created", { postId });
    return { postId };
  });

// 3. Start
await signal.start();

// 4. Execute queries/mutations
const posts = await signal.query("posts.public", {}, context);
const result = await signal.mutation("posts.create", {
  title: "Hello",
}, context);

📚 Architecture

Packages

/packages
  /core          # Signal.ts, Registry, Collection, Lifecycle, Context
  /db            # Database abstraction + adapters
  /transport     # Event bus + transport adapters
  /http          # HTTP handler, router, validation
  /security      # Auth, access control
  /utils         # Utilities (freeze, hash, logger)

Core Concepts

Signal Instance

The main entry point, manages lifecycle and registry.

const signal = new Signal();
signal.configure({ db, transport, logger });
signal.collection("posts").query(...).mutation(...);
await signal.start();

Collections

Groups queries and mutations under a single namespace with shared access control.

signal.collection("posts").access({
  query: { public: "public", mine: (ctx) => ctx.auth.user != null },
  mutation: { create: "auth" },
});

Queries

Read-only operations with optional access control.

.query("public", async (params, ctx) => {
  // params: { limit?: number, offset?: number, ... }
  // ctx: immutable context with db, auth, emit
  return await ctx.db.find("posts", { published: true });
})

Mutations

Write operations, the exclusive write path.

.mutation("create", async (params, ctx) => {
  const id = await ctx.db.insert("posts", { ...params });
  await ctx.emit("posts.created", { id, ...params });
  return { id };
})

Context

Request-scoped, immutable, serializable.

interface SignalContext {
  db: SignalDB;           // Database adapter
  auth: SignalAuth;       // Authentication/authorization
  emit: (name, payload) => Promise<void>;
  request?: HTTPRequest;  // Original request
  env?: Record<string, any>;
}

Access Control

Declarative rules enforced before execution.

access: {
  query: {
    public: "public",                // Anyone
    mine: (ctx) => ctx.auth.user != null,  // Custom function
  },
  mutation: {
    create: "auth",                  // Authenticated users
    delete: "admin",                 // Admin users (custom rule)
  },
}

Events

Emitted only from mutations, at-least-once semantics.

await ctx.emit("posts.created", {
  id: postId,
  title: params.title,
  authorId: ctx.auth.user?.id,
});

🔒 Security

Authentication

Extract auth from headers or request:

import { AuthProvider } from "signal";

const auth = AuthProvider.fromHeaders(req.headers);
// or
const auth = AuthProvider.authenticated("user123", ["editor"]);

Access Control

Declarative rules at collection level:

.access({
  query: { public: "public", private: (ctx) => ctx.auth.user != null },
  mutation: { create: "auth", delete: (ctx) => ctx.auth.user?.roles?.includes("admin") },
})

Built-in rules: "public", "auth", "admin"

Custom rules: Functions (ctx) => boolean | Promise<boolean>

💾 Database Abstraction

Adapters

MemoryAdapter (development/testing)

new MemoryAdapter() // In-memory, no persistence

SqlAdapterBase (abstract base)

// Implement for PostgreSQL, MySQL, etc.
class PostgresAdapter extends SqlAdapterBase { ... }

Methods

// Queries
await db.find("posts", { published: true });
await db.findOne("posts", { _id: "123" });
await db.findById("posts", "123");
await db.count("posts", { published: true });

// Writes (from mutations only)
const id = await db.insert("posts", { title: "Hello" });
await db.update("posts", id, { title: "Updated" });
await db.delete("posts", id);

📡 Events & Transport

In-Memory Transport

const transport = new InMemoryTransport();
signal.configure({ transport });

// Subscribe
const unsub = await transport.getEventBus().subscribe("posts.*", async (event) => {
  console.log("Event:", event.name, event.payload);
});

Custom Transport

Implement SignalTransport:

export interface SignalTransport {
  emit(event: SignalEvent): Promise<void>;
  subscribe(pattern: string, handler: EventSubscriber): Promise<() => void>;
}

🌐 HTTP Interface

Handler

import { createHandler } from "signal";

const handler = createHandler(signal);
app.post("/signal/query", handler);
app.post("/signal/mutation", handler);

Endpoints

POST /signal/query

{
  "key": "posts.public",
  "params": { "limit": 10 }
}

Response:

{
  "ok": true,
  "data": [...]
}

POST /signal/mutation

{
  "key": "posts.create",
  "params": { "title": "Hello" }
}

Response:

{
  "ok": true,
  "data": { "id": "..." }
}

GET /signal/introspect

{
  "ok": true,
  "data": {
    "collections": ["posts", "comments"],
    "queries": ["posts.public", "posts.mine", "comments.thread"],
    "mutations": ["posts.create", "posts.delete", "comments.reply"]
  }
}

🧪 Production Test Scenario

npm run test

Demonstrates:

  • Framework boot and configuration
  • Collection registration with access control
  • Query execution (public and authenticated)
  • Mutation execution with event emission
  • Access control enforcement
  • Event subscription and delivery
  • Registry immutability

📋 Lifecycle Phases

  1. CONFIGURING - Initial state, configure with configure()
  2. REGISTERING - Register collections, queries, mutations
  3. RUNNING - Operational, registry immutable
  4. FAILED - Unrecoverable error
const signal = new Signal(); // CONFIGURING
signal.configure({...});     // REGISTERING
signal.collection("posts").query(...).mutation(...);
await signal.start();        // RUNNING

After start(), no new collections/queries/mutations can be registered.

🛡️ Error Handling

Error Types

SignalError               // Base error
SignalAuthError          // Authentication required
SignalForbiddenError     // Access denied
SignalValidationError    // Input validation failed
SignalNotFoundError      // Resource not found
SignalConflictError      // Conflict (duplicate, etc.)
SignalInternalError      // Internal error
SignalLifecycleError     // Lifecycle violation
SignalRegistryError      // Registry error

Safe Responses

No stack traces in production. Deterministic error codes:

{
  "ok": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Input validation failed"
  }
}

📦 Type Safety

Full TypeScript support with strict mode:

interface QueryDef<Params, Result> {
  name: string;
  collectionName: string;
  handler: QueryHandler<Params, Result>;
}

🎨 Design Patterns

Immutability

// Configuration is frozen after creation
const config = new Config({ db, transport });
// Cannot mutate config

Explicit Phases

// Clear lifecycle prevents accidental misuse
const signal = new Signal();
signal.configure(...);      // Phase 1
signal.collection(...);     // Phase 2
await signal.start();       // Phase 3 - locked

Named Operations

// No implicit queries/mutations
await signal.query("posts.public", params, ctx);
await signal.mutation("posts.create", params, ctx);
// Not: db.posts.insert()

Deterministic Handlers

// Same input, same output, every time
const query = async (params, ctx) => {
  return await ctx.db.find("posts", params.filter);
};
// No side effects during query

🚀 Deployment

Vercel

export default createHandler(signal);

Fly.io

const handler = createHandler(signal);
Deno.serve({ port: 3000 }, handler);

Express

const handler = createHandler(signal);
app.post("/signal/query", handler);
app.post("/signal/mutation", handler);

📖 API Reference

Signal

  • configure(config) - Configure framework
  • collection(name) - Create collection
  • start() - Start framework
  • query(key, params, ctx) - Execute query
  • mutation(key, params, ctx) - Execute mutation
  • getRegistry() - Get registry for introspection
  • getConfig() - Get configuration
  • getLogger() - Get logger
  • getPhase() - Get lifecycle phase

Collection

  • access(rules) - Set access control
  • query(name, handler) - Register query
  • mutation(name, handler) - Register mutation

AuthProvider

  • fromHeaders(headers) - Extract auth from headers
  • authenticated(userId, roles) - Create authenticated auth
  • anonymous() - Create anonymous auth
  • isAuthenticated(auth) - Check if authenticated
  • hasRole(auth, role) - Check role

Database

  • find(collection, query) - Find documents
  • findOne(collection, query) - Find single document
  • findById(collection, id) - Find by ID
  • insert(collection, doc) - Insert document
  • update(collection, id, update) - Update document
  • delete(collection, id) - Delete document
  • count(collection, query) - Count documents

📝 License

MIT

🤝 Contributing

This framework is production-ready and fully featured. Contributions should maintain the design constraints and production guarantees.