@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:
- Create a Next.js app (TypeScript, Tailwind, App Router,
srcdirectory):npx create-next-app@latest my-site --typescript --tailwind --app --src-dir - From the project root, run:
npx @infuro/cms-core init - Copy
.env.exampleto.env, setDATABASE_URL,NEXTAUTH_SECRET,NEXTAUTH_URL, then runnpm run seed(or migrations) andnpm 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-website2. Install the package
npm install @infuro/cms-core typeorm reflect-metadata bcryptjs next-auth next-themes sonner
npm install -D @types/nodePeer 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:3000Create 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)
- In Google Cloud Console, create a Web OAuth client.
- Add authorized redirect URI:
{NEXTAUTH_URL}/api/auth/callback/google(e.g.http://localhost:3000/api/auth/callback/google). - In admin → Plugins → Authentication Provider, enable Google, paste Client ID and Client Secret, save.
- Ensure
NEXTAUTH_URLmatches the redirect host. - Invite or create the user with the same email as their Google account (existing users only).
- 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.tsThis 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.tsYour 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
- Define your TypeORM entity
- 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 };- Pass the merged map to
getDataSource()entities andcreateCmsApiHandler({ 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:
- Platform admin — Plugins → Email and WhatsApp: configure global templates (
GET/PUT /api/message-templates/event-order). WhatsApp requires a Meta-approved template name matchingexternalTemplateRef(defaultevent_order_placed). - Vendor — Event edit → Order notifications: enable notifications, pick Email / WhatsApp, optionally override templates. API:
GET/PUT /api/events/:id/notifications, test viaPOST /api/events/:id/notifications/test. - Consumer app — Register
whatsappPlugin()andemailPlugin()ingetCms(), plus optionalqueuePluginfor async sends. CalldispatchEventOrderNotifications(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 devWatch mode (developing core + website simultaneously)
Terminal 1:
cd core && npm run devTerminal 2:
cd my-website && npm run devChanges to core are picked up automatically by the website's dev server.
