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

@infuro/cms-core

v1.0.61

Published

Infuro CMS core - headless CMS library for Next.js

Readme

@infuro/cms-core

A headless CMS framework built on Next.js and TypeORM. It provides a ready-to-use admin panel, CRUD API layer, authentication, plugin system, and UI components — so you only write what's unique to your website.

Overview

You don't set up or clone core in each website. Install the published package and run the init command to scaffold a new site.

Typical workflow for a new site:

  1. Create a Next.js app (TypeScript, Tailwind, App Router, src directory):
    npx create-next-app@latest my-site --typescript --tailwind --app --src-dir
  2. From the project root, run:
    npx @infuro/cms-core init
  3. Copy .env.example to .env, set DATABASE_URL, NEXTAUTH_SECRET, NEXTAUTH_URL, then run npm run seed (or migrations) and npm run dev.

Init creates all required files (data-source, auth-helpers, cms, API routes, admin layout and page, middleware, providers, seed, migration runner, default theme, basic home and contact pages), and can patch next.config, tailwind.config, layout, and package.json and install dependencies. Use --force to overwrite existing files, --dry-run to see what would be created, --no-deps to skip npm install, --no-patch-config to skip config changes.

Manual setup (if you prefer not to use init): Install from npm (npm install @infuro/cms-core typeorm reflect-metadata bcryptjs next-auth next-themes sonner and npm install -D @types/node), then follow the step-by-step setup below. For local development of core itself, use "@infuro/cms-core": "file:../core" in the site's package.json.

Project structure (your site after setup)

your-website/
├── src/
│   ├── app/
│   │   ├── admin/            # layout.tsx + [[...slug]]/page.tsx
│   │   └── api/
│   │       ├── auth/         # NextAuth route
│   │       └── [[...path]]/  # Catch-all for CMS API
│   ├── lib/
│   │   ├── data-source.ts
│   │   ├── auth-helpers.ts
│   │   ├── cms.ts
│   │   ├── theme-registry.ts
│   │   └── seed.ts
│   ├── themes/               # Optional: default theme from init
│   ├── migrations/
│   ├── middleware.ts
│   └── ...                   # Your pages, components, etc.

Getting Started

1. Create a Next.js app

npx create-next-app@latest my-website --typescript --tailwind --app --src-dir
cd my-website

2. Install the package

npm install @infuro/cms-core typeorm reflect-metadata bcryptjs next-auth next-themes sonner
npm install -D @types/node

Peer dependencies (Next.js app usually has these): next ≥14, react ≥18, react-dom ≥18, next-auth ^4.24. For local development, use "@infuro/cms-core": "file:../core" in package.json and run npm install.

3. Configure next.config.js

const nextConfig = {
  reactStrictMode: false,
  serverExternalPackages: ['@infuro/cms-core', 'typeorm'],
};
module.exports = nextConfig;

serverExternalPackages is required so TypeORM decorators and reflect-metadata work correctly on the server.

4. Set up the database

Create a .env file:

DATABASE_URL=postgres://user:password@localhost:5432/mydb
NEXTAUTH_SECRET=your-random-secret
NEXTAUTH_URL=http://localhost:3000

Create src/lib/data-source.ts:

import 'reflect-metadata';
import { DataSource } from 'typeorm';
import { CMS_ENTITY_MAP } from '@infuro/cms-core';

let dataSource: DataSource | null = null;

export function getDataSource(): DataSource {
  if (!dataSource) {
    dataSource = new DataSource({
      type: 'postgres',
      url: process.env.DATABASE_URL,
      entities: Object.values(CMS_ENTITY_MAP),
      synchronize: false,
    });
  }
  return dataSource;
}

export async function getDataSourceInitialized(): Promise<DataSource> {
  const ds = getDataSource();
  if (!ds.isInitialized) await ds.initialize();
  return ds;
}

Note: synchronize: false — use TypeORM migrations (see Migrations).

5. Set up auth helpers

Create src/lib/auth-helpers.ts:

import { getServerSession } from 'next-auth';
import { NextResponse } from 'next/server';
import { createAuthHelpers } from '@infuro/cms-core/auth';

const helpers = createAuthHelpers(
  async () => {
    const s = await getServerSession();
    return s ? { user: s.user } : null;
  },
  NextResponse
);

export const requireAuth = helpers.requireAuth;
export const requirePermission = helpers.requirePermission;
export const requireEntityPermission = helpers.requireEntityPermission;
export const requireAdminAccess = helpers.requireAdminAccess;
export const getAuthenticatedUser = helpers.getAuthenticatedUser;

6. Set up CMS with plugins

Create src/lib/cms.ts:

import {
  createCmsApp,
  localStoragePlugin,
  type CmsApp,
} from '@infuro/cms-core';
import { getDataSourceInitialized } from './data-source';

let cmsPromise: Promise<CmsApp> | null = null;

export async function getCms(): Promise<CmsApp> {
  if (cmsPromise) return cmsPromise;
  const dataSource = await getDataSourceInitialized();
  cmsPromise = createCmsApp({
    dataSource,
    config: process.env as unknown as Record<string, string>,
    plugins: [
      localStoragePlugin({ dir: 'public/uploads' }),
      // Add more: emailPlugin({...}), analyticsPlugin({...}), etc.
    ],
  });
  return cmsPromise;
}

7. Mount the API

Create src/app/api/[[...path]]/route.ts:

import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { createCmsApiHandler } from '@infuro/cms-core/api';
import { CMS_ENTITY_MAP } from '@infuro/cms-core';
import { getDataSourceInitialized } from '@/lib/data-source';
import { createCmsAuthBundle } from '@infuro/cms-core/auth';
import { authOptions } from '@/lib/auth-options';
import { getCms } from '@/lib/cms';
import bcrypt from 'bcryptjs';

const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';

const auth = createCmsAuthBundle(
  () => getServerSession(authOptions).then((s) => s ?? null),
  NextResponse
);

async function requireAdminApiAuth(req: Request) {
  const a = await auth.requireAuth(req);
  if (a) return a;
  return auth.requireAdminAccess(req);
}

let handlerPromise: Promise<ReturnType<typeof createCmsApiHandler>> | null = null;

async function getHandler() {
  if (!handlerPromise) {
    const dataSource = await getDataSourceInitialized();
    handlerPromise = Promise.resolve(
      createCmsApiHandler({
        dataSource,
        entityMap: CMS_ENTITY_MAP,
        requireAuth: requireAdminApiAuth,
        requireEntityPermission: auth.requireEntityPermission,
        getSessionUser: auth.getSessionUser,
        getAuthenticatedUser: auth.getAuthenticatedUser,
        json: NextResponse.json.bind(NextResponse),
        getCms,
        userAuth: {
          dataSource,
          entityMap: CMS_ENTITY_MAP,
          json: NextResponse.json.bind(NextResponse),
          baseUrl,
          hashPassword: (p) => Promise.resolve(bcrypt.hashSync(p, 12)),
          comparePassword: (p, h) => Promise.resolve(bcrypt.compareSync(p, h)),
          resetExpiryHours: 1,
          getSession: () =>
            getServerSession().then((s) => (s ? { user: s.user } : null)),
        },
        dashboard: {
          dataSource,
          entityMap: CMS_ENTITY_MAP,
          json: NextResponse.json.bind(NextResponse),
          requireAuth: requireAdminApiAuth,
          requirePermission: requireAdminApiAuth,
        },
        upload: {
          json: NextResponse.json.bind(NextResponse),
          requireAuth: requireAdminApiAuth,
          storage: () => getCms().then((cms) => cms.getPlugin('storage')),
          localUploadDir: 'public/uploads',
        },
        blogBySlug: {
          dataSource,
          entityMap: CMS_ENTITY_MAP,
          json: NextResponse.json.bind(NextResponse),
          requireAuth: async () => null,
        },
        formBySlug: {
          dataSource,
          entityMap: CMS_ENTITY_MAP,
          json: NextResponse.json.bind(NextResponse),
          requireAuth: async () => null,
        },
        usersApi: {
          dataSource,
          entityMap: CMS_ENTITY_MAP,
          json: NextResponse.json.bind(NextResponse),
          requireAuth: requireAdminApiAuth,
          baseUrl,
        },
      })
    );
  }
  return handlerPromise;
}

async function handle(method: string, req: Request, context: { params: Promise<{ path?: string[] }> }) {
  try {
    const handler = await getHandler();
    const { path = [] } = await context.params;
    return handler.handle(method, path, req);
  } catch {
    return NextResponse.json({ error: 'Server Error' }, { status: 500 });
  }
}

export async function GET(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('GET', req, ctx); }
export async function POST(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('POST', req, ctx); }
export async function PUT(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('PUT', req, ctx); }
export async function PATCH(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('PATCH', req, ctx); }
export async function DELETE(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('DELETE', req, ctx); }

8. Mount NextAuth

Create src/app/api/auth/[...nextauth]/route.ts:

import NextAuth from 'next-auth';
import { buildNextAuthOptions } from '@infuro/cms-core/auth';
import { getDataSourceInitialized } from '@/lib/data-source';
import { CMS_ENTITY_MAP, enrichUserWithVendorContext } from '@infuro/cms-core';
import bcrypt from 'bcryptjs';

async function getOptions() {
  const dataSource = await getDataSourceInitialized();
  const userRepo = dataSource.getRepository(CMS_ENTITY_MAP.users);
  // Loads Google OAuth from Plugins → Authentication Provider (and GOOGLE_* env fallback)
  return buildNextAuthOptions({
    dataSource,
    entityMap: CMS_ENTITY_MAP,
    getUserByEmail: async (email: string) => {
      const user = await userRepo.findOne({
        where: { email },
        relations: ['group', 'group.permissions'],
        select: ['id', 'email', 'name', 'password', 'blocked', 'inviteStatus', 'deleted', 'groupId', 'adminAccess'],
      });
      return enrichUserWithVendorContext(dataSource, user as any);
    },
    comparePassword: (plain, hash) => Promise.resolve(bcrypt.compareSync(plain, hash)),
    signInPage: '/admin/signin',
  });
}

type NextAuthContext = { params: Promise<{ nextauth?: string[] }> };

export async function GET(req: Request, context: NextAuthContext) {
  return NextAuth(await getOptions())(req, context);
}
export async function POST(req: Request, context: NextAuthContext) {
  return NextAuth(await getOptions())(req, context);
}

Google sign-in (Authentication Provider)

  1. In Google Cloud Console, create a Web OAuth client.
  2. Add authorized redirect URI: {NEXTAUTH_URL}/api/auth/callback/google (e.g. http://localhost:3000/api/auth/callback/google).
  3. In admin → Plugins → Authentication Provider, enable Google, paste Client ID and Client Secret, save.
  4. Ensure NEXTAUTH_URL matches the redirect host.
  5. Invite or create the user with the same email as their Google account (existing users only).
  6. On /admin/signin, use Continue with Google.

Optional env fallback (without Plugins UI): GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_AUTH_ENABLED=true.

Host apps already using a cached getNextAuthOptions handler should switch to buildNextAuthOptions (as above) so plugin settings apply without a process restart.

9. Mount the admin panel

Create src/app/admin/layout.tsx:

'use client';
import '@infuro/cms-core/admin.css';
import AdminLayout from '@infuro/cms-core/admin';

export default function AdminLayoutWrapper({ children }: { children: React.ReactNode }) {
  return (
    <AdminLayout
      customNavItems={[]}
      customNavSections={[]}
      customCrudConfigs={{}}
    >
      {children}
    </AdminLayout>
  );
}

Create src/app/admin/[[...slug]]/page.tsx:

import { AdminPageResolver } from '@infuro/cms-core/admin';

export default async function AdminPage({ params }: { params: Promise<{ slug?: string[] }> }) {
  const { slug } = await params;
  return <AdminPageResolver slug={slug} />;
}

The admin at /admin is rendered by the package (layout, sidebar, header, built-in pages). Pass customNavSections and customCrudConfigs to add your own sidebar links and CRUD list pages (see Adding custom pages and admin nav).

10. Configure Tailwind

Core's admin components use Tailwind classes. Include the package in content so those classes aren't purged:

content: [
  "./src/**/*.{js,ts,jsx,tsx,mdx}",
  // When using from npm:
  "./node_modules/@infuro/cms-core/dist/**/*.{js,cjs}",
  // When using file:../core (local):
  // "../core/src/**/*.{js,ts,jsx,tsx}",
],

You also need the shadcn/ui color mappings in theme.extend.colors — see the Tailwind Config section below.

11. Add middleware

Create src/middleware.ts:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { createCmsMiddleware } from '@infuro/cms-core/auth';

const cmsMiddleware = createCmsMiddleware({
  // Optional: allow unauthenticated access to specific API paths/methods (e.g. public form submit)
  publicApiMethods: {
    '/api/contacts': ['POST'],
    '/api/form-submissions': ['POST'],
    '/api/blogs': ['GET'],
    '/api/forms': ['GET'],
    '/api/auth': ['GET', 'POST'],
    '/api/users/forgot-password': ['POST'],
    '/api/users/set-password': ['POST'],
    '/api/users/invite': ['POST'],
    '/api/settings/auth_providers': ['GET'],
  },
});

export async function middleware(request: NextRequest) {
  const result = await cmsMiddleware({
    nextUrl: request.nextUrl,
    url: request.url,
    method: request.method,
    cookies: request.cookies,
    req: request,
  });

  if (result.type === 'next') return NextResponse.next();
  if (result.type === 'redirect') return NextResponse.redirect(result.url);
  if (result.type === 'json') return NextResponse.json(result.body, { status: result.status });
  return NextResponse.next();
}

export const config = {
  // Next.js matchers don't support lookaheads — don't match /api here or large
  // uploads break FormData. API auth still runs in route handlers.
  // Also set experimental.proxyClientMaxBodySize: '100mb' in next.config.
  matcher: ['/admin/:path*'],
};

Also raise Next.js body limits in next.config (needed if middleware/proxy ever matches upload routes):

experimental: {
  proxyClientMaxBodySize: '100mb',
},

12. Add providers

Wrap your root layout with session and theme providers:

// src/app/providers.tsx
"use client";
import { ThemeProvider } from "next-themes";
import { SessionProvider } from "next-auth/react";
import { Toaster } from "sonner";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <SessionProvider>
      <ThemeProvider attribute="class" defaultTheme="light" forcedTheme="light" enableSystem={false} enableColorScheme>
        {children}
        <Toaster position="top-right" />
      </ThemeProvider>
    </SessionProvider>
  );
}

Use it in src/app/layout.tsx:

import { Providers } from './providers';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Adding custom pages and admin nav

Custom sidebar links: Pass customNavItems or customNavSections to AdminLayout in your src/app/admin/layout.tsx. Each item has href, label, and optional icon. Use href like /admin/locations so the link opens under the admin.

Custom CRUD (list + optional add/edit): Define a CustomCrudConfig (title, apiEndpoint, columns, addEditPageUrl, optional filters) and pass it as customCrudConfigs={{ myResource: config }} to AdminLayout. You must have a corresponding API (e.g. under your catch-all or a custom route) and entity. The first path segment (e.g. locations) is the key; add a nav item with href: '/admin/locations'.

Custom full pages: For a page that isn’t a CRUD list, add a Next.js route under admin, e.g. src/app/admin/reports/page.tsx, and render your component there. The admin layout wraps all /admin/* routes, so your page appears inside the same shell. Add a link in customNavItems or customNavSections with href: '/admin/reports'.

Types are exported from @infuro/cms-core/admin: CustomNavItem, CustomNavSection, CustomCrudConfig, CustomCrudColumn, etc.

Database Setup

First-time setup (quick)

For initial development, temporarily set synchronize: true in your data-source.ts, then run the seed script:

npx tsx src/lib/seed.ts

This creates all tables and inserts default data (admin user, categories, tags, forms). Switch synchronize back to false afterwards.

Migrations (production)

TypeORM CLI requires tsx and dotenv/config to load TypeScript data sources with .env support. The TYPEORM_CLI=1 env var enables the migrations path (kept off at runtime to avoid Next.js loading .ts migration files):

# Generate a migration from entity changes
TYPEORM_CLI=1 npx tsx -r dotenv/config node_modules/typeorm/cli.js migration:generate -d src/lib/data-source.ts src/migrations/MyMigration

# Run pending migrations
TYPEORM_CLI=1 npx tsx -r dotenv/config node_modules/typeorm/cli.js migration:run -d src/lib/data-source.ts

# Revert last migration
TYPEORM_CLI=1 npx tsx -r dotenv/config node_modules/typeorm/cli.js migration:revert -d src/lib/data-source.ts

Your data-source.ts should conditionally include migrations and export a default:

export function getDataSource(): DataSource {
  if (!dataSource) {
    dataSource = new DataSource({
      type: 'postgres',
      url: process.env.DATABASE_URL,
      entities: Object.values(CMS_ENTITY_MAP),
      synchronize: false,
      ...(process.env.TYPEORM_CLI && { migrations: ['src/migrations/*.ts'] }),
    });
  }
  return dataSource;
}

export default getDataSource();

Domain model (SSOT)

The authoritative description of entities, catalog ownership (admin vs vendor), events, storefront, API scope, and feature flags lives in docs/SSOT.md.

For a quick CMS-only orientation, the package still includes users, groups, permissions, blogs, tags, comments, contacts, forms, SEO, and configs — plus the full commerce / multi-vendor model documented in the SSOT (catalog taxonomy is product_categories).

API Endpoints

All mounted under /api via the single catch-all route:

| Endpoint | Methods | Auth | Description | |----------|---------|------|-------------| | /api/{resource} | GET, POST | Yes | CRUD list/create for any entity in CMS_ENTITY_MAP | | /api/{resource}/{id} | GET, PUT, DELETE | Yes | CRUD get/update/delete by ID | | /api/blogs/slug/{slug} | GET | No | Public blog by slug | | /api/forms/slug/{slug} | GET | No | Public form by slug | | /api/users | GET, POST | Yes | User management | | /api/users/{id} | GET, PUT, DELETE | Yes | User by ID | | /api/users/forgot-password | POST | No | Password reset request | | /api/users/set-password | POST | No | Set new password | | /api/users/invite | POST | No | Accept invite | | /api/dashboard/stats | GET | Yes | Dashboard statistics | | /api/analytics | GET | Yes | Analytics data | | /api/upload | POST | Yes | File upload | | /api/auth/* | GET, POST | No | NextAuth routes |

Plugin System

Plugins are initialized via createCmsApp and accessed with cms.getPlugin('name').

Built-in Plugins

| Plugin | Factory | Purpose | |--------|---------|---------| | Storage (S3) | s3StoragePlugin({...}) | S3 file uploads | | Storage (Local) | localStoragePlugin({dir}) | Local file uploads | | Email | emailPlugin({type, from, ...}) | Email via SMTP/SES/Gmail | | Analytics | analyticsPlugin({...}) | Google Analytics integration | | ERP | erpPlugin({...}) | ERP/CRM integration | | SMS | smsPlugin({...}) | SMS notifications | | Payment | paymentPlugin({...}) | Payment processing |

Custom Plugins

Implement the CmsPlugin interface:

import type { CmsPlugin, PluginContext } from '@infuro/cms-core';

export const myPlugin: CmsPlugin<MyService> = {
  name: 'my-plugin',
  version: '1.0.0',
  async init(context: PluginContext) {
    return new MyService(context.config);
  },
};

Register it in cms.ts:

plugins: [
  localStoragePlugin({ dir: 'public/uploads' }),
  myPlugin,
],

Access it anywhere:

const cms = await getCms();
const service = cms.getPlugin<MyService>('my-plugin');

Package Exports

| Import Path | Contents | |-------------|----------| | @infuro/cms-core | Entities, plugins, registry, utilities | | @infuro/cms-core/api | createCmsApiHandler, CRUD handlers, auth handlers | | @infuro/cms-core/auth | createAuthHelpers, createCmsMiddleware, getNextAuthOptions, buildNextAuthOptions, Google auth helpers | | @infuro/cms-core/admin | Admin layout, pages, components (React, 'use client') | | @infuro/cms-core/hooks | useIsMobile, useAnalytics, usePlugin |

Extending

Adding custom entities

  1. Define your TypeORM entity
  2. Add it to a merged entity map:
import { CMS_ENTITY_MAP } from '@infuro/cms-core';
import { Product } from './entities/product.entity';

const ENTITY_MAP = { ...CMS_ENTITY_MAP, products: Product };
  1. Pass the merged map to getDataSource() entities and createCmsApiHandler({ entityMap })

Adding custom API routes

Add files alongside the catch-all (e.g. src/app/api/my-custom/route.ts). Next.js resolves specific routes before the catch-all.

Customizing middleware

Pass config to createCmsMiddleware():

createCmsMiddleware({
  publicAdminPaths: ['/admin/signin', '/admin/custom-public-page'],
  publicApiMethods: {
    '/api/products': ['GET'],
  },
});

Tailwind Config

Admin is light-only. With Tailwind v4, add this so dark: utilities follow the .dark class (not the OS preference); otherwise headings/labels can render white-on-white when the browser is in dark mode:

@custom-variant dark (&:where(.dark, .dark *));

The admin panel and UI components use shadcn/ui and require CSS variable-based color mappings. Your tailwind.config.js needs these in theme.extend.colors:

module.exports = {
  content: [
    "./src/**/*.{js,ts,jsx,tsx,mdx}",
    "./node_modules/@infuro/cms-core/dist/**/*.{js,cjs}",
  ],
  darkMode: "class",
  theme: {
    extend: {
      colors: {
        background: "hsl(var(--background))",
        foreground: "hsl(var(--foreground))",
        card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" },
        popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))" },
        primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
        secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" },
        muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },
        accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" },
        destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },
        border: "hsl(var(--border))",
        input: "hsl(var(--input))",
        ring: "hsl(var(--ring))",
        sidebar: {
          DEFAULT: "hsl(var(--sidebar-background))",
          foreground: "hsl(var(--sidebar-foreground))",
          primary: "hsl(var(--sidebar-primary))",
          "primary-foreground": "hsl(var(--sidebar-primary-foreground))",
          accent: "hsl(var(--sidebar-accent))",
          "accent-foreground": "hsl(var(--sidebar-accent-foreground))",
          border: "hsl(var(--sidebar-border))",
          ring: "hsl(var(--sidebar-ring))",
        },
      },
      borderRadius: {
        lg: "var(--radius)",
        md: "calc(var(--radius) - 2px)",
        sm: "calc(var(--radius) - 4px)",
      },
    },
  },
  plugins: [require("tailwindcss-animate")],
};

The CSS variables are injected by the admin layout at runtime. Your website's own CSS can also define them in :root if your public pages use shadcn/ui components.

Environment Variables

| Variable | Required | Description | |----------|----------|-------------| | DATABASE_URL | Yes | PostgreSQL connection string | | NEXTAUTH_SECRET | Yes | NextAuth JWT secret | | NEXTAUTH_URL | Yes | App base URL | | GOOGLE_CLIENT_ID | If Google login (env) | Web OAuth client ID (or set via Plugins → Authentication Provider) | | GOOGLE_CLIENT_SECRET | If Google login (env) | Web OAuth client secret | | GOOGLE_AUTH_ENABLED | No | When using env credentials, set true to enable Google (defaults on if both ID/secret set) | | STORAGE_TYPE | No | s3 or local (default: local) | | AWS_BUCKET_NAME | If S3 | S3 bucket name | | AWS_REGION | If S3 | Default AWS region | | AWS_SES_REGION | If SMTP_TYPE=AWS | SES region for email (required when AWS_REGION has no SMTP, e.g. use ap-south-1 if AWS_REGION=ap-south-2) | | AWS_ACCESS_KEY_ID | If S3 / SES API | IAM access key (not SES SMTP username) | | AWS_SECRET_ACCESS_KEY | If S3 / SES API | IAM secret key (not SES SMTP password) | | SMTP_TYPE | No | SMTP, AWS, or GMAIL | | SMTP_FROM | If email | Sender email (must be verified in SES) | | SMTP_TO | If email | Default recipient | | SMTP_USER | If SMTP / SMTP_TYPE=AWS | SMTP username; for AWS use SES SMTP credentials from the SES console | | SMTP_PASSWORD | If SMTP / SMTP_TYPE=AWS | SMTP password; for AWS use the SES SMTP password (not the IAM secret) | | SMTP_HOST | No | Override SMTP host (optional for AWS; default is SES SMTP endpoint) | | SMTP_PORT | No | Default 587 for AWS SES SMTP | | GOOGLE_ANALYTICS_PRIVATE_KEY | If analytics | GA service account key | | GOOGLE_ANALYTICS_CLIENT_EMAIL | If analytics | GA service account email | | GOOGLE_ANALYTICS_VIEW_ID | If analytics | GA property/view ID | | WHATSAPP_ACCESS_TOKEN | If WhatsApp notifications | Meta Cloud API permanent/system user token | | WHATSAPP_PHONE_NUMBER_ID | If WhatsApp notifications | WhatsApp Business phone number ID from Meta developer console | | WHATSAPP_API_VERSION | No | Graph API version (default v21.0) |

Event order notifications

When an order is created (storefront checkout or admin Place order) and it includes products linked to an event via event_products, the core can notify the customer on enabled channels:

  1. Platform admin — Plugins → Email and WhatsApp: configure global templates (GET/PUT /api/message-templates/event-order). WhatsApp requires a Meta-approved template name matching externalTemplateRef (default event_order_placed).
  2. Vendor — Event edit → Order notifications: enable notifications, pick Email / WhatsApp, optionally override templates. API: GET/PUT /api/events/:id/notifications, test via POST /api/events/:id/notifications/test.
  3. Consumer app — Register whatsappPlugin() and emailPlugin() in getCms(), plus optional queuePlugin for async sends. Call dispatchEventOrderNotifications(orderId, deps) is wired automatically from storefront checkout and admin order create.

Template variables: {{eventName}}, {{orderNumber}}, {{customerName}}, {{customerEmail}}, {{customerPhone}}, {{orderTotal}}, {{currency}}, {{venue}}, {{startDate}}, {{productNames}}.

Development

Quick start (existing website using core)

# 1. Build core (once, or use watch mode)
cd core
npm run build

# 2. Install website dependencies (links core via file:../core)
cd ../my-website
npm install

# 3. Set up .env (DATABASE_URL, NEXTAUTH_SECRET, NEXTAUTH_URL)

# 4. Create tables & seed (set synchronize: true in data-source.ts first)
npx tsx src/lib/seed.ts
# Then set synchronize back to false

# 5. Start dev server
npm run dev

Watch mode (developing core + website simultaneously)

Terminal 1:

cd core && npm run dev

Terminal 2:

cd my-website && npm run dev

Changes to core are picked up automatically by the website's dev server.