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

@hidayetcanozcan/nucleus-core

v2.0.0

Published

Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs with Bun and Elysia. Features automatic CRUD generation, schema-based multi-tenancy, type-safe entity definitions, hybrid search, audit logging, and seamless frontend i

Readme

@nucleus-ts/core

Version License TypeScript Bun Node

Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs


Table of Contents

  1. Overview
  2. Installation
  3. Core Concepts
  4. Backend Usage
  5. Frontend Usage
  6. API Reference
  7. Multi-Tenancy

Overview

@nucleus-ts/core is a comprehensive TypeScript framework for building scalable, multi-tenant SaaS applications. It provides end-to-end type safety from database schema to frontend API calls.

What This Framework Does

  1. Database Layer: Define tables using Drizzle ORM column builders
  2. Entity System: Wrap tables with metadata (search config, access control)
  3. Auto-Generated API: Get REST endpoints for all CRUD operations automatically
  4. Frontend Integration: Type-safe React hooks for API calls
  5. Multi-Tenancy: Schema-per-tenant isolation with PostgreSQL

Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│                         FRONTEND (React)                        │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  useGenericApiActions() → actions.GET_PRODUCTS.start()  │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                      BACKEND (Bun + Elysia)                     │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  nucleus() plugin → Auto-generated CRUD routes          │   │
│  └─────────────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  entityRegistry → All entity definitions                │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                    DATABASE (PostgreSQL)                        │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐          │
│  │ schema: main │  │ schema: t1   │  │ schema: t2   │          │
│  │ - products   │  │ - products   │  │ - products   │          │
│  │ - users      │  │ - users      │  │ - users      │          │
│  └──────────────┘  └──────────────┘  └──────────────┘          │
└─────────────────────────────────────────────────────────────────┘

Installation

# Install the core package
bun add @nucleus-ts/core

# Install peer dependencies
bun add drizzle-orm pg elysia
bun add -d drizzle-kit @types/pg typescript

Core Concepts

What is an Entity?

An Entity is a complete definition of a database table plus its metadata:

| Component | Description | |-----------|-------------| | tablename | The actual database table name (e.g., "products") | | columns | Drizzle ORM column definitions | | searchConfig | How this entity can be searched, filtered, sorted | | available_app_ids | Which applications can access this entity | | available_schemas | Which tenant schemas include this entity | | type carriers | TypeScript types for Create, Read, Update, Delete operations |

What are Type Carriers?

Type Carriers are undefined values with explicit type annotations that carry TypeScript types through the entity system. They enable end-to-end type inference:

// Type carrier example
entity_type: undefined as Product | undefined,
entity_create_type: undefined as Create | undefined,

Why Type Carriers?

  • Types cannot be passed as runtime values in JavaScript
  • These undefined values carry the type information at compile time
  • The frontend can infer correct types for API payloads and responses

Base Columns

Every entity automatically includes these columns via ...base:

| Column | Type | Description | |--------|------|-------------| | id | uuid | Primary key, auto-generated with gen_random_uuid() | | is_active | boolean | Soft delete flag, defaults to true | | created_at | timestamp | Creation timestamp, auto-set | | updated_at | timestamp | Last update timestamp, auto-updated | | version | integer | Optimistic locking counter, starts at 1 |


Backend Usage

Complete Entity Definition Workflow

Creating an entity involves 4 steps:

  1. Define columns
  2. Create entity with defineEntity()
  3. Define custom types
  4. Export with type carriers

Below is a fully annotated example:


1. Column Definitions

Columns are defined using Drizzle ORM column builders. Import them from the package:

import {
  base,           // Base columns (id, is_active, created_at, updated_at, version)
  varchar,        // Variable-length string: varchar("name", { length: 255 })
  text,           // Unlimited text: text("description")
  boolean,        // Boolean: boolean("is_active")
  integer,        // Integer: integer("count")
  timestamp,      // Timestamp: timestamp("created_at")
  uuid,           // UUID: uuid("user_id")
  jsonb,          // JSON binary: jsonb("metadata")
} from "@nucleus-ts/core";

Column Definition Syntax:

const columns = {
  // REQUIRED: Spread base columns first
  ...base,
  
  // String columns
  name: varchar("name", { length: 255 }).notNull(),
  slug: varchar("slug", { length: 100 }).unique().notNull(),
  description: text("description"),  // nullable by default
  
  // Boolean columns
  is_featured: boolean("is_featured").default(false),
  is_published: boolean("is_published").default(true).notNull(),
  
  // Numeric columns
  price: varchar("price", { length: 50 }),  // Use varchar for decimal precision
  stock_count: integer("stock_count").default(0),
  
  // Foreign key columns
  category_id: uuid("category_id"),  // References another table
  owner_id: uuid("owner_id").notNull(),  // Required foreign key
  
  // JSON columns
  metadata: jsonb("metadata"),  // Stores arbitrary JSON
  settings: jsonb("settings").default({}),
  
  // Timestamps
  published_at: timestamp("published_at"),
  expires_at: timestamp("expires_at"),
};

Column Modifiers:

| Modifier | Description | Example | |----------|-------------|---------| | .notNull() | Column cannot be null | name: varchar("name", { length: 255 }).notNull() | | .default(value) | Set default value | is_active: boolean("is_active").default(true) | | .unique() | Column must be unique | email: text("email").unique().notNull() | | .primaryKey() | Mark as primary key | (already handled by base.id) |


2. Entity Definition

After defining columns, create the entity using defineEntity():

import { defineEntity, createHybridSearchConfigFromColumns } from "@nucleus-ts/core";

const _entity = defineEntity({
  // REQUIRED: Database table name (lowercase, plural, snake_case)
  tablename: "products",
  
  // REQUIRED: The columns object defined above
  columns,
  
  // OPTIONAL: Columns to create database indexes on (improves query performance)
  indexColumns: ["name", "category_id", "is_featured"],
  
  // REQUIRED: Which application IDs can access this entity
  // Use ["*"] for all apps, or specific IDs like ["app1", "app2"]
  available_app_ids: ["*"],
  
  // REQUIRED: Which database schemas include this entity
  // Use ["*"] for all schemas, or specific schemas like ["main", "tenant_1"]
  available_schemas: ["*"],
  
  // OPTIONAL: Schemas to exclude (overrides available_schemas)
  excluded_schemas: [],
  
  // OPTIONAL: CRUD methods to disable for this entity
  // Options: CREATE, READ, UPDATE, DELETE, TOGGLE, VERIFICATION
  excluded_methods: [],
  
  // OPTIONAL: Set to true if this entity accepts file uploads
  is_formdata: false,
  
  // REQUIRED: Search and filter configuration
  searchConfig: createHybridSearchConfigFromColumns("T_Products", columns, {
    fields: {
      // Columns to exclude from search config
      exclude: ["metadata"],
      
      // Override default field behavior
      overrides: {
        name: { searchable: true },           // Full-text searchable
        description: { searchable: true },
        category_id: { filterable: true },    // Can filter by this
        is_featured: { filterable: true },
        price: { sortable: true },            // Can sort by this
      },
    },
    
    // Default sorting
    defaultOrderBy: "created_at",
    defaultOrderDirection: "desc",  // "asc" or "desc"
  }),
});

Search Config Field Options:

| Option | Type | Description | |--------|------|-------------| | searchable | boolean | Include in full-text search queries | | filterable | boolean | Allow filtering by exact match, in, like, etc. | | sortable | boolean | Allow sorting by this column | | operators | string[] | Allowed filter operators: ["eq", "in", "like", "gte", "lte"] |


3. Type Definitions

Define TypeScript types for CRUD operations. These types are custom per entity:

import type {
  DefaultFilter,
  DefaultOmitted,
  DefaultOrderBy,
  OrderDirection,
  Pagination,
  Serialize,
} from "@nucleus-ts/core";

// Base entity type (inferred from columns)
type Product = typeof _entity.infer.select;

// JSON-serialized type (dates become strings, etc.)
type ProductJSON = Serialize<Product> & {
  // Add relation types here if needed
  category?: CategoryJSON;
  images?: ImageJSON[];
};

// Create payload - omit auto-generated fields
type Create = Omit<Product, DefaultOmitted>;
// DefaultOmitted = "id" | "created_at" | "updated_at" | "version"

// Read/List payload - query parameters
type Read = {
  page?: number;
  limit?: number;
  search?: string;
  orderBy?: DefaultOrderBy | "name" | "price";  // Add custom sort fields
  orderDirection?: OrderDirection;
  filters?: DefaultFilter & {
    // Add custom filter fields
    name?: string;
    category_id?: string;
    is_featured?: boolean;
    price_min?: string;
    price_max?: string;
  };
};

// Update payload - partial create + id
type Update = Partial<Create> & { id?: string };

// Delete payload
type Delete = { id: string };

// List response type
type ListReturn = {
  data: ProductJSON[];
  pagination: Pagination;
};

Pagination Type:

type Pagination = {
  page: number;
  limit: number;
  total: number;
  totalPages: number;
  hasNext: boolean;
  hasPrev: boolean;
};

4. Entity Export with Type Carriers

Export the entity with type carriers attached:

export const ProductEntity = {
  // Spread the base entity (includes table, infer, createTableForSchema, etc.)
  ..._entity,
  
  // TYPE CARRIERS - These carry TypeScript types through the system
  entity_type: undefined as Product | undefined,
  entity_json_type: undefined as ProductJSON | undefined,
  entity_create_type: undefined as Create | undefined,
  entity_read_type: undefined as Read | undefined,
  entity_update_type: undefined as Update | undefined,
  entity_delete_type: undefined as Delete | undefined,
  entity_list_return_type: undefined as ListReturn | undefined,
  entity_store_type: undefined as ProductJSON | undefined,
};

Complete File Example:

// entities/product.ts
import {
  base,
  boolean,
  createHybridSearchConfigFromColumns,
  defineEntity,
  text,
  uuid,
  varchar,
} from "@nucleus-ts/core";
import type {
  DefaultFilter,
  DefaultOmitted,
  DefaultOrderBy,
  OrderDirection,
  Pagination,
  Serialize,
} from "@nucleus-ts/core";

// ============================================================================
// COLUMNS
// ============================================================================

const columns = {
  ...base,
  name: varchar("name", { length: 255 }).notNull(),
  slug: varchar("slug", { length: 100 }).notNull(),
  description: text("description"),
  price: varchar("price", { length: 50 }),
  category_id: uuid("category_id"),
  is_featured: boolean("is_featured").default(false),
  is_available: boolean("is_available").default(true),
};

// ============================================================================
// ENTITY
// ============================================================================

const _entity = defineEntity({
  tablename: "products",
  columns,
  indexColumns: ["name", "slug", "category_id", "is_featured"],
  available_app_ids: ["*"],
  available_schemas: ["*"],
  searchConfig: createHybridSearchConfigFromColumns("T_Products", columns, {
    fields: {
      overrides: {
        name: { searchable: true },
        slug: { searchable: true },
        description: { searchable: true },
        category_id: { filterable: true },
        is_featured: { filterable: true },
        is_available: { filterable: true },
        price: { sortable: true },
      },
    },
    defaultOrderBy: "created_at",
    defaultOrderDirection: "desc",
  }),
});

// ============================================================================
// TYPES
// ============================================================================

type Product = typeof _entity.infer.select;
type ProductJSON = Serialize<Product>;
type Create = Omit<Product, DefaultOmitted>;
type Read = {
  page?: number;
  limit?: number;
  search?: string;
  orderBy?: DefaultOrderBy | "name" | "price";
  orderDirection?: OrderDirection;
  filters?: DefaultFilter & {
    name?: string;
    slug?: string;
    category_id?: string;
    is_featured?: boolean;
    is_available?: boolean;
  };
};
type Update = Partial<Create> & { id?: string };
type Delete = { id: string };
type ListReturn = {
  data: ProductJSON[];
  pagination: Pagination;
};

// ============================================================================
// EXPORT
// ============================================================================

export const ProductEntity = {
  ..._entity,
  entity_type: undefined as Product | undefined,
  entity_json_type: undefined as ProductJSON | undefined,
  entity_create_type: undefined as Create | undefined,
  entity_read_type: undefined as Read | undefined,
  entity_update_type: undefined as Update | undefined,
  entity_delete_type: undefined as Delete | undefined,
  entity_list_return_type: undefined as ListReturn | undefined,
  entity_store_type: undefined as ProductJSON | undefined,
};

5. Entity Registry

Combine all entities into a registry:

// config/index.ts
import { createNucleusRegistry } from "@nucleus-ts/core";
import { ProductEntity } from "./entities/product";
import { CategoryEntity } from "./entities/category";

// Core entities from package (authentication, authorization, etc.)
export const nucleusRegistry = createNucleusRegistry({
  audit: { enabled: true },
  authentication: { enabled: true },
  authorization: { enabled: true },
  verification: { enabled: true },
  notification: { enabled: true },
  verificationNotification: { enabled: true },
  tenant: { enabled: true },
});

// Combined registry: core + custom entities
export const entityRegistry = {
  ...nucleusRegistry.entities,
  T_Products: ProductEntity,
  T_Categories: CategoryEntity,
} as const;

export type EntityName = keyof typeof entityRegistry;

6. Server Setup

// src/server.ts
import { Elysia } from "elysia";
import { nucleus } from "@nucleus-ts/core";
import { pushSchema } from "drizzle-kit/api";
import { entityRegistry } from "../config";

const app = new Elysia()
  .use(
    nucleus({
      registry: entityRegistry,
      appId: process.env.NUCLEUS_APP_ID ?? "*",
      pushSchema,  // Required for auto-migrations
      schemaName: "main",
      verbose: true,
    })
  )
  .listen(3000);

Generated Endpoints:

For each entity (e.g., T_Products), these endpoints are created:

| Method | Endpoint | Description | |--------|----------|-------------| | GET | /v1/products | List with pagination, search, filters | | GET | /v1/products/:id | Get single record by ID | | POST | /v1/products | Create new record | | PUT | /v1/products/:id | Update existing record | | DELETE | /v1/products/:id | Soft delete (set is_active=false) | | PATCH | /v1/products/:id/toggle | Toggle is_active status |


Frontend Usage

useGenericApiActions Hook

This hook provides type-safe API actions for all entities:

import { useGenericApiActions } from "./hooks/useGenericApiActions";

function MyComponent() {
  const actions = useGenericApiActions();
  
  // Available actions per entity:
  // actions.GET_PRODUCTS      - List products
  // actions.CREATE_PRODUCT    - Create a product
  // actions.UPDATE_PRODUCT    - Update a product
  // actions.DELETE_PRODUCT    - Delete a product
  // actions.TOGGLE_PRODUCT    - Toggle is_active
}

Best Practices

Pattern 1: useEffectEvent for Data Fetching

ALWAYS use useEffectEvent for API calls in effects to avoid stale closures and infinite loops:

"use client";

import { useEffect, useEffectEvent } from "react";
import { useGenericApiActions } from "./hooks/useGenericApiActions";

export default function ProductList() {
  const actions = useGenericApiActions();

  // ✅ CORRECT: Wrap API call in useEffectEvent
  const fetchProducts = useEffectEvent(() => {
    actions.GET_PRODUCTS.start({
      payload: {
        page: 1,
        limit: 20,
        orderBy: "created_at",
        orderDirection: "desc",
      },
      onAfterHandle: (data) => {
        // data is fully typed as ListReturn
        console.log("Fetched products:", data.data.length);
        console.log("Total pages:", data.pagination.totalPages);
      },
      onErrorHandle: (error) => {
        console.error("Failed to fetch:", error.message);
      },
    });
  });

  // ✅ CORRECT: Call in useEffect with empty deps
  useEffect(() => {
    fetchProducts();
  }, []);

  return (
    <div>
      {/* Check loading state */}
      {actions.GET_PRODUCTS.state.isPending && <p>Loading...</p>}
      
      {/* Access data */}
      {actions.GET_PRODUCTS.state.data?.data.map((product) => (
        <div key={product.id}>
          <h3>{product.name}</h3>
          <p>{product.description}</p>
          <span>${product.price}</span>
        </div>
      ))}
    </div>
  );
}

Pattern 2: Event Handlers

For button clicks and form submissions, call actions directly:

function CreateProductForm() {
  const actions = useGenericApiActions();
  const [name, setName] = useState("");

  const handleSubmit = () => {
    actions.CREATE_PRODUCT.start({
      payload: {
        name,
        description: "New product",
        price: "99.99",
        is_available: true,
      },
      onAfterHandle: (product) => {
        // product is typed as ProductJSON
        toast.success(`Created: ${product.name}`);
        setName("");
      },
      onErrorHandle: (error) => {
        toast.error(error.message);
      },
    });
  };

  return (
    <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <button disabled={actions.CREATE_PRODUCT.state.isPending}>
        {actions.CREATE_PRODUCT.state.isPending ? "Creating..." : "Create"}
      </button>
    </form>
  );
}

What NOT To Do

// ❌ WRONG: Don't use try/catch
try {
  await actions.GET_PRODUCTS.start(...);
} catch (e) {}

// ❌ WRONG: Don't await the result
const data = await actions.GET_PRODUCTS.start(...);

// ❌ WRONG: Don't use type assertions
onAfterHandle: (data) => {
  const products = data as Product[];  // WRONG - data is already typed
};

// ❌ WRONG: Don't add start to dependency array (causes infinite loop!)
useEffect(() => {
  actions.GET_PRODUCTS.start(...);
}, [actions.GET_PRODUCTS.start]);  // INFINITE LOOP!

// ❌ WRONG: Don't call start directly in useEffect without useEffectEvent
useEffect(() => {
  actions.GET_PRODUCTS.start(...);  // May cause issues
}, []);

CRUD Operations

Create

actions.CREATE_PRODUCT.start({
  payload: {
    name: "New Product",
    description: "Product description",
    price: "99.99",
    category_id: "uuid-here",
    is_featured: false,
    is_available: true,
  },
  onAfterHandle: (product) => {
    console.log("Created:", product.id);
  },
});

Read (List)

actions.GET_PRODUCTS.start({
  payload: {
    page: 1,
    limit: 20,
    search: "laptop",  // Full-text search
    orderBy: "name",
    orderDirection: "asc",
    filters: {
      is_available: true,
      is_featured: true,
      category_id: "uuid-here",
    },
  },
  onAfterHandle: (data) => {
    // data.data: ProductJSON[]
    // data.pagination: { page, limit, total, totalPages, hasNext, hasPrev }
  },
});

Update

actions.UPDATE_PRODUCT.start({
  payload: {
    id: "product-uuid",
    name: "Updated Name",
    price: "149.99",
  },
  onAfterHandle: (product) => {
    console.log("Updated:", product.name);
  },
});

Delete

actions.DELETE_PRODUCT.start({
  payload: { id: "product-uuid" },
  onAfterHandle: () => {
    console.log("Deleted successfully");
  },
});

Toggle Active Status

actions.TOGGLE_PRODUCT.start({
  payload: { id: "product-uuid" },
  onAfterHandle: (product) => {
    console.log("New status:", product.is_active);
  },
});

State Properties

| Property | Type | Description | |----------|------|-------------| | state.isPending | boolean | Request is in progress | | state.data | T \| null | Last successful response data | | state.error | Error \| null | Last error |


API Reference

defineEntity Options

| Option | Type | Required | Description | |--------|------|----------|-------------| | tablename | string | Yes | Database table name | | columns | object | Yes | Drizzle column definitions | | indexColumns | string[] | No | Columns to index | | available_app_ids | string[] | Yes | App IDs that can access | | available_schemas | string[] | Yes | Schema names to include | | excluded_schemas | string[] | No | Schemas to exclude | | excluded_methods | GenericMethods[] | No | CRUD methods to disable | | is_formdata | boolean | No | Accept multipart/form-data | | searchConfig | HybridSearchConfig | Yes | Search configuration |

nucleus Plugin Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | registry | EntityRegistry | - | Entity registry object | | appId | string | "*" | Application ID | | schemaName | string | "main" | Database schema name | | pushSchema | function | - | drizzle-kit pushSchema function | | verbose | boolean | true | Log SQL statements | | allowDataLoss | boolean | false | Allow destructive migrations |


Multi-Tenancy

Schema-per-tenant isolation:

import { getTenantDB, bootstrapSchema } from "@nucleus-ts/core";

// Get database connection for specific tenant
const db = await getTenantDB("tenant_slug");

// Create new tenant schema
await bootstrapSchema({
  schema_name: "tenant_new",
  entities: entityRegistry,
  app_id: process.env.NUCLEUS_APP_ID!,
  pushSchema,
});

License

This software is proprietary and confidential. See LICENSE for terms.


Built with ❤️ by Hidayet Can Özcan