@infuro/cms-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,693 @@
1
+ # @infuro/cms-core
2
+
3
+ A headless CMS framework built on Next.js and TypeORM. 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.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ core/ # This package
9
+ ├── src/
10
+ │ ├── entities/ # TypeORM entities (User, Blog, Form, etc.)
11
+ │ ├── api/ # API handlers (CRUD, auth, CMS-specific)
12
+ │ ├── auth/ # NextAuth config, middleware, helpers
13
+ │ ├── admin/ # Admin panel (layout, pages, page resolver)
14
+ │ ├── plugins/ # Plugin system (email, storage, analytics, ERP, etc.)
15
+ │ ├── components/ # Shared UI (shadcn/ui + Admin components)
16
+ │ ├── hooks/ # React hooks (analytics, mobile, plugin)
17
+ │ └── lib/ # Utilities (cn, etc.)
18
+
19
+ your-website/ # Your Next.js app
20
+ ├── src/
21
+ │ ├── app/
22
+ │ │ ├── admin/ # 2 files: layout.tsx + [[...slug]]/page.tsx
23
+ │ │ └── api/
24
+ │ │ ├── auth/ # NextAuth route
25
+ │ │ └── [[...path]]/ # Single catch-all mounting core's API
26
+ │ ├── lib/
27
+ │ │ ├── data-source.ts # TypeORM DataSource init
28
+ │ │ ├── auth-helpers.ts # Wire core auth to NextResponse
29
+ │ │ └── cms.ts # CmsApp init with plugins
30
+ │ ├── middleware.ts # Wire core middleware to Next.js
31
+ │ └── ... # Your custom pages, components, etc.
32
+ ```
33
+
34
+ ## Getting Started
35
+
36
+ ### 1. Create a Next.js app
37
+
38
+ ```bash
39
+ npx create-next-app@latest my-website --typescript --tailwind --app --src-dir
40
+ cd my-website
41
+ ```
42
+
43
+ ### 2. Install core
44
+
45
+ Link the local package (or install from npm once published):
46
+
47
+ ```bash
48
+ # In your website's package.json, add:
49
+ # "@infuro/cms-core": "file:../core"
50
+ npm install
51
+ ```
52
+
53
+ Required peer dependencies (should already be in a Next.js app):
54
+
55
+ ```
56
+ next >= 14, react >= 18, react-dom >= 18, next-auth ^4.24
57
+ ```
58
+
59
+ Additional dependencies you'll need:
60
+
61
+ ```bash
62
+ npm install typeorm reflect-metadata bcryptjs next-auth next-themes sonner
63
+ npm install -D @types/node typescript
64
+ ```
65
+
66
+ ### 3. Configure `next.config.js`
67
+
68
+ ```js
69
+ const nextConfig = {
70
+ reactStrictMode: false,
71
+ serverExternalPackages: ['@infuro/cms-core', 'typeorm'],
72
+ };
73
+ module.exports = nextConfig;
74
+ ```
75
+
76
+ `serverExternalPackages` is required so TypeORM decorators and reflect-metadata work correctly on the server.
77
+
78
+ ### 4. Set up the database
79
+
80
+ Create a `.env` file:
81
+
82
+ ```env
83
+ DATABASE_URL=postgres://user:password@localhost:5432/mydb
84
+ NEXTAUTH_SECRET=your-random-secret
85
+ NEXTAUTH_URL=http://localhost:3000
86
+ ```
87
+
88
+ Create `src/lib/data-source.ts`:
89
+
90
+ ```ts
91
+ import 'reflect-metadata';
92
+ import { DataSource } from 'typeorm';
93
+ import { CMS_ENTITY_MAP } from '@infuro/cms-core';
94
+
95
+ let dataSource: DataSource | null = null;
96
+
97
+ export function getDataSource(): DataSource {
98
+ if (!dataSource) {
99
+ dataSource = new DataSource({
100
+ type: 'postgres',
101
+ url: process.env.DATABASE_URL,
102
+ entities: Object.values(CMS_ENTITY_MAP),
103
+ synchronize: false,
104
+ });
105
+ }
106
+ return dataSource;
107
+ }
108
+
109
+ export async function getDataSourceInitialized(): Promise<DataSource> {
110
+ const ds = getDataSource();
111
+ if (!ds.isInitialized) await ds.initialize();
112
+ return ds;
113
+ }
114
+ ```
115
+
116
+ > **Note:** `synchronize: false` — use TypeORM migrations (see [Migrations](#migrations)).
117
+
118
+ ### 5. Set up auth helpers
119
+
120
+ Create `src/lib/auth-helpers.ts`:
121
+
122
+ ```ts
123
+ import { getServerSession } from 'next-auth';
124
+ import { NextResponse } from 'next/server';
125
+ import { createAuthHelpers } from '@infuro/cms-core/auth';
126
+
127
+ const helpers = createAuthHelpers(
128
+ async () => {
129
+ const s = await getServerSession();
130
+ return s ? { user: s.user } : null;
131
+ },
132
+ NextResponse
133
+ );
134
+
135
+ export const requireAuth = helpers.requireAuth;
136
+ export const requirePermission = helpers.requirePermission;
137
+ export const getAuthenticatedUser = helpers.getAuthenticatedUser;
138
+ ```
139
+
140
+ ### 6. Set up CMS with plugins
141
+
142
+ Create `src/lib/cms.ts`:
143
+
144
+ ```ts
145
+ import {
146
+ createCmsApp,
147
+ localStoragePlugin,
148
+ type CmsApp,
149
+ } from '@infuro/cms-core';
150
+ import { getDataSourceInitialized } from './data-source';
151
+
152
+ let cmsPromise: Promise<CmsApp> | null = null;
153
+
154
+ export async function getCms(): Promise<CmsApp> {
155
+ if (cmsPromise) return cmsPromise;
156
+ const dataSource = await getDataSourceInitialized();
157
+ cmsPromise = createCmsApp({
158
+ dataSource,
159
+ config: process.env as unknown as Record<string, string>,
160
+ plugins: [
161
+ localStoragePlugin({ dir: 'public/uploads' }),
162
+ // Add more: emailPlugin({...}), analyticsPlugin({...}), etc.
163
+ ],
164
+ });
165
+ return cmsPromise;
166
+ }
167
+ ```
168
+
169
+ ### 7. Mount the API
170
+
171
+ Create `src/app/api/[[...path]]/route.ts`:
172
+
173
+ ```ts
174
+ import { NextResponse } from 'next/server';
175
+ import { getServerSession } from 'next-auth';
176
+ import { createCmsApiHandler } from '@infuro/cms-core/api';
177
+ import { CMS_ENTITY_MAP } from '@infuro/cms-core';
178
+ import { getDataSourceInitialized } from '@/lib/data-source';
179
+ import { requireAuth } from '@/lib/auth-helpers';
180
+ import { getCms } from '@/lib/cms';
181
+ import bcrypt from 'bcryptjs';
182
+
183
+ const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
184
+
185
+ let handlerPromise: Promise<ReturnType<typeof createCmsApiHandler>> | null = null;
186
+
187
+ async function getHandler() {
188
+ if (!handlerPromise) {
189
+ const dataSource = await getDataSourceInitialized();
190
+ handlerPromise = Promise.resolve(
191
+ createCmsApiHandler({
192
+ dataSource,
193
+ entityMap: CMS_ENTITY_MAP,
194
+ requireAuth,
195
+ json: NextResponse.json.bind(NextResponse),
196
+ getCms,
197
+ userAuth: {
198
+ dataSource,
199
+ entityMap: CMS_ENTITY_MAP,
200
+ json: NextResponse.json.bind(NextResponse),
201
+ baseUrl,
202
+ hashPassword: (p) => Promise.resolve(bcrypt.hashSync(p, 12)),
203
+ comparePassword: (p, h) => Promise.resolve(bcrypt.compareSync(p, h)),
204
+ resetExpiryHours: 1,
205
+ getSession: () =>
206
+ getServerSession().then((s) => (s ? { user: s.user } : null)),
207
+ },
208
+ dashboard: {
209
+ dataSource,
210
+ entityMap: CMS_ENTITY_MAP,
211
+ json: NextResponse.json.bind(NextResponse),
212
+ requireAuth,
213
+ requirePermission: requireAuth,
214
+ },
215
+ upload: {
216
+ json: NextResponse.json.bind(NextResponse),
217
+ requireAuth,
218
+ storage: () => getCms().then((cms) => cms.getPlugin('storage')),
219
+ localUploadDir: 'public/uploads',
220
+ },
221
+ blogBySlug: {
222
+ dataSource,
223
+ entityMap: CMS_ENTITY_MAP,
224
+ json: NextResponse.json.bind(NextResponse),
225
+ requireAuth: async () => null,
226
+ },
227
+ formBySlug: {
228
+ dataSource,
229
+ entityMap: CMS_ENTITY_MAP,
230
+ json: NextResponse.json.bind(NextResponse),
231
+ requireAuth: async () => null,
232
+ },
233
+ usersApi: {
234
+ dataSource,
235
+ entityMap: CMS_ENTITY_MAP,
236
+ json: NextResponse.json.bind(NextResponse),
237
+ requireAuth,
238
+ baseUrl,
239
+ },
240
+ })
241
+ );
242
+ }
243
+ return handlerPromise;
244
+ }
245
+
246
+ async function handle(method: string, req: Request, context: { params: Promise<{ path?: string[] }> }) {
247
+ try {
248
+ const handler = await getHandler();
249
+ const { path = [] } = await context.params;
250
+ return handler.handle(method, path, req);
251
+ } catch {
252
+ return NextResponse.json({ error: 'Server Error' }, { status: 500 });
253
+ }
254
+ }
255
+
256
+ export async function GET(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('GET', req, ctx); }
257
+ export async function POST(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('POST', req, ctx); }
258
+ export async function PUT(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('PUT', req, ctx); }
259
+ export async function PATCH(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('PATCH', req, ctx); }
260
+ export async function DELETE(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('DELETE', req, ctx); }
261
+ ```
262
+
263
+ ### 8. Mount NextAuth
264
+
265
+ Create `src/app/api/auth/[...nextauth]/route.ts`:
266
+
267
+ ```ts
268
+ import NextAuth from 'next-auth';
269
+ import { getNextAuthOptions } from '@infuro/cms-core/auth';
270
+ import { getDataSourceInitialized } from '@/lib/data-source';
271
+ import { CMS_ENTITY_MAP } from '@infuro/cms-core';
272
+ import bcrypt from 'bcryptjs';
273
+
274
+ async function getOptions() {
275
+ const dataSource = await getDataSourceInitialized();
276
+ const userRepo = dataSource.getRepository(CMS_ENTITY_MAP.users);
277
+ return getNextAuthOptions({
278
+ getUserByEmail: async (email: string) => {
279
+ return userRepo.findOne({
280
+ where: { email },
281
+ relations: ['group', 'group.permissions'],
282
+ select: ['id', 'email', 'name', 'password', 'blocked', 'deleted', 'groupId'],
283
+ }) as any;
284
+ },
285
+ comparePassword: (plain, hash) => Promise.resolve(bcrypt.compareSync(plain, hash)),
286
+ signInPage: '/admin/signin',
287
+ });
288
+ }
289
+
290
+ let handler: ReturnType<typeof NextAuth> | null = null;
291
+
292
+ async function getHandler() {
293
+ if (!handler) handler = NextAuth(await getOptions());
294
+ return handler;
295
+ }
296
+
297
+ export async function GET(req: Request) {
298
+ return ((await getHandler()) as any).GET(req);
299
+ }
300
+ export async function POST(req: Request) {
301
+ return ((await getHandler()) as any).POST(req);
302
+ }
303
+ ```
304
+
305
+ ### 9. Mount the admin panel
306
+
307
+ Create `src/app/admin/layout.tsx`:
308
+
309
+ ```ts
310
+ export { default } from '@infuro/cms-core/admin';
311
+ ```
312
+
313
+ Create `src/app/admin/[[...slug]]/page.tsx`:
314
+
315
+ ```tsx
316
+ import { AdminPageResolver } from '@infuro/cms-core/admin';
317
+
318
+ export default async function AdminPage({ params }: { params: Promise<{ slug?: string[] }> }) {
319
+ const { slug } = await params;
320
+ return <AdminPageResolver slug={slug} />;
321
+ }
322
+ ```
323
+
324
+ That's it — the admin panel at `/admin` is fully rendered by core (layout, sidebar, header, pages). Core injects its own CSS theme variables via `<style>` tag, so no additional CSS setup is needed for the admin panel.
325
+
326
+ ### 10. Configure Tailwind
327
+
328
+ Core's admin components use Tailwind classes. Your `tailwind.config.js` must scan core's source so those classes aren't purged in production:
329
+
330
+ ```js
331
+ module.exports = {
332
+ content: [
333
+ "./src/**/*.{js,ts,jsx,tsx,mdx}",
334
+ "../core/src/**/*.{js,ts,jsx,tsx}", // include core
335
+ ],
336
+ // ... rest of your config
337
+ };
338
+ ```
339
+
340
+ You also need the shadcn/ui color mappings in `theme.extend.colors` — see the [Tailwind Config](#tailwind-config) section below.
341
+
342
+ ### 11. Add middleware
343
+
344
+ Create `src/middleware.ts`:
345
+
346
+ ```ts
347
+ import { NextResponse } from 'next/server';
348
+ import type { NextRequest } from 'next/server';
349
+ import { createCmsMiddleware } from '@infuro/cms-core/auth';
350
+
351
+ const cmsMiddleware = createCmsMiddleware();
352
+
353
+ export function middleware(request: NextRequest) {
354
+ const result = cmsMiddleware({
355
+ nextUrl: request.nextUrl,
356
+ url: request.url,
357
+ method: request.method,
358
+ cookies: request.cookies,
359
+ });
360
+
361
+ if (result.type === 'next') return NextResponse.next();
362
+ if (result.type === 'redirect') return NextResponse.redirect(result.url);
363
+ if (result.type === 'json') return NextResponse.json(result.body, { status: result.status });
364
+ return NextResponse.next();
365
+ }
366
+
367
+ export const config = {
368
+ matcher: ['/admin/:path*', '/api/:path*'],
369
+ };
370
+ ```
371
+
372
+ ### 12. Add providers
373
+
374
+ Wrap your root layout with session and theme providers:
375
+
376
+ ```tsx
377
+ // src/app/providers.tsx
378
+ "use client";
379
+ import { ThemeProvider } from "next-themes";
380
+ import { SessionProvider } from "next-auth/react";
381
+ import { Toaster } from "sonner";
382
+
383
+ export function Providers({ children }: { children: React.ReactNode }) {
384
+ return (
385
+ <SessionProvider>
386
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
387
+ {children}
388
+ <Toaster position="top-right" />
389
+ </ThemeProvider>
390
+ </SessionProvider>
391
+ );
392
+ }
393
+ ```
394
+
395
+ Use it in `src/app/layout.tsx`:
396
+
397
+ ```tsx
398
+ import { Providers } from './providers';
399
+
400
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
401
+ return (
402
+ <html lang="en">
403
+ <body>
404
+ <Providers>{children}</Providers>
405
+ </body>
406
+ </html>
407
+ );
408
+ }
409
+ ```
410
+
411
+ ## Database Setup
412
+
413
+ ### First-time setup (quick)
414
+
415
+ For initial development, temporarily set `synchronize: true` in your `data-source.ts`, then run the seed script:
416
+
417
+ ```bash
418
+ npx tsx src/lib/seed.ts
419
+ ```
420
+
421
+ This creates all tables and inserts default data (admin user, categories, tags, forms). Switch `synchronize` back to `false` afterwards.
422
+
423
+ ### Migrations (production)
424
+
425
+ 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):
426
+
427
+ ```bash
428
+ # Generate a migration from entity changes
429
+ TYPEORM_CLI=1 npx tsx -r dotenv/config node_modules/typeorm/cli.js migration:generate -d src/lib/data-source.ts src/migrations/MyMigration
430
+
431
+ # Run pending migrations
432
+ TYPEORM_CLI=1 npx tsx -r dotenv/config node_modules/typeorm/cli.js migration:run -d src/lib/data-source.ts
433
+
434
+ # Revert last migration
435
+ TYPEORM_CLI=1 npx tsx -r dotenv/config node_modules/typeorm/cli.js migration:revert -d src/lib/data-source.ts
436
+ ```
437
+
438
+ Your `data-source.ts` should conditionally include migrations and export a default:
439
+
440
+ ```ts
441
+ export function getDataSource(): DataSource {
442
+ if (!dataSource) {
443
+ dataSource = new DataSource({
444
+ type: 'postgres',
445
+ url: process.env.DATABASE_URL,
446
+ entities: Object.values(CMS_ENTITY_MAP),
447
+ synchronize: false,
448
+ ...(process.env.TYPEORM_CLI && { migrations: ['src/migrations/*.ts'] }),
449
+ });
450
+ }
451
+ return dataSource;
452
+ }
453
+
454
+ export default getDataSource();
455
+ ```
456
+
457
+ ## Core Entities
458
+
459
+ | Entity | Table | Purpose |
460
+ |--------|-------|---------|
461
+ | `User` | `users` | Admin users with groups/permissions |
462
+ | `UserGroup` | `user_groups` | Role-based groups |
463
+ | `Permission` | `permissions` | Granular permissions |
464
+ | `Blog` | `blogs` | Blog posts with slug, SEO, tags, categories |
465
+ | `Category` | `categories` | Blog categories |
466
+ | `Tag` | `tags` | Blog tags (many-to-many with blogs) |
467
+ | `Comment` | `comments` | Blog comments |
468
+ | `Contact` | `contacts` | Contact form submissions |
469
+ | `Form` | `forms` | Dynamic forms |
470
+ | `FormField` | `form_fields` | Form field definitions |
471
+ | `FormSubmission` | `form_submissions` | Form submission data |
472
+ | `Seo` | `seos` | SEO metadata |
473
+ | `Config` | `configs` | Key-value configuration |
474
+ | `PasswordResetToken` | `password_reset_tokens` | Password reset flow |
475
+
476
+ ## API Endpoints
477
+
478
+ All mounted under `/api` via the single catch-all route:
479
+
480
+ | Endpoint | Methods | Auth | Description |
481
+ |----------|---------|------|-------------|
482
+ | `/api/{resource}` | GET, POST | Yes | CRUD list/create for any entity in `CMS_ENTITY_MAP` |
483
+ | `/api/{resource}/{id}` | GET, PUT, DELETE | Yes | CRUD get/update/delete by ID |
484
+ | `/api/blogs/slug/{slug}` | GET | No | Public blog by slug |
485
+ | `/api/forms/slug/{slug}` | GET | No | Public form by slug |
486
+ | `/api/users` | GET, POST | Yes | User management |
487
+ | `/api/users/{id}` | GET, PUT, DELETE | Yes | User by ID |
488
+ | `/api/users/forgot-password` | POST | No | Password reset request |
489
+ | `/api/users/set-password` | POST | No | Set new password |
490
+ | `/api/users/invite` | POST | No | Accept invite |
491
+ | `/api/dashboard/stats` | GET | Yes | Dashboard statistics |
492
+ | `/api/analytics` | GET | Yes | Analytics data |
493
+ | `/api/upload` | POST | Yes | File upload |
494
+ | `/api/auth/*` | GET, POST | No | NextAuth routes |
495
+
496
+ ## Plugin System
497
+
498
+ Plugins are initialized via `createCmsApp` and accessed with `cms.getPlugin('name')`.
499
+
500
+ ### Built-in Plugins
501
+
502
+ | Plugin | Factory | Purpose |
503
+ |--------|---------|---------|
504
+ | Storage (S3) | `s3StoragePlugin({...})` | S3 file uploads |
505
+ | Storage (Local) | `localStoragePlugin({dir})` | Local file uploads |
506
+ | Email | `emailPlugin({type, from, ...})` | Email via SMTP/SES/Gmail |
507
+ | Analytics | `analyticsPlugin({...})` | Google Analytics integration |
508
+ | ERP | `erpPlugin({...})` | ERP/CRM integration |
509
+ | SMS | `smsPlugin({...})` | SMS notifications |
510
+ | Payment | `paymentPlugin({...})` | Payment processing |
511
+
512
+ ### Custom Plugins
513
+
514
+ Implement the `CmsPlugin` interface:
515
+
516
+ ```ts
517
+ import type { CmsPlugin, PluginContext } from '@infuro/cms-core';
518
+
519
+ export const myPlugin: CmsPlugin<MyService> = {
520
+ name: 'my-plugin',
521
+ version: '1.0.0',
522
+ async init(context: PluginContext) {
523
+ return new MyService(context.config);
524
+ },
525
+ };
526
+ ```
527
+
528
+ Register it in `cms.ts`:
529
+
530
+ ```ts
531
+ plugins: [
532
+ localStoragePlugin({ dir: 'public/uploads' }),
533
+ myPlugin,
534
+ ],
535
+ ```
536
+
537
+ Access it anywhere:
538
+
539
+ ```ts
540
+ const cms = await getCms();
541
+ const service = cms.getPlugin<MyService>('my-plugin');
542
+ ```
543
+
544
+ ## Package Exports
545
+
546
+ | Import Path | Contents |
547
+ |-------------|----------|
548
+ | `@infuro/cms-core` | Entities, plugins, registry, utilities |
549
+ | `@infuro/cms-core/api` | `createCmsApiHandler`, CRUD handlers, auth handlers |
550
+ | `@infuro/cms-core/auth` | `createAuthHelpers`, `createCmsMiddleware`, `getNextAuthOptions` |
551
+ | `@infuro/cms-core/admin` | Admin layout, pages, components (React, `'use client'`) |
552
+ | `@infuro/cms-core/hooks` | `useIsMobile`, `useAnalytics`, `usePlugin` |
553
+
554
+ ## Extending
555
+
556
+ ### Adding custom entities
557
+
558
+ 1. Define your TypeORM entity
559
+ 2. Add it to a merged entity map:
560
+
561
+ ```ts
562
+ import { CMS_ENTITY_MAP } from '@infuro/cms-core';
563
+ import { Product } from './entities/product.entity';
564
+
565
+ const ENTITY_MAP = { ...CMS_ENTITY_MAP, products: Product };
566
+ ```
567
+
568
+ 3. Pass the merged map to `getDataSource()` entities and `createCmsApiHandler({ entityMap })`
569
+
570
+ ### Adding custom API routes
571
+
572
+ Add files alongside the catch-all (e.g. `src/app/api/my-custom/route.ts`). Next.js resolves specific routes before the catch-all.
573
+
574
+ ### Customizing middleware
575
+
576
+ Pass config to `createCmsMiddleware()`:
577
+
578
+ ```ts
579
+ createCmsMiddleware({
580
+ publicAdminPaths: ['/admin/signin', '/admin/custom-public-page'],
581
+ publicApiMethods: {
582
+ '/api/products': ['GET'],
583
+ },
584
+ });
585
+ ```
586
+
587
+ ## Tailwind Config
588
+
589
+ Core's admin panel and UI components use shadcn/ui which requires CSS variable-based color mappings. Your `tailwind.config.js` needs these in `theme.extend.colors`:
590
+
591
+ ```js
592
+ module.exports = {
593
+ content: [
594
+ "./src/**/*.{js,ts,jsx,tsx,mdx}",
595
+ "../core/src/**/*.{js,ts,jsx,tsx}",
596
+ ],
597
+ darkMode: "class",
598
+ theme: {
599
+ extend: {
600
+ colors: {
601
+ background: "hsl(var(--background))",
602
+ foreground: "hsl(var(--foreground))",
603
+ card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" },
604
+ popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))" },
605
+ primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
606
+ secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" },
607
+ muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },
608
+ accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" },
609
+ destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },
610
+ border: "hsl(var(--border))",
611
+ input: "hsl(var(--input))",
612
+ ring: "hsl(var(--ring))",
613
+ sidebar: {
614
+ DEFAULT: "hsl(var(--sidebar-background))",
615
+ foreground: "hsl(var(--sidebar-foreground))",
616
+ primary: "hsl(var(--sidebar-primary))",
617
+ "primary-foreground": "hsl(var(--sidebar-primary-foreground))",
618
+ accent: "hsl(var(--sidebar-accent))",
619
+ "accent-foreground": "hsl(var(--sidebar-accent-foreground))",
620
+ border: "hsl(var(--sidebar-border))",
621
+ ring: "hsl(var(--sidebar-ring))",
622
+ },
623
+ },
624
+ borderRadius: {
625
+ lg: "var(--radius)",
626
+ md: "calc(var(--radius) - 2px)",
627
+ sm: "calc(var(--radius) - 4px)",
628
+ },
629
+ },
630
+ },
631
+ plugins: [require("tailwindcss-animate")],
632
+ };
633
+ ```
634
+
635
+ The CSS variables themselves are injected by core's AdminLayout at runtime. Your website's own CSS can also define them in `:root` if your public pages use shadcn/ui components.
636
+
637
+ ## Environment Variables
638
+
639
+ | Variable | Required | Description |
640
+ |----------|----------|-------------|
641
+ | `DATABASE_URL` | Yes | PostgreSQL connection string |
642
+ | `NEXTAUTH_SECRET` | Yes | NextAuth JWT secret |
643
+ | `NEXTAUTH_URL` | Yes | App base URL |
644
+ | `STORAGE_TYPE` | No | `s3` or `local` (default: local) |
645
+ | `AWS_BUCKET_NAME` | If S3 | S3 bucket name |
646
+ | `AWS_REGION` | If S3/SES | AWS region |
647
+ | `AWS_ACCESS_KEY_ID` | If S3/SES | AWS access key |
648
+ | `AWS_SECRET_ACCESS_KEY` | If S3/SES | AWS secret key |
649
+ | `SMTP_TYPE` | No | `SMTP`, `AWS`, or `GMAIL` |
650
+ | `SMTP_FROM` | If email | Sender email |
651
+ | `SMTP_TO` | If email | Default recipient |
652
+ | `SMTP_USER` | If SMTP | SMTP username |
653
+ | `SMTP_PASSWORD` | If SMTP | SMTP password |
654
+ | `GOOGLE_ANALYTICS_PRIVATE_KEY` | If analytics | GA service account key |
655
+ | `GOOGLE_ANALYTICS_CLIENT_EMAIL` | If analytics | GA service account email |
656
+ | `GOOGLE_ANALYTICS_VIEW_ID` | If analytics | GA property/view ID |
657
+
658
+ ## Development
659
+
660
+ ### Quick start (existing website using core)
661
+
662
+ ```bash
663
+ # 1. Build core (once, or use watch mode)
664
+ cd core
665
+ npm run build
666
+
667
+ # 2. Install website dependencies (links core via file:../core)
668
+ cd ../my-website
669
+ npm install
670
+
671
+ # 3. Set up .env (DATABASE_URL, NEXTAUTH_SECRET, NEXTAUTH_URL)
672
+
673
+ # 4. Create tables & seed (set synchronize: true in data-source.ts first)
674
+ npx tsx src/lib/seed.ts
675
+ # Then set synchronize back to false
676
+
677
+ # 5. Start dev server
678
+ npm run dev
679
+ ```
680
+
681
+ ### Watch mode (developing core + website simultaneously)
682
+
683
+ Terminal 1:
684
+ ```bash
685
+ cd core && npm run dev
686
+ ```
687
+
688
+ Terminal 2:
689
+ ```bash
690
+ cd my-website && npm run dev
691
+ ```
692
+
693
+ Changes to core are picked up automatically by the website's dev server.