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

@agelum/backend

v0.1.5

Published

Zero configuration, maximum intelligence. Reactive database library for Drizzle ORM with real-time synchronization.

Readme

@agelum/backend

Zero configuration, maximum intelligence. Reactive everywhere with no boilerplate.

A reactive database library that transforms any Drizzle + tRPC setup into a reactive, real-time system with minimal configuration and zero boilerplate code changes.

✨ Features

  • 🚀 Zero Configuration: Single config file with just table relations
  • ⚡ Instant Cache: Shows cached data immediately, revalidates smartly
  • 🔄 Real-time Sync: Built-in Server-Sent Events for cache invalidation
  • 🎯 Smart Invalidation: Only invalidates relevant queries based on relations
  • 📱 Offline Ready: Handles page refresh and session gaps gracefully
  • 🔒 Type Safe: 100% automatic type safety with tRPC integration
  • ☁️ Vercel Compatible: Works perfectly with serverless deployment
  • 🧠 Intelligent: Prioritizes active hooks for better UX

🚀 Quick Start

Installation

pnpm add @agelum/backend drizzle-orm @trpc/server @trpc/client zod

📖 Core Usage Patterns

1. Define Reactive Functions

Key Concept: Reactive functions work both standalone (server-side) AND via tRPC. The name property is crucial for cache keys and tRPC procedures. Handlers receive { input, db } and you can use db.db to access the underlying Drizzle instance inside handlers.

// server/functions/users.ts
import { defineReactiveFunction } from "@agelum/backend/server";
import { z } from "zod";

// 1. Define a reactive function with explicit name
export const getUsers =
  defineReactiveFunction({
    name: "users.getAll", // 🔑 This becomes the cache key and tRPC procedure name

    input: z.object({
      companyId: z.string(), // Generic, not hardcoded organizationId
      limit: z
        .number()
        .optional()
        .default(50),
    }),

    dependencies: ["user"], // What tables this function reads from

    handler: async ({ input, db }) => {
      // Clean signature: ({ input, db })
      return db.db.query.users.findMany(
        {
          where: (users, { eq }) =>
            eq(
              users.companyId,
              input.companyId,
            ),
          limit: input.limit,
        },
      );
    },
  });

export const createUser =
  defineReactiveFunction({
    name: "users.create",

    input: z.object({
      name: z.string(),
      email: z.string().email(),
      companyId: z.string(),
    }),

    dependencies: ["user"],

    handler: async ({ input, db }) => {
      return db.db
        .insert(users)
        .values(input)
        .returning();
    },
  });

export const getUserProfile =
  defineReactiveFunction({
    name: "users.profile.getDetailed", // 🏷️ Nested names work perfectly

    input: z.object({
      userId: z.string(),
    }),

    dependencies: [
      "user",
      "profile",
      "preferences",
    ],

    handler: async ({ input, db }) => {
      const user =
        await db.db.query.users.findFirst(
          {
            where: (users, { eq }) =>
              eq(
                users.id,
                input.userId,
              ),
            with: {
              profile: true,
              preferences: true,
            },
          },
        );
      return user;
    },
  });

Optional Cache Settings (per function):

export const getUsers =
  defineReactiveFunction({
    name: "users.getAll",
    input: z.object({
      companyId: z.string(),
    }),
    dependencies: ["user"],
    cacheEnabled: true,
    cache: {
      ttl: 300,
      key: (input) =>
        `users.getAll:${JSON.stringify(input)}`,
    },
    handler: async ({ input, db }) => {
      return db.db.query.users.findMany(
        {
          where: (users, { eq }) =>
            eq(
              users.companyId,
              input.companyId,
            ),
        },
      );
    },
  });

Cache is opt-in per function via cacheEnabled: true or cache.enabled: true.

Typing Notes

If you want explicit handler typing, use ReactiveFunctionContext<TInput> with a Zod-inferred input type:

import type { ReactiveFunctionContext } from "@agelum/backend/server";
import { z } from "zod";

const getUsersInput = z.object({
  companyId: z.string(),
  limit: z.number().optional(),
});

type GetUsersInput = z.infer<
  typeof getUsersInput
>;

export const getUsers =
  defineReactiveFunction({
    name: "users.getAll",
    input: getUsersInput,
    dependencies: ["user"],
    handler: async ({
      input,
      db,
    }: ReactiveFunctionContext<GetUsersInput>) => {
      return db.db.query.users.findMany(
        {
          where: (users, { eq }) =>
            eq(
              users.companyId,
              input.companyId,
            ),
          limit: input.limit ?? 50,
        },
      );
    },
  });

2. Server-Side Execution (Without tRPC)

Use Case: Background jobs, API routes, server actions, webhooks, etc.

// server/api/users/route.ts - Next.js API route
import {
  getUsers,
  createUser,
} from "../functions/users";
import { db } from "../db";

export async function GET(
  request: Request,
) {
  const { searchParams } = new URL(
    request.url,
  );
  const companyId = searchParams.get(
    "companyId",
  )!;

  // ✅ Execute reactive function directly on server
  const users = await getUsers.execute(
    { companyId, limit: 20 },
    db, // Your reactive database instance
  );

  return Response.json({ users });
}

export async function POST(
  request: Request,
) {
  const body = await request.json();

  // ✅ Execute reactive function directly on server
  const newUser =
    await createUser.execute(body, db);

  return Response.json({
    user: newUser,
  });
}
// server/jobs/daily-stats.ts - Background job
import { getUsers } from "../functions/users";
import { db } from "../db";

export async function generateDailyStats() {
  const companies =
    await db.db.query.companies.findMany();

  for (const company of companies) {
    // ✅ Execute reactive function in background job
    const users =
      await getUsers.execute(
        { companyId: company.id },
        db,
      );

    // Process stats...
    console.log(
      `Company ${company.name} has ${users.length} users`,
    );
  }
}

3. tRPC Integration (Auto-Generated)

Key Feature: The tRPC router automatically uses the function name as the procedure name. Call .build() to get the final tRPC router instance.

// server/trpc/router.ts
import { createReactiveRouter } from "@agelum/backend/server";
import {
  getUsers,
  createUser,
  getUserProfile,
} from "../functions/users";
import { db } from "../db";

export const appRouter =
  createReactiveRouter({ db })
    .addQuery(getUsers) // 🔄 Creates procedure: users.getAll
    .addMutation(createUser) // 🔄 Creates procedure: users.create
    .addQuery(getUserProfile) // 🔄 Creates procedure: users.profile.getDetailed
    .build();

// ✅ Auto-generated procedures from function names:
// - users.getAll (query)
// - users.create (mutation)
// - users.profile.getDetailed (query)

export type AppRouter =
  typeof appRouter;

4. Client-Side Usage (React Hooks)

Zero Configuration: Just use the tRPC procedure names (which match function names).

// client/components/UserList.tsx
import { useReactive } from '@agelum/backend/client'

function UserList({ companyId }: { companyId: string }) {
  // ✅ Uses the function name automatically: 'users.getAll'
  const {
    data: users,
    isStale,
    isLoading,
  } = useReactive('users.getAll', {
    companyId,
    limit: 20,
  })

  if (isLoading) return <div>Loading...</div>

  return (
    <div>
      {isStale && <div className="text-orange-500">Syncing...</div>}

      {users?.map((user) => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  )
}

function UserProfile({ userId }: { userId: string }) {
  // ✅ Nested function names work perfectly
  const { data: profile } = useReactive('users.profile.getDetailed', {
    userId,
  })

  return (
    <div>
      <h1>{profile?.name}</h1>
      <p>{profile?.email}</p>
      {/* Profile details... */}
    </div>
  )
}

5. Mutations with Real-time Updates

// client/components/CreateUserForm.tsx
import { useMutation } from '@trpc/react-query'
import { trpc } from '../trpc'

function CreateUserForm({ companyId }: { companyId: string }) {
  const createUserMutation = trpc.users.create.useMutation({
    onSuccess: () => {
      // ✅ Automatic cache invalidation happens via SSE
      // No manual invalidation needed!
    },
  })

  const handleSubmit = (data: FormData) => {
    createUserMutation.mutate({
      name: data.get('name') as string,
      email: data.get('email') as string,
      companyId,
    })
  }

  return <form onSubmit={handleSubmit}>{/* Form fields... */}</form>
}

🏗️ Setup

1. Database Configuration

// server/db.ts
import { createReactiveDb } from "@agelum/backend/server";
import { drizzle } from "drizzle-orm/postgres-js";

const config = {
  relations: {
    // Relations are table names (not column paths)
    // When user table changes, invalidate these queries
    user: ["profile", "preferences"],

    // When profile table changes, invalidate these queries
    profile: ["user"],

    // When preferences table changes, invalidate these queries
    preferences: ["user"],
  },
};

export const db = createReactiveDb(
  drizzle(pool),
  config,
);

2. Server Cache (Redis or Memory)

const config = {
  relations: {
    user: ["profile", "preferences"],
    profile: ["user"],
    preferences: ["user"],
  },
  cache: {
    server: {
      provider: "redis",
      redis: {
        url: process.env.REDIS_URL,
      },
    },
  },
};

export const db = createReactiveDb(
  drizzle(pool),
  config,
);

You can also pass an existing Redis client via cache.server.redis.client.

3. SSE Endpoint (Next.js)

// app/api/events/route.ts
import { createSSEStream } from "@agelum/backend/server";

export async function GET(
  request: Request,
) {
  const { searchParams } = new URL(
    request.url,
  );
  const organizationId =
    searchParams.get("organizationId")!;

  return createSSEStream(
    organizationId,
  );
}
// app/api/events/ack/route.ts
// Required for reliable delivery: client acks invalidation events
import { acknowledgeEvent } from "@agelum/backend/server";

export async function POST(
  request: Request,
) {
  const { eventId } =
    await request.json();
  acknowledgeEvent(eventId);
  return Response.json({ ok: true });
}

4. Client Setup

// client/providers/ReactiveProvider.tsx
// Recommended: use the built-in TrpcReactiveProvider to wire revalidation generically
'use client'
import { TrpcReactiveProvider } from '@agelum/backend/client'
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'
import type { AppRouter } from '../server/trpc'
import { reactiveRelations } from '@your-db-package/reactive-config'

const trpcClient = createTRPCProxyClient<AppRouter>({
  links: [httpBatchLink({ url: '/api/trpc' })],
})

export function AppProviders({ children }: { children: React.ReactNode }) {
  const organizationId = 'your-organization-id'
  return (
    <TrpcReactiveProvider
      organizationId={organizationId}
      relations={reactiveRelations}
      trpcClient={trpcClient}
    >
      {children}
    </TrpcReactiveProvider>
  )
}

// Alternatively, you can create your own revalidateFn with createTrpcRevalidateFn
// and pass it to ReactiveProvider if you need custom behavior.

If you use ReactiveProvider directly, make sure to pass a revalidateFn for production; the default revalidator returns mock data.

4.1 Client Storage & Revalidation Details

  • The hook composes cache keys as name::JSON(input).
  • LocalStorage is sharded per query to avoid large single entries:
    • Index per organization: reactive_registry_<orgId> stores metadata (last revalidated, last server change, connection status).
    • Per-query entry key: @agelum/backend:entry:<orgId>:<hash> stores { name, input, queryKey, data }.
  • On initial render, cached data (if present) is shown immediately; background revalidation respects a minimum time window to avoid thrashing on quick navigations/refreshes.
  • Errors during revalidation do not overwrite existing cache (no-write-on-error), keeping previously known-good data.
  • Real-time invalidation uses SSE with client acknowledgments and retry; no heartbeats are sent.

Cache Keys

Client cache keys are automatically composed as name::JSON(input):

  • users.getAll::{"companyId":"123","limit":50}
  • users.profile.getDetailed::{"userId":"456"}

Server cache keys for reactive functions default to name:JSON(input) and can be customized with cache.key.

Access cache key programmatically:

const cacheKey = getUsers.getCacheKey({
  companyId: "test",
  limit: 50,
});

Example value: users.getAll:{"companyId":"test","limit":50}

4.2 Multi-tenant Tips (Optional)

  • Resolve tenant databases via a main database lookup (e.g., organization.databaseName), not by using IDs directly as database names.
  • Read paths should not create databases; handle missing DB (3D000) by propagating the error or returning empty based on product policy.
  • Provisioning (create DB/schemas) belongs to explicit setup flows.

🎯 Key Benefits Over Manual Approach

| Feature | Manual tRPC | @agelum/backend | | ----------------------- | ---------------------------------- | ------------------------------- | | Function Definition | Separate function + tRPC procedure | Single defineReactiveFunction | | Cache Keys | Manual generation | Auto from function name | | Invalidation | Manual invalidateQueries | Automatic via relations | | Real-time | Manual WebSocket setup | Built-in SSE | | Server Execution | Separate function needed | Same function works everywhere | | Type Safety | Manual type wiring | 100% automatic |

📈 Advanced Usage

Custom tRPC Procedure Names

// If you need different tRPC names than function names
const router = createReactiveRouter({
  db,
})
  .addQueryWithName(
    getUsers,
    "getAllUsers",
  ) // Custom name
  .addQuery(getUserProfile); // Uses function name: 'users.profile.getDetailed'

Background Revalidation

// client/hooks.ts
function MyComponent() {
  useReactivePriorities([
    "users.getAll", // High priority (visible)
    "users.profile.getDetailed", // Medium priority (likely next)
  ]);

  // Component content...
}

🔧 How It Works

  1. Function Definition: defineReactiveFunction creates functions that work both server-side and via tRPC
  2. Name-Based Mapping: The name property becomes both the cache key and tRPC procedure name
  3. Auto-Generated Router: createReactiveRouter automatically creates tRPC procedures from functions
  4. Smart Caching: Server cache uses the configured provider (memory or Redis) and function dependencies to invalidate cached results. The React hook composes a key as name::JSON(input) internally to uniquely cache and revalidate by input.
  5. Real-time Updates: SSE automatically invalidates affected queries when data changes. No heartbeats are sent; reliability is ensured via client acknowledgments and retry.
  6. Session Recovery: Smart revalidation on page load handles offline scenarios and avoids thrashing with a minimum revalidation window.

🤝 Contributing

We welcome contributions! Please see our Contributing Guide for details.

📄 License

MIT License - see LICENSE for details.


Made with ❤️ by the TeamHub team