npm package: @soulbatical/tetra-core
Description: The foundation framework for Tetra platform projects (Soulbatical BV) — config-driven Express + Supabase + TypeScript
Author: Albert Barth
Latest version: 0.6.0

# @soulbatical/tetra-core

The foundation framework for Tetra platform projects (Soulbatical BV) — config-driven Express + Supabase + TypeScript.

## Installation

```bash
npm install @soulbatical/tetra-core
```

Only install the peer dependencies you actually use. Most are optional:

```bash
# Always needed
npm install express @supabase/supabase-js

# Only if you use billing
npm install stripe            # and/or @mollie/api-client

# Only if you use storage (image uploads)
npm install multer sharp

# Only if you use MCP server
npm install @modelcontextprotocol/sdk

# Only if you use structured logging
npm install pino
```

## Subpath Exports

Import only the modules you need. Each subpath isolates its heavy dependencies:

```typescript
// Main barrel — core framework (controllers, factories, middleware, validators, db clients)
import { createApp, authenticateToken, BaseQueryController, addQueryRoutes } from '@soulbatical/tetra-core';

// Frontend hooks + components (React, for Next.js frontends)
import { useFeature, DataTable, FilterBar } from '@soulbatical/tetra-core/frontend';

// Billing — Stripe + Mollie integration
import { BillingService, addBillingRoutes } from '@soulbatical/tetra-core/billing';

// Storage — file upload + image processing (multer + sharp)
import { addStorageUploadRoutes, ImageProcessingService } from '@soulbatical/tetra-core/storage';

// MCP — expose backend as Model Context Protocol server
import { addMcpRoutes, addMcpAuthRoutes } from '@soulbatical/tetra-core/mcp';

// Email — Mailgun, SMTP, Gmail OAuth
import { EmailService, sendMailgunEmail, GmailClient } from '@soulbatical/tetra-core/email';

// Planner — scheduling, availability, Google Calendar
import { PlannerService, addPlannerRoutes } from '@soulbatical/tetra-core/planner';

// Affiliate — attribution, tracking, commissions
import { AffiliateAttributionService, addAffiliateAdminRoutes } from '@soulbatical/tetra-core/affiliate';

// Auth — public auth routes (login, register, invite, magic link)
import { addPublicAuthRoutes } from '@soulbatical/tetra-core/auth';

// Generators — RLS + RPC SQL generation from FeatureConfig
import { RPCGenerator, generateRLS, runRLSCheck } from '@soulbatical/tetra-core/generators';

// License — offline key validation for Pro features
import { validateLicense, isFeatureAvailable, generateLicenseKey } from '@soulbatical/tetra-core/license';
```

| Subpath | What you get | Extra peer deps |
|---------|-------------|-----------------|
| `.` | App bootstrap, controllers, factories, middleware, validators, DB clients, cron, logging, crypto | express, @supabase/supabase-js |
| `./frontend` | useFeature, DataTable, FilterBar (React hooks + components) | react, @tanstack/react-table |
| `./billing` | BillingService, Stripe/Mollie routes + webhooks | stripe, @mollie/api-client |
| `./storage` | Upload routes, image processing, storage proxy | multer, sharp |
| `./mcp` | MCP server routes, auth, token management | @modelcontextprotocol/sdk |
| `./email` | EmailService, Mailgun, SMTP, Gmail OAuth | — (nodemailer is a hard dep) |
| `./planner` | PlannerService, Google Calendar, availability, booking | — |
| `./affiliate` | Attribution, click tracking, commission routes | — |
| `./auth` | Public auth routes (login, register, invite, magic link) | — |
| `./generators` | RPCGenerator, RLS generator, RLS auditor/checker | — |
| `./license` | License key validation, generation, feature gating | — |

## Licensing

Tetra is publicly available on npm. Pro features (billing, planner, email, storage, affiliate, MCP) require a license key. Free features (createApp, auth, DB clients, middleware, validators) work without a key.

```bash
# Backend — set in .env or Doppler
TETRA_LICENSE_KEY=your-key-here

# Frontend (Next.js) — must be browser-accessible
NEXT_PUBLIC_TETRA_LICENSE_KEY=your-key-here
```

```typescript
import { validateLicense, isFeatureAvailable } from '@soulbatical/tetra-core/license';

const result = validateLicense(); // reads TETRA_LICENSE_KEY from env
if (isFeatureAvailable('pro', 'core')) {
  // Pro feature is available
}
```

Validation is 100% offline — zero network calls. Generate keys with `tetra-license generate` (CLI) or the license management API.

## Core Concepts

### App Bootstrap

```typescript
import { createApp } from '@soulbatical/tetra-core';

const app = createApp({
  name: 'my-app',
  cors: { origin: process.env.FRONTEND_URL },
  security: { rateLimit: { windowMs: 15 * 60 * 1000, max: 100 } },
});
```

### Database Clients

Six access levels, each with appropriate RLS behavior:

| Client | When | RLS |
|--------|------|-----|
| `publicDB()` | Public endpoints, no auth | Yes |
| `userDB(req)` | User's own data | Yes |
| `adminDB(req)` | Org-wide (admin routes) | Yes |
| `systemDB(ctx)` | Cron, webhooks (never user code) | Bypass |
| `superadminDB` | Platform admin | Bypass |
| `webhookDB(src)` | External webhooks | Bypass |

```typescript
import { userDB, adminDB, systemDB } from '@soulbatical/tetra-core';

// In a controller
const db = userDB(req);
const { data } = await db.from('tracks').select('*');
```

### Authentication Middleware

```typescript
import { configureAuth, authenticateToken, requireRole, requireOrgAdmin } from '@soulbatical/tetra-core';

configureAuth({
  supabaseUrl: process.env.SUPABASE_URL!,
  supabaseServiceKey: process.env.SUPABASE_SERVICE_ROLE_KEY!,
  jwtSecret: process.env.SUPABASE_JWT_SECRET!,
  loadUserContext: async (supabaseUser, token) => {
    if (supabaseUser.is_anonymous) {
      return { id: supabaseUser.id, is_anonymous: true };
    }
    const session = await sessionService.getUserSession(supabaseUser.id);
    return session ? { email: session.email, organizationId: session.active_organization_id, role: session.role } : null;
  },
});

// Routes
router.get('/profile', authenticateToken, controller.get);
router.get('/admin/users', authenticateToken, requireOrgAdmin, controller.list);
router.delete('/admin/users/:id', authenticateToken, requireRole('admin', 'owner'), controller.delete);
```

| Middleware | Purpose |
|-----------|---------|
| `authenticateToken` | Verify JWT, reject if missing/invalid |
| `authenticateTokenOnly` / `optionalAuth` | Verify JWT if present, continue without user if not |
| `requireSuperAdmin` | Require superadmin flag |
| `requireOrgAdmin` | Require admin/owner role in active org |
| `requireRole(...roles)` | Require specific role(s) |
| `validateOrganizationAccess` | Block org ID override attacks |

### Feature Config System

One config drives everything — filters, counts, sorting, pagination, RPC names, REST paths:

```typescript
import type { FeatureConfig } from '@soulbatical/tetra-core';

export const trackConfig: FeatureConfig = {
  tableName: 'tracks',
  organizationIdField: 'organization_id',
  resultsRpcName: 'get_tracks_results',
  countsRpcName: 'get_tracks_counts',
  detailRpcName: 'get_tracks_detail',
  restBasePath: '/api/admin/tracks',
  filters: [
    { key: 'status', type: 'enum', column: 'status', options: ['active', 'completed', 'paused'] },
    { key: 'search', type: 'search', columns: ['name', 'description'] },
    { key: 'period', type: 'time', column: 'created_at' },
  ],
  counts: {
    total: { type: 'total' },
    byStatus: { type: 'enum', column: 'status' },
  },
  sortFields: [
    { key: 'created_at', column: 'created_at', label: 'Created', default: true, defaultOrder: 'desc' },
    { key: 'name', column: 'name', label: 'Name' },
  ],
  pagination: { defaultLimit: 20, maxLimit: 100 },
};
```

### Controllers + Route Factories

```typescript
import { BaseQueryController, BaseMutationController, addQueryRoutes, addMutationRoutes } from '@soulbatical/tetra-core';

class TrackQueryController extends BaseQueryController { /* overrides only */ }
class TrackMutationController extends BaseMutationController { /* overrides only */ }

addQueryRoutes(router, { config: trackConfig, controller: trackQueryController });
addMutationRoutes(router, { config: trackConfig, controller: trackMutationController });
// Generates: GET /list, GET /:id, GET /counts, POST, PUT/:id, DELETE/:id
```

### RPC Generator

Generate SQL from your FeatureConfig — never write list/filter/count queries by hand:

```typescript
import { RPCGenerator, DetailRPCGenerator } from '@soulbatical/tetra-core/generators';

const generator = new RPCGenerator(trackConfig, {
  aliasMap: { tracks: 't', users_public: 'u' },
});
const { resultsSQL, countsSQL } = generator.generate();
generator.writeMigrations({ force: true });
```

### RLS Generator + Auditor

```typescript
import { generateRLS, runRLSCheck } from '@soulbatical/tetra-core/generators';

const rls = generateRLS({ tableName: 'tracks', tableType: 'org_owned' });
const report = await runRLSCheck(supabase); // 26 audit checks
```

### Permissions (Config-driven RBAC)

```typescript
import { registerFeaturePermissions, requirePermission } from '@soulbatical/tetra-core';

registerFeaturePermissions('tracks', {
  admin: { view: true, create: true, edit: true, delete: true },
  coach: { view: true, create: true, edit: 'own', delete: false },
  client: { view: 'own', create: false, edit: false, delete: false },
});

router.delete('/:id', requirePermission('tracks', 'delete'), controller.delete);
```

### Entitlements (Plan-based Feature Gating)

```typescript
import { requireFeature, requireWithinLimit } from '@soulbatical/tetra-core';

router.post('/', requireFeature('tracks'), requireWithinLimit('tracks', 'maxTracks'), controller.create);
```

### Error Handling (RFC 7807)

```typescript
import { RFC7807ErrorResponse, handleControllerError } from '@soulbatical/tetra-core';

// Standardized error responses — never use custom formats
throw new RFC7807ErrorResponse(404, 'Track not found', { trackId });
```

### Cron Services

```typescript
import { BaseCronService, CronRegistry } from '@soulbatical/tetra-core';

class TrackReminderCron extends BaseCronService {
  async run() { /* your logic */ }
}

CronRegistry.register(new TrackReminderCron({ schedule: '0 9 * * *', name: 'track-reminders' }));
```