chore: remove all NestJS backend references, replace with Hono/Bun

- Delete nestjs-backend.md guideline (replaced by hono-server.md)
- Delete Dockerfile.nestjs-base and Dockerfile.nestjs templates
- Delete stale BACKEND_ARCHITECTURE.md doc (NestJS-era, obsolete)
- Update CLAUDE.md, GUIDELINES.md, authentication.md to Hono/Bun first
- Update all app CLAUDE.md files: backend/ → server/, NestJS → Hono+Bun
- Update all app package.json files: @*/backend → @*/server
- Update docs: LOCAL_DEVELOPMENT, PORT_SCHEMA, ENVIRONMENT_VARIABLES,
  DATABASE_MIGRATIONS, MAC_MINI_SERVER, PROJECT_OVERVIEW
- Update scripts: generate-env.mjs, setup-databases.sh, build-app.sh
- Update CI/CD: cd-macmini.yml backend → server paths
- Update Astro docs site: @chat/backend → @chat/server

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-31 16:52:25 +02:00
parent 708299b35e
commit ab387b9b3d
43 changed files with 598 additions and 2398 deletions

View file

@ -9,7 +9,7 @@ This directory contains comprehensive guidelines for working in the Mana Univers
| [Code Style](./guidelines/code-style.md) | Formatting, naming conventions, linting rules |
| [Database](./guidelines/database.md) | Drizzle ORM patterns, schema design, migrations |
| [Testing](./guidelines/testing.md) | Jest/Vitest patterns, mock factories, coverage |
| [NestJS Backend](./guidelines/nestjs-backend.md) | Controllers, services, DTOs, modules |
| [Hono Server](./guidelines/hono-server.md) | Compute servers (Hono + Bun) |
| [Error Handling](./guidelines/error-handling.md) | Go-style errors, error codes, Result types |
| [SvelteKit Web](./guidelines/sveltekit-web.md) | Svelte 5 runes, stores, routing |
| [Expo Mobile](./guidelines/expo-mobile.md) | React Native, NativeWind, navigation |
@ -44,7 +44,7 @@ This directory contains comprehensive guidelines for working in the Mana Univers
|-------|------------|-------|
| **Package Manager** | pnpm 9.15+ | Workspace monorepo |
| **Build System** | Turborepo | Parallel task execution |
| **Backend** | NestJS 10-11 | TypeScript, Drizzle ORM |
| **Server** | Hono + Bun | TypeScript, Drizzle ORM |
| **Web** | SvelteKit 2 + Svelte 5 | Runes mode only |
| **Mobile** | Expo SDK 52-54 | React Native, NativeWind |
| **Database** | PostgreSQL | Via Drizzle ORM |
@ -62,15 +62,15 @@ manacore-monorepo/
├── apps/ # Product applications
│ └── {project}/
│ ├── apps/
│ │ ├── backend/ # NestJS API
│ │ ├── server/ # Hono/Bun compute server
│ │ ├── web/ # SvelteKit web
│ │ ├── mobile/ # Expo app
│ │ └── landing/ # Astro landing
│ └── packages/ # Project-specific shared
├── packages/ # Monorepo-wide shared
│ ├── shared-errors/ # Error codes & Result types
│ ├── shared-nestjs-auth/ # NestJS auth guards
│ ├── shared-auth/ # Client auth service
│ ├── local-store/ # Local-first data layer (Dexie.js + sync)
│ └── ...
├── services/ # Standalone microservices
│ └── mana-core-auth/ # Central auth service
@ -114,7 +114,8 @@ See [Error Handling](./guidelines/error-handling.md) for complete details.
# Development
pnpm install # Install dependencies
pnpm {project}:dev # Start project (all apps)
pnpm dev:{project}:backend # Start just backend
pnpm dev:{project}:server # Start just Hono server
pnpm dev:{project}:local # Start sync + server + web (no auth)
pnpm dev:{project}:web # Start just web
# Quality
@ -122,7 +123,7 @@ pnpm type-check # TypeScript validation
pnpm format # Format code
pnpm test # Run tests
# Database
pnpm {project}:db:push # Push schema changes
pnpm {project}:db:studio # Open Drizzle Studio
# Database (for apps with Drizzle in server)
pnpm --filter @{project}/server db:push # Push schema changes
pnpm --filter @{project}/server db:studio # Open Drizzle Studio
```

View file

@ -8,8 +8,8 @@ All authentication is handled by **Mana Core Auth**, a centralized authenticatio
```
┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐
│ Web/Mobile │────>│ Backend API │────>│ mana-core-auth
│ Client │ │ (NestJS) │ │ (port 3001) │
│ Web/Mobile │────>│ Compute Server │────>│ mana-auth
│ Client │ │ (Hono/Bun) │ │ (port 3001) │
└─────────────────┘ └─────────────────┘ └──────────────────┘
│ │ │
│ 1. Login │ │
@ -85,101 +85,48 @@ Always use `text` type for `user_id` columns in all database schemas.
| Package | Purpose | Use Case |
|---------|---------|----------|
| `@manacore/shared-nestjs-auth` | NestJS guards/decorators | Backend APIs |
| `@mana-core/nestjs-integration` | Auth + Credits integration | Backends with credits |
| `@manacore/shared-hono` | Hono auth middleware + helpers | All compute servers (Hono/Bun) |
| `@manacore/shared-auth` | Client auth service | Web/Mobile apps |
| `@mana-core/nestjs-integration` | Auth + Credits for NestJS | `@arcade/backend` only |
## Backend Integration
## Server Integration (Hono/Bun)
### Option 1: Simple Auth Only
Use `@manacore/shared-nestjs-auth` for JWT validation:
All compute servers use `@manacore/shared-hono`:
```typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { Hono } from 'hono';
import { authMiddleware, healthRoute, errorHandler, notFoundHandler } from '@manacore/shared-hono';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
// No auth module needed - guards handle it
],
})
export class AppModule {}
const app = new Hono();
app.onError(errorHandler);
app.notFound(notFoundHandler);
app.route('/health', healthRoute('my-server'));
// Protect all /api/* routes
app.use('/api/*', authMiddleware());
// Access user in route handlers
app.get('/api/v1/data', (c) => {
const userId = c.get('userId'); // Better Auth user ID (not UUID)
const email = c.get('userEmail');
return c.json({ userId });
});
```
```typescript
// file.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
### NestJS (arcade only)
@Controller('files')
@UseGuards(JwtAuthGuard) // Apply to all routes
export class FileController {
@Get()
async listFiles(@CurrentUser() user: CurrentUserData) {
// user.userId, user.email, user.role available
return this.fileService.findAll(user.userId);
}
}
```
### Option 2: Auth + Credits
Use `@mana-core/nestjs-integration` for full integration:
`@arcade/backend` still uses NestJS with `@mana-core/nestjs-integration`:
```typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ManaCoreModule } from '@mana-core/nestjs-integration';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
ManaCoreModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
appId: config.get('APP_ID'),
serviceKey: config.get('MANA_CORE_SERVICE_KEY'),
debug: config.get('NODE_ENV') === 'development',
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
```
```typescript
// generation.controller.ts
import { Controller, Post, UseGuards, Body } from '@nestjs/common';
import { AuthGuard } from '@mana-core/nestjs-integration/guards';
import { CurrentUser } from '@mana-core/nestjs-integration/decorators';
import { CreditClientService } from '@mana-core/nestjs-integration';
@Controller('generations')
@Controller('api')
@UseGuards(AuthGuard)
export class GenerationController {
constructor(private creditClient: CreditClientService) {}
@Post()
async generate(@CurrentUser() user: any, @Body() dto: GenerateDto) {
// Check and consume credits
const result = await this.creditClient.consumeCredits(
user.sub,
'ai_generation',
10,
'AI image generation'
);
if (!result.ok) {
throw new AppException(result.error);
}
// Proceed with generation
return this.generationService.generate(user.sub, dto);
export class ApiController {
@Get('data')
getData(@CurrentUser() user: any) {
return this.service.findAll(user.sub);
}
}
```
@ -187,7 +134,7 @@ export class GenerationController {
## Environment Variables
```env
# Required for all backends
# Required for all servers
MANA_CORE_AUTH_URL=http://localhost:3001
# Development bypass (optional)

View file

@ -236,13 +236,12 @@ All SvelteKit web apps use **Phosphor icons** via `@manacore/shared-icons` (re-e
- **Ideal**: 10-25 lines
- Extract complex logic into helper functions
### Module Structure (NestJS)
### Module Structure (Hono/Bun Server)
```
feature/
├── feature.controller.ts # HTTP layer
├── feature.routes.ts # HTTP routes
├── feature.service.ts # Business logic
├── feature.module.ts # DI configuration
├── feature.spec.ts # Tests
└── dto/
├── create-feature.dto.ts
@ -340,5 +339,5 @@ pnpm format
pnpm format:check
# Format specific project
pnpm --filter @chat/backend format
pnpm --filter @chat/server format
```

View file

@ -0,0 +1,137 @@
# Hono Server Guidelines
## Overview
All app compute servers use Hono + Bun with a lightweight architecture. Servers handle only what can't run client-side: file uploads (S3), AI calls, RRULE expansion, external API integration, etc. All CRUD is handled client-side via local-first (Dexie.js + mana-sync).
## Project Structure
```
apps/{project}/apps/server/
├── src/
│ ├── index.ts # Entry point + route mounting
│ ├── routes/ # Route handlers
│ │ ├── {feature}.ts
│ │ └── admin.ts
│ ├── lib/ # Shared utilities
│ └── db/ # Drizzle schema (if needed)
│ ├── schema/
│ ├── connection.ts
│ └── drizzle.config.ts
├── package.json
└── tsconfig.json
```
## Entry Point (index.ts)
```typescript
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { authMiddleware, healthRoute, errorHandler, notFoundHandler } from '@manacore/shared-hono';
const PORT = parseInt(process.env.PORT || '3031', 10);
const CORS_ORIGINS = (process.env.CORS_ORIGINS || 'http://localhost:5173').split(',');
const app = new Hono();
// Standard middleware stack
app.onError(errorHandler);
app.notFound(notFoundHandler);
app.use('*', cors({ origin: CORS_ORIGINS, credentials: true }));
app.route('/health', healthRoute('my-server'));
app.use('/api/*', authMiddleware());
// Routes
app.route('/api/v1/compute', computeRoutes);
export default { port: PORT, fetch: app.fetch };
```
## Shared Hono Package
Use `@manacore/shared-hono` for consistent middleware across all servers:
```typescript
import {
authMiddleware, // JWT validation via mana-auth JWKS
healthRoute, // Standard /health endpoint
errorHandler, // Global error handler with logging
notFoundHandler, // 404 handler
} from '@manacore/shared-hono';
```
## Route Handlers
```typescript
// src/routes/upload.ts
import { Hono } from 'hono';
export const uploadRoutes = new Hono();
uploadRoutes.post('/avatar', async (c) => {
const userId = c.get('userId'); // Set by authMiddleware
const formData = await c.req.formData();
const file = formData.get('file') as File | null;
if (!file) return c.json({ error: 'No file' }, 400);
if (file.size > 5 * 1024 * 1024) return c.json({ error: 'Max 5MB' }, 400);
// ... handle upload
return c.json({ url: result.url }, 201);
});
```
## Database (Optional)
Only servers that need their own database use Drizzle. Most apps rely on mana-sync for data persistence.
**Servers with Drizzle:** chat, todo, moodlit, context, planta, presi, traces, uload, wisekeep, news
**Servers without Drizzle (mana-sync only):** calendar, contacts, manadeck, mukke, nutriphi, picture, questions, storage
## Running Servers
```bash
# Development (with watch)
cd apps/{project}/apps/server && bun run --watch src/index.ts
# Via root scripts
pnpm dev:{project}:server # Just the server
pnpm dev:{project}:local # sync + server + web (no auth needed)
pnpm dev:{project}:full # auth + sync + server + web
```
## When to Add a Server
Add a Hono server when the app needs:
- **File operations**: S3 uploads, image processing
- **AI/LLM calls**: Gemini, OpenAI, etc. (API keys can't be client-side)
- **External APIs**: Google Calendar sync, vCard parsing, etc.
- **Heavy compute**: RRULE expansion, PDF generation, etc.
- **Admin endpoints**: GDPR compliance, data export
Do NOT add a server for pure CRUD — use local-first + mana-sync instead.
## Environment Variables
Servers read `.env` from their directory (Bun auto-loads it):
```env
PORT=3031
CORS_ORIGINS=http://localhost:5173,http://localhost:5188
MANA_CORE_AUTH_URL=http://localhost:3001
DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/myapp # if Drizzle
```
## Key Differences from Old NestJS Pattern
| Aspect | Old (NestJS) | New (Hono + Bun) |
| ----------- | ----------------------- | ------------------------------------- |
| Runtime | Node.js | Bun |
| Framework | NestJS (decorators, DI) | Hono (functional, minimal) |
| CRUD | Server handles all CRUD | Client-side (local-first + mana-sync) |
| Server role | Full backend | Compute-only endpoints |
| Auth | NestJS guards | `@manacore/shared-hono` middleware |
| Startup | `nest start --watch` | `bun run --watch src/index.ts` |
| Config | `ConfigModule` + DI | `process.env` directly |

View file

@ -1,659 +0,0 @@
# NestJS Backend Guidelines
## Overview
All backend services use NestJS with a consistent architecture. This guide covers controllers, services, DTOs, modules, and integration with the error handling system.
## Project Structure
```
apps/{project}/apps/backend/
├── src/
│ ├── main.ts # Bootstrap
│ ├── app.module.ts # Root module
│ ├── db/
│ │ ├── schema/ # Drizzle schemas
│ │ ├── connection.ts # DB singleton
│ │ ├── database.module.ts # NestJS module
│ │ └── migrations/ # Migration files
│ ├── common/
│ │ ├── filters/ # Exception filters
│ │ ├── guards/ # Custom guards
│ │ └── decorators/ # Custom decorators
│ ├── health/
│ │ ├── health.controller.ts
│ │ └── health.module.ts
│ └── {feature}/
│ ├── {feature}.controller.ts
│ ├── {feature}.service.ts
│ ├── {feature}.module.ts
│ ├── {feature}.spec.ts
│ └── dto/
│ ├── create-{feature}.dto.ts
│ └── update-{feature}.dto.ts
├── test/
│ ├── jest-e2e.json
│ └── app.e2e-spec.ts
├── drizzle.config.ts
├── nest-cli.json
├── package.json
└── tsconfig.json
```
## Bootstrap (main.ts)
```typescript
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe, Logger } from '@nestjs/common';
import { AppModule } from './app.module';
import { AppExceptionFilter } from './common/filters/app-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const logger = new Logger('Bootstrap');
// CORS
const corsOrigins = process.env.CORS_ORIGINS?.split(',').map((o) => o.trim()) || [
'http://localhost:3000',
'http://localhost:5173',
'http://localhost:8081',
];
app.enableCors({
origin: corsOrigins,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
});
// Global validation pipe
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Strip unknown properties
forbidNonWhitelisted: true, // Reject unknown properties
transform: true, // Auto-transform types
transformOptions: {
enableImplicitConversion: true,
},
})
);
// Global exception filter
app.useGlobalFilters(new AppExceptionFilter());
// API prefix
app.setGlobalPrefix('api/v1');
const port = process.env.PORT || 3000;
await app.listen(port);
logger.log(`Application running on http://localhost:${port}`);
}
bootstrap();
```
## App Module
```typescript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './db/database.module';
import { HealthModule } from './health/health.module';
import { FileModule } from './file/file.module';
import { FolderModule } from './folder/folder.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
DatabaseModule,
HealthModule,
FileModule,
FolderModule,
],
})
export class AppModule {}
```
## Controllers
### Basic Pattern
```typescript
// src/file/file.controller.ts
import {
Controller,
Get,
Post,
Patch,
Delete,
Param,
Body,
Query,
UseGuards,
ParseUUIDPipe,
} from '@nestjs/common';
import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
import { AppException } from '@manacore/shared-errors';
import { FileService } from './file.service';
import { CreateFileDto, UpdateFileDto, QueryFilesDto } from './dto';
@Controller('files')
@UseGuards(JwtAuthGuard) // Apply to all routes in controller
export class FileController {
constructor(private readonly fileService: FileService) {}
@Get()
async list(@CurrentUser() user: CurrentUserData, @Query() query: QueryFilesDto) {
const result = await this.fileService.findAll(user.userId, query);
if (!result.ok) throw new AppException(result.error);
return { files: result.data };
}
@Get(':id')
async getById(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: CurrentUserData) {
const result = await this.fileService.findById(id, user.userId);
if (!result.ok) throw new AppException(result.error);
return { file: result.data };
}
@Post()
async create(@Body() dto: CreateFileDto, @CurrentUser() user: CurrentUserData) {
const result = await this.fileService.create(user.userId, dto);
if (!result.ok) throw new AppException(result.error);
return { file: result.data };
}
@Patch(':id')
async update(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdateFileDto,
@CurrentUser() user: CurrentUserData
) {
const result = await this.fileService.update(id, user.userId, dto);
if (!result.ok) throw new AppException(result.error);
return { file: result.data };
}
@Delete(':id')
async delete(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: CurrentUserData) {
const result = await this.fileService.delete(id, user.userId);
if (!result.ok) throw new AppException(result.error);
return { success: true };
}
}
```
### Public Endpoints (No Auth)
```typescript
@Controller('public')
export class PublicController {
@Get('shares/:token') // No @UseGuards - public access
async getSharedItem(@Param('token') token: string) {
const result = await this.shareService.findByToken(token);
if (!result.ok) throw new AppException(result.error);
return { item: result.data };
}
}
```
## Services
### Basic Pattern with Result Types
```typescript
// src/file/file.service.ts
import { Injectable, Inject, Logger } from '@nestjs/common';
import { Result, ok, err, ErrorCode } from '@manacore/shared-errors';
import { DATABASE_CONNECTION, Database } from '../db/database.module';
import { files, File, NewFile } from '../db/schema';
import { eq, and, desc } from 'drizzle-orm';
import { CreateFileDto, UpdateFileDto, QueryFilesDto } from './dto';
@Injectable()
export class FileService {
private readonly logger = new Logger(FileService.name);
constructor(@Inject(DATABASE_CONNECTION) private db: Database) {}
async findAll(userId: string, query: QueryFilesDto): Promise<Result<File[]>> {
try {
const conditions = [eq(files.userId, userId), eq(files.isDeleted, false)];
if (query.folderId) {
conditions.push(eq(files.parentFolderId, query.folderId));
}
const result = await this.db
.select()
.from(files)
.where(and(...conditions))
.orderBy(desc(files.createdAt))
.limit(query.limit ?? 50)
.offset(query.offset ?? 0);
return ok(result);
} catch (error) {
this.logger.error('Failed to fetch files', { userId, error: error.message });
return err(ErrorCode.DATABASE_ERROR, 'Failed to fetch files');
}
}
async findById(id: string, userId: string): Promise<Result<File>> {
try {
const [file] = await this.db
.select()
.from(files)
.where(and(eq(files.id, id), eq(files.userId, userId), eq(files.isDeleted, false)));
if (!file) {
return err(ErrorCode.FILE_NOT_FOUND, `File ${id} not found`);
}
return ok(file);
} catch (error) {
this.logger.error('Failed to fetch file', { id, userId, error: error.message });
return err(ErrorCode.DATABASE_ERROR, 'Failed to fetch file');
}
}
async create(userId: string, dto: CreateFileDto): Promise<Result<File>> {
// Validation
if (!dto.name?.trim()) {
return err(ErrorCode.MISSING_REQUIRED_FIELD, 'File name is required');
}
try {
const newFile: NewFile = {
userId,
name: dto.name.trim(),
originalName: dto.originalName,
mimeType: dto.mimeType,
size: dto.size,
storagePath: dto.storagePath,
storageKey: dto.storageKey,
parentFolderId: dto.folderId ?? null,
};
const [created] = await this.db.insert(files).values(newFile).returning();
return ok(created);
} catch (error) {
if (error.code === '23505') {
return err(ErrorCode.DUPLICATE_ENTRY, 'A file with this name already exists');
}
this.logger.error('Failed to create file', { userId, error: error.message });
return err(ErrorCode.DATABASE_ERROR, 'Failed to create file');
}
}
async update(id: string, userId: string, dto: UpdateFileDto): Promise<Result<File>> {
// Check ownership first
const existingResult = await this.findById(id, userId);
if (!existingResult.ok) return existingResult;
try {
const [updated] = await this.db
.update(files)
.set({
...(dto.name && { name: dto.name.trim() }),
...(dto.parentFolderId !== undefined && { parentFolderId: dto.parentFolderId }),
updatedAt: new Date(),
})
.where(eq(files.id, id))
.returning();
return ok(updated);
} catch (error) {
this.logger.error('Failed to update file', { id, error: error.message });
return err(ErrorCode.DATABASE_ERROR, 'Failed to update file');
}
}
async delete(id: string, userId: string): Promise<Result<void>> {
// Check ownership first
const existingResult = await this.findById(id, userId);
if (!existingResult.ok) return existingResult;
try {
await this.db
.update(files)
.set({ isDeleted: true, deletedAt: new Date() })
.where(eq(files.id, id));
return ok(undefined);
} catch (error) {
this.logger.error('Failed to delete file', { id, error: error.message });
return err(ErrorCode.DATABASE_ERROR, 'Failed to delete file');
}
}
}
```
### Service with External Dependencies
```typescript
@Injectable()
export class UploadService {
private readonly logger = new Logger(UploadService.name);
constructor(
@Inject(DATABASE_CONNECTION) private db: Database,
private readonly storageService: StorageService,
private readonly fileService: FileService
) {}
async uploadFile(
userId: string,
file: Express.Multer.File,
folderId?: string
): Promise<Result<File>> {
// 1. Upload to storage
const storageResult = await this.storageService.upload(
generateStorageKey(userId, file.originalname),
file.buffer,
{ contentType: file.mimetype }
);
if (!storageResult.ok) {
return err(ErrorCode.UPLOAD_FAILED, 'Failed to upload file to storage');
}
// 2. Create database record
const createResult = await this.fileService.create(userId, {
name: file.originalname,
originalName: file.originalname,
mimeType: file.mimetype,
size: file.size,
storagePath: storageResult.data.path,
storageKey: storageResult.data.key,
folderId,
});
if (!createResult.ok) {
// Cleanup on failure
await this.storageService.delete(storageResult.data.key);
return createResult;
}
return createResult;
}
}
```
## DTOs
### Create DTO
```typescript
// src/file/dto/create-file.dto.ts
import { IsString, IsOptional, IsNumber, IsUUID, MaxLength, Min } from 'class-validator';
export class CreateFileDto {
@IsString()
@MaxLength(500)
name: string;
@IsOptional()
@IsString()
@MaxLength(500)
originalName?: string;
@IsString()
@MaxLength(255)
mimeType: string;
@IsNumber()
@Min(0)
size: number;
@IsString()
@MaxLength(1000)
storagePath: string;
@IsString()
@MaxLength(500)
storageKey: string;
@IsOptional()
@IsUUID()
folderId?: string;
}
```
### Update DTO (Partial)
```typescript
// src/file/dto/update-file.dto.ts
import { IsString, IsOptional, IsUUID, MaxLength } from 'class-validator';
export class UpdateFileDto {
@IsOptional()
@IsString()
@MaxLength(500)
name?: string;
@IsOptional()
@IsUUID()
parentFolderId?: string | null;
}
```
### Query DTO
```typescript
// src/file/dto/query-files.dto.ts
import { IsOptional, IsUUID, IsNumber, Min, Max } from 'class-validator';
import { Transform } from 'class-transformer';
export class QueryFilesDto {
@IsOptional()
@IsUUID()
folderId?: string;
@IsOptional()
@Transform(({ value }) => parseInt(value, 10))
@IsNumber()
@Min(1)
@Max(100)
limit?: number = 50;
@IsOptional()
@Transform(({ value }) => parseInt(value, 10))
@IsNumber()
@Min(0)
offset?: number = 0;
}
```
### DTO Index
```typescript
// src/file/dto/index.ts
export * from './create-file.dto';
export * from './update-file.dto';
export * from './query-files.dto';
```
## Modules
```typescript
// src/file/file.module.ts
import { Module } from '@nestjs/common';
import { FileController } from './file.controller';
import { FileService } from './file.service';
import { UploadService } from './upload.service';
import { StorageModule } from '../storage/storage.module';
@Module({
imports: [StorageModule],
controllers: [FileController],
providers: [FileService, UploadService],
exports: [FileService], // Export for use in other modules
})
export class FileModule {}
```
## Exception Filter
```typescript
// src/common/filters/app-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus, Logger } from '@nestjs/common';
import { Response } from 'express';
import { AppException, ERROR_STATUS_MAP, ErrorCode } from '@manacore/shared-errors';
@Catch(AppException)
export class AppExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(AppExceptionFilter.name);
catch(exception: AppException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status = ERROR_STATUS_MAP[exception.error.code] ?? HttpStatus.INTERNAL_SERVER_ERROR;
// Log server errors
if (status >= 500) {
this.logger.error('Server error', {
code: exception.error.code,
message: exception.error.message,
details: exception.error.details,
});
}
response.status(status).json({
ok: false,
error: {
code: exception.error.code,
message: exception.error.message,
...(process.env.NODE_ENV === 'development' && {
details: exception.error.details,
}),
},
});
}
}
```
## File Upload
```typescript
// src/file/file.controller.ts
import { UseInterceptors, UploadedFile, ParseFilePipe, MaxFileSizeValidator } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller('files')
@UseGuards(JwtAuthGuard)
export class FileController {
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
async uploadFile(
@UploadedFile(
new ParseFilePipe({
validators: [
new MaxFileSizeValidator({ maxSize: 100 * 1024 * 1024 }), // 100MB
],
})
)
file: Express.Multer.File,
@Query('folderId') folderId: string | undefined,
@CurrentUser() user: CurrentUserData
) {
const result = await this.uploadService.uploadFile(user.userId, file, folderId);
if (!result.ok) throw new AppException(result.error);
return { file: result.data };
}
}
```
## Health Check
```typescript
// src/health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import { Inject } from '@nestjs/common';
import { DATABASE_CONNECTION, Database } from '../db/database.module';
import { sql } from 'drizzle-orm';
@Controller('health')
export class HealthController {
constructor(@Inject(DATABASE_CONNECTION) private db: Database) {}
@Get()
async check() {
try {
await this.db.execute(sql`SELECT 1`);
return {
status: 'ok',
timestamp: new Date().toISOString(),
database: 'connected',
};
} catch (error) {
return {
status: 'error',
timestamp: new Date().toISOString(),
database: 'disconnected',
};
}
}
}
```
## API Response Format
### Success Responses
```typescript
// Single resource
{ file: { id: '...', name: '...', ... } }
// Multiple resources
{ files: [...] }
// With pagination
{ files: [...], total: 100, page: 1, limit: 20 }
// Action success
{ success: true }
// Action with data
{ success: true, message: 'File moved', file: {...} }
```
### Error Responses
```typescript
{
ok: false,
error: {
code: 'ERR_4003',
message: 'File not found'
}
}
```
## Environment Variables
```env
# Required
NODE_ENV=development
PORT=3016
DATABASE_URL=postgresql://user:pass@localhost:5432/db
MANA_CORE_AUTH_URL=http://localhost:3001
# CORS
CORS_ORIGINS=http://localhost:5173,http://localhost:3000
# Storage
S3_ENDPOINT=http://localhost:9000
S3_REGION=us-east-1
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
# Optional - Development bypass
DEV_BYPASS_AUTH=true
DEV_USER_ID=dev-user-123
```

View file

@ -546,7 +546,7 @@ pnpm test
pnpm test:cov
# Run specific project
pnpm --filter @storage/backend test
pnpm --filter @storage/server test
# Run in watch mode
pnpm test:watch

View file

@ -259,16 +259,6 @@ jobs:
SERVICES="${{ steps.services.outputs.services }}"
DEPLOY_ALL="${{ steps.services.outputs.deploy-all }}"
# Check if any backend service is being deployed
NEEDS_BASE=false
if [ "$DEPLOY_ALL" == "true" ]; then
NEEDS_BASE=true
else
for svc in $SERVICES; do
case "$svc" in *-backend) NEEDS_BASE=true; break ;; esac
done
fi
NEEDS_WEB_BASE=false
if [ "$DEPLOY_ALL" == "true" ]; then
NEEDS_WEB_BASE=true
@ -278,14 +268,6 @@ jobs:
done
fi
if [ "$NEEDS_BASE" == "true" ]; then
echo "=== Building shared NestJS base image ==="
docker build -f docker/Dockerfile.nestjs-base -t nestjs-base:local . 2>&1 | tail -5
echo "NestJS base image built"
else
echo "No backends to deploy, skipping NestJS base image"
fi
if [ "$NEEDS_WEB_BASE" == "true" ]; then
echo "=== Building shared SvelteKit base image ==="
docker build -f docker/Dockerfile.sveltekit-base -t sveltekit-base:local . 2>&1 | tail -5

144
CLAUDE.md
View file

@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Monorepo Overview
This is a pnpm workspace monorepo containing multiple product applications with shared packages. All projects use Supabase for database/auth and follow similar architectural patterns.
This is a pnpm workspace monorepo containing multiple product applications with shared packages. All projects use a local-first architecture (Dexie.js + mana-sync) with Hono/Bun compute servers and follow consistent patterns.
**Package Manager:** pnpm 9.15.0 (use `pnpm` for all commands)
**Build System:** Turborepo
@ -20,7 +20,7 @@ For comprehensive guidelines on code patterns and conventions, see the `.claude/
| [`.claude/guidelines/code-style.md`](.claude/guidelines/code-style.md) | Formatting, naming, linting |
| [`.claude/guidelines/database.md`](.claude/guidelines/database.md) | Drizzle ORM, schema patterns |
| [`.claude/guidelines/testing.md`](.claude/guidelines/testing.md) | Jest/Vitest, mock factories |
| [`.claude/guidelines/nestjs-backend.md`](.claude/guidelines/nestjs-backend.md) | Controllers, services, DTOs |
| [`.claude/guidelines/hono-server.md`](.claude/guidelines/hono-server.md) | Hono/Bun compute servers |
| [`.claude/guidelines/error-handling.md`](.claude/guidelines/error-handling.md) | Go-style Result types, error codes |
| [`.claude/guidelines/sveltekit-web.md`](.claude/guidelines/sveltekit-web.md) | Svelte 5 runes, stores |
| [`.claude/guidelines/expo-mobile.md`](.claude/guidelines/expo-mobile.md) | React Native, NativeWind |
@ -123,8 +123,9 @@ pnpm run contacts:dev
# Start specific app within project
pnpm run dev:chat:mobile # Just mobile app
pnpm run dev:chat:backend # Just NestJS backend
pnpm run dev:chat:app # Web + backend together
pnpm run dev:chat:server # Just Hono/Bun server
pnpm run dev:chat:local # sync + server + web (no auth needed)
pnpm run dev:chat:app # Server + web together
# Build & quality
pnpm run build
@ -143,7 +144,7 @@ manacore-monorepo/
├── apps/ # Active SaaS product applications
│ ├── chat/
│ │ ├── apps/
│ │ │ ├── backend/ # NestJS API
│ │ │ ├── server/ # Hono/Bun compute server
│ │ │ ├── mobile/ # Expo React Native app
│ │ │ ├── web/ # SvelteKit web app
│ │ │ └── landing/ # Astro marketing page
@ -193,7 +194,7 @@ manacore-monorepo/
```
apps/{project}/
├── apps/
│ ├── backend/ # NestJS API (when present)
│ ├── server/ # Hono/Bun compute server (when present)
│ ├── mobile/ # Expo React Native app
│ ├── web/ # SvelteKit web app
│ └── landing/ # Astro marketing page
@ -259,11 +260,11 @@ Parent workspace packages (e.g., `apps/chat/package.json`, `apps/zitare/package.
- Tailwind CSS
- Static site generation
**Backends (NestJS):**
**Compute Servers (Hono + Bun):**
- NestJS 10-11
- TypeScript
- Supabase integration
- Hono 4.x + Bun runtime
- TypeScript, Drizzle ORM (where needed)
- `@manacore/shared-hono` for auth middleware
### Authentication Architecture
@ -271,8 +272,8 @@ All projects use **mana-core-auth** as the central authentication service:
```
┌─────────────┐ ┌─────────────┐ ┌────────────────┐
│ Client │────>│ Backend │────>│ mana-core-auth
│ (Web/Mobile)│ │ (NestJS) │ │ (port 3001) │
│ Client │────>│ Server │────>│ mana-auth
│ (Web/Mobile)│ │ (Hono/Bun) │ │ (port 3001) │
└─────────────┘ └─────────────┘ └────────────────┘
│ │ │
│ Bearer token │ POST /validate │
@ -287,82 +288,36 @@ All projects use **mana-core-auth** as the central authentication service:
| Component | Purpose |
| ------------------------------- | -------------------------------------------------- |
| `services/mana-core-auth` | Central auth service (Better Auth + EdDSA JWT) |
| `@manacore/shared-nestjs-auth` | Shared NestJS guards/decorators for JWT validation |
| `@mana-core/nestjs-integration` | Extended NestJS module with auth + credits |
| `services/mana-auth` | Central auth service (Better Auth + EdDSA JWT) |
| `@manacore/shared-hono` | Shared Hono middleware for JWT validation |
| `@manacore/shared-auth` | Client-side auth for web/mobile apps |
#### NestJS Backend Integration
#### Hono Server Auth Integration
**Option 1: Simple auth only** - Use `@manacore/shared-nestjs-auth`:
All compute servers use `@manacore/shared-hono` for auth:
```typescript
// In your controller
import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
import { authMiddleware, healthRoute, errorHandler, notFoundHandler } from '@manacore/shared-hono';
@Controller('api')
@UseGuards(JwtAuthGuard)
export class MyController {
@Get('profile')
getProfile(@CurrentUser() user: CurrentUserData) {
return { userId: user.userId, email: user.email };
}
}
```
const app = new Hono();
app.onError(errorHandler);
app.notFound(notFoundHandler);
app.route('/health', healthRoute('my-server'));
app.use('/api/*', authMiddleware());
**Option 2: Auth + Credits** - Use `@mana-core/nestjs-integration`:
```typescript
// app.module.ts
import { ManaCoreModule } from '@mana-core/nestjs-integration';
@Module({
imports: [
ManaCoreModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
appId: config.get('APP_ID'),
serviceKey: config.get('MANA_CORE_SERVICE_KEY'),
debug: config.get('NODE_ENV') === 'development',
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
// In controller
import { AuthGuard } from '@mana-core/nestjs-integration/guards';
import { CurrentUser } from '@mana-core/nestjs-integration/decorators';
import { CreditClientService } from '@mana-core/nestjs-integration';
@Controller('api')
@UseGuards(AuthGuard)
export class ApiController {
constructor(private creditClient: CreditClientService) {}
@Post('generate')
async generate(@CurrentUser() user: any) {
await this.creditClient.consumeCredits(user.sub, 'generation', 10, 'AI generation');
// ... do work
}
}
// In route handlers, get user from context:
app.get('/api/v1/data', (c) => {
const userId = c.get('userId');
// ...
});
```
#### Required Environment Variables
```env
# All backends need this
# All servers need this
MANA_CORE_AUTH_URL=http://localhost:3001
# For development bypass (optional)
NODE_ENV=development
DEV_BYPASS_AUTH=true
DEV_USER_ID=your-test-user-id
# For credit operations (optional)
MANA_CORE_SERVICE_KEY=your-service-key
APP_ID=your-app-id
CORS_ORIGINS=http://localhost:5173
```
#### JWT Token Structure (EdDSA)
@ -382,11 +337,11 @@ APP_ID=your-app-id
#### Testing Auth Integration
```bash
# 1. Start mana-core-auth
# 1. Start mana-auth
pnpm dev:auth
# 2. Start a backend (e.g., Zitare)
pnpm dev:zitare:backend
# 2. Start an app locally
pnpm dev:contacts:local
# 3. Get a token
TOKEN=$(curl -s -X POST http://localhost:3001/api/v1/auth/login \
@ -394,20 +349,10 @@ TOKEN=$(curl -s -X POST http://localhost:3001/api/v1/auth/login \
-d '{"email": "test@example.com", "password": "password"}' | jq -r '.accessToken')
# 4. Call protected endpoint
curl http://localhost:3007/api/favorites \
curl http://localhost:3033/api/v1/import/vcard \
-H "Authorization: Bearer $TOKEN"
```
#### Integrated Backends
| Backend | Package | Port |
| -------- | ------------------------------- | ---- |
| Chat | `@mana-core/nestjs-integration` | 3002 |
| Picture | `@manacore/shared-nestjs-auth` | 3006 |
| Zitare | `@manacore/shared-nestjs-auth` | 3007 |
| Presi | Custom (same pattern) | 3008 |
| ManaDeck | `@mana-core/nestjs-integration` | 3009 |
#### Adding a New App to SSO
When adding a new app that should participate in cross-app SSO, update **all three** locations:
@ -585,7 +530,7 @@ CACHE_SEARCH_TTL=3600
CACHE_EXTRACT_TTL=86400
```
#### Usage in Backend
#### Usage in Server
```typescript
// Direct fetch
@ -674,8 +619,8 @@ Logged in: App → IndexedDB → UI → SyncEngine → mana-sync (Go) → Postg
pnpm dev:sync # Go sync server (port 3050)
pnpm dev:sync:build # Compile Go binary
pnpm dev:todo:server # Hono/Bun compute server (port 3019)
pnpm dev:todo:local # Web + sync + Hono (no auth/NestJS needed)
pnpm dev:todo:full # Everything incl. auth + NestJS legacy
pnpm dev:todo:local # Web + sync + server (no auth needed)
pnpm dev:todo:full # Everything incl. auth + DB setup
```
### Adding Local-First to a New App
@ -696,8 +641,7 @@ Full migration plan: `.claude/plans/local-first-architecture-migration.md`
| Package | Purpose |
| ------------------------------- | ----------------------------------------------- |
| `@manacore/local-store` | Local-first data layer (Dexie.js + sync engine) |
| `@manacore/shared-nestjs-auth` | NestJS JWT validation guards via mana-core-auth |
| `@mana-core/nestjs-integration` | NestJS module with auth guards + credit client |
| `@manacore/shared-hono` | Shared Hono middleware (auth, health, errors) |
| `@manacore/shared-auth` | Client-side auth service for web/mobile apps |
| `@manacore/shared-storage` | S3-compatible storage (MinIO) |
| `@manacore/shared-types` | Common TypeScript types |
@ -756,7 +700,7 @@ pnpm docker:up
| `contacts-storage` | Contacts | Contact avatars/files |
| `storage-storage` | Storage | Cloud drive files |
### Usage in Backend
### Usage in Server
```typescript
import { createPictureStorage, generateUserFileKey, getContentType } from '@manacore/shared-storage';
@ -944,7 +888,7 @@ docker compose -f docker-compose.macmini.yml logs -f # View logs
All apps build on shared base images to reduce build time and memory usage:
- **`sveltekit-base:local`** (`docker/Dockerfile.sveltekit-base`) — All shared packages for SvelteKit web apps
- **`nestjs-base:local`** (`docker/Dockerfile.nestjs-base`) — All shared packages for NestJS backends
- **`hono-server`** (`docker/Dockerfile.hono-server`) — Hono/Bun compute server template
Rebuild base images after shared package changes: `./scripts/mac-mini/build-app.sh --base`
@ -1010,7 +954,7 @@ The script reads `.env.development` and generates platform-specific `.env` files
- **Expo mobile**: `EXPO_PUBLIC_*` prefix
- **SvelteKit web**: `PUBLIC_*` prefix
- **NestJS backend**: No prefix
- **Hono/Bun server**: No prefix
### Key Files
@ -1041,12 +985,12 @@ PUBLIC_SUPABASE_URL=...
PUBLIC_SUPABASE_ANON_KEY=...
```
**Backend (NestJS):**
**Server (Hono/Bun):**
```
SUPABASE_URL=...
SUPABASE_SERVICE_ROLE_KEY=...
PORT=...
DATABASE_URL=...
MANA_CORE_AUTH_URL=...
```
## Project-Specific Documentation

View file

@ -6,7 +6,7 @@
| App | Port | URL |
|-----|------|-----|
| Backend | 3014 | http://localhost:3014 |
| Server | 3014 | http://localhost:3014 |
| Web App | 5179 | http://localhost:5179 |
| Landing Page | 4322 | http://localhost:4322 |
| Mobile | 8081 | Expo Go |
@ -16,7 +16,7 @@
```
apps/calendar/
├── apps/
│ ├── backend/ # NestJS API server (@calendar/backend)
│ ├── server/ # Hono/Bun compute server (@calendar/server)
│ │ └── src/
│ │ ├── main.ts
│ │ ├── app.module.ts
@ -114,11 +114,12 @@ apps/calendar/
pnpm calendar:dev # Run all calendar apps
# Einzelne Apps starten
pnpm dev:calendar:backend # Start backend server (port 3014)
pnpm dev:calendar:server # Start server (port 3014)
pnpm dev:calendar:web # Start web app (port 5179)
pnpm dev:calendar:landing # Start landing page (port 4322)
pnpm dev:calendar:mobile # Start mobile app [TODO]
pnpm dev:calendar:app # Start web + backend together
pnpm dev:calendar:app # Start web + server together
pnpm dev:calendar:local # Start web + sync (no auth needed)
# Datenbank
pnpm calendar:db:push # Push schema to database
@ -126,7 +127,7 @@ pnpm calendar:db:studio # Open Drizzle Studio
pnpm calendar:db:seed # Seed initial data
```
### Backend (apps/calendar/apps/backend)
### Server (apps/calendar/apps/server)
```bash
pnpm dev # Start with hot reload
@ -157,7 +158,7 @@ pnpm preview # Preview build
| Layer | Technology |
|-------|------------|
| **Backend** | NestJS 10, Drizzle ORM, PostgreSQL |
| **Server** | Hono + Bun, Drizzle ORM, PostgreSQL |
| **Web** | SvelteKit 2.x, Svelte 5 (runes mode), Tailwind CSS 4 |
| **Landing** | Astro 5.x, Tailwind CSS |
| **Mobile** | React Native 0.81 + Expo SDK 54, NativeWind [TODO] |
@ -224,7 +225,7 @@ eventsStore.updateEvent(id, data)
eventsStore.deleteEvent(id)
```
### Backend API Endpoints
### Server API Endpoints
#### Health
@ -400,7 +401,7 @@ FREQ=WEEKLY;UNTIL=20241231T235959Z # Wöchentlich bis Ende 2024
## Environment Variables
### Backend (.env)
### Server (.env)
```env
NODE_ENV=development
@ -544,11 +545,11 @@ pnpm calendar:db:push
### 2. Apps starten
```bash
# Backend + Web zusammen
# Server + Web zusammen
pnpm dev:calendar:app
# Oder einzeln:
pnpm dev:calendar:backend # Terminal 1
pnpm dev:calendar:server # Terminal 1
pnpm dev:calendar:web # Terminal 2
pnpm dev:calendar:landing # Terminal 3 (optional)
```
@ -653,7 +654,7 @@ pnpm --filter @calendar/web test:e2e
1. **Authentication**: Nutzt Mana Core Auth (JWT im Authorization Header)
2. **Database**: PostgreSQL mit Drizzle ORM (Port 5432)
3. **Port**: Backend läuft auf Port 3014
3. **Port**: Server läuft auf Port 3014
4. **Recurrence**: Verwendet RFC 5545 RRULE Format
5. **i18n**: 5 Sprachen unterstützt (DE, EN, FR, ES, IT)
6. **Theme**: Ocean-Theme (Blautöne) als Standard

View file

@ -5,13 +5,13 @@
"description": "Calendar App - Personal and Shared Calendars with CalDAV/iCal Sync",
"scripts": {
"dev": "pnpm run --filter=@calendar/* --parallel dev",
"dev:backend": "pnpm --filter @calendar/backend dev",
"dev:server": "pnpm --filter @calendar/server dev",
"dev:web": "pnpm --filter @calendar/web dev",
"dev:landing": "pnpm --filter @calendar/landing dev",
"dev:mobile": "pnpm --filter @calendar/mobile dev",
"db:push": "pnpm --filter @calendar/backend db:push",
"db:studio": "pnpm --filter @calendar/backend db:studio",
"db:seed": "pnpm --filter @calendar/backend db:seed"
"db:push": "pnpm --filter @calendar/server db:push",
"db:studio": "pnpm --filter @calendar/server db:studio",
"db:seed": "pnpm --filter @calendar/server db:seed"
},
"devDependencies": {
"typescript": "^5.9.3"

View file

@ -5,7 +5,7 @@
```
apps/chat/
├── apps/
│ ├── backend/ # NestJS API server (@chat/backend)
│ ├── server/ # Hono/Bun compute server (@chat/server)
│ ├── landing/ # Astro marketing landing page (@chat/landing)
│ ├── web/ # SvelteKit web application (@chat/web)
│ └── mobile/ # Expo/React Native mobile app (@chat/mobile)
@ -23,8 +23,9 @@ pnpm chat:dev # Run all chat apps
pnpm dev:chat:mobile # Start mobile app
pnpm dev:chat:web # Start web app
pnpm dev:chat:landing # Start landing page
pnpm dev:chat:backend # Start backend server
pnpm dev:chat:full # Start backend + web + auth together
pnpm dev:chat:server # Start server
pnpm dev:chat:local # Start web + sync (no auth needed)
pnpm dev:chat:full # Start server + web + auth together
```
### Mobile App (chat/apps/mobile)
@ -38,7 +39,7 @@ pnpm build:preview # Build preview version
pnpm build:prod # Build production version
```
### Backend (apps/chat/apps/backend)
### Server (apps/chat/apps/server)
```bash
pnpm start:dev # Start with hot reload
@ -70,13 +71,13 @@ pnpm preview # Preview production build
- **Mobile**: React Native 0.76.7 + Expo SDK 52, NativeWind, Expo Router
- **Web**: SvelteKit 2.x, Svelte 5, Tailwind CSS 4
- **Landing**: Astro 5.16, Tailwind CSS
- **Backend**: NestJS 10, OpenRouter AI + mana-llm (local), Drizzle ORM, PostgreSQL
- **Server**: Hono + Bun, OpenRouter AI + mana-llm (local), Drizzle ORM, PostgreSQL
- **Auth**: Mana Core Auth (JWT)
- **Types**: TypeScript 5.x
## Architecture
### Backend API Endpoints
### Server API Endpoints
| Endpoint | Method | Description |
| --------------------------------- | ------ | --------------------------- |
@ -91,7 +92,7 @@ pnpm preview # Preview production build
### Environment Variables
#### Backend (.env)
#### Server (.env)
```env
# Cloud AI models via OpenRouter (optional if using only local models)
@ -162,13 +163,13 @@ PUBLIC_BACKEND_URL=http://localhost:3002
```bash
# Add new models to existing database
pnpm --filter @chat/backend db:add-local-models
pnpm --filter @chat/server db:add-local-models
```
## Quick Start
1. **Get OpenRouter API key** at https://openrouter.ai/keys
2. **Create `.env`** in `apps/chat/apps/backend/`:
2. **Create `.env`** in `apps/chat/apps/server/`:
```env
OPENROUTER_API_KEY=sk-or-v1-xxx
DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/chat
@ -182,13 +183,13 @@ pnpm --filter @chat/backend db:add-local-models
```
4. **Seed database** (first time only):
```bash
pnpm --filter @chat/backend db:push
pnpm --filter @chat/backend db:seed
pnpm --filter @chat/server db:push
pnpm --filter @chat/server db:seed
```
## Important Notes
1. **Security**: API keys are stored in the backend only - never in client apps
1. **Security**: API keys are stored in the server only - never in client apps
2. **Authentication**: Uses Mana Core Auth (JWT tokens)
3. **Database**: PostgreSQL with Drizzle ORM (uses shared Docker container)
4. **Deployment**: Backend runs on port 3002
4. **Deployment**: Server runs on port 3002

View file

@ -5,7 +5,7 @@
```
apps/contacts/
├── apps/
│ ├── backend/ # NestJS API server (@contacts/backend) - Port 3015
│ ├── server/ # Hono/Bun compute server (@contacts/server) - Port 3015
│ ├── landing/ # Astro marketing landing page (@contacts/landing)
│ ├── web/ # SvelteKit web application (@contacts/web) - Port 5184
│ └── mobile/ # Expo/React Native mobile app (@contacts/mobile)
@ -23,8 +23,9 @@ pnpm contacts:dev # Run all contacts apps
pnpm dev:contacts:mobile # Start mobile app
pnpm dev:contacts:web # Start web app
pnpm dev:contacts:landing # Start landing page
pnpm dev:contacts:backend # Start backend server
pnpm dev:contacts:app # Start web + backend together
pnpm dev:contacts:server # Start server
pnpm dev:contacts:app # Start web + server together
pnpm dev:contacts:local # Start web + sync (no auth needed)
```
### Mobile App (apps/contacts/apps/mobile)

View file

@ -5,13 +5,13 @@
"description": "Contacts App - Contact Management with Manacore Integration",
"scripts": {
"dev": "pnpm run --filter=@contacts/* --parallel dev",
"dev:backend": "pnpm --filter @contacts/backend dev",
"dev:server": "pnpm --filter @contacts/server dev",
"dev:web": "pnpm --filter @contacts/web dev",
"dev:landing": "pnpm --filter @contacts/landing dev",
"dev:mobile": "pnpm --filter @contacts/mobile dev",
"db:push": "pnpm --filter @contacts/backend db:push",
"db:studio": "pnpm --filter @contacts/backend db:studio",
"db:seed": "pnpm --filter @contacts/backend db:seed"
"db:push": "pnpm --filter @contacts/server db:push",
"db:studio": "pnpm --filter @contacts/server db:studio",
"db:seed": "pnpm --filter @contacts/server db:seed"
},
"devDependencies": {
"typescript": "^5.9.3"

View file

@ -13,7 +13,7 @@ AI-powered document management and context system for knowledge organization.
```
apps/context/
├── apps/
│ ├── backend/ # NestJS API server (@context/backend)
│ ├── backend/ # Hono/Bun compute server (@context/server)
│ │ └── src/
│ │ ├── main.ts
│ │ ├── app.module.ts
@ -45,7 +45,7 @@ apps/context/
```bash
# From monorepo root
pnpm dev:context:full # Start auth + backend + web (with DB setup)
pnpm dev:context:backend # Start backend only (port 3020)
pnpm dev:context:server # Start backend only (port 3020)
pnpm dev:context:web # Start web only (port 5192)
pnpm dev:context:app # Start web + backend together
pnpm dev:context:mobile # Start mobile app
@ -61,7 +61,7 @@ pnpm setup:db:context # Create DB + push schema
| Layer | Technology |
|-------|------------|
| **Backend** | NestJS 10, Drizzle ORM, PostgreSQL |
| **Backend** | Hono + Bun, Drizzle ORM, PostgreSQL |
| **Web** | SvelteKit 2.x, Svelte 5 (runes mode), Tailwind CSS 4 |
| **Mobile** | React Native 0.76 + Expo SDK 52, NativeWind |
| **Auth** | Mana Core Auth (JWT) |

View file

@ -5,7 +5,7 @@
"scripts": {
"dev": "turbo run dev",
"dev:web": "pnpm --filter @context/web dev",
"dev:backend": "pnpm --filter @context/backend dev",
"dev:server": "pnpm --filter @context/server dev",
"dev:mobile": "pnpm --filter @context/mobile dev"
}
}

View file

@ -25,16 +25,16 @@ Each backend service has its own database and schema:
```bash
# Push schema changes (development)
pnpm --filter @chat/backend db:push
pnpm --filter @chat/server db:push
# Generate migration files
pnpm --filter @chat/backend db:generate
pnpm --filter @chat/server db:generate
# Run migrations
pnpm --filter @chat/backend db:migrate
pnpm --filter @chat/server db:migrate
# Open Drizzle Studio (database GUI)
pnpm --filter @chat/backend db:studio
pnpm --filter @chat/server db:studio
```
## Development Workflow
@ -60,13 +60,13 @@ For local development, use `db:push` to quickly sync schema changes:
2. **Push changes**
```bash
pnpm --filter @chat/backend db:push
pnpm --filter @chat/server db:push
```
3. **Verify in Studio**
```bash
pnpm --filter @chat/backend db:studio
pnpm --filter @chat/server db:studio
```
</Steps>
@ -82,7 +82,7 @@ For production, use proper migration files:
1. **Generate migration**
```bash
pnpm --filter @chat/backend db:generate
pnpm --filter @chat/server db:generate
```
This creates a timestamped SQL file in `drizzle/migrations/`.
@ -99,7 +99,7 @@ For production, use proper migration files:
3. **Run migration**
```bash
pnpm --filter @chat/backend db:migrate
pnpm --filter @chat/server db:migrate
```
</Steps>
@ -241,7 +241,7 @@ export class UsersService {
Schema not pushed:
```bash
pnpm --filter @chat/backend db:push --force
pnpm --filter @chat/server db:push --force
```
### Migration conflicts
@ -253,7 +253,7 @@ Reset and regenerate:
rm -rf apps/chat/apps/backend/drizzle/migrations/*
# Regenerate
pnpm --filter @chat/backend db:generate
pnpm --filter @chat/server db:generate
```
### Connection refused

View file

@ -139,7 +139,7 @@ If you see errors about missing tables/columns:
```bash
# Push the latest schema
pnpm --filter @chat/backend db:push --force
pnpm --filter @chat/server db:push --force
```
### Port already in use

View file

@ -16,13 +16,13 @@ Manacore uses Jest for backend testing and Vitest for frontend testing.
pnpm test
# Run tests for specific project
pnpm --filter @chat/backend test
pnpm --filter @chat/server test
# Run tests in watch mode
pnpm --filter @chat/backend test:watch
pnpm --filter @chat/server test:watch
# Run with coverage
pnpm --filter @chat/backend test:cov
pnpm --filter @chat/server test:cov
```
## Test Structure

View file

@ -124,7 +124,7 @@ Turborepo handles task orchestration with:
| Type | Pattern | Example |
|------|---------|---------|
| App package | `@{project}/{app}` | `@chat/backend` |
| App package | `@{project}/{app}` | `@chat/server` |
| Shared package | `@manacore/shared-{name}` | `@manacore/shared-auth` |
| Service | `@mana-{name}/service` | `@mana-search/service` |

View file

@ -15,7 +15,7 @@ AI chat application supporting multiple language models including local (Ollama)
| Component | Technology | Port |
|-----------|------------|------|
| Backend | NestJS | 3002 |
| Server | Hono/Bun | 3002 |
| Web | SvelteKit | 5173 |
| Mobile | Expo | - |
| Landing | Astro | - |
@ -27,7 +27,7 @@ AI chat application supporting multiple language models including local (Ollama)
pnpm dev:chat:full
# Or individual components
pnpm dev:chat:backend
pnpm dev:chat:server
pnpm dev:chat:web
pnpm dev:chat:mobile
```
@ -36,8 +36,8 @@ pnpm dev:chat:mobile
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Client │────>│ Backend │────>│ AI Models │
│ (Web/Mobile)│ │ (NestJS) │ │ Ollama/API │
│ Client │────>│ Server │────>│ AI Models │
│ (Web/Mobile)│ │ (Hono/Bun) │ │ Ollama/API │
└─────────────┘ └──────┬──────┘ └─────────────┘
@ -147,21 +147,21 @@ export const messages = pgTable('messages', {
First time setup requires seeding AI models:
```bash
pnpm --filter @chat/backend db:push
pnpm --filter @chat/backend db:seed
pnpm --filter @chat/server db:push
pnpm --filter @chat/server db:seed
```
### Run Tests
```bash
pnpm --filter @chat/backend test
pnpm --filter @chat/backend test:e2e
pnpm --filter @chat/server test
pnpm --filter @chat/server test:e2e
```
### Open Database GUI
```bash
pnpm --filter @chat/backend db:studio
pnpm --filter @chat/server db:studio
```
## Links

View file

@ -7,7 +7,7 @@ Mukke is a web application for managing your music library, playing tracks, and
```
apps/mukke/
├── apps/
│ ├── backend/ # NestJS API (port 3010)
│ ├── backend/ # Hono/Bun server (port 3010)
│ ├── web/ # SvelteKit app (port 5180)
│ └── landing/ # Astro marketing page
├── packages/
@ -23,7 +23,7 @@ pnpm dev:mukke:full
# Or start components individually
pnpm docker:up # Start PostgreSQL, Redis, MinIO
pnpm --filter @mukke/backend dev # Backend on port 3010
pnpm --filter @mukke/server dev # Server on port 3010
pnpm --filter @mukke/web dev # Web on port 5180
pnpm --filter @mukke/landing dev # Landing page
```
@ -150,7 +150,7 @@ Playback uses HTML5 Audio (browser-native codec support). Upload accepts any `au
| Waveform | wavesurfer.js 7.x |
| BPM Detection | Web Audio API (peak detection) |
| Metadata | music-metadata (server-side) |
| Backend | NestJS 10, Drizzle ORM |
| Backend | Hono + Bun, Drizzle ORM |
| Database | PostgreSQL |
| Storage | MinIO (S3-compatible) |
| Auth | mana-core-auth |
@ -178,14 +178,14 @@ PUBLIC_BACKEND_URL=http://localhost:3010
```bash
# Database
pnpm --filter @mukke/backend db:push # Push schema
pnpm --filter @mukke/backend db:studio # Open Drizzle Studio
pnpm --filter @mukke/server db:push # Push schema
pnpm --filter @mukke/server db:studio # Open Drizzle Studio
# Type checking
pnpm --filter @mukke/backend type-check
pnpm --filter @mukke/server type-check
pnpm --filter @mukke/web type-check
# Build
pnpm --filter @mukke/backend build
pnpm --filter @mukke/server build
pnpm --filter @mukke/web build
```

View file

@ -15,7 +15,7 @@
```
apps/nutriphi/
├── apps/
│ ├── backend/ # NestJS API server (@nutriphi/backend)
│ ├── backend/ # Hono/Bun compute server (@nutriphi/server)
│ │ └── src/
│ │ ├── main.ts
│ │ ├── app.module.ts
@ -71,7 +71,7 @@ apps/nutriphi/
pnpm nutriphi:dev
# Individual apps
pnpm dev:nutriphi:backend # Backend (port 3015)
pnpm dev:nutriphi:server # Backend (port 3015)
pnpm dev:nutriphi:web # Web app (port 5180)
pnpm dev:nutriphi:landing # Landing page (port 4323)
pnpm dev:nutriphi:app # Web + backend together
@ -108,7 +108,7 @@ pnpm build # Build for production
| Layer | Technology |
|-------|------------|
| **Backend** | NestJS 10, Drizzle ORM, PostgreSQL |
| **Backend** | Hono + Bun, Drizzle ORM, PostgreSQL |
| **AI** | Google Gemini 2.5 Flash |
| **Web** | SvelteKit 2.x, Svelte 5 (runes mode), Tailwind CSS 4 |
| **Landing** | Astro 5.x, Tailwind CSS |
@ -306,7 +306,7 @@ GEMINI_API_KEY=your-gemini-api-key
pnpm dev:nutriphi:app
# Or individually:
pnpm dev:nutriphi:backend # Terminal 1
pnpm dev:nutriphi:server # Terminal 1
pnpm dev:nutriphi:web # Terminal 2
pnpm dev:nutriphi:landing # Terminal 3
```

View file

@ -5,17 +5,17 @@
"description": "NutriPhi - AI-powered nutrition tracking with photo analysis",
"scripts": {
"dev": "pnpm run --filter=@nutriphi/* --parallel dev",
"dev:backend": "pnpm --filter @nutriphi/backend dev",
"dev:server": "pnpm --filter @nutriphi/server dev",
"dev:web": "pnpm --filter @nutriphi/web dev",
"dev:landing": "pnpm --filter @nutriphi/landing dev",
"db:push": "pnpm --filter @nutriphi/backend db:push",
"db:studio": "pnpm --filter @nutriphi/backend db:studio",
"db:seed": "pnpm --filter @nutriphi/backend db:seed",
"test": "pnpm --filter @nutriphi/backend test && pnpm --filter @nutriphi/shared test && pnpm --filter @nutriphi/web test",
"test:backend": "pnpm --filter @nutriphi/backend test",
"db:push": "pnpm --filter @nutriphi/server db:push",
"db:studio": "pnpm --filter @nutriphi/server db:studio",
"db:seed": "pnpm --filter @nutriphi/server db:seed",
"test": "pnpm --filter @nutriphi/server test && pnpm --filter @nutriphi/shared test && pnpm --filter @nutriphi/web test",
"test:backend": "pnpm --filter @nutriphi/server test",
"test:web": "pnpm --filter @nutriphi/web test",
"test:shared": "pnpm --filter @nutriphi/shared test",
"test:cov": "pnpm --filter @nutriphi/backend test:cov"
"test:cov": "pnpm --filter @nutriphi/server test:cov"
},
"devDependencies": {
"typescript": "^5.9.3"

View file

@ -7,7 +7,7 @@ AI image generation app using Replicate API with freemium credit system.
```
apps/picture/
├── apps/
│ ├── backend/ # NestJS API (port 3006)
│ ├── backend/ # Hono/Bun server (port 3006)
│ ├── mobile/ # Expo React Native app
│ ├── web/ # SvelteKit web app
│ └── landing/ # Astro marketing page
@ -21,7 +21,7 @@ apps/picture/
pnpm dev:picture:full # Start backend + web + auto DB setup
# Individual apps
pnpm --filter @picture/backend dev # Backend only (port 3006)
pnpm --filter @picture/server dev # Backend only (port 3006)
pnpm --filter @picture/web dev # Web only
pnpm --filter @picture/mobile dev # Mobile only
```

View file

@ -5,7 +5,7 @@
```
apps/planta/
├── apps/
│ ├── backend/ # NestJS API server (@planta/backend)
│ ├── backend/ # Hono/Bun compute server (@planta/server)
│ └── web/ # SvelteKit web application (@planta/web)
├── packages/
│ └── shared/ # Shared types, utils (@planta/shared)
@ -19,7 +19,7 @@ apps/planta/
```bash
pnpm planta:dev # Run all planta apps
pnpm dev:planta:web # Start web app
pnpm dev:planta:backend # Start backend server
pnpm dev:planta:server # Start backend server
pnpm dev:planta:app # Start web + backend together
pnpm dev:planta:full # Start auth + backend + web with DB setup
```
@ -45,7 +45,7 @@ pnpm preview # Preview production build
## Technology Stack
- **Web**: SvelteKit 2.x, Svelte 5 (runes mode), Tailwind CSS
- **Backend**: NestJS 10, Drizzle ORM, PostgreSQL
- **Backend**: Hono + Bun, Drizzle ORM, PostgreSQL
- **AI**: Google Gemini Vision for plant analysis
- **Storage**: MinIO (S3-compatible)
- **Auth**: Mana Core Auth (JWT)

View file

@ -5,11 +5,11 @@
"description": "Planta - Plant Documentation & Care App",
"scripts": {
"dev": "turbo run dev",
"dev:backend": "pnpm --filter @planta/backend dev",
"dev:server": "pnpm --filter @planta/server dev",
"dev:web": "pnpm --filter @planta/web dev",
"db:push": "pnpm --filter @planta/backend db:push",
"db:studio": "pnpm --filter @planta/backend db:studio",
"db:seed": "pnpm --filter @planta/backend db:seed"
"db:push": "pnpm --filter @planta/server db:push",
"db:studio": "pnpm --filter @planta/server db:studio",
"db:seed": "pnpm --filter @planta/server db:seed"
},
"devDependencies": {
"typescript": "^5.9.3"

View file

@ -5,7 +5,7 @@
```
apps/storage/
├── apps/
│ ├── backend/ # NestJS API server (@storage/backend) - Port 3016
│ ├── backend/ # Hono/Bun compute server (@storage/server) - Port 3016
│ ├── landing/ # Astro marketing landing page (@storage/landing)
│ └── web/ # SvelteKit web application (@storage/web) - Port 5185
├── packages/

View file

@ -15,7 +15,7 @@
```
apps/todo/
├── apps/
│ ├── backend/ # NestJS API server (@todo/backend)
│ ├── backend/ # Hono/Bun compute server (@todo/server)
│ ├── web/ # SvelteKit web application (@todo/web)
│ └── landing/ # Astro marketing landing page (@todo/landing)
├── packages/
@ -75,7 +75,7 @@ pnpm preview # Preview build
| Layer | Technology |
|-------|------------|
| **Backend** | NestJS 10, Drizzle ORM, PostgreSQL |
| **Backend** | Hono + Bun, Drizzle ORM, PostgreSQL |
| **Web** | SvelteKit 2.x, Svelte 5 (runes mode), Tailwind CSS 4 |
| **Landing** | Astro 5.x, Tailwind CSS |
| **Auth** | Mana Core Auth (JWT) |
@ -300,7 +300,7 @@ Priority: explicit duration in text > history estimate > default fallback > none
```bash
# Unit tests
pnpm --filter @todo/backend test
pnpm --filter @todo/server test
pnpm --filter @todo/web test
# E2E tests

View file

@ -5,12 +5,12 @@
"description": "Todo App - Task Management for ManaCore Ecosystem",
"scripts": {
"dev": "pnpm run --filter=@todo/* --parallel dev",
"dev:backend": "pnpm --filter @todo/backend dev",
"dev:server": "pnpm --filter @todo/server dev",
"dev:web": "pnpm --filter @todo/web dev",
"dev:landing": "pnpm --filter @todo/landing dev",
"db:push": "pnpm --filter @todo/backend db:push",
"db:studio": "pnpm --filter @todo/backend db:studio",
"db:seed": "pnpm --filter @todo/backend db:seed"
"db:push": "pnpm --filter @todo/server db:push",
"db:studio": "pnpm --filter @todo/server db:studio",
"db:seed": "pnpm --filter @todo/server db:seed"
},
"devDependencies": {
"typescript": "^5.9.3"

View file

@ -8,7 +8,7 @@ GPS tracking app with AI city guides. Location tracking runs locally via AsyncSt
apps/traces/
├── package.json # Orchestrator (name: traces)
├── apps/
│ ├── backend/ # @traces/backend (NestJS, Port 3026)
│ ├── backend/ # @traces/server (NestJS, Port 3026)
│ │ └── src/
│ │ ├── main.ts
│ │ ├── app.module.ts
@ -31,7 +31,7 @@ apps/traces/
```bash
# Development
pnpm dev:traces:mobile # Start Expo app
pnpm dev:traces:backend # Start NestJS backend
pnpm dev:traces:server # Start NestJS backend
pnpm dev:traces:full # Start auth + backend + mobile
# Database

View file

@ -1,58 +0,0 @@
# syntax=docker/dockerfile:1
# Shared builder base for all NestJS backends
# Contains pre-built shared packages and dependencies
#
# Usage in backend Dockerfiles:
# FROM nestjs-base:local AS builder
# COPY apps/myapp/apps/backend ./apps/myapp/apps/backend
# RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile --ignore-scripts
# WORKDIR /app/apps/myapp/apps/backend
# RUN pnpm build
FROM node:20-alpine
# Install pnpm
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
WORKDIR /app
# Copy root workspace files
COPY pnpm-workspace.yaml ./
COPY package.json ./
COPY pnpm-lock.yaml ./
COPY patches ./patches
# Copy ALL shared packages used by NestJS backends
COPY packages/credit-operations ./packages/credit-operations
COPY packages/mana-core-nestjs-integration ./packages/mana-core-nestjs-integration
COPY packages/manadeck-database ./packages/manadeck-database
COPY packages/shared-drizzle-config ./packages/shared-drizzle-config
COPY packages/shared-errors ./packages/shared-errors
COPY packages/shared-error-tracking ./packages/shared-error-tracking
COPY packages/shared-nestjs-auth ./packages/shared-nestjs-auth
COPY packages/shared-nestjs-health ./packages/shared-nestjs-health
COPY packages/shared-nestjs-metrics ./packages/shared-nestjs-metrics
COPY packages/shared-nestjs-setup ./packages/shared-nestjs-setup
COPY packages/shared-llm ./packages/shared-llm
COPY packages/shared-storage ./packages/shared-storage
COPY packages/shared-tsconfig ./packages/shared-tsconfig
# Install dependencies
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile --ignore-scripts
# Build all shared packages (in dependency order)
RUN cd packages/shared-errors && pnpm build \
&& cd /app/packages/shared-nestjs-auth && pnpm build \
&& cd /app/packages/shared-nestjs-health && pnpm build \
&& cd /app/packages/shared-nestjs-metrics && pnpm build \
&& cd /app/packages/shared-nestjs-setup && pnpm build \
&& cd /app/packages/shared-error-tracking && pnpm build \
&& cd /app/packages/shared-storage && pnpm build \
&& cd /app/packages/shared-llm && pnpm build \
&& cd /app/packages/shared-drizzle-config && pnpm build 2>/dev/null || true \
&& cd /app/packages/credit-operations && pnpm build \
&& cd /app/packages/mana-core-nestjs-integration && pnpm build \
&& cd /app/packages/manadeck-database && pnpm build 2>/dev/null || true
WORKDIR /app

View file

@ -1,89 +0,0 @@
# syntax=docker/dockerfile:1
# Multi-stage Dockerfile for NestJS backend services
# This is a template - copy and customize for each backend service
# ============================================
# Build Stage
# ============================================
FROM node:20-alpine AS builder
# Install pnpm
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
WORKDIR /app
# Copy workspace files
COPY pnpm-workspace.yaml ./
COPY package.json ./
COPY pnpm-lock.yaml ./
# Copy all shared packages (adjust based on dependencies)
COPY packages/ ./packages/
# Copy the specific backend service
# CUSTOMIZE THIS: Replace with your service path
# Example: COPY apps/chat/apps/backend ./apps/chat/apps/backend
ARG SERVICE_PATH
COPY ${SERVICE_PATH} ./${SERVICE_PATH}
# Install all dependencies (including devDependencies for build)
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile
# Build shared packages first
RUN pnpm run build:packages
# Build the backend service
WORKDIR /app/${SERVICE_PATH}
RUN pnpm build
# ============================================
# Production Stage
# ============================================
FROM node:20-alpine AS production
# Install pnpm and system dependencies
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate \
&& apk add --no-cache \
postgresql-client \
curl \
wget
WORKDIR /app
# Copy workspace files
COPY --from=builder /app/pnpm-workspace.yaml ./
COPY --from=builder /app/package.json ./
COPY --from=builder /app/pnpm-lock.yaml ./
# Copy built packages and service
COPY --from=builder /app/packages ./packages
ARG SERVICE_PATH
COPY --from=builder /app/${SERVICE_PATH} ./${SERVICE_PATH}
# Install production dependencies only
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --prod --frozen-lockfile
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nestjs -u 1001
# Change ownership
RUN chown -R nestjs:nodejs /app
# Switch to non-root user
USER nestjs
# Set working directory to service
WORKDIR /app/${SERVICE_PATH}
# Expose port (customize per service)
ARG PORT=3000
EXPOSE ${PORT}
# Health check (customize endpoint per service)
ARG HEALTH_PATH=/health
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:${PORT}${HEALTH_PATH} || exit 1
# Start the application
CMD ["node", "dist/main.js"]

View file

@ -1,945 +0,0 @@
# Backend-Architektur im Manacore Monorepo
Diese Dokumentation beschreibt die Backend-Implementierungen aller Projekte im Manacore Monorepo.
## Übersicht
Das Monorepo enthält 6 Hauptprojekte mit unterschiedlichen Backend-Architekturen:
| Projekt | Backend-Typ | Datenbank | Status |
|---------|-------------|-----------|--------|
| **Maerchenzauber** | NestJS v10 | Supabase (PostgreSQL) | Aktiv |
| **Manadeck** | NestJS v11 | PostgreSQL + Drizzle ORM | Aktiv |
| **Uload** | NestJS v11 | PostgreSQL + Drizzle ORM | Aktiv |
| **Picture** | Kein Backend | - | Frontend-only |
| **Memoro** | Kein Backend | - | Frontend-only |
| **Manacore** | Kein Backend (extern) | - | Externes Backend |
---
## 1. Maerchenzauber
**Pfad:** `/maerchenzauber/apps/backend`
**Zweck:** KI-gestützte Kindergeschichten-Generierung mit benutzerdefinierten Charakteren.
### Technologie-Stack
- **Framework:** NestJS 10.0.0
- **Datenbank:** Supabase (PostgreSQL)
- **ORM:** `@supabase/supabase-js` v2.81.1
- **AI-Services:** Azure OpenAI, Google Gemini, Replicate
### Architektur
```
apps/backend/
├── src/
│ ├── character/ # Charakter-Modul
│ │ ├── character.controller.ts
│ │ ├── character.service.ts
│ │ └── character.repository.ts
│ ├── story/ # Story-Modul
│ │ ├── story.controller.ts
│ │ ├── story.service.ts
│ │ └── pipelines/ # Story-Generierung-Pipelines
│ ├── core/ # Kern-Services
│ │ └── services/
│ │ └── prompting.service.ts
│ ├── settings/ # Benutzereinstellungen
│ ├── health/ # Health-Checks
│ └── feedback/ # Feedback-Modul
```
### Datenbank-Schema
**Tabellen:**
- `characters` - Benutzercharaktere
- `stories` - Generierte Geschichten
- `story_collections` - Sammlungen von Geschichten
- `user_settings` - Benutzereinstellungen
**Sicherheit:** Row-Level Security (RLS) für Datenzugriffskontrolle
### Authentifizierung
Mana Core Integration via `@mana-core/nestjs-integration`:
```typescript
// Beispiel: Geschützter Endpoint
@UseGuards(AuthGuard)
@Get('characters')
async getCharacters(@CurrentUser() user: User) {
return this.characterService.findByUser(user.id);
}
```
**Auth-Endpoint:** `https://mana-core-middleware-111768794939.europe-west3.run.app`
### AI-Services
| Service | Verwendung | API |
|---------|------------|-----|
| Azure OpenAI (GPT-4) | Story-Generierung | `MAERCHENZAUBER_AZURE_OPENAI_ENDPOINT` |
| Google Gemini | Charakter-Generierung | `GOOGLE_GEMINI_API_KEY` |
| Replicate (Flux) | Bildgenerierung | `REPLICATE_API_TOKEN` |
### File Storage
- **Provider:** Supabase Storage
- **Bucket:** `maerchenzauber`
- **Verwendung:** Charakter- und Story-Bilder
### Deployment
- **Plattform:** Google Cloud Run
- **Region:** europe-west3
- **URL:** `https://storyteller-backend-111768794939.europe-west3.run.app`
- **Port:** 3002 (Development)
---
## 2. Manadeck
**Pfad:** `/manadeck/apps/backend`
**Zweck:** KI-gestützte Lernkarten-Generierung (Flashcards, Quizzes, Mixed).
### Technologie-Stack
- **Framework:** NestJS 11.0.1
- **Datenbank:** PostgreSQL 16
- **ORM:** Drizzle ORM
- **AI-Service:** Google Gemini API
### Architektur
```
apps/backend/
├── src/
│ ├── api.controller.ts # Haupt-API-Endpoints
│ ├── public.controller.ts # Öffentliche Endpoints
│ ├── health.controller.ts # Health-Checks
│ ├── ai.service.ts # AI-Generierung
│ └── repositories/
│ ├── deck.repository.ts
│ ├── card.repository.ts
│ ├── user-stats.repository.ts
│ └── deck-template.repository.ts
```
### Datenbank-Package
Das Datenbank-Schema ist in einem separaten Package ausgelagert:
**Pfad:** `/packages/manadeck-database`
```typescript
// Verwendung im Backend
import { db, schema } from '@manacore/manadeck-database';
const decks = await db.query.decks.findMany({
where: eq(schema.decks.userId, userId)
});
```
**Drizzle-Konfiguration:**
```typescript
// drizzle.config.ts
export default {
schema: './src/schema/*',
out: './migrations',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL
}
};
```
### Authentifizierung
```typescript
import { AuthGuard, CurrentUser } from '@mana-core/nestjs-integration';
@Controller('api')
@UseGuards(AuthGuard)
export class ApiController {
@Post('decks')
async createDeck(@CurrentUser() user: User, @Body() dto: CreateDeckDto) {
// Credit-Prüfung und Deck-Erstellung
}
}
```
### Credit-System
Integration mit Mana Core Credit Service:
```typescript
import { CreditClientService } from '@mana-core/nestjs-integration';
@Injectable()
export class AiService {
constructor(private creditClient: CreditClientService) {}
async generateDeck(userId: string, input: GenerateInput) {
// 1. Credit-Balance prüfen
const hasCredits = await this.creditClient.checkBalance(userId, 'DECK_CREATION');
// 2. Deck generieren
const deck = await this.generateWithGemini(input);
// 3. Credits abziehen
await this.creditClient.deduct(userId, 'DECK_CREATION');
return deck;
}
}
```
### AI-Generierung
**Unterstützte Kartentypen:**
- `text` - Textbasierte Karten
- `flashcard` - Klassische Lernkarten
- `quiz` - Multiple-Choice Quiz
- `mixed` - Gemischte Inhalte
**Schwierigkeitsgrade:**
- `beginner`
- `intermediate`
- `advanced`
### Docker-Setup
```yaml
# docker-compose.yml (Lokale Entwicklung)
services:
postgres:
image: postgres:16
ports:
- "5433:5432"
environment:
POSTGRES_DB: manadeck
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
pgadmin:
image: dpage/pgadmin4
ports:
- "5050:80"
```
### Deployment
- **Docker Image:** Multi-stage Build (Node 18-alpine)
- **Port:** 8080
- **Health-Check:** `/health`
---
## 3. Uload
**Pfad:** `/uload/apps/backend`
**Zweck:** URL-Shortener mit Link-Analytics.
### Technologie-Stack
- **Framework:** NestJS 11.0.1
- **Datenbank:** PostgreSQL 16
- **ORM:** Drizzle ORM
- **Cache:** Redis (optional)
### Architektur
```
uload/apps/backend/
├── src/
│ ├── main.ts
│ ├── app.module.ts
│ ├── config/
│ │ └── validation.schema.ts
│ ├── controllers/
│ │ ├── redirect.controller.ts # GET /:code (public redirect)
│ │ ├── links.controller.ts # CRUD /api/links
│ │ ├── analytics.controller.ts # GET /api/analytics
│ │ └── health.controller.ts
│ ├── services/
│ │ ├── links.service.ts
│ │ ├── redirect.service.ts
│ │ └── analytics.service.ts
│ └── database/
│ ├── database.module.ts
│ └── repositories/
│ ├── link.repository.ts
│ └── click.repository.ts
├── Dockerfile
└── package.json
```
### Datenbank-Package
**Pfad:** `/packages/uload-database`
```typescript
// Verwendung im Backend
import { db, links, clicks, eq, desc } from '@manacore/uload-database';
const userLinks = await db.query.links.findMany({
where: eq(links.userId, userId),
orderBy: desc(links.createdAt)
});
```
### API Endpoints
| Endpoint | Method | Auth | Beschreibung |
|----------|--------|------|--------------|
| `/:code` | GET | Public | Redirect zu Original-URL |
| `/api/links` | GET | Protected | Liste aller Links |
| `/api/links` | POST | Protected | Link erstellen |
| `/api/links/:id` | GET | Protected | Link Details |
| `/api/links/:id` | PATCH | Protected | Link aktualisieren |
| `/api/links/:id` | DELETE | Protected | Link löschen |
| `/api/analytics/:linkId` | GET | Protected | Link-Statistiken |
| `/health` | GET | Public | Health Check |
### Authentifizierung
Mana Core Integration via `@mana-core/nestjs-integration`:
```typescript
import { AuthGuard, CurrentUser } from '@mana-core/nestjs-integration';
@Controller('api/links')
@UseGuards(AuthGuard)
export class LinksController {
@Get()
async getLinks(@CurrentUser() user: any) {
return this.linksService.getLinks(user.sub);
}
}
```
### Deployment
- **Docker Image:** Multi-stage Build (Node 20-alpine)
- **Port:** 3003
- **Health-Check:** `/health`
---
## 4. Picture
**Pfad:** `/picture`
**Zweck:** Bild- und Medienverwaltung.
### Architektur
**Kein dediziertes Backend.** Picture verwendet:
- SvelteKit Server-Routes für Backend-Logik
- Mana Core für Authentifizierung
- Shared Packages aus `/packages`
```
picture/
├── apps/
│ ├── mobile/ # React Native Expo
│ ├── web/ # SvelteKit
│ └── landing/ # Astro
└── packages/
├── design-tokens/ # Design System
├── mobile-ui/ # Mobile UI Components
└── shared/ # Utilities
```
---
## 5. Memoro
**Pfad:** `/memoro`
**Zweck:** Legacy-Content und Memory-Preservation.
### Architektur
**Kein dediziertes Backend.** Memoro verwendet:
- SvelteKit Server-Routes
- Mana Core für Authentifizierung
- Supabase (Legacy-Konfiguration vorhanden)
```
memoro/
├── apps/
│ ├── mobile/
│ ├── web/
│ └── landing/
└── supabase/ # Legacy Supabase Config
```
---
## 6. Manacore
**Pfad:** `/manacore`
**Zweck:** Core-Authentifizierung und Credit-System.
### Architektur
Das Manacore-Backend ist **extern gehostet** und nicht Teil des Monorepos:
- **URL:** `https://mana-core-middleware-111768794939.europe-west3.run.app`
- **Integration:** Via `@mana-core/nestjs-integration` Package
```
manacore/
├── apps/
│ ├── mobile/ # Auth-Flow UI
│ ├── web/ # Dashboard
│ └── landing/ # Marketing
```
---
## Shared Packages für Backend
### @manacore/manadeck-database
PostgreSQL-Datenbankschema für Manadeck.
```
packages/manadeck-database/
├── src/
│ ├── schema/ # Drizzle Schema
│ ├── client.ts # DB Client
│ └── index.ts # Exports
├── drizzle.config.ts
└── docker-compose.yml
```
### @manacore/uload-database
PostgreSQL-Datenbankschema für Uload URL-Shortener.
```
packages/uload-database/
├── src/
│ ├── schema/
│ │ ├── users.ts
│ │ ├── links.ts
│ │ ├── clicks.ts
│ │ ├── tags.ts
│ │ ├── workspaces.ts
│ │ ├── accounts.ts
│ │ └── relations.ts
│ ├── client.ts # DB Client
│ └── index.ts # Exports
├── drizzle.config.ts
└── docker-compose.yml
```
### @mana-core/nestjs-integration
Externe Dependency für Backend-Integration:
```typescript
// Installation via git
"@mana-core/nestjs-integration": "git+https://github.com/mana-core/nestjs-integration.git"
```
**Bereitgestellte Features:**
- `AuthGuard` - JWT-Authentifizierung
- `@CurrentUser()` - User-Context Decorator
- `CreditClientService` - Credit-Operationen
- Konfigurationsmodule
---
## Authentifizierungs-Pattern
Alle Projekte nutzen zentrale **Mana Core Authentifizierung**:
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Frontend │────▶│ Project Backend │────▶│ Mana Core │
│ (Web/Mobile) │ │ (NestJS/etc.) │ │ Middleware │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ │ │
▼ ▼ ▼
shared-auth AuthGuard JWT Validation
shared-auth-ui @CurrentUser Credit Service
shared-auth-stores CreditClient User Management
```
### Frontend-Integration
```typescript
// Shared Auth Store (Svelte)
import { authStore } from '@manacore/shared-auth-stores';
// Login
await authStore.login(email, password);
// Token für API-Requests
const token = authStore.getAccessToken();
```
### Backend-Integration
```typescript
// NestJS Module Setup
@Module({
imports: [
ManaCoreModule.forRoot({
serviceKey: process.env.MANA_CORE_SERVICE_KEY,
baseUrl: process.env.MANA_CORE_URL,
}),
],
})
export class AppModule {}
```
---
## Datenbank-Migrationen
### Manadeck (Drizzle)
```bash
# Migration generieren
pnpm --filter @manacore/manadeck-database drizzle-kit generate
# Migration ausführen
pnpm --filter @manacore/manadeck-database drizzle-kit push
```
### Maerchenzauber (Supabase)
```bash
# Supabase CLI
supabase migration new <name>
supabase db push
```
---
## Umgebungsvariablen
### Maerchenzauber Backend
```env
# Supabase
SUPABASE_URL=
SUPABASE_SERVICE_ROLE_KEY=
# Mana Core
MANA_CORE_URL=https://mana-core-middleware-111768794939.europe-west3.run.app
MANA_CORE_SERVICE_KEY=
# AI Services
MAERCHENZAUBER_AZURE_OPENAI_ENDPOINT=
AZURE_OPENAI_API_KEY=
GOOGLE_GEMINI_API_KEY=
REPLICATE_API_TOKEN=
```
### Manadeck Backend
```env
# Database
DATABASE_URL=postgresql://postgres:postgres@localhost:5433/manadeck
# Mana Core
MANA_CORE_URL=
MANA_CORE_SERVICE_KEY=
# AI
GOOGLE_GEMINI_API_KEY=
# Server
PORT=8080
```
### Uload
```env
# Database
DATABASE_URL=postgresql://...
# Redis
REDIS_URL=redis://localhost:6379
# PocketBase
POCKETBASE_URL=
```
---
## Lokale Entwicklung
### Maerchenzauber Backend
```bash
cd maerchenzauber/apps/backend
pnpm install
pnpm run start:dev
# Läuft auf Port 3002
```
### Manadeck Backend
```bash
# 1. Datenbank starten
cd packages/manadeck-database
docker-compose up -d
# 2. Backend starten
cd manadeck/apps/backend
pnpm install
pnpm run start:dev
# Läuft auf Port 8080
```
### Uload
```bash
cd uload
docker-compose up -d # PostgreSQL + Redis
pnpm install
pnpm run dev
```
---
## Zusammenfassung
Das Manacore Monorepo verwendet verschiedene Backend-Strategien:
1. **Full Backend (NestJS):** Maerchenzauber, Manadeck - Für komplexe Geschäftslogik und AI-Integration
2. **Embedded Database (PocketBase):** Uload - Für einfache CRUD-Operationen
3. **Frontend-only:** Picture, Memoro - Server-Routes in SvelteKit
4. **External Backend:** Manacore - Zentrale Auth/Credit-Services
Alle Projekte teilen sich:
- Gemeinsame Authentifizierung via Mana Core
- Shared Packages für UI, Auth, Types
- Einheitliches Deployment-Pattern (Docker + Cloud Run)
---
## Vereinheitlichungs-Roadmap
### Aktuelle Fragmentierung
| Aspekt | Maerchenzauber | Manadeck | Uload |
|--------|----------------|----------|-------|
| Framework | NestJS v10 | NestJS v11 | PocketBase |
| Datenbank | Supabase | PostgreSQL | PocketBase + PG |
| ORM | @supabase/js | Drizzle | Drizzle |
| Auth | Mana Core | Mana Core | PocketBase + Mana Core |
---
### Strategie 1: Shared NestJS Backend Package
**Ziel:** Ein gemeinsames `@manacore/shared-backend` Package mit wiederverwendbaren Modulen.
```
packages/shared-backend/
├── src/
│ ├── auth/
│ │ ├── auth.module.ts
│ │ ├── auth.guard.ts
│ │ └── current-user.decorator.ts
│ ├── database/
│ │ ├── database.module.ts
│ │ ├── drizzle.provider.ts
│ │ └── base.repository.ts
│ ├── health/
│ │ └── health.module.ts
│ ├── credits/
│ │ └── credits.module.ts
│ └── common/
│ ├── filters/
│ ├── interceptors/
│ └── pipes/
```
**Vorteile:**
- Einheitliche Auth-Integration
- Wiederverwendbare Module
- Konsistente Error-Handling
**Aufwand:** Mittel
---
### Strategie 2: Einheitliche Datenbank-Strategie
#### Option A: Alles auf Drizzle + PostgreSQL (Empfohlen)
```typescript
// packages/shared-database/src/base-schema.ts
export const baseColumns = {
id: uuid('id').primaryKey().defaultRandom(),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
userId: text('user_id').notNull(),
};
// Projekt-spezifische Erweiterung
// maerchenzauber/database/schema/characters.ts
import { baseColumns } from '@manacore/shared-database';
export const characters = pgTable('characters', {
...baseColumns,
name: text('name').notNull(),
traits: jsonb('traits'),
});
```
**Migration von Supabase:**
- Supabase ist PostgreSQL → Schema kann übernommen werden
- RLS-Policies in Application-Layer verschieben
- Storage → S3/Cloudflare R2
#### Option B: Alles auf Supabase
```typescript
export const createProjectClient = (project: 'maerchenzauber' | 'manadeck' | 'uload') => {
return createClient(
process.env[`${project.toUpperCase()}_SUPABASE_URL`],
process.env[`${project.toUpperCase()}_SUPABASE_KEY`]
);
};
```
**Vorteile Supabase:**
- Eingebaute Auth (optional nutzbar)
- Storage inklusive
- Realtime-Subscriptions
- Edge Functions möglich
**Nachteile Supabase:**
- Vendor Lock-in
- Weniger Kontrolle über Schema
**Empfehlung:** Drizzle + PostgreSQL wegen Type-Safety, moderner API und keinem Vendor Lock-in.
---
### Strategie 3: Einheitliche Monorepo Backend Struktur
**Ziel-Architektur:**
```
packages/
├── shared-backend/ # Gemeinsame NestJS Module
│ ├── auth/
│ ├── database/
│ ├── health/
│ └── credits/
├── shared-database/ # Drizzle Basis-Schema
│ ├── base-schema.ts
│ ├── migrations/
│ └── client.ts
├── maerchenzauber-database/ # Projekt-Schema
├── manadeck-database/ # ✓ Existiert bereits
└── uload-database/ # Neu
apps/
├── maerchenzauber-backend/ # Nutzt shared-backend
├── manadeck-backend/ # Nutzt shared-backend
└── uload-backend/ # Neues NestJS Backend (ersetzt PocketBase)
```
---
### Strategie 4: Shared Backend als Service-Layer
**Ziel:** Gemeinsamer Service-Layer, projekt-spezifische Controller.
```typescript
// packages/shared-backend/src/services/ai.service.ts
@Injectable()
export class BaseAiService {
constructor(
private gemini: GeminiClient,
private credits: CreditService,
) {}
protected async generateWithCredits<T>(
userId: string,
operation: string,
generator: () => Promise<T>
): Promise<T> {
await this.credits.check(userId, operation);
const result = await generator();
await this.credits.deduct(userId, operation);
return result;
}
}
// maerchenzauber/backend/src/story/story.service.ts
@Injectable()
export class StoryService extends BaseAiService {
async generateStory(userId: string, input: StoryInput) {
return this.generateWithCredits(userId, 'STORY_GENERATION', async () => {
// Projekt-spezifische Logik
});
}
}
```
---
### Strategie 5: API-Gateway Pattern (Optional)
**Ziel:** Ein zentrales Gateway vor allen Backends.
```
┌─────────────────┐
│ API Gateway │
│ (Kong/Traefik) │
└────────┬────────┘
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Maerchenzauber│ │ Manadeck │ │ Uload │
│ Backend │ │ Backend │ │ Backend │
└───────────────┘ └───────────────┘ └───────────────┘
```
**Vorteile:**
- Zentrale Auth-Validierung
- Rate Limiting
- Request Logging
- Einheitliche API-Struktur
**Aufwand:** Hoch - Empfohlen erst bei Skalierungsbedarf
---
### Empfohlene Implementierungsreihenfolge
#### Phase 1: Shared Backend Package
**Priorität:** Hoch
**Aufwand:** 2-3 Wochen
Neues Package `packages/shared-backend/` mit:
- Auth Module (wraps @mana-core/nestjs-integration)
- Health Module
- Credits Module
- Base Repository Pattern
- Common Decorators, Guards, Filters
#### Phase 2: Datenbank-Vereinheitlichung
**Priorität:** Hoch
**Aufwand:** 3-4 Wochen
1. `packages/shared-database/` mit Drizzle Basis-Schema erstellen
2. Maerchenzauber von Supabase auf Drizzle migrieren
3. Uload PocketBase durch PostgreSQL + Drizzle ersetzen
#### Phase 3: Uload Backend Neubau (Optional)
**Priorität:** Mittel
**Aufwand:** 2-3 Wochen
PocketBase → NestJS Migration:
- Konsistenz mit anderen Projekten
- Bessere Integration mit Mana Core
- Einheitliches Deployment
---
### Optionen-Vergleich
| Option | Aufwand | Benefit | Empfehlung |
|--------|---------|---------|------------|
| Shared Backend Package | Mittel | Hoch | ✅ Priorität 1 |
| Drizzle überall | Mittel-Hoch | Hoch | ✅ Priorität 2 |
| Uload auf NestJS | Hoch | Mittel | ⚡ Optional |
| API Gateway | Sehr Hoch | Mittel | ⏳ Später |
---
### Quick Wins (sofort umsetzbar)
1. **NestJS Version angleichen** → Alle auf v11
2. **Einheitliche Health-Endpoints**`/health`, `/health/ready`
3. **Gemeinsame ESLint/Prettier Config**`@manacore/eslint-config-backend`
4. **Einheitliche Error-Response-Struktur:**
```typescript
// Einheitliches Error-Format für alle Backends
interface ApiError {
statusCode: number;
error: string;
message: string;
timestamp: string;
path: string;
}
```
5. **Einheitliche Logging-Struktur:**
```typescript
// packages/shared-backend/src/logging/logger.service.ts
@Injectable()
export class AppLogger {
log(context: string, message: string, meta?: Record<string, any>) {
console.log(JSON.stringify({
level: 'info',
context,
message,
timestamp: new Date().toISOString(),
...meta,
}));
}
}
```
---
### Ziel-Architektur nach Vereinheitlichung
```
┌─────────────────────────────────────────────────────────────┐
│ Shared Packages │
├─────────────────┬─────────────────┬─────────────────────────┤
│ shared-backend │ shared-database │ shared-types │
│ - AuthModule │ - baseColumns │ - ApiError │
│ - HealthModule │ - drizzleClient │ - User │
│ - CreditsModule │ - migrations │ - CreditOperation │
│ - BaseRepo │ │ │
└────────┬────────┴────────┬────────┴────────┬────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Maerchenzauber │ │ Manadeck │ │ Uload │
│ Backend │ │ Backend │ │ Backend │
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
│ NestJS v11 │ │ NestJS v11 │ │ NestJS v11 │
│ PostgreSQL │ │ PostgreSQL │ │ PostgreSQL │
│ Drizzle ORM │ │ Drizzle ORM │ │ Drizzle ORM │
│ Port: 3002 │ │ Port: 8080 │ │ Port: 3003 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└─────────────────┼─────────────────┘
┌─────────────────────┐
│ Mana Core │
│ (Auth + Credits) │
└─────────────────────┘
```

View file

@ -246,8 +246,8 @@ pnpm --filter mana-core-auth db:generate
pnpm --filter mana-core-auth db:migrate
# chat-backend
pnpm --filter @chat/backend db:push
pnpm --filter @chat/backend db:migrate
pnpm --filter @chat/server db:push
pnpm --filter @chat/server db:migrate
```
---

View file

@ -32,7 +32,7 @@ The generator reads `.env.development` and creates app-specific `.env` files wit
|----------|--------|---------|
| Expo (mobile) | `EXPO_PUBLIC_` | `EXPO_PUBLIC_SUPABASE_URL` |
| SvelteKit (web) | `PUBLIC_` | `PUBLIC_SUPABASE_URL` |
| NestJS (backend) | None | `SUPABASE_URL` |
| Hono/Bun (server) | None | `DATABASE_URL` |
## File Locations
@ -40,19 +40,17 @@ The generator reads `.env.development` and creates app-specific `.env` files wit
- **`.env.development`** - Single source of truth, committed to git
### Generated Files (gitignored)
- `services/mana-core-auth/.env`
- `apps/chat/apps/backend/.env`
- `services/mana-auth/.env`
- `apps/chat/apps/server/.env`
- `apps/chat/apps/mobile/.env`
- `apps/chat/apps/web/.env`
- `apps/maerchenzauber/apps/backend/.env`
- `apps/maerchenzauber/apps/mobile/.env`
- `apps/maerchenzauber/apps/web/.env`
- `apps/manacore/apps/mobile/.env`
- `apps/manacore/apps/web/.env`
- `apps/memoro/apps/mobile/.env`
- `apps/memoro/apps/web/.env`
- `apps/manadeck/apps/backend/.env`
- `apps/manadeck/apps/server/.env`
- `apps/manadeck/apps/web/.env`
- `apps/*/apps/server/.env` (all apps with compute servers)
- `apps/*/apps/web/.env` (all web apps)
- `apps/*/apps/mobile/.env` (all mobile apps)
## Variable Reference
@ -98,28 +96,6 @@ The generator reads `.env.development` and creates app-specific `.env` files wit
| `CHAT_SUPABASE_URL` | Supabase project URL | - |
| `CHAT_SUPABASE_ANON_KEY` | Supabase anonymous key | - |
### Maerchenzauber Project
| Variable | Description | Default |
|----------|-------------|---------|
| `MAERCHENZAUBER_BACKEND_PORT` | Backend service port | `3003` |
| `MAERCHENZAUBER_APP_ID` | Mana Core app ID | - |
| `MAERCHENZAUBER_SUPABASE_URL` | Supabase project URL | - |
| `MAERCHENZAUBER_SUPABASE_ANON_KEY` | Supabase anonymous key | - |
| `MAERCHENZAUBER_JWT_SECRET` | JWT secret for Supabase | - |
| `MAERCHENZAUBER_AZURE_OPENAI_KEY` | Azure OpenAI key | - |
| `MAERCHENZAUBER_AZURE_OPENAI_ENDPOINT` | Azure OpenAI endpoint | - |
| `MAERCHENZAUBER_REPLICATE_API_KEY` | Replicate API key (images) | - |
### Memoro Project
| Variable | Description |
|----------|-------------|
| `MEMORO_SUPABASE_URL` | Supabase project URL |
| `MEMORO_SUPABASE_ANON_KEY` | Supabase anonymous key |
| `MEMORO_MIDDLEWARE_API_URL` | Middleware API URL |
| `MEMORO_APPID` | Mana Core app ID |
### Manacore Project
| Variable | Description |
@ -168,9 +144,9 @@ Edit `scripts/generate-env.mjs` and add your app config:
},
},
{
path: 'apps/my-project/apps/backend/.env',
path: 'apps/my-project/apps/server/.env',
vars: {
// For NestJS, no prefix needed
// For Hono/Bun servers, no prefix needed
API_KEY: (env) => env.MY_NEW_PROJECT_API_KEY,
API_URL: (env) => env.MY_NEW_PROJECT_URL,
},

View file

@ -1,35 +1,94 @@
# Local Development Guide
This guide explains how to set up and run applications locally with automatic database setup.
This guide explains how to set up and run applications locally. All apps use a **local-first architecture** with IndexedDB (Dexie.js) for reads/writes and mana-sync for background synchronization. Server-side compute (AI, file operations, RRULE expansion, etc.) is handled by lightweight **Hono/Bun servers**.
## Quick Start
For any project with a backend, use the `dev:*:full` command:
### Fastest: `dev:*:local` (recommended for daily development)
No auth service needed. Starts mana-sync + server (if any) + web:
```bash
pnpm dev:chat:full # Start chat with auth + database setup
pnpm dev:zitare:full # Start zitare with auth + database setup
pnpm dev:contacts:full # Start contacts with auth + database setup
# ... etc
pnpm dev:todo:local # sync + server + web
pnpm dev:chat:local # sync + server + web
pnpm dev:zitare:local # sync + web (no server needed)
pnpm dev:clock:local # sync + web (no server needed)
```
Guest mode works out of the box -- data lives in IndexedDB, no login required.
### Full stack: `dev:*:full` (with auth + database setup)
Use this when you need authentication, cross-device sync, or to test the full production flow:
```bash
pnpm dev:todo:full # DB setup + auth + sync + server + web
pnpm dev:chat:full # DB setup + auth + sync + server + web
pnpm dev:zitare:full # auth + sync + web
pnpm dev:calendar:full # DB setup + auth + sync + server + web
```
These commands automatically:
1. Create the database if it doesn't exist
2. Push the latest schema (Drizzle `db:push`)
3. Start the auth service (mana-core-auth)
4. Start the backend and web app with colored output
1. Create the database if it doesn't exist (for apps with Drizzle schemas)
2. Push the latest schema (`drizzle-kit push --force`)
3. Start the auth service (mana-auth)
4. Start mana-sync (Go WebSocket server)
5. Start the Hono/Bun server (if the app has one)
6. Start the web app with colored output
## Available Full Dev Commands
## Available Dev Commands
| Command | Database | Backend Port | Web Port |
|---------|----------|--------------|----------|
| `pnpm dev:chat:full` | chat | 3002 | 5173 |
| `pnpm dev:zitare:full` | zitare | 3007 | 5177 |
| `pnpm dev:contacts:full` | contacts | 3015 | 5184 |
| `pnpm dev:calendar:full` | calendar | 3014 | 5179 |
| `pnpm dev:clock:full` | clock | 3017 | 5187 |
| `pnpm dev:todo:full` | todo | 3018 | 5188 |
| `pnpm dev:picture:full` | picture | 3006 | 5175 |
### Apps with Hono/Bun Servers
These apps have server-side compute and support both `local` and `full` modes:
| App | Server Port | Web Port | `local` | `full` |
|-----|-------------|----------|---------|--------|
| Todo | 3019 | 5188 | Yes | Yes |
| Chat | 3002 | 5174 | Yes | Yes |
| Calendar | 3003 | 5179 | Yes | Yes |
| Contacts | 3004 | 5184 | Yes | Yes |
| Picture | 3006 | 5175 | Yes | Yes |
| ManaDeck | 3009 | 5176 | Yes | Yes |
| Mukke | 3010 | 5180 | Yes | Yes |
| Questions | 3011 | 5111 | Yes | Yes |
| Storage | 3016 | 5185 | Yes | Yes |
| Context | 3020 | 5192 | Yes | Yes |
| Planta | 3022 | 5191 | Yes | Yes |
| NutriPhi | 3023 | 5180 | Yes | Yes |
| Traces | 3026 | mobile | Yes | Yes |
| Presi | 3008 | 5178 | Yes | Yes |
| uLoad | 3070 | 5173 | Yes | Yes |
| News | 3071 | 5173 | Yes | Yes |
| WiseKeep | 3072 | 5173 | Yes | Yes |
| Moodlit | 3073 | 5173 | Yes | Yes |
### Apps without Servers (sync + web only)
These apps use only IndexedDB + mana-sync, no server-side compute:
| App | Web Port | `local` | `full` |
|-----|----------|---------|--------|
| Zitare | 5107 | Yes | Yes |
| Clock | 5187 | Yes | Yes |
| SkilltTree | 5195 | Yes | Yes |
| Photos | 5189 | Yes | Yes |
| CityCorners | 5196 | Yes | Yes |
| Inventar | 5190 | Yes | Yes |
| Times | 5197 | Yes | Yes |
| Calc | 5198 | Yes | Yes |
| ManaVoxel | 5028 | Yes | Yes |
### Infrastructure Ports
| Service | Port | Description |
|---------|------|-------------|
| mana-auth | 3001 | Central auth (Better Auth + EdDSA JWT) |
| mana-sync | 3050 | Data sync (Go, WebSocket, field-level LWW) |
| PostgreSQL | 5432 | Database |
| Redis | 6379 | Cache |
| MinIO API | 9000 | S3-compatible storage |
| MinIO Console | 9001 | Storage web UI |
## Prerequisites
@ -39,23 +98,34 @@ Before running any `dev:*:full` command:
# 1. Start Docker infrastructure (PostgreSQL, Redis, MinIO)
pnpm docker:up
# 2. Generate environment files (runs automatically on pnpm install)
# 2. Build mana-sync (first time only, or after Go changes)
pnpm dev:sync:build
# 3. Generate environment files (runs automatically on pnpm install)
pnpm setup:env
```
For `dev:*:local`, only mana-sync needs to be built. No Docker required unless your server uses a database.
## Database Setup Commands
### Individual Service Setup
```bash
pnpm setup:db:auth # Setup mana-core-auth database + schema
pnpm setup:db:auth # Setup mana-auth database + schema
pnpm setup:db:chat # Setup chat database + schema
pnpm setup:db:zitare # Setup zitare database + schema
pnpm setup:db:todo # Setup todo database + schema
pnpm setup:db:contacts # Setup contacts database + schema
pnpm setup:db:calendar # Setup calendar database + schema
pnpm setup:db:clock # Setup clock database + schema
pnpm setup:db:todo # Setup todo database + schema
pnpm setup:db:picture # Setup picture database + schema
pnpm setup:db:uload # Setup uload database + schema
pnpm setup:db:context # Setup context database + schema
pnpm setup:db:storage # Setup storage database + schema
pnpm setup:db:mukke # Setup mukke database + schema
pnpm setup:db:planta # Setup planta database + schema
pnpm setup:db:nutriphi # Setup nutriphi database + schema
pnpm setup:db:questions # Setup questions database + schema
pnpm setup:db:traces # Setup traces database + schema
```
### Setup All Databases
@ -68,12 +138,25 @@ This is useful when setting up a fresh environment or after pulling new schema c
## How It Works
### Architecture
```
Guest: App -> IndexedDB (Dexie.js) -> UI (no sync)
Logged in: App -> IndexedDB -> UI -> SyncEngine -> mana-sync (Go) -> PostgreSQL
<- WebSocket push <-
```
**mana-sync** (Go, port 3050) handles WebSocket-based sync with field-level last-write-wins conflict resolution. All CRUD goes through IndexedDB first, making the UI instant regardless of network.
**Hono/Bun servers** handle operations that cannot run client-side:
- AI/LLM calls (chat, questions, picture generation)
- File operations (storage, media processing)
- RRULE expansion and reminder scheduling (todo, calendar)
- Server-side data processing (news extraction, transcription)
### Docker Init Script
On first `pnpm docker:up`, the PostgreSQL container runs `docker/init-db/01-create-databases.sql` which creates all databases:
- manacore, chat, zitare, contacts, calendar, clock, todo, manadeck
- storage, mail, moodlit, finance, inventory, techbase, voxel_lava, figgos
On first `pnpm docker:up`, the PostgreSQL container runs `docker/init-db/01-create-databases.sql` which creates all databases.
### Setup Script
@ -84,6 +167,16 @@ The `scripts/setup-databases.sh` script:
The `--force` flag auto-approves schema changes without interactive prompts.
### What each command starts
| Command pattern | What it runs |
|-----------------|-------------|
| `dev:*:local` | mana-sync + server (if any) + web |
| `dev:*:full` | DB setup + mana-auth + mana-sync + server (if any) + web |
| `dev:*:server` | Just the Hono/Bun server (`bun run --watch src/index.ts`) |
| `dev:*:web` | Just the SvelteKit web app |
| `dev:*:app` | Server + web (no sync, no auth) |
## Troubleshooting
### Database doesn't exist
@ -103,17 +196,26 @@ PGPASSWORD=devpassword psql -h localhost -U manacore -d postgres -c "CREATE DATA
If you see errors about missing tables/columns:
```bash
# Push the latest schema
pnpm --filter @chat/backend db:push --force
# Push the latest schema from the server package
pnpm --filter @chat/server db:push --force
```
### mana-sync not built
If you see `./server: No such file or directory`:
```bash
pnpm dev:sync:build
```
### Port already in use
If auth (port 3001) is already running:
If auth (port 3001) or sync (port 3050) is already running:
```bash
# Check what's using the port
lsof -i :3001
lsof -i :3050
# Kill the process if needed
kill <PID>
@ -134,23 +236,6 @@ pnpm docker:up
pnpm setup:db
```
## Apps Without Full Commands
Some apps don't have backends or don't use Drizzle:
| App | Reason |
|-----|--------|
| manacore | No backend (uses other services) |
| manadeck | Backend exists but no db:push |
| bauntown, context, maerchenzauber, memoro, news, nutriphi, presi, quote, reader, storage, wisekeep | No backends |
For these, use the regular dev commands:
```bash
pnpm dev:manacore:web
pnpm dev:manadeck:app
```
## Adding a New Application
### Step 1: Create Project Structure
@ -160,7 +245,7 @@ Create the standard project structure under `apps/`:
```
apps/newproject/
├── apps/
│ ├── backend/ # NestJS API (if needed)
│ ├── server/ # Hono/Bun server (if needed for compute)
│ ├── mobile/ # Expo React Native app
│ ├── web/ # SvelteKit web app
│ └── landing/ # Astro marketing page
@ -170,9 +255,24 @@ apps/newproject/
└── CLAUDE.md # Project documentation
```
### Step 2: Configure Backend Database (if applicable)
### Step 2: Set Up Local-First Data Layer
If your backend uses Drizzle ORM:
1. Create `apps/newproject/apps/web/src/lib/data/local-store.ts` with `createLocalStore()`
2. Create `apps/newproject/apps/web/src/lib/data/guest-seed.ts` for onboarding data
3. Use `collection.getAll()` / `collection.insert()` in stores (not API calls)
4. In layout: `await store.initialize()`, `store.startSync()` on login
### Step 3: Create Hono/Bun Server (if needed)
Only needed if your app requires server-side compute (AI, file ops, etc.).
Create `apps/newproject/apps/server/` with:
- `src/index.ts` -- Hono app entry point
- `src/config.ts` -- Port and env config
- `src/routes/health.ts` -- Health check endpoint
- `package.json` with `"name": "@newproject/server"`
### Step 4: Configure Database (if the server uses Drizzle)
1. **Add database to Docker init** (`docker/init-db/01-create-databases.sql`):
```sql
@ -181,84 +281,59 @@ If your backend uses Drizzle ORM:
```
2. **Add to setup script** (`scripts/setup-databases.sh`):
In the `setup_service()` function, add a new case:
```bash
newproject)
create_db_if_not_exists "newproject"
push_schema "@newproject/backend" "newproject"
push_schema "@newproject/server" "newproject"
;;
```
Also add to the `ALL_DATABASES` array:
```bash
ALL_DATABASES=(
...
"newproject"
)
```
And to the services loop at the bottom:
```bash
for service in auth chat ... newproject; do
```
3. **Add DATABASE_URL to `.env.development`**:
```env
NEWPROJECT_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/newproject
```
4. **Update `scripts/generate-env.mjs`** to generate the backend `.env` file.
4. **Update `scripts/generate-env.mjs`** to generate the server `.env` file.
### Step 3: Add Package.json Scripts
### Step 5: Add Package.json Scripts
Add to root `package.json`:
```json
{
"scripts": {
// Project-level dev (all apps)
"newproject:dev": "turbo run dev --filter=newproject...",
// Individual app commands
"dev:newproject:web": "pnpm --filter @newproject/web dev",
"dev:newproject:mobile": "pnpm --filter @newproject/mobile dev",
"dev:newproject:backend": "pnpm --filter @newproject/backend dev",
"dev:newproject:landing": "pnpm --filter @newproject/landing dev",
"dev:newproject:app": "turbo run dev --filter=@newproject/web --filter=@newproject/backend",
// Full dev with auto database setup
"dev:newproject:full": "./scripts/setup-databases.sh newproject && ./scripts/setup-databases.sh auth && concurrently -n auth,backend,web -c blue,green,cyan \"pnpm dev:auth\" \"pnpm dev:newproject:backend\" \"pnpm dev:newproject:web\"",
// Database shortcuts
"newproject:db:push": "pnpm --filter @newproject/backend db:push",
"newproject:db:studio": "pnpm --filter @newproject/backend db:studio",
// Setup shortcut
"dev:newproject:server": "cd apps/newproject/apps/server && bun run --watch src/index.ts",
"dev:newproject:local": "concurrently -n sync,server,web -c magenta,yellow,cyan \"pnpm dev:sync\" \"pnpm dev:newproject:server\" \"pnpm dev:newproject:web\"",
"dev:newproject:full": "./scripts/setup-databases.sh newproject && ./scripts/setup-databases.sh auth && concurrently -n auth,sync,server,web -c blue,magenta,yellow,cyan \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:newproject:server\" \"pnpm dev:newproject:web\"",
"setup:db:newproject": "./scripts/setup-databases.sh newproject"
}
}
```
### Step 4: Create Project CLAUDE.md
For apps **without** a server (sync + web only):
Create `apps/newproject/CLAUDE.md` with:
- Project overview
- Structure diagram
- Available commands
- API endpoints (if backend)
- Environment variables
- Tech stack details
```json
{
"scripts": {
"dev:newproject:web": "pnpm --filter @newproject/web dev",
"dev:newproject:local": "concurrently -n sync,web -c magenta,cyan \"pnpm dev:sync\" \"pnpm dev:newproject:web\"",
"dev:newproject:full": "concurrently -n auth,sync,web -c blue,magenta,cyan \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:newproject:web\""
}
}
```
See existing projects like `apps/chat/CLAUDE.md` for reference.
### Step 6: Create Project CLAUDE.md
### Step 5: Test the Setup
Create `apps/newproject/CLAUDE.md` with project overview, structure, commands, and API endpoints.
### Step 7: Test the Setup
```bash
# Create database and push schema
pnpm setup:db:newproject
# Quick start (no auth needed)
pnpm dev:newproject:local
# Start with full dev command
# Full stack (with auth + DB setup)
pnpm dev:newproject:full
```
@ -266,10 +341,13 @@ pnpm dev:newproject:full
- [ ] Create project structure under `apps/newproject/`
- [ ] Add `pnpm-workspace.yaml` in project root
- [ ] Add database to `docker/init-db/01-create-databases.sql`
- [ ] Add service to `scripts/setup-databases.sh`
- [ ] Add DATABASE_URL to `.env.development`
- [ ] Set up local-store with Dexie.js collections
- [ ] Create guest seed data
- [ ] Add Hono/Bun server (if compute needed)
- [ ] Add database to `docker/init-db/01-create-databases.sql` (if using Drizzle)
- [ ] Add service to `scripts/setup-databases.sh` (if using Drizzle)
- [ ] Add DATABASE_URL to `.env.development` (if using Drizzle)
- [ ] Update `scripts/generate-env.mjs` for env generation
- [ ] Add scripts to root `package.json`
- [ ] Create `CLAUDE.md` with project documentation
- [ ] Test with `pnpm dev:newproject:full`
- [ ] Test with `pnpm dev:newproject:local`

View file

@ -454,16 +454,15 @@ git pull
### Docker Base Images
Alle Apps werden auf vorgebauten Base Images aufgebaut, um Build-Zeit und Memory-Verbrauch zu reduzieren:
Alle Web-Apps werden auf einem vorgebauten Base Image aufgebaut, um Build-Zeit und Memory-Verbrauch zu reduzieren. Backend-Server verwenden `docker/Dockerfile.hono-server` (Hono + Bun) direkt.
| Base Image | Dockerfile | Verwendet von |
|------------|-----------|---------------|
| `sveltekit-base:local` | `docker/Dockerfile.sveltekit-base` | Alle SvelteKit Web-Apps |
| `nestjs-base:local` | `docker/Dockerfile.nestjs-base` | Alle NestJS Backends |
Die Base Images enthalten alle Shared Packages (`packages/`) vorinstalliert und vorgebaut. App-Dockerfiles müssen nur noch ihren app-spezifischen Code kopieren.
Das Base Image enthaelt alle Shared Packages (`packages/`) vorinstalliert und vorgebaut. App-Dockerfiles muessen nur noch ihren app-spezifischen Code kopieren.
**Base Images neu bauen** wenn sich Shared Packages ändern:
**Base Image neu bauen** wenn sich Shared Packages aendern:
```bash
./scripts/mac-mini/build-app.sh --base

View file

@ -46,23 +46,23 @@ Canonical port assignments for all ManaCore services. This is the single source
| 3024 | mana-voice-bot | Python | Voice-to-voice assistant |
| 3025-3029 | *(reserved)* | | |
## 3030-3059: App Backends
## 3030-3059: App Compute Servers
Only apps that need server-side compute (AI, external APIs, file operations).
Pure CRUD apps use mana-sync directly.
| Port | Service | Runtime | Description |
|------|---------|---------|-------------|
| 3030 | chat-backend | NestJS | AI chat, streaming, spaces |
| 3030 | chat-server | Hono/Bun | AI chat, streaming, spaces |
| 3031 | todo-server | Hono/Bun | RRULE expansion, reminders |
| 3032 | calendar-backend | NestJS | CalDAV sync, Google Calendar, notifications |
| 3033 | contacts-backend | NestJS | Google Contacts, vCard import/export |
| 3034 | storage-backend | NestJS | S3 file ops, versioning, shares |
| 3035 | picture-backend | NestJS | Replicate AI, generation orchestration |
| 3036 | manadeck-backend | NestJS | AI card generation |
| 3037 | mukke-backend | NestJS | Audio processing, BPM, ID3 tags |
| 3038 | nutriphi-backend | NestJS | Gemini meal analysis |
| 3039 | planta-backend | NestJS | Gemini plant analysis |
| 3032 | calendar-server | Hono/Bun | CalDAV sync, Google Calendar, notifications |
| 3033 | contacts-server | Hono/Bun | Google Contacts, vCard import/export |
| 3034 | storage-server | Hono/Bun | S3 file ops, versioning, shares |
| 3035 | picture-server | Hono/Bun | Replicate AI, generation orchestration |
| 3036 | manadeck-server | Hono/Bun | AI card generation |
| 3037 | mukke-server | Hono/Bun | Audio processing, BPM, ID3 tags |
| 3038 | nutriphi-server | Hono/Bun | Gemini meal analysis |
| 3039 | planta-server | Hono/Bun | Gemini plant analysis |
| 3040 | presi-server | Hono/Bun | Share links |
| 3041-3059 | *(reserved)* | | |
@ -78,28 +78,28 @@ Pure CRUD apps use mana-sync directly.
## 5000-5059: Web Frontends (SvelteKit)
| Port | Service | Corresponds to Backend |
| Port | Service | Corresponds to Server |
|------|---------|----------------------|
| 5000 | mana-web | Hub/Dashboard |
| 5010 | chat-web | 3030 chat-backend |
| 5010 | chat-web | 3030 chat-server |
| 5011 | todo-web | 3031 todo-server |
| 5012 | calendar-web | 3032 calendar-backend |
| 5012 | calendar-web | 3032 calendar-server |
| 5013 | clock-web | *(local-first only)* |
| 5014 | contacts-web | 3033 contacts-backend |
| 5015 | storage-web | 3034 storage-backend |
| 5014 | contacts-web | 3033 contacts-server |
| 5015 | storage-web | 3034 storage-server |
| 5016 | presi-web | 3040 presi-server |
| 5017 | nutriphi-web | 3038 nutriphi-backend |
| 5017 | nutriphi-web | 3038 nutriphi-server |
| 5018 | zitare-web | *(local-first only)* |
| 5019 | photos-web | *(local-first + mana-media)* |
| 5020 | skilltree-web | *(local-first only)* |
| 5021 | picture-web | 3035 picture-backend |
| 5021 | picture-web | 3035 picture-server |
| 5022 | citycorners-web | *(local-first only)* |
| 5023 | manadeck-web | 3036 manadeck-backend |
| 5024 | mukke-web | 3037 mukke-backend |
| 5023 | manadeck-web | 3036 manadeck-server |
| 5024 | mukke-web | 3037 mukke-server |
| 5025 | inventar-web | *(local-first only)* |
| 5026 | context-web | *(local-first only)* |
| 5027 | questions-web | *(local-first only)* |
| 5028 | planta-web | 3039 planta-backend |
| 5028 | planta-web | 3039 planta-server |
| 5029 | moodlit-web | *(future)* |
| 5030-5049 | *(reserved)* | |

View file

@ -412,7 +412,7 @@ nutriphi/
│ ├── mobile/ # Expo React Native App (@nutriphi/mobile)
│ ├── web/ # SvelteKit Web App (@nutriphi/web)
│ └── landing/ # Astro Landing Page (@nutriphi/landing)
├── backend/ # NestJS API Server (@nutriphi/backend)
├── server/ # Hono/Bun server (@nutriphi/server)
```
#### API Endpoints

View file

@ -70,9 +70,9 @@ const APP_CONFIGS = [
},
},
// Chat Backend
// Chat Server (Hono/Bun)
{
path: 'apps/chat/apps/backend/.env',
path: 'apps/chat/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.CHAT_BACKEND_PORT || '3002',
@ -130,9 +130,9 @@ const APP_CONFIGS = [
},
},
// Manadeck Backend
// Manadeck Server (Hono/Bun)
{
path: 'apps/manadeck/apps/backend/.env',
path: 'apps/manadeck/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.MANADECK_BACKEND_PORT || '3004',
@ -154,9 +154,9 @@ const APP_CONFIGS = [
},
},
// Picture Backend (NestJS)
// Picture Server (Hono/Bun)
{
path: 'apps/picture/apps/backend/.env',
path: 'apps/picture/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.PICTURE_BACKEND_PORT || '3006',
@ -204,9 +204,9 @@ const APP_CONFIGS = [
},
},
// Nutriphi Backend (NestJS)
// Nutriphi Server (Hono/Bun)
{
path: 'apps/nutriphi/apps/backend/.env',
path: 'apps/nutriphi/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.NUTRIPHI_BACKEND_PORT || '3002',
@ -282,9 +282,9 @@ const APP_CONFIGS = [
},
},
// Mana Games Backend (NestJS)
// Arcade Backend (NestJS)
{
path: 'games/mana-games/apps/backend/.env',
path: 'games/arcade/apps/backend/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.MANA_GAMES_BACKEND_PORT || '3011',
@ -304,17 +304,17 @@ const APP_CONFIGS = [
},
},
// Mana Games Web (Astro)
// Arcade Web (Astro)
{
path: 'games/mana-games/apps/web/.env',
path: 'games/arcade/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.MANA_GAMES_BACKEND_PORT || '3011'}`,
},
},
// Context Backend (NestJS)
// Context Server (Hono/Bun)
{
path: 'apps/context/apps/backend/.env',
path: 'apps/context/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.CONTEXT_BACKEND_PORT || '3020',
@ -338,9 +338,9 @@ const APP_CONFIGS = [
},
},
// Calendar Backend (NestJS)
// Calendar Server (Hono/Bun)
{
path: 'apps/calendar/apps/backend/.env',
path: 'apps/calendar/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.CALENDAR_BACKEND_PORT || '3014',
@ -379,9 +379,9 @@ const APP_CONFIGS = [
},
},
// Contacts Backend (NestJS)
// Contacts Server (Hono/Bun)
{
path: 'apps/contacts/apps/backend/.env',
path: 'apps/contacts/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.CONTACTS_BACKEND_PORT || '3015',
@ -422,9 +422,9 @@ const APP_CONFIGS = [
},
},
// Storage Backend (NestJS)
// Storage Server (Hono/Bun)
{
path: 'apps/storage/apps/backend/.env',
path: 'apps/storage/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.STORAGE_BACKEND_PORT || '3016',
@ -467,9 +467,9 @@ const APP_CONFIGS = [
},
},
// Todo Backend (NestJS)
// Todo Server (Hono/Bun)
{
path: 'apps/todo/apps/backend/.env',
path: 'apps/todo/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.TODO_BACKEND_PORT || '3018',
@ -492,9 +492,9 @@ const APP_CONFIGS = [
},
},
// Moodlit Backend (NestJS)
// Moodlit Server (Hono/Bun)
{
path: 'apps/moodlit/apps/backend/.env',
path: 'apps/moodlit/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.MOODLIT_BACKEND_PORT || '3012',
@ -524,37 +524,7 @@ const APP_CONFIGS = [
},
},
// Finance Backend (NestJS)
{
path: 'apps/finance/apps/backend/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.FINANCE_BACKEND_PORT || '3019',
DATABASE_URL: (env) => env.FINANCE_DATABASE_URL,
MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL,
DEV_BYPASS_AUTH: () => 'true',
DEV_USER_ID: (env) => env.DEV_USER_ID || '00000000-0000-0000-0000-000000000000',
CORS_ORIGINS: (env) => env.CORS_ORIGINS,
},
},
// Finance Mobile (Expo)
{
path: 'apps/finance/apps/mobile/.env',
vars: {
EXPO_PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.FINANCE_BACKEND_PORT || '3019'}`,
EXPO_PUBLIC_MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL,
},
},
// Finance Web (SvelteKit)
{
path: 'apps/finance/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.FINANCE_BACKEND_PORT || '3019'}`,
PUBLIC_MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL,
},
},
// Finance: REMOVED
// Worldream Web (SvelteKit)
{
@ -576,29 +546,11 @@ const APP_CONFIGS = [
},
},
// TechBase Backend (NestJS)
{
path: 'apps/techbase/apps/backend/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.TECHBASE_BACKEND_PORT || '3021',
DATABASE_URL: (env) => env.TECHBASE_DATABASE_URL,
MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL,
CORS_ORIGINS: () => 'http://localhost:4321,http://localhost:5173',
},
},
// TechBase: REMOVED
// TechBase Web (Astro)
// Traces Server (Hono/Bun)
{
path: 'apps/techbase/apps/web/.env',
vars: {
PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.TECHBASE_BACKEND_PORT || '3021'}`,
},
},
// Traces Backend (NestJS)
{
path: 'apps/traces/apps/backend/.env',
path: 'apps/traces/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.TRACES_BACKEND_PORT || '3026',
@ -657,9 +609,9 @@ const APP_CONFIGS = [
},
},
// Mukke Backend (NestJS)
// Mukke Server (Hono/Bun)
{
path: 'apps/mukke/apps/backend/.env',
path: 'apps/mukke/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.MUKKE_BACKEND_PORT || '3010',

View file

@ -111,11 +111,6 @@ build_base_images() {
$DOCKER build -f "$PROJECT_ROOT/docker/Dockerfile.sveltekit-base" -t sveltekit-base:local "$PROJECT_ROOT"
echo "sveltekit-base:local built."
echo ""
echo "=== Building nestjs-base image ==="
$DOCKER build -f "$PROJECT_ROOT/docker/Dockerfile.nestjs-base" -t nestjs-base:local "$PROJECT_ROOT"
echo "nestjs-base:local built."
echo ""
}
build_services() {

View file

@ -66,13 +66,8 @@ ALL_DATABASES=(
"manadeck"
"storage"
"presi"
"mail"
"moodlit"
"finance"
"inventory"
"techbase"
"voxel_lava"
"figgos"
"planta"
"nutriphi"
"photos"
@ -108,7 +103,7 @@ setup_service() {
;;
chat)
create_db_if_not_exists "chat"
push_schema "@chat/backend" "chat"
push_schema "@chat/server" "chat"
;;
zitare)
create_db_if_not_exists "zitare"
@ -116,11 +111,11 @@ setup_service() {
;;
contacts)
create_db_if_not_exists "contacts"
push_schema "@contacts/backend" "contacts"
# Schema managed by mana-sync (local-first)
;;
calendar)
create_db_if_not_exists "calendar"
push_schema "@calendar/backend" "calendar"
# Schema managed by mana-sync (local-first)
;;
clock)
create_db_if_not_exists "clock"
@ -128,47 +123,31 @@ setup_service() {
;;
todo)
create_db_if_not_exists "todo"
push_schema "@todo/backend" "todo"
push_schema "@todo/server" "todo"
;;
manadeck)
create_db_if_not_exists "manadeck"
push_schema "@manadeck/backend" "manadeck"
;;
mail)
create_db_if_not_exists "mail"
push_schema "@mail/backend" "mail"
# Schema managed by mana-sync (local-first)
;;
moodlit)
create_db_if_not_exists "moodlit"
push_schema "@moodlit/backend" "moodlit"
push_schema "@moodlit/server" "moodlit"
;;
picture)
create_db_if_not_exists "picture"
push_schema "@picture/backend" "picture"
# Schema managed by mana-sync (local-first)
;;
photos)
create_db_if_not_exists "photos"
# Schema managed by mana-sync (backend removed)
;;
finance)
create_db_if_not_exists "finance"
push_schema "@finance/backend" "finance"
;;
voxel-lava)
create_db_if_not_exists "voxel_lava"
push_schema "@voxel-lava/backend" "voxel-lava"
;;
figgos)
create_db_if_not_exists "figgos"
push_schema "@figgos/backend" "figgos"
;;
planta)
create_db_if_not_exists "planta"
push_schema "@planta/backend" "planta"
push_schema "@planta/server" "planta"
;;
nutriphi)
create_db_if_not_exists "nutriphi"
push_schema "@nutriphi/backend" "nutriphi"
# Schema managed by mana-sync (local-first)
;;
presi)
create_db_if_not_exists "presi"
@ -176,7 +155,7 @@ setup_service() {
;;
storage)
create_db_if_not_exists "storage"
push_schema "@storage/backend" "storage"
# Schema managed by mana-sync (local-first)
;;
projectdoc)
create_db_if_not_exists "projectdoc"
@ -196,7 +175,7 @@ setup_service() {
;;
questions)
create_db_if_not_exists "questions"
push_schema "@questions/backend" "questions"
# Schema managed by mana-sync (local-first)
;;
skilltree)
create_db_if_not_exists "skilltree"
@ -204,15 +183,15 @@ setup_service() {
;;
mukke)
create_db_if_not_exists "mukke"
push_schema "@mukke/backend" "mukke"
push_schema "@mukke/server" "mukke"
;;
traces)
create_db_if_not_exists "traces"
push_schema "@traces/backend" "traces"
push_schema "@traces/server" "traces"
;;
context)
create_db_if_not_exists "context"
push_schema "@context/backend" "context"
push_schema "@context/server" "context"
;;
citycorners)
create_db_if_not_exists "citycorners"
@ -224,7 +203,7 @@ setup_service() {
;;
*)
echo -e "${RED}Unknown service: $service${NC}"
echo "Available services: auth, chat, zitare, contacts, calendar, clock, todo, manadeck, mail, moodlit, picture, photos, finance, voxel-lava, figgos, planta, nutriphi, presi, storage, projectdoc, zitare_bot, todo_bot, nutriphi_bot, questions, skilltree, mukke, traces, context, citycorners, uload"
echo "Available services: auth, chat, zitare, contacts, calendar, clock, todo, manadeck, moodlit, picture, photos, planta, nutriphi, presi, storage, projectdoc, zitare_bot, todo_bot, nutriphi_bot, questions, skilltree, mukke, traces, context, citycorners, uload"
exit 1
;;
esac
@ -248,7 +227,7 @@ echo -e "\n${GREEN}Step 2: Pushing schemas${NC}"
echo "--------------------------------------"
# Push schemas for all known services
for service in auth chat zitare contacts calendar clock todo manadeck picture photos mail moodlit finance voxel-lava figgos planta nutriphi presi storage questions skilltree mukke traces context citycorners; do
for service in auth chat zitare contacts calendar clock todo manadeck picture photos moodlit planta nutriphi presi storage questions skilltree mukke traces context citycorners; do
setup_service "$service" 2>/dev/null || true
done

View file

@ -90,12 +90,6 @@ function getWorkspaceDeps(pkgJsonPath) {
return deps;
}
// Check if a Dockerfile uses nestjs-base:local as its base image
function usesNestjsBase(dockerfilePath) {
const content = readFileSync(dockerfilePath, 'utf8');
return content.includes('FROM nestjs-base:local');
}
// Check if a Dockerfile is a non-monorepo build (standalone, no workspace COPY needed)
function isStandaloneBuild(dockerfilePath) {
const content = readFileSync(dockerfilePath, 'utf8');
@ -133,22 +127,6 @@ function getDockerfileCopyPaths(dockerfilePath) {
return { copyPaths, hasPatchesCopy };
}
// Get packages pre-built in nestjs-base image
function getNestjsBasePackages() {
const baseDockerfile = join(ROOT, 'docker', 'Dockerfile.nestjs-base');
if (!existsSync(baseDockerfile)) return new Set();
const content = readFileSync(baseDockerfile, 'utf8');
const paths = new Set();
for (const line of content.split('\n')) {
const trimmed = line.trim();
const copyMatch = trimmed.match(/^COPY\s+(packages\/\S+)/);
if (copyMatch) {
paths.add(copyMatch[1]);
}
}
return paths;
}
// Extract @scope/package imports from a source file
function extractImports(filePath) {
if (!existsSync(filePath)) return [];
@ -173,13 +151,7 @@ function extractImports(filePath) {
// Validate a single Dockerfile and return result
function validateDockerfile(dockerfilePath, pkgJsonPath, relPath, packageMap, opts = {}) {
const {
isNestjsBase = false,
nestjsBasePackagePaths = new Set(),
checkImports = false,
appDir = null,
checkPatches = false,
} = opts;
const { checkImports = false, appDir = null, checkPatches = false } = opts;
if (!existsSync(pkgJsonPath)) {
return {
@ -203,14 +175,6 @@ function validateDockerfile(dockerfilePath, pkgJsonPath, relPath, packageMap, op
continue;
}
// For nestjs-base backends, packages/* are pre-built in the base image
if (isNestjsBase && dirPath.startsWith('packages/')) {
const isCoveredByBase = [...nestjsBasePackagePaths].some(
(bp) => bp === dirPath || dirPath.startsWith(bp + '/') || bp.startsWith(dirPath)
);
if (isCoveredByBase) continue;
}
// Check if any COPY path matches or is a parent directory of this package
const found = [...copyPaths].some(
(cp) => cp === dirPath || dirPath.startsWith(cp + '/') || cp.startsWith(dirPath)
@ -221,7 +185,7 @@ function validateDockerfile(dockerfilePath, pkgJsonPath, relPath, packageMap, op
}
// Check patches (only for web apps that need them)
if (checkPatches && !isNestjsBase && !hasPatchesCopy) {
if (checkPatches && !hasPatchesCopy) {
errors.push('MISSING: patches/ directory → add: COPY patches/ ./patches/');
}
@ -281,7 +245,6 @@ function main() {
const servicesDir = join(ROOT, 'services');
let hasErrors = false;
const results = [];
const nestjsBasePackagePaths = getNestjsBasePackages();
// Find all app directories
const appDirs = readdirSync(appsDir, { withFileTypes: true })
@ -317,12 +280,8 @@ function main() {
const pkgJsonPath = join(appsDir, appName, 'apps', 'backend', 'package.json');
const relPath = `apps/${appName}/apps/backend/Dockerfile`;
const isNestjsBase = usesNestjsBase(dockerfilePath);
const result = validateDockerfile(dockerfilePath, pkgJsonPath, relPath, packageMap, {
isNestjsBase,
nestjsBasePackagePaths,
});
const result = validateDockerfile(dockerfilePath, pkgJsonPath, relPath, packageMap);
if (result.errors.length > 0) hasErrors = true;
results.push(result);
printResult(result);