📝 docs: add Astro Starlight public documentation site

Add comprehensive documentation site using Astro Starlight with:
- Getting Started guides (introduction, quick-start, project structure)
- Development docs (local dev, env vars, docker, migrations, testing)
- Architecture docs (overview, auth, backend, web, mobile, storage, search)
- Guidelines (code style, error handling, database, design/UX)
- Deployment docs (overview, Cloudflare Pages, Mac Mini, self-hosting)
- Project pages (overview, chat)
- API reference structure

Features:
- Dark mode support
- Full-text search (Pagefind)
- Tailwind CSS styling
- Cloudflare Pages deployment ready
- Edit on GitHub links
This commit is contained in:
Till-JS 2026-01-29 18:01:15 +01:00
parent dff153ca1e
commit 4b322f59b1
38 changed files with 7497 additions and 234 deletions

101
apps/docs/astro.config.mjs Normal file
View file

@ -0,0 +1,101 @@
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
import tailwind from '@astrojs/tailwind';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://docs.manacore.app',
integrations: [
starlight({
title: 'Manacore Docs',
description:
'Documentation for the Manacore ecosystem - a multi-app platform with shared infrastructure.',
logo: {
light: './src/assets/logo-light.svg',
dark: './src/assets/logo-dark.svg',
replacesTitle: false,
},
social: {
github: 'https://github.com/manacore/manacore-monorepo',
},
editLink: {
baseUrl: 'https://github.com/manacore/manacore-monorepo/edit/main/apps/docs/',
},
customCss: ['./src/styles/custom.css'],
sidebar: [
{
label: 'Getting Started',
items: [
{ label: 'Introduction', slug: 'getting-started/introduction' },
{ label: 'Quick Start', slug: 'getting-started/quick-start' },
{ label: 'Project Structure', slug: 'getting-started/project-structure' },
],
},
{
label: 'Development',
items: [
{ label: 'Local Development', slug: 'development/local-development' },
{ label: 'Environment Variables', slug: 'development/environment-variables' },
{ label: 'Docker Setup', slug: 'development/docker' },
{ label: 'Database Migrations', slug: 'development/database-migrations' },
{ label: 'Testing', slug: 'development/testing' },
],
},
{
label: 'Architecture',
items: [
{ label: 'Overview', slug: 'architecture/overview' },
{ label: 'Authentication', slug: 'architecture/authentication' },
{ label: 'Backend (NestJS)', slug: 'architecture/backend' },
{ label: 'Web (SvelteKit)', slug: 'architecture/web' },
{ label: 'Mobile (Expo)', slug: 'architecture/mobile' },
{ label: 'Search Service', slug: 'architecture/search' },
{ label: 'Storage', slug: 'architecture/storage' },
],
},
{
label: 'Guidelines',
items: [
{ label: 'Code Style', slug: 'guidelines/code-style' },
{ label: 'Error Handling', slug: 'guidelines/error-handling' },
{ label: 'Database Patterns', slug: 'guidelines/database' },
{ label: 'Design & UX', slug: 'guidelines/design-ux' },
],
},
{
label: 'Deployment',
items: [
{ label: 'Overview', slug: 'deployment/overview' },
{ label: 'Cloudflare Pages', slug: 'deployment/cloudflare-pages' },
{ label: 'Mac Mini Server', slug: 'deployment/mac-mini-server' },
{ label: 'Self-Hosting', slug: 'deployment/self-hosting' },
],
},
{
label: 'Projects',
collapsed: true,
items: [
{ label: 'Overview', slug: 'projects' },
{ label: 'Chat', slug: 'projects/chat' },
],
},
{
label: 'API Reference',
collapsed: true,
items: [{ label: 'Overview', slug: 'api' }],
},
],
head: [
{
tag: 'meta',
attrs: {
property: 'og:image',
content: 'https://docs.manacore.app/og-image.png',
},
},
],
}),
tailwind({ applyBaseStyles: false }),
sitemap(),
],
});

28
apps/docs/package.json Normal file
View file

@ -0,0 +1,28 @@
{
"name": "@manacore/docs",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro",
"type-check": "astro check"
},
"dependencies": {
"@astrojs/check": "^0.9.0",
"@astrojs/sitemap": "^3.2.1",
"@astrojs/starlight": "^0.32.0",
"astro": "^5.16.0",
"sharp": "^0.33.0",
"typescript": "^5.0.0"
},
"devDependencies": {
"@astrojs/starlight-tailwind": "^3.0.0",
"@astrojs/tailwind": "^6.0.0",
"@tailwindcss/typography": "^0.5.16",
"tailwindcss": "^3.4.17"
}
}

View file

@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="8" fill="#0ea5e9"/>
<path d="M8 12L16 8L24 12V20L16 24L8 20V12Z" stroke="white" stroke-width="2" fill="none"/>
<circle cx="16" cy="16" r="3" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 298 B

View file

@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="8" fill="#0ea5e9"/>
<path d="M8 12L16 8L24 12V20L16 24L8 20V12Z" stroke="white" stroke-width="2" fill="none"/>
<circle cx="16" cy="16" r="3" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 298 B

View file

@ -0,0 +1,6 @@
import { defineCollection } from 'astro:content';
import { docsSchema } from '@astrojs/starlight/schema';
export const collections = {
docs: defineCollection({ schema: docsSchema() }),
};

View file

@ -0,0 +1,151 @@
---
title: API Reference
description: API documentation for Manacore services.
---
# API Reference
This section contains API documentation for all Manacore backend services.
## Authentication
All API endpoints (except public ones) require authentication via JWT bearer token:
```bash
curl -X GET https://api.manacore.app/v1/users/me \
-H "Authorization: Bearer <your-token>"
```
### Getting a Token
```bash
# Login
curl -X POST https://api.manacore.app/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "password"}'
# Response
{
"accessToken": "eyJ...",
"refreshToken": "eyJ...",
"expiresIn": 900
}
```
## Base URLs
| Service | Development | Production |
|---------|-------------|------------|
| Auth | `http://localhost:3001` | `https://auth.manacore.app` |
| Chat | `http://localhost:3002` | `https://chat-api.manacore.app` |
| Picture | `http://localhost:3006` | `https://picture-api.manacore.app` |
| Zitare | `http://localhost:3007` | `https://zitare-api.manacore.app` |
## Common Response Format
### Success Response
```json
{
"data": { ... },
"meta": {
"total": 100,
"page": 1,
"limit": 20
}
}
```
### Error Response
```json
{
"statusCode": 400,
"message": "Validation failed",
"error": "Bad Request",
"details": [
{
"field": "email",
"message": "Invalid email format"
}
]
}
```
## HTTP Status Codes
| Code | Meaning |
|------|---------|
| 200 | Success |
| 201 | Created |
| 204 | No Content |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 409 | Conflict |
| 422 | Unprocessable Entity |
| 429 | Too Many Requests |
| 500 | Internal Server Error |
## Rate Limiting
API endpoints are rate limited:
| Endpoint Type | Limit |
|---------------|-------|
| Auth endpoints | 10 req/min |
| Read endpoints | 100 req/min |
| Write endpoints | 30 req/min |
Rate limit headers are included in responses:
```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640000000
```
## Pagination
List endpoints support pagination:
```bash
GET /api/v1/items?page=2&limit=20
```
Response includes pagination metadata:
```json
{
"data": [...],
"meta": {
"total": 150,
"page": 2,
"limit": 20,
"totalPages": 8
}
}
```
## Filtering & Sorting
```bash
# Filter by status
GET /api/v1/items?status=active
# Sort by date descending
GET /api/v1/items?sort=-createdAt
# Multiple filters
GET /api/v1/items?status=active&category=tech&sort=-createdAt
```
## Service APIs
Select a service from the sidebar to view its API documentation:
- **Auth API** - Authentication and user management
- **Chat API** - Conversations and AI completions
- **Picture API** - Image generation
- **Zitare API** - Quotes and favorites

View file

@ -0,0 +1,283 @@
---
title: Authentication
description: Central authentication architecture using Mana Core Auth.
---
import { Aside, Tabs, TabItem, Steps } from '@astrojs/starlight/components';
# Authentication
All Manacore applications use **Mana Core Auth** as the central authentication service, providing EdDSA JWT-based authentication.
## Overview
```
┌─────────────┐ ┌─────────────┐ ┌────────────────┐
│ Client │────>│ Backend │────>│ mana-core-auth │
│ (Web/Mobile)│ │ (NestJS) │ │ (port 3001) │
└─────────────┘ └─────────────┘ └────────────────┘
│ │ │
│ Bearer token │ POST /validate │
│ │ {token} │
│ │<────────────────────│
│ │ {valid, payload} │
│<──────────────────│ │
│ Response │ │
```
## JWT Token Structure
Mana Core Auth uses **EdDSA (Ed25519)** for JWT signing:
```json
{
"sub": "user-uuid",
"email": "user@example.com",
"role": "user",
"sid": "session-uuid",
"exp": 1764606251,
"iss": "manacore",
"aud": "manacore"
}
```
| Claim | Description |
|-------|-------------|
| `sub` | User ID (UUID) |
| `email` | User's email address |
| `role` | User role (`user`, `admin`) |
| `sid` | Session ID for invalidation |
| `exp` | Expiration timestamp |
| `iss` | Issuer (`manacore`) |
| `aud` | Audience (`manacore`) |
## Backend Integration
### Option 1: Simple Auth Only
Use `@manacore/shared-nestjs-auth` for JWT validation:
```typescript
// app.module.ts
import { JwtAuthModule } from '@manacore/shared-nestjs-auth';
@Module({
imports: [
JwtAuthModule.register({
authServiceUrl: process.env.MANA_CORE_AUTH_URL,
}),
],
})
export class AppModule {}
```
```typescript
// controller.ts
import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
@Controller('api')
@UseGuards(JwtAuthGuard)
export class MyController {
@Get('profile')
getProfile(@CurrentUser() user: CurrentUserData) {
return { userId: user.userId, email: user.email };
}
}
```
### Option 2: Auth + Credits
Use `@mana-core/nestjs-integration` for full 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 {}
```
```typescript
// controller.ts
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) {
// Consume credits for the operation
await this.creditClient.consumeCredits(
user.sub,
'generation',
10,
'AI generation'
);
// ... perform operation
}
}
```
## Client Integration
### Web (SvelteKit)
```typescript
// src/lib/auth.ts
import { createAuthService } from '@manacore/shared-auth';
export const auth = createAuthService({
authUrl: import.meta.env.PUBLIC_MANA_CORE_AUTH_URL,
});
// Usage in component
const { data, error } = await auth.login(email, password);
if (data) {
// Store token, redirect to app
}
```
### Mobile (Expo)
```typescript
// src/services/auth.ts
import { createAuthService } from '@manacore/shared-auth';
export const auth = createAuthService({
authUrl: process.env.EXPO_PUBLIC_MANA_CORE_AUTH_URL,
storage: AsyncStorage, // Expo AsyncStorage
});
```
## API Endpoints
### Public Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/auth/register` | POST | Create new account |
| `/api/v1/auth/login` | POST | Login with credentials |
| `/api/v1/auth/refresh` | POST | Refresh access token |
| `/api/v1/auth/logout` | POST | Invalidate session |
### Protected Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/auth/me` | GET | Get current user |
| `/api/v1/auth/sessions` | GET | List active sessions |
| `/api/v1/auth/sessions/:id` | DELETE | Revoke session |
## Environment Variables
### Backend Services
```env
# Required
MANA_CORE_AUTH_URL=http://localhost:3001
# For development bypass (optional)
NODE_ENV=development
DEV_BYPASS_AUTH=true
DEV_USER_ID=test-user-uuid
# For credit operations
MANA_CORE_SERVICE_KEY=your-service-key
APP_ID=your-app-id
```
### Client Apps
<Tabs>
<TabItem label="SvelteKit">
```env
PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
```
</TabItem>
<TabItem label="Expo">
```env
EXPO_PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
```
</TabItem>
</Tabs>
## Testing Authentication
<Steps>
1. **Start Mana Core Auth**
```bash
pnpm dev:auth
```
2. **Get a token**
```bash
TOKEN=$(curl -s -X POST http://localhost:3001/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "test@example.com", "password": "password"}' \
| jq -r '.accessToken')
```
3. **Call protected endpoint**
```bash
curl http://localhost:3002/api/v1/profile \
-H "Authorization: Bearer $TOKEN"
```
</Steps>
## Development Bypass
For faster development, you can bypass authentication:
```typescript
// Only in development!
if (process.env.DEV_BYPASS_AUTH === 'true') {
// Use mock user
return {
userId: process.env.DEV_USER_ID,
email: 'dev@example.com',
role: 'admin',
};
}
```
<Aside type="danger">
Never enable auth bypass in production!
</Aside>
## Security Best Practices
1. **Always use HTTPS** in production
2. **Rotate JWT keys** periodically
3. **Keep tokens short-lived** (15m access, 7d refresh)
4. **Validate on every request** - don't trust client storage
5. **Implement rate limiting** on auth endpoints
6. **Log authentication events** for audit trails
## Integrated Services
| Backend | Package | Port |
|---------|---------|------|
| Chat | `@mana-core/nestjs-integration` | 3002 |
| Picture | `@manacore/shared-nestjs-auth` | 3006 |
| Zitare | `@manacore/shared-nestjs-auth` | 3007 |
| ManaDeck | `@mana-core/nestjs-integration` | 3009 |
| Contacts | `@manacore/shared-nestjs-auth` | 3015 |

View file

@ -0,0 +1,353 @@
---
title: Backend (NestJS)
description: NestJS backend architecture patterns in Manacore.
---
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
# Backend Architecture
All Manacore backends use **NestJS 10-11** with consistent patterns for structure, validation, and error handling.
## Project Structure
```
apps/{project}/apps/backend/
├── src/
│ ├── main.ts # Application entry point
│ ├── app.module.ts # Root module
│ ├── app.controller.ts # Health check, root routes
│ ├── config/
│ │ └── configuration.ts # Environment config
│ ├── drizzle/
│ │ ├── schema.ts # Database schema
│ │ └── drizzle.module.ts # Drizzle ORM setup
│ ├── {feature}/
│ │ ├── {feature}.module.ts
│ │ ├── {feature}.controller.ts
│ │ ├── {feature}.service.ts
│ │ ├── dto/
│ │ │ ├── create-{feature}.dto.ts
│ │ │ └── update-{feature}.dto.ts
│ │ └── entities/
│ │ └── {feature}.entity.ts
│ └── common/
│ ├── filters/
│ ├── guards/
│ ├── interceptors/
│ └── decorators/
├── drizzle/
│ └── migrations/ # Generated migrations
├── drizzle.config.ts # Drizzle CLI config
└── package.json
```
## Module Pattern
Each feature is a self-contained module:
```typescript
// users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // Export for use in other modules
})
export class UsersModule {}
```
## Controllers
Controllers handle HTTP requests and delegate to services:
```typescript
import {
Controller,
Get,
Post,
Body,
Param,
UseGuards,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('api/v1/users')
@UseGuards(JwtAuthGuard)
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll(@CurrentUser() user: CurrentUserData) {
return this.usersService.findAllForUser(user.userId);
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findById(id);
}
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() dto: CreateUserDto, @CurrentUser() user: CurrentUserData) {
return this.usersService.create(dto, user.userId);
}
}
```
## Services
Services contain business logic and database operations:
```typescript
import { Injectable, NotFoundException } from '@nestjs/common';
import { Inject } from '@nestjs/common';
import { DRIZZLE } from '@manacore/shared-drizzle';
import { eq, and } from 'drizzle-orm';
import { users } from '../drizzle/schema';
@Injectable()
export class UsersService {
constructor(@Inject(DRIZZLE) private db: DrizzleDB) {}
async findAllForUser(userId: string) {
return this.db
.select()
.from(users)
.where(eq(users.ownerId, userId));
}
async findById(id: string) {
const [user] = await this.db
.select()
.from(users)
.where(eq(users.id, id));
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
async create(data: CreateUserDto, ownerId: string) {
const [user] = await this.db
.insert(users)
.values({ ...data, ownerId })
.returning();
return user;
}
}
```
## DTOs and Validation
Use class-validator for input validation:
```typescript
// dto/create-user.dto.ts
import { IsString, IsEmail, IsOptional, MinLength, MaxLength } from 'class-validator';
export class CreateUserDto {
@IsString()
@MinLength(2)
@MaxLength(100)
name: string;
@IsEmail()
email: string;
@IsOptional()
@IsString()
@MaxLength(500)
bio?: string;
}
// dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(CreateUserDto) {}
```
Enable validation globally:
```typescript
// main.ts
import { ValidationPipe } from '@nestjs/common';
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Strip unknown properties
forbidNonWhitelisted: true, // Error on unknown properties
transform: true, // Auto-transform types
}),
);
```
## Error Handling
Use Go-style Result types for explicit error handling:
```typescript
// common/result.ts
export type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
export const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
export const err = <E>(error: E): Result<never, E> => ({ ok: false, error });
```
```typescript
// Usage in service
async findById(id: string): Promise<Result<User, 'NOT_FOUND'>> {
const [user] = await this.db
.select()
.from(users)
.where(eq(users.id, id));
if (!user) {
return err('NOT_FOUND');
}
return ok(user);
}
// Usage in controller
@Get(':id')
async findOne(@Param('id') id: string) {
const result = await this.usersService.findById(id);
if (!result.ok) {
throw new NotFoundException('User not found');
}
return result.value;
}
```
## Configuration
Use NestJS ConfigModule for environment variables:
```typescript
// config/configuration.ts
export default () => ({
port: parseInt(process.env.PORT, 10) || 3000,
database: {
url: process.env.DATABASE_URL,
},
auth: {
url: process.env.MANA_CORE_AUTH_URL,
},
});
// app.module.ts
import { ConfigModule } from '@nestjs/config';
import configuration from './config/configuration';
@Module({
imports: [
ConfigModule.forRoot({
load: [configuration],
isGlobal: true,
}),
],
})
export class AppModule {}
```
## Database Setup
Use Drizzle ORM with PostgreSQL:
```typescript
// drizzle/drizzle.module.ts
import { Module, Global } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
export const DRIZZLE = Symbol('DRIZZLE');
@Global()
@Module({
providers: [
{
provide: DRIZZLE,
inject: [ConfigService],
useFactory: (config: ConfigService) => {
const client = postgres(config.get('DATABASE_URL'));
return drizzle(client, { schema });
},
},
],
exports: [DRIZZLE],
})
export class DrizzleModule {}
```
## API Response Format
Consistent response structure:
```typescript
// Success
{
"data": { ... },
"meta": {
"total": 100,
"page": 1,
"limit": 10
}
}
// Error
{
"statusCode": 404,
"message": "User not found",
"error": "Not Found"
}
```
## Health Checks
Every backend should have a health endpoint:
```typescript
@Controller()
export class AppController {
@Get('health')
health() {
return {
status: 'ok',
timestamp: new Date().toISOString(),
};
}
@Get('api/v1/health')
healthApi() {
return this.health();
}
}
```
## Best Practices
<Aside type="tip">
1. **Keep controllers thin** - Delegate logic to services
2. **Use DTOs** - Never trust raw input
3. **Handle errors explicitly** - Use Result types
4. **Inject dependencies** - Don't instantiate services manually
5. **Write tests** - Unit test services, integration test controllers
</Aside>

View file

@ -0,0 +1,399 @@
---
title: Mobile (Expo)
description: Expo React Native mobile application patterns in Manacore.
---
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
# Mobile Architecture
Manacore mobile apps use **Expo SDK 52+** with React Native, Expo Router, and NativeWind for styling.
## Project Structure
```
apps/{project}/apps/mobile/
├── app/ # Expo Router routes
│ ├── _layout.tsx # Root layout
│ ├── index.tsx # Home screen
│ ├── (auth)/ # Auth screens
│ │ ├── _layout.tsx
│ │ ├── login.tsx
│ │ └── register.tsx
│ └── (tabs)/ # Tab navigation
│ ├── _layout.tsx
│ ├── index.tsx
│ ├── settings.tsx
│ └── profile.tsx
├── src/
│ ├── components/ # React components
│ │ ├── ui/ # Base UI components
│ │ └── features/ # Feature components
│ ├── hooks/ # Custom hooks
│ ├── stores/ # Zustand stores
│ ├── services/ # API clients
│ ├── utils/ # Helper functions
│ └── types/ # TypeScript types
├── assets/ # Images, fonts
├── app.json # Expo config
├── tailwind.config.js # NativeWind config
└── package.json
```
## Expo Router
### Root Layout
```tsx
// app/_layout.tsx
import { Stack } from 'expo-router';
import { useEffect } from 'react';
import { useAuth } from '../src/hooks/useAuth';
import '../global.css'; // NativeWind styles
export default function RootLayout() {
const { isLoading, isAuthenticated } = useAuth();
if (isLoading) {
return <LoadingScreen />;
}
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(auth)" />
<Stack.Screen name="(tabs)" />
</Stack>
);
}
```
### Tab Navigation
```tsx
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Home, Settings, User } from 'lucide-react-native';
export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#3b82f6',
headerShown: false,
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color, size }) => <Home color={color} size={size} />,
}}
/>
<Tabs.Screen
name="settings"
options={{
title: 'Settings',
tabBarIcon: ({ color, size }) => <Settings color={color} size={size} />,
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
tabBarIcon: ({ color, size }) => <User color={color} size={size} />,
}}
/>
</Tabs>
);
}
```
### Protected Routes
```tsx
// app/(tabs)/_layout.tsx
import { Redirect, Tabs } from 'expo-router';
import { useAuth } from '../../src/hooks/useAuth';
export default function TabLayout() {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Redirect href="/login" />;
}
return (
<Tabs>
{/* ... */}
</Tabs>
);
}
```
## NativeWind (Tailwind)
### Setup
```tsx
// tailwind.config.js
module.exports = {
content: [
'./app/**/*.{js,jsx,ts,tsx}',
'./src/**/*.{js,jsx,ts,tsx}',
],
presets: [require('nativewind/preset')],
theme: {
extend: {},
},
plugins: [],
};
```
### Usage
```tsx
import { View, Text, Pressable } from 'react-native';
export function Button({ title, onPress }) {
return (
<Pressable
className="bg-blue-500 px-4 py-2 rounded-lg active:bg-blue-600"
onPress={onPress}
>
<Text className="text-white font-semibold text-center">
{title}
</Text>
</Pressable>
);
}
```
## State Management (Zustand)
```tsx
// src/stores/auth.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface AuthState {
user: User | null;
token: string | null;
setAuth: (user: User, token: string) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
user: null,
token: null,
setAuth: (user, token) => set({ user, token }),
logout: () => set({ user: null, token: null }),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
```
### Usage in Components
```tsx
import { useAuthStore } from '../stores/auth';
export function ProfileScreen() {
const { user, logout } = useAuthStore();
return (
<View className="flex-1 p-4">
<Text className="text-xl font-bold">{user?.name}</Text>
<Text className="text-gray-500">{user?.email}</Text>
<Pressable
className="mt-4 bg-red-500 p-3 rounded-lg"
onPress={logout}
>
<Text className="text-white text-center">Logout</Text>
</Pressable>
</View>
);
}
```
## API Integration
### API Client
```tsx
// src/services/api.ts
import { useAuthStore } from '../stores/auth';
const API_URL = process.env.EXPO_PUBLIC_API_URL;
class ApiClient {
private async fetch<T>(path: string, options: RequestInit = {}): Promise<T> {
const token = useAuthStore.getState().token;
const response = await fetch(`${API_URL}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
});
if (!response.ok) {
if (response.status === 401) {
useAuthStore.getState().logout();
}
throw new Error(`API Error: ${response.status}`);
}
return response.json();
}
// API methods
getUser = () => this.fetch<User>('/api/v1/me');
getItems = () => this.fetch<Item[]>('/api/v1/items');
createItem = (data: CreateItemDto) =>
this.fetch<Item>('/api/v1/items', {
method: 'POST',
body: JSON.stringify(data),
});
}
export const api = new ApiClient();
```
### React Query Integration
```tsx
// src/hooks/useItems.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '../services/api';
export function useItems() {
return useQuery({
queryKey: ['items'],
queryFn: api.getItems,
});
}
export function useCreateItem() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.createItem,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['items'] });
},
});
}
```
## Components
### Base Component Pattern
```tsx
// src/components/ui/Card.tsx
import { View, ViewProps } from 'react-native';
import { cn } from '../../utils/cn';
interface CardProps extends ViewProps {
variant?: 'default' | 'elevated';
}
export function Card({
variant = 'default',
className,
children,
...props
}: CardProps) {
return (
<View
className={cn(
'bg-white rounded-xl p-4',
variant === 'elevated' && 'shadow-lg',
className
)}
{...props}
>
{children}
</View>
);
}
```
### List Component
```tsx
// src/components/features/ItemList.tsx
import { FlatList, View, Text, ActivityIndicator } from 'react-native';
import { useItems } from '../../hooks/useItems';
import { Card } from '../ui/Card';
export function ItemList() {
const { data: items, isLoading, error } = useItems();
if (isLoading) {
return <ActivityIndicator className="flex-1" />;
}
if (error) {
return (
<View className="flex-1 items-center justify-center">
<Text className="text-red-500">Error loading items</Text>
</View>
);
}
return (
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<Card className="mb-3">
<Text className="font-semibold">{item.title}</Text>
<Text className="text-gray-500">{item.description}</Text>
</Card>
)}
contentContainerClassName="p-4"
/>
);
}
```
## Best Practices
<Aside type="tip">
1. **Use Expo Router** for file-based navigation
2. **NativeWind** for consistent Tailwind-style styling
3. **Zustand** for global state (persisted with AsyncStorage)
4. **React Query** for server state and caching
5. **Organize by feature** in the `src/` directory
6. **Type everything** with TypeScript
</Aside>
## Common Commands
```bash
# Start development server
pnpm dev
# Run on iOS simulator
pnpm ios
# Run on Android emulator
pnpm android
# Build development version
pnpm build:dev
# Build production version
pnpm build:prod
```

View file

@ -0,0 +1,159 @@
---
title: Architecture Overview
description: High-level architecture of the Manacore ecosystem.
---
import { Card, CardGrid } from '@astrojs/starlight/components';
# Architecture Overview
Manacore is a multi-app ecosystem with shared infrastructure, enabling rapid development of interconnected applications.
## System Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ Client Layer │
├─────────────────┬─────────────────┬─────────────────────────────┤
│ Mobile Apps │ Web Apps │ Landing Pages │
│ (Expo/RN) │ (SvelteKit) │ (Astro) │
└────────┬────────┴────────┬────────┴──────────────┬──────────────┘
│ │ │
└─────────────────┼───────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ API Gateway │
│ (Individual NestJS backends) │
└─────────────────────────────┬───────────────────────────────────┘
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Mana Core Auth │ │ Mana Search │ │ Mana Storage │
│ (Port 3001) │ │ (Port 3021) │ │ (S3/MinIO) │
└────────┬────────┘ └────────┬────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ PostgreSQL │ │ Redis │
│ │ │ │
└─────────────────┘ └─────────────────┘
```
## Core Principles
<CardGrid>
<Card title="Shared Authentication" icon="shield">
All apps authenticate through **Mana Core Auth** using EdDSA JWT tokens.
</Card>
<Card title="Independent Databases" icon="database">
Each service has its own PostgreSQL database for isolation.
</Card>
<Card title="Shared Packages" icon="puzzle">
Common code lives in `packages/` for reuse across apps.
</Card>
<Card title="Consistent Patterns" icon="document">
All apps follow the same architectural patterns and conventions.
</Card>
</CardGrid>
## Technology Stack
### Frontend
| Layer | Technology | Purpose |
|-------|------------|---------|
| **Mobile** | Expo SDK 52+, React Native | iOS & Android apps |
| **Web** | SvelteKit 2, Svelte 5 | Web applications |
| **Landing** | Astro 5 | Marketing pages |
| **Styling** | Tailwind CSS, NativeWind | Consistent design |
### Backend
| Layer | Technology | Purpose |
|-------|------------|---------|
| **API** | NestJS 10-11 | REST APIs |
| **ORM** | Drizzle ORM | Database access |
| **Database** | PostgreSQL | Data persistence |
| **Cache** | Redis | Sessions, caching |
| **Storage** | MinIO/Hetzner S3 | File storage |
### Infrastructure
| Component | Technology | Purpose |
|-----------|------------|---------|
| **Package Manager** | pnpm 9.15+ | Dependency management |
| **Build System** | Turborepo | Monorepo orchestration |
| **Containerization** | Docker | Local dev & deployment |
| **CDN** | Cloudflare Pages | Static hosting |
## Service Communication
### Authentication Flow
```
┌──────────┐ ┌──────────┐ ┌─────────────────┐
│ Client │────>│ Backend │────>│ Mana Core Auth │
└──────────┘ └──────────┘ └─────────────────┘
│ │ │
│ Bearer JWT │ POST /validate │
│ │ {token} │
│ │<───────────────────│
│ │ {valid, payload} │
│<──────────────│ │
│ Response │ │
```
### Service Ports
| Service | Port | Purpose |
|---------|------|---------|
| mana-core-auth | 3001 | Authentication |
| chat-backend | 3002 | Chat API |
| picture-backend | 3006 | Image generation |
| zitare-backend | 3007 | Quotes API |
| manadeck-backend | 3009 | Card management |
| contacts-backend | 3015 | Contacts API |
| calendar-backend | 3014 | Calendar API |
| mana-search | 3021 | Search service |
## Data Flow
### Request Lifecycle
1. **Client** sends request with JWT token
2. **Backend** validates token with Mana Core Auth
3. **Service** processes request with Drizzle ORM
4. **Database** returns data
5. **Backend** transforms and returns response
6. **Client** receives and displays data
### Caching Strategy
- **Redis**: Session tokens, rate limiting
- **CDN**: Static assets, landing pages
- **Application**: In-memory caches for hot data
## Deployment Architecture
### Development
- Local Docker containers
- Hot reload on all services
- MinIO for S3-compatible storage
### Production
- Docker Compose on Mac Mini server
- Cloudflare Pages for static sites
- Hetzner Object Storage for files
- PostgreSQL for persistence
## Next Steps
- [Authentication](/architecture/authentication) - How auth works
- [Backend (NestJS)](/architecture/backend) - API patterns
- [Web (SvelteKit)](/architecture/web) - Frontend patterns
- [Storage](/architecture/storage) - File handling

View file

@ -0,0 +1,274 @@
---
title: Search Service
description: Centralized web search and content extraction with Mana Search.
---
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
# Search Service
**Mana Search** provides web search and content extraction capabilities for Manacore applications.
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Consumer Apps │
│ Questions │ Chat │ Project Doc Bot │ Future Apps │
└─────────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ mana-search (Port 3021) │
│ Search API │ Extract API │ Redis Cache │
└─────────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ SearXNG (Port 8080, internal) │
│ Google │ Bing │ DuckDuckGo │ Wikipedia │ arXiv │ ... │
└─────────────────────────────────────────────────────────────┘
```
## Quick Start
```bash
# Start SearXNG + Redis (for local development)
cd services/mana-search
docker-compose -f docker-compose.dev.yml up -d
# Start NestJS API
pnpm --filter @mana-search/service dev
# Or start everything via Docker
cd services/mana-search
docker-compose up -d
```
## API Endpoints
### Web Search
```bash
POST /api/v1/search
Content-Type: application/json
{
"query": "quantum computing basics",
"options": {
"categories": ["general", "science"],
"engines": ["google", "wikipedia"],
"limit": 10
}
}
```
Response:
```json
{
"results": [
{
"title": "Quantum Computing - Wikipedia",
"url": "https://en.wikipedia.org/wiki/Quantum_computing",
"snippet": "Quantum computing is a type of computation...",
"engine": "wikipedia"
}
],
"meta": {
"query": "quantum computing basics",
"totalResults": 10,
"searchTime": 0.523
}
}
```
### Content Extraction
```bash
POST /api/v1/extract
Content-Type: application/json
{
"url": "https://example.com/article",
"options": {
"includeMarkdown": true
}
}
```
Response:
```json
{
"title": "Article Title",
"content": "Full article text...",
"markdown": "# Article Title\n\nFull article text...",
"metadata": {
"author": "John Doe",
"publishedAt": "2024-01-15"
}
}
```
### Bulk Extract
```bash
POST /api/v1/extract/bulk
Content-Type: application/json
{
"urls": [
"https://example.com/article1",
"https://example.com/article2"
],
"options": {
"includeMarkdown": true
}
}
```
## Search Categories
| Category | Engines |
|----------|---------|
| `general` | Google, Bing, DuckDuckGo, Brave, Wikipedia |
| `news` | Google News, Bing News |
| `science` | arXiv, Google Scholar, PubMed, Semantic Scholar |
| `it` | GitHub, StackOverflow, NPM, MDN |
## Usage in Backend
### Direct Fetch
```typescript
const response = await fetch('http://mana-search:3021/api/v1/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: 'machine learning basics',
options: {
categories: ['general', 'science'],
limit: 5,
},
}),
});
const { results, meta } = await response.json();
```
### Service Class
```typescript
// src/services/search.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class SearchService {
private readonly baseUrl: string;
constructor(config: ConfigService) {
this.baseUrl = config.get('MANA_SEARCH_URL');
}
async search(query: string, options?: SearchOptions) {
const response = await fetch(`${this.baseUrl}/api/v1/search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, options }),
});
if (!response.ok) {
throw new Error(`Search failed: ${response.status}`);
}
return response.json();
}
async extract(url: string, options?: ExtractOptions) {
const response = await fetch(`${this.baseUrl}/api/v1/extract`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, options }),
});
if (!response.ok) {
throw new Error(`Extract failed: ${response.status}`);
}
return response.json();
}
}
```
## Caching
Mana Search uses Redis for caching:
| Cache | TTL | Purpose |
|-------|-----|---------|
| Search results | 1 hour | Reduce API calls to search engines |
| Extracted content | 24 hours | Cache parsed articles |
<Aside type="tip">
Caching significantly reduces load on external services and improves response times for repeated queries.
</Aside>
## Environment Variables
<Tabs>
<TabItem label="Consumer Apps">
```env
MANA_SEARCH_URL=http://localhost:3021
```
</TabItem>
<TabItem label="Mana Search Service">
```env
SEARXNG_URL=http://localhost:8080
REDIS_HOST=localhost
REDIS_PORT=6379
CACHE_SEARCH_TTL=3600
CACHE_EXTRACT_TTL=86400
```
</TabItem>
</Tabs>
## Health Check
```bash
GET /health
# Response
{
"status": "ok",
"searxng": "connected",
"redis": "connected"
}
```
## Rate Limiting
The service includes built-in rate limiting:
- **Search**: 30 requests/minute per IP
- **Extract**: 60 requests/minute per IP
- **Bulk Extract**: 10 requests/minute per IP
## Error Handling
```typescript
try {
const results = await searchService.search('query');
} catch (error) {
if (error.status === 429) {
// Rate limited - wait and retry
} else if (error.status === 503) {
// Search engines unavailable
}
}
```
## Best Practices
1. **Cache aggressively** - Search results rarely change
2. **Use appropriate categories** - More specific = better results
3. **Limit results** - Only fetch what you need
4. **Handle failures gracefully** - External services can be unreliable
5. **Respect rate limits** - Implement backoff strategies

View file

@ -0,0 +1,313 @@
---
title: Storage
description: S3-compatible object storage for files and media in Manacore.
---
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
# Storage
Manacore uses S3-compatible object storage for file uploads, generated images, and other media.
## Architecture
| Environment | Service | Purpose |
|-------------|---------|---------|
| **Local** | MinIO (Docker) | S3-compatible local storage |
| **Production** | Hetzner Object Storage | Cost-effective cloud storage |
## Local Development
```bash
# Start infrastructure (includes MinIO)
pnpm docker:up
# MinIO Web Console
open http://localhost:9001
# Credentials
# Username: minioadmin
# Password: minioadmin
```
## Pre-configured Buckets
| Bucket | Project | Purpose |
|--------|---------|---------|
| `picture-storage` | Picture | AI-generated images |
| `chat-storage` | Chat | User file uploads |
| `manadeck-storage` | ManaDeck | Card/deck assets |
| `nutriphi-storage` | NutriPhi | Meal photos |
| `contacts-storage` | Contacts | Contact avatars |
| `calendar-storage` | Calendar | Event attachments |
## Usage
### Backend Integration
```typescript
import {
createPictureStorage,
generateUserFileKey,
getContentType,
} from '@manacore/shared-storage';
const storage = createPictureStorage();
// Upload a file
async function uploadImage(userId: string, file: Buffer, filename: string) {
const key = generateUserFileKey(userId, filename);
const result = await storage.upload(key, file, {
contentType: getContentType(filename),
public: true,
});
return result.url;
}
// Download a file
async function downloadImage(key: string) {
const data = await storage.download(key);
return data;
}
// Generate presigned URLs
async function getUploadUrl(key: string) {
return storage.getUploadUrl(key, { expiresIn: 3600 });
}
async function getDownloadUrl(key: string) {
return storage.getDownloadUrl(key, { expiresIn: 3600 });
}
// Delete a file
async function deleteImage(key: string) {
await storage.delete(key);
}
// List files
async function listUserFiles(userId: string) {
const prefix = `users/${userId}/`;
return storage.list(prefix);
}
```
### Factory Functions
```typescript
import {
createPictureStorage,
createChatStorage,
createManaDeckStorage,
createContactsStorage,
} from '@manacore/shared-storage';
// Each creates a client configured for that bucket
const pictureStorage = createPictureStorage();
const chatStorage = createChatStorage();
```
### Custom Storage Client
```typescript
import { createStorageClient } from '@manacore/shared-storage';
const customStorage = createStorageClient({
bucket: 'my-custom-bucket',
region: process.env.S3_REGION,
endpoint: process.env.S3_ENDPOINT,
accessKeyId: process.env.S3_ACCESS_KEY,
secretAccessKey: process.env.S3_SECRET_KEY,
});
```
## File Key Patterns
```typescript
// User-specific files
const key = generateUserFileKey(userId, 'photo.jpg');
// → users/{userId}/photo.jpg
// With subfolder
const key = generateUserFileKey(userId, 'avatars/profile.jpg');
// → users/{userId}/avatars/profile.jpg
// Public assets
const key = `public/assets/${filename}`;
// Temporary files
const key = `temp/${Date.now()}-${filename}`;
```
## Environment Variables
<Tabs>
<TabItem label="Local (.env.development)">
```env
S3_ENDPOINT=http://localhost:9000
S3_REGION=us-east-1
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
```
</TabItem>
<TabItem label="Production">
```env
S3_ENDPOINT=https://fsn1.your-objectstorage.com
S3_REGION=fsn1
S3_ACCESS_KEY=your-access-key
S3_SECRET_KEY=your-secret-key
```
</TabItem>
</Tabs>
## Direct Upload (Presigned URLs)
For large files, use presigned URLs to upload directly from the client:
### Backend: Generate URL
```typescript
@Post('upload-url')
async getUploadUrl(
@CurrentUser() user: CurrentUserData,
@Body() dto: { filename: string; contentType: string }
) {
const key = generateUserFileKey(user.userId, dto.filename);
const uploadUrl = await storage.getUploadUrl(key, {
expiresIn: 300, // 5 minutes
contentType: dto.contentType,
});
return { uploadUrl, key };
}
```
### Client: Upload File
```typescript
// 1. Get presigned URL from backend
const { uploadUrl, key } = await api.getUploadUrl({
filename: file.name,
contentType: file.type,
});
// 2. Upload directly to S3
await fetch(uploadUrl, {
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type,
},
});
// 3. Notify backend of completed upload
await api.confirmUpload({ key });
```
## Public vs Private Files
### Public Files
```typescript
// Upload as public (anyone can access)
await storage.upload(key, buffer, {
contentType: 'image/png',
public: true,
});
// Access via direct URL
const url = `${S3_ENDPOINT}/${bucket}/${key}`;
```
### Private Files
```typescript
// Upload as private (default)
await storage.upload(key, buffer, {
contentType: 'image/png',
});
// Access via presigned URL
const url = await storage.getDownloadUrl(key, {
expiresIn: 3600, // 1 hour
});
```
## Image Processing
For image uploads, consider processing:
```typescript
import sharp from 'sharp';
async function processAndUpload(userId: string, buffer: Buffer, filename: string) {
// Create thumbnail
const thumbnail = await sharp(buffer)
.resize(200, 200, { fit: 'cover' })
.jpeg({ quality: 80 })
.toBuffer();
// Create optimized version
const optimized = await sharp(buffer)
.resize(1200, 1200, { fit: 'inside', withoutEnlargement: true })
.jpeg({ quality: 85 })
.toBuffer();
// Upload both
const [thumbResult, fullResult] = await Promise.all([
storage.upload(
generateUserFileKey(userId, `thumbs/${filename}`),
thumbnail,
{ contentType: 'image/jpeg', public: true }
),
storage.upload(
generateUserFileKey(userId, filename),
optimized,
{ contentType: 'image/jpeg', public: true }
),
]);
return {
thumbnailUrl: thumbResult.url,
fullUrl: fullResult.url,
};
}
```
## Best Practices
<Aside type="tip">
1. **Use presigned URLs** for large file uploads
2. **Generate unique keys** to avoid collisions
3. **Set appropriate content types** for proper browser handling
4. **Use private by default** - only make public what needs to be
5. **Clean up orphaned files** - delete when parent records are deleted
6. **Validate file types** before uploading
</Aside>
## Troubleshooting
### Connection Refused
Check MinIO is running:
```bash
docker ps | grep minio
pnpm docker:logs minio
```
### Access Denied
Verify credentials in `.env`:
```bash
echo $S3_ACCESS_KEY
echo $S3_SECRET_KEY
```
### Bucket Not Found
Create the bucket in MinIO console or via CLI:
```bash
docker exec minio mc mb /data/my-bucket
```

View file

@ -0,0 +1,386 @@
---
title: Web (SvelteKit)
description: SvelteKit web application patterns in Manacore.
---
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
# Web Architecture
All Manacore web applications use **SvelteKit 2** with **Svelte 5** (runes mode) and Tailwind CSS.
## Project Structure
```
apps/{project}/apps/web/
├── src/
│ ├── app.html # HTML template
│ ├── app.css # Global styles (Tailwind)
│ ├── lib/
│ │ ├── components/ # Reusable components
│ │ │ ├── ui/ # Base UI components
│ │ │ └── features/ # Feature-specific components
│ │ ├── stores/ # Svelte stores
│ │ ├── services/ # API clients, auth
│ │ ├── utils/ # Helper functions
│ │ └── types/ # TypeScript types
│ └── routes/
│ ├── +layout.svelte # Root layout
│ ├── +page.svelte # Home page
│ ├── (auth)/ # Auth route group
│ │ ├── login/
│ │ └── register/
│ └── (app)/ # App route group (protected)
│ ├── +layout.svelte
│ ├── dashboard/
│ └── settings/
├── static/ # Static assets
├── svelte.config.js
├── vite.config.ts
└── tailwind.config.js
```
## Svelte 5 Runes
<Aside type="caution">
Always use Svelte 5 runes mode. Never use the old `$:` reactive syntax.
</Aside>
### State Management
```svelte
<script lang="ts">
// Reactive state
let count = $state(0);
// Derived values
let doubled = $derived(count * 2);
// Deep reactive objects
let user = $state({
name: 'John',
settings: { theme: 'dark' }
});
// Effects (side effects on state change)
$effect(() => {
console.log('Count changed:', count);
// Cleanup function (optional)
return () => console.log('Cleanup');
});
function increment() {
count++;
}
</script>
<button onclick={increment}>
Count: {count} (doubled: {doubled})
</button>
```
### Props
```svelte
<script lang="ts">
interface Props {
title: string;
count?: number;
onUpdate?: (value: number) => void;
}
let { title, count = 0, onUpdate }: Props = $props();
</script>
<h1>{title}</h1>
<p>Count: {count}</p>
<button onclick={() => onUpdate?.(count + 1)}>Update</button>
```
### Bindings
```svelte
<script lang="ts">
let value = $state('');
let checked = $state(false);
</script>
<input bind:value />
<input type="checkbox" bind:checked />
```
## Routing
### Route Groups
Use route groups for layout organization:
```
routes/
├── (marketing)/ # Public pages
│ ├── +layout.svelte # Marketing layout
│ ├── +page.svelte # Landing page
│ └── pricing/
├── (auth)/ # Auth pages (no sidebar)
│ ├── +layout.svelte
│ ├── login/
│ └── register/
└── (app)/ # Protected app pages
├── +layout.svelte # App layout with sidebar
├── +layout.server.ts # Auth check
└── dashboard/
```
### Layout Data
```typescript
// routes/(app)/+layout.server.ts
import { redirect } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => {
if (!locals.user) {
throw redirect(302, '/login');
}
return {
user: locals.user,
};
};
```
### Page Data
```typescript
// routes/(app)/dashboard/+page.server.ts
import type { PageServerLoad } from './$types';
import { api } from '$lib/services/api';
export const load: PageServerLoad = async ({ locals }) => {
const stats = await api.getStats(locals.user.id);
return {
stats,
};
};
```
```svelte
<!-- routes/(app)/dashboard/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<h1>Dashboard</h1>
<p>Total items: {data.stats.total}</p>
```
## API Integration
### API Client
```typescript
// src/lib/services/api.ts
import { PUBLIC_API_URL } from '$env/static/public';
class ApiClient {
private baseUrl = PUBLIC_API_URL;
private token: string | null = null;
setToken(token: string) {
this.token = token;
}
private async fetch<T>(path: string, options: RequestInit = {}): Promise<T> {
const response = await fetch(`${this.baseUrl}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(this.token && { Authorization: `Bearer ${this.token}` }),
...options.headers,
},
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
}
async getUsers() {
return this.fetch<User[]>('/api/v1/users');
}
async createUser(data: CreateUserDto) {
return this.fetch<User>('/api/v1/users', {
method: 'POST',
body: JSON.stringify(data),
});
}
}
export const api = new ApiClient();
```
## Stores
Use Svelte stores for global state:
```typescript
// src/lib/stores/auth.ts
import { writable, derived } from 'svelte/store';
interface AuthState {
user: User | null;
token: string | null;
loading: boolean;
}
function createAuthStore() {
const { subscribe, set, update } = writable<AuthState>({
user: null,
token: null,
loading: true,
});
return {
subscribe,
setUser: (user: User, token: string) => {
update((state) => ({ ...state, user, token, loading: false }));
},
logout: () => {
set({ user: null, token: null, loading: false });
},
setLoading: (loading: boolean) => {
update((state) => ({ ...state, loading }));
},
};
}
export const auth = createAuthStore();
export const isAuthenticated = derived(auth, ($auth) => !!$auth.user);
```
## Components
### Base Component Pattern
```svelte
<!-- src/lib/components/ui/Button.svelte -->
<script lang="ts">
import { cn } from '$lib/utils';
interface Props {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
class?: string;
onclick?: () => void;
}
let {
variant = 'primary',
size = 'md',
disabled = false,
class: className,
onclick,
...rest
}: Props = $props();
const variants = {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
ghost: 'bg-transparent hover:bg-gray-100',
};
const sizes = {
sm: 'px-2 py-1 text-sm',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg',
};
</script>
<button
class={cn(
'rounded-md font-medium transition-colors',
variants[variant],
sizes[size],
disabled && 'opacity-50 cursor-not-allowed',
className
)}
{disabled}
{onclick}
{...rest}
>
<slot />
</button>
```
### Utility Function
```typescript
// src/lib/utils/cn.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
```
## Forms
### Form Actions
```typescript
// routes/settings/+page.server.ts
import type { Actions } from './$types';
import { fail } from '@sveltejs/kit';
export const actions: Actions = {
updateProfile: async ({ request, locals }) => {
const data = await request.formData();
const name = data.get('name') as string;
if (!name || name.length < 2) {
return fail(400, { error: 'Name must be at least 2 characters' });
}
await api.updateUser(locals.user.id, { name });
return { success: true };
},
};
```
```svelte
<!-- routes/settings/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
import type { ActionData } from './$types';
let { form }: { form: ActionData } = $props();
</script>
<form method="POST" action="?/updateProfile" use:enhance>
<input name="name" required minlength="2" />
{#if form?.error}
<p class="text-red-500">{form.error}</p>
{/if}
<button type="submit">Save</button>
</form>
```
## Best Practices
1. **Use runes** - Always use `$state`, `$derived`, `$effect`
2. **Type everything** - Use TypeScript for all components
3. **Server-side data loading** - Use `+page.server.ts` for API calls
4. **Form actions** - Use SvelteKit form actions for mutations
5. **Tailwind** - Use utility classes, avoid custom CSS
6. **Component composition** - Build small, reusable components

View file

@ -0,0 +1,242 @@
---
title: Cloudflare Pages
description: Deploying static sites and landing pages to Cloudflare Pages.
---
import { Steps, Tabs, TabItem, Aside } from '@astrojs/starlight/components';
# Cloudflare Pages
All landing pages and static sites are deployed to **Cloudflare Pages** using Direct Upload via Wrangler CLI.
## Deployed Sites
| Project | Package | Cloudflare Project | URL |
|---------|---------|-------------------|-----|
| Chat | `@chat/landing` | `chat-landing` | chat.manacore.app |
| Picture | `@picture/landing` | `picture-landing` | picture.manacore.app |
| Manacore | `@manacore/landing` | `manacore-landing` | manacore.app |
| ManaDeck | `@manadeck/landing` | `manadeck-landing` | manadeck.manacore.app |
| Zitare | `@zitare/landing` | `zitare-landing` | zitare.manacore.app |
| Docs | `@manacore/docs` | `manacore-docs` | docs.manacore.app |
## Quick Deploy
```bash
# Login to Cloudflare (first time only)
pnpm cf:login
# Deploy a landing page
pnpm deploy:landing:chat
pnpm deploy:landing:picture
# Deploy documentation
pnpm deploy:docs
# Deploy all landing pages
pnpm deploy:landing:all
```
## Setup Guide
<Steps>
1. **Login to Cloudflare**
```bash
npx wrangler login
```
This opens a browser to authenticate with your Cloudflare account.
2. **Create a project** (first time only)
```bash
npx wrangler pages project create chat-landing --production-branch=main
```
3. **Configure wrangler.toml**
Each landing page needs a `wrangler.toml`:
```toml
name = "chat-landing"
compatibility_date = "2024-12-01"
pages_build_output_dir = "dist"
```
4. **Deploy**
```bash
# Build and deploy
pnpm --filter @chat/landing build
npx wrangler pages deploy apps/chat/apps/landing/dist --project-name=chat-landing
```
</Steps>
## Project Configuration
### wrangler.toml
```toml
# Cloudflare Pages configuration
name = "project-landing"
compatibility_date = "2024-12-01"
pages_build_output_dir = "dist"
```
### Deploy Script Pattern
Each project has a deploy script in root `package.json`:
```json
{
"scripts": {
"deploy:landing:chat": "pnpm --filter @chat/landing build && npx wrangler pages deploy apps/chat/apps/landing/dist --project-name=chat-landing"
}
}
```
## Custom Domains
### Add a Custom Domain
```bash
# Via CLI
npx wrangler pages project add-domain chat-landing chat.manacore.app
# Or via Cloudflare Dashboard:
# 1. Go to Pages > chat-landing > Custom domains
# 2. Add domain: chat.manacore.app
# 3. Configure DNS (automatic if using Cloudflare DNS)
```
### DNS Configuration
If using Cloudflare DNS (recommended):
| Type | Name | Content | Proxy |
|------|------|---------|-------|
| CNAME | chat | chat-landing.pages.dev | Proxied |
If using external DNS:
| Type | Name | Content |
|------|------|---------|
| CNAME | chat | chat-landing.pages.dev |
## Environment Variables
### Build-time Variables
Set in Cloudflare Dashboard or via `wrangler.toml`:
```toml
[vars]
PUBLIC_API_URL = "https://api.manacore.app"
```
### Via Dashboard
1. Go to Pages > Project > Settings > Environment variables
2. Add variables for Production and Preview environments
## Deployment Previews
Every push to a branch creates a preview deployment:
- **Production**: `https://chat-landing.pages.dev`
- **Preview**: `https://<branch>.<project>.pages.dev`
## Rollback
### Via CLI
```bash
# List deployments
npx wrangler pages deployment list --project-name=chat-landing
# Promote a previous deployment to production
npx wrangler pages deployment promote <deployment-id> --project-name=chat-landing
```
### Via Dashboard
1. Go to Pages > Project > Deployments
2. Find the deployment to rollback to
3. Click "..." > "Rollback to this deployment"
## Adding a New Landing Page
<Steps>
1. **Create the landing page**
```
apps/{project}/apps/landing/
├── src/
├── public/
├── astro.config.mjs
├── wrangler.toml
└── package.json
```
2. **Add wrangler.toml**
```toml
name = "{project}-landing"
compatibility_date = "2024-12-01"
pages_build_output_dir = "dist"
```
3. **Create Cloudflare project**
```bash
npx wrangler pages project create {project}-landing --production-branch=main
```
4. **Add deploy script to root package.json**
```json
"deploy:landing:{project}": "pnpm --filter @{project}/landing build && npx wrangler pages deploy apps/{project}/apps/landing/dist --project-name={project}-landing"
```
5. **Deploy**
```bash
pnpm deploy:landing:{project}
```
6. **Add custom domain** (optional)
```bash
npx wrangler pages project add-domain {project}-landing {project}.manacore.app
```
</Steps>
## Troubleshooting
### "Project not found"
Create the project first:
```bash
npx wrangler pages project create project-name --production-branch=main
```
### Build Fails
Check the build locally:
```bash
pnpm --filter @project/landing build
```
### Domain Not Working
1. Verify DNS records are correct
2. Check SSL/TLS mode is "Full" in Cloudflare
3. Wait for DNS propagation (up to 24 hours)
<Aside type="tip">
Use `dig` to check DNS propagation:
```bash
dig chat.manacore.app
```
</Aside>

View file

@ -0,0 +1,316 @@
---
title: Mac Mini Server
description: Production server setup and management for Manacore backends.
---
import { Steps, Aside, Tabs, TabItem } from '@astrojs/starlight/components';
# Mac Mini Server
The production environment runs on a Mac Mini, accessible via Cloudflare Tunnel.
## Server Access
**Domain**: mana.how
### SSH Configuration
Add to `~/.ssh/config`:
```
Host mana-server
HostName mac-mini.mana.how
User till
ProxyCommand /opt/homebrew/bin/cloudflared access ssh --hostname %h
```
### Connect
```bash
ssh mana-server
```
<Aside type="note">
Requires `cloudflared` installed: `brew install cloudflare/cloudflare/cloudflared`
</Aside>
## Directory Structure
```
~/projects/manacore-monorepo/
├── docker-compose.macmini.yml # Production compose file
├── .env.production # Production environment
├── scripts/mac-mini/ # Server management scripts
│ ├── status.sh # Check all services
│ ├── deploy.sh # Pull & restart
│ └── health-check.sh # Run health checks
└── ...
```
## Common Operations
### Check Status
```bash
ssh mana-server
cd ~/projects/manacore-monorepo
./scripts/mac-mini/status.sh
```
Output:
```
=== Service Status ===
postgres running (healthy)
redis running (healthy)
mana-auth running (healthy)
chat-backend running (healthy)
...
```
### Deploy Updates
```bash
ssh mana-server
cd ~/projects/manacore-monorepo
./scripts/mac-mini/deploy.sh
```
This script:
1. Pulls latest code from git
2. Rebuilds Docker images
3. Restarts containers with zero-downtime
### View Logs
```bash
# All services
docker compose -f docker-compose.macmini.yml logs -f
# Specific service
docker compose -f docker-compose.macmini.yml logs -f chat-backend
# Last 100 lines
docker compose -f docker-compose.macmini.yml logs --tail=100 mana-auth
```
### Restart Services
```bash
# Single service
docker compose -f docker-compose.macmini.yml restart chat-backend
# All services
docker compose -f docker-compose.macmini.yml restart
# Full rebuild
docker compose -f docker-compose.macmini.yml up -d --build
```
## Docker Compose Configuration
### docker-compose.macmini.yml
```yaml
version: '3.8'
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: manacore
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U manacore"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
mana-auth:
build:
context: .
dockerfile: services/mana-core-auth/Dockerfile
environment:
- DATABASE_URL=postgresql://manacore:${POSTGRES_PASSWORD}@postgres:5432/manacore
- REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3001/health"]
interval: 30s
timeout: 10s
retries: 3
chat-backend:
build:
context: .
dockerfile: apps/chat/apps/backend/Dockerfile
environment:
- DATABASE_URL=postgresql://manacore:${POSTGRES_PASSWORD}@postgres:5432/chat
- MANA_CORE_AUTH_URL=http://mana-auth:3001
depends_on:
- mana-auth
- postgres
volumes:
postgres_data:
redis_data:
```
## Health Checks
### Run Health Checks
```bash
./scripts/mac-mini/health-check.sh
```
### Check Individual Service
```bash
# Auth service
curl http://localhost:3001/health
# Chat backend
curl http://localhost:3002/api/v1/health
# From outside via tunnel
curl https://api.mana.how/health
```
## Cloudflare Tunnel
### Configuration
The tunnel routes traffic from Cloudflare to local services:
```yaml
# ~/.cloudflared/config.yml
tunnel: mana-tunnel
credentials-file: ~/.cloudflared/mana-tunnel.json
ingress:
- hostname: api.mana.how
service: http://localhost:3001
- hostname: chat-api.mana.how
service: http://localhost:3002
- hostname: mac-mini.mana.how
service: ssh://localhost:22
- service: http_status:404
```
### Manage Tunnel
```bash
# Check tunnel status
cloudflared tunnel info mana-tunnel
# Restart tunnel
sudo launchctl stop com.cloudflare.cloudflared
sudo launchctl start com.cloudflare.cloudflared
# View tunnel logs
tail -f /var/log/cloudflared.log
```
## Backup & Recovery
### Database Backup
```bash
# Manual backup
docker exec postgres pg_dump -U manacore manacore > backup_$(date +%Y%m%d).sql
# Automated daily backups (via cron)
0 2 * * * /home/till/scripts/backup-databases.sh
```
### Restore Database
```bash
# Stop services
docker compose -f docker-compose.macmini.yml stop chat-backend
# Restore
docker exec -i postgres psql -U manacore manacore < backup_20240115.sql
# Start services
docker compose -f docker-compose.macmini.yml start chat-backend
```
## Monitoring
### Resource Usage
```bash
# Container stats
docker stats
# Disk usage
docker system df
# System resources
htop
```
### Alerts
<Aside type="note">
Monitoring with Prometheus/Grafana is planned. Currently using manual checks.
</Aside>
## Troubleshooting
### Container Won't Start
```bash
# Check logs
docker compose -f docker-compose.macmini.yml logs service-name
# Check container status
docker ps -a
# Restart with fresh build
docker compose -f docker-compose.macmini.yml up -d --build service-name
```
### Out of Disk Space
```bash
# Check disk usage
df -h
# Clean Docker
docker system prune -a --volumes
# Remove old images
docker image prune -a
```
### Database Connection Issues
```bash
# Check postgres is running
docker compose -f docker-compose.macmini.yml ps postgres
# Test connection
docker exec -it postgres psql -U manacore -c "SELECT 1"
# Check logs
docker compose -f docker-compose.macmini.yml logs postgres
```

View file

@ -0,0 +1,151 @@
---
title: Deployment Overview
description: Deployment strategies and infrastructure for Manacore applications.
---
import { Card, CardGrid, Aside } from '@astrojs/starlight/components';
# Deployment Overview
Manacore uses multiple deployment strategies depending on the application type.
## Deployment Targets
<CardGrid>
<Card title="Cloudflare Pages" icon="cloud">
Static sites: Landing pages, documentation
</Card>
<Card title="Mac Mini Server" icon="server">
Docker containers: Backends, databases
</Card>
<Card title="Hetzner Storage" icon="document">
S3-compatible object storage for files
</Card>
</CardGrid>
## Application Types
| Type | Deployment Target | Method |
|------|-------------------|--------|
| Landing Pages (Astro) | Cloudflare Pages | Direct Upload |
| Web Apps (SvelteKit) | Cloudflare Pages | Adapter |
| Backends (NestJS) | Mac Mini / Docker | Docker Compose |
| Databases | Mac Mini / Docker | Docker Compose |
| Static Assets | Hetzner S3 | Direct Upload |
## Infrastructure Overview
```
┌─────────────────────────────────────────────────────────────┐
│ Cloudflare │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ chat.mana │ │ docs.mana │ │ picture.mana│ ... │
│ │ core.app │ │ core.app │ │ core.app │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ │ │
│ Cloudflare Tunnel │
└──────────────────────────┼──────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Mac Mini Server │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ mana-core- │ │ chat- │ │ zitare- │ ... │
│ │ auth:3001 │ │ backend:3002│ │ backend:3007│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ PostgreSQL:5432 │ Redis:6379 │ MinIO:9000 │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Deployment Commands
### Landing Pages
```bash
# Deploy individual landing page
pnpm deploy:landing:chat
pnpm deploy:landing:picture
pnpm deploy:landing:zitare
# Deploy all landing pages
pnpm deploy:landing:all
```
### Documentation
```bash
# Deploy documentation site
pnpm deploy:docs
```
### Backends (via SSH)
```bash
# Connect to server
ssh mana-server
# Pull latest changes
cd ~/projects/manacore-monorepo
git pull
# Restart services
./scripts/mac-mini/deploy.sh
```
## Environments
| Environment | Purpose | URL Pattern |
|-------------|---------|-------------|
| **Development** | Local testing | `localhost:*` |
| **Staging** | Pre-production testing | `staging.*.manacore.app` |
| **Production** | Live users | `*.manacore.app` |
## CI/CD
<Aside type="note">
Full CI/CD pipelines are planned. Currently deployment is manual.
</Aside>
### Planned Pipeline
1. **On PR**: Run tests, type-check, lint
2. **On merge to main**: Deploy to staging
3. **On release tag**: Deploy to production
## Rollback Procedures
### Cloudflare Pages
```bash
# View deployments
npx wrangler pages deployment list --project-name=chat-landing
# Rollback to previous
npx wrangler pages deployment tail <deployment-id> --project-name=chat-landing
```
### Docker Services
```bash
ssh mana-server
cd ~/projects/manacore-monorepo
# Revert to previous commit
git checkout HEAD~1
# Rebuild and restart
./scripts/mac-mini/deploy.sh
```
## Next Steps
- [Cloudflare Pages](/deployment/cloudflare-pages) - Static site deployment
- [Mac Mini Server](/deployment/mac-mini-server) - Backend deployment
- [Self-Hosting](/deployment/self-hosting) - Host your own instance

View file

@ -0,0 +1,322 @@
---
title: Self-Hosting
description: Host your own Manacore instance with Docker Compose.
---
import { Steps, Aside, Tabs, TabItem } from '@astrojs/starlight/components';
# Self-Hosting
Run your own Manacore instance using Docker Compose.
## Requirements
- **Docker** 24.0+
- **Docker Compose** v2.0+
- **4GB RAM** minimum (8GB recommended)
- **20GB disk space** minimum
- **Domain name** (optional, for HTTPS)
## Quick Start
<Steps>
1. **Clone the repository**
```bash
git clone https://github.com/manacore/manacore-monorepo.git
cd manacore-monorepo
```
2. **Create environment file**
```bash
cp .env.example .env.production
```
Edit `.env.production` with your settings:
```env
# Database
POSTGRES_USER=manacore
POSTGRES_PASSWORD=your-secure-password
# Redis
REDIS_PASSWORD=your-redis-password
# JWT (generate new keys!)
JWT_PRIVATE_KEY=your-private-key
JWT_PUBLIC_KEY=your-public-key
# Domain
DOMAIN=your-domain.com
```
3. **Start services**
```bash
docker compose -f docker-compose.production.yml up -d
```
4. **Verify deployment**
```bash
# Check services
docker compose -f docker-compose.production.yml ps
# Check health
curl http://localhost:3001/health
```
</Steps>
## Configuration
### docker-compose.production.yml
```yaml
version: '3.8'
services:
# Database
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./docker/init-db:/docker-entrypoint-initdb.d
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
# Cache
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
restart: unless-stopped
# Object Storage
minio:
image: minio/minio
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${S3_ACCESS_KEY}
MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY}
volumes:
- minio_data:/data
restart: unless-stopped
# Auth Service
mana-auth:
image: ghcr.io/manacore/mana-core-auth:latest
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/manacore
REDIS_HOST: redis
REDIS_PASSWORD: ${REDIS_PASSWORD}
JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY}
JWT_PUBLIC_KEY: ${JWT_PUBLIC_KEY}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
# Chat Backend
chat-backend:
image: ghcr.io/manacore/chat-backend:latest
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/chat
MANA_CORE_AUTH_URL: http://mana-auth:3001
JWT_PUBLIC_KEY: ${JWT_PUBLIC_KEY}
depends_on:
- mana-auth
restart: unless-stopped
volumes:
postgres_data:
redis_data:
minio_data:
```
## Generate JWT Keys
Manacore uses EdDSA (Ed25519) for JWT signing:
```bash
# Generate private key
openssl genpkey -algorithm ed25519 -out private.pem
# Extract public key
openssl pkey -in private.pem -pubout -out public.pem
# Convert to base64 for .env
echo "JWT_PRIVATE_KEY=$(cat private.pem | base64 -w 0)"
echo "JWT_PUBLIC_KEY=$(cat public.pem | base64 -w 0)"
```
## Reverse Proxy Setup
### Nginx
```nginx
server {
listen 80;
server_name api.your-domain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name api.your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
location / {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
### Caddy
```
api.your-domain.com {
reverse_proxy localhost:3001
}
chat-api.your-domain.com {
reverse_proxy localhost:3002
}
```
## SSL with Let's Encrypt
```bash
# Install certbot
apt install certbot python3-certbot-nginx
# Get certificates
certbot --nginx -d api.your-domain.com -d chat-api.your-domain.com
# Auto-renewal (added automatically)
certbot renew --dry-run
```
## Backup Strategy
### Daily Database Backup
Create `/etc/cron.daily/manacore-backup`:
```bash
#!/bin/bash
BACKUP_DIR=/var/backups/manacore
DATE=$(date +%Y%m%d)
# Create backup directory
mkdir -p $BACKUP_DIR
# Backup all databases
docker exec postgres pg_dumpall -U manacore > $BACKUP_DIR/all_$DATE.sql
# Compress
gzip $BACKUP_DIR/all_$DATE.sql
# Keep last 7 days
find $BACKUP_DIR -name "*.gz" -mtime +7 -delete
```
### Restore from Backup
```bash
# Stop services
docker compose -f docker-compose.production.yml stop
# Restore
gunzip -c /var/backups/manacore/all_20240115.sql.gz | docker exec -i postgres psql -U manacore
# Start services
docker compose -f docker-compose.production.yml start
```
## Updating
<Steps>
1. **Pull latest images**
```bash
docker compose -f docker-compose.production.yml pull
```
2. **Backup database** (always!)
```bash
docker exec postgres pg_dumpall -U manacore > backup_before_update.sql
```
3. **Restart services**
```bash
docker compose -f docker-compose.production.yml up -d
```
4. **Verify**
```bash
docker compose -f docker-compose.production.yml ps
curl http://localhost:3001/health
```
</Steps>
## Troubleshooting
### Services Not Starting
```bash
# Check logs
docker compose -f docker-compose.production.yml logs
# Check specific service
docker compose -f docker-compose.production.yml logs mana-auth
```
### Database Connection Failed
```bash
# Verify postgres is running
docker compose -f docker-compose.production.yml ps postgres
# Check connection
docker exec -it postgres psql -U manacore -c "SELECT 1"
```
### Out of Memory
Increase Docker memory limits or add swap:
```bash
# Add 2GB swap
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
```
<Aside type="caution">
Always test updates on a staging environment before applying to production.
</Aside>

View file

@ -0,0 +1,274 @@
---
title: Database Migrations
description: Managing database schemas with Drizzle ORM in the Manacore monorepo.
---
import { Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components';
# Database Migrations
Manacore uses **Drizzle ORM** for database management with PostgreSQL.
## Overview
Each backend service has its own database and schema:
| Service | Database | Port |
|---------|----------|------|
| mana-core-auth | manacore | 3001 |
| chat | chat | 3002 |
| zitare | zitare | 3007 |
| contacts | contacts | 3015 |
| calendar | calendar | 3014 |
## Quick Commands
```bash
# Push schema changes (development)
pnpm --filter @chat/backend db:push
# Generate migration files
pnpm --filter @chat/backend db:generate
# Run migrations
pnpm --filter @chat/backend db:migrate
# Open Drizzle Studio (database GUI)
pnpm --filter @chat/backend db:studio
```
## Development Workflow
For local development, use `db:push` to quickly sync schema changes:
<Steps>
1. **Modify your schema**
Edit the schema file (e.g., `src/drizzle/schema.ts`):
```typescript
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: varchar('email', { length: 255 }).notNull().unique(),
name: varchar('name', { length: 255 }),
createdAt: timestamp('created_at').defaultNow(),
// Add new field
avatarUrl: varchar('avatar_url', { length: 500 }),
});
```
2. **Push changes**
```bash
pnpm --filter @chat/backend db:push
```
3. **Verify in Studio**
```bash
pnpm --filter @chat/backend db:studio
```
</Steps>
<Aside type="tip">
`db:push` is fast for development but doesn't create migration files. Use `db:generate` for production migrations.
</Aside>
## Production Migrations
For production, use proper migration files:
<Steps>
1. **Generate migration**
```bash
pnpm --filter @chat/backend db:generate
```
This creates a timestamped SQL file in `drizzle/migrations/`.
2. **Review migration**
Check the generated SQL:
```sql
-- 0001_add_avatar_url.sql
ALTER TABLE "users" ADD COLUMN "avatar_url" varchar(500);
```
3. **Run migration**
```bash
pnpm --filter @chat/backend db:migrate
```
</Steps>
## Schema Patterns
### Basic Table
```typescript
import { pgTable, uuid, varchar, timestamp, boolean } from 'drizzle-orm/pg-core';
export const posts = pgTable('posts', {
id: uuid('id').primaryKey().defaultRandom(),
title: varchar('title', { length: 255 }).notNull(),
content: text('content'),
published: boolean('published').default(false),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
```
### Foreign Keys
```typescript
export const comments = pgTable('comments', {
id: uuid('id').primaryKey().defaultRandom(),
postId: uuid('post_id')
.notNull()
.references(() => posts.id, { onDelete: 'cascade' }),
userId: uuid('user_id')
.notNull()
.references(() => users.id),
content: text('content').notNull(),
createdAt: timestamp('created_at').defaultNow(),
});
```
### Indexes
```typescript
import { pgTable, uuid, varchar, index } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: varchar('email', { length: 255 }).notNull().unique(),
organizationId: uuid('organization_id'),
}, (table) => ({
emailIdx: index('users_email_idx').on(table.email),
orgIdx: index('users_org_idx').on(table.organizationId),
}));
```
### Enums
```typescript
import { pgEnum } from 'drizzle-orm/pg-core';
export const roleEnum = pgEnum('role', ['user', 'admin', 'moderator']);
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
role: roleEnum('role').default('user'),
});
```
## Drizzle Configuration
Each backend has a `drizzle.config.ts`:
```typescript
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/drizzle/schema.ts',
out: './drizzle/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
verbose: true,
strict: true,
});
```
## Connecting in Services
### NestJS Module
```typescript
import { DrizzleModule } from '@manacore/shared-drizzle';
@Module({
imports: [
DrizzleModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
connectionString: config.get('DATABASE_URL'),
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
```
### Using in Services
```typescript
import { Inject, Injectable } from '@nestjs/common';
import { DRIZZLE } from '@manacore/shared-drizzle';
import { eq } from 'drizzle-orm';
import { users } from '../drizzle/schema';
@Injectable()
export class UsersService {
constructor(@Inject(DRIZZLE) private db: DrizzleDB) {}
async findById(id: string) {
const [user] = await this.db
.select()
.from(users)
.where(eq(users.id, id));
return user;
}
async create(data: { email: string; name: string }) {
const [user] = await this.db
.insert(users)
.values(data)
.returning();
return user;
}
}
```
## Troubleshooting
### "relation does not exist"
Schema not pushed:
```bash
pnpm --filter @chat/backend db:push --force
```
### Migration conflicts
Reset and regenerate:
```bash
# Delete old migrations
rm -rf apps/chat/apps/backend/drizzle/migrations/*
# Regenerate
pnpm --filter @chat/backend db:generate
```
### Connection refused
Check PostgreSQL is running:
```bash
pnpm docker:up
docker ps | grep postgres
```
## Best Practices
1. **Always review generated migrations** before running in production
2. **Use transactions** for multi-step operations
3. **Test migrations** on a copy of production data
4. **Keep schemas in sync** across services that share data
5. **Use `db:push` for development**, `db:migrate` for production

View file

@ -0,0 +1,292 @@
---
title: Docker Setup
description: Working with Docker for local development and production in Manacore.
---
import { Tabs, TabItem, Aside, Steps } from '@astrojs/starlight/components';
# Docker Setup
Manacore uses Docker for local development services and production deployment.
## Overview
Docker provides:
- **Development**: Local PostgreSQL, Redis, MinIO (S3-compatible storage)
- **CI/CD**: Automated builds and tests
- **Production**: Container deployment
## Quick Start
```bash
# Start all development infrastructure
pnpm docker:up
# View logs
pnpm docker:logs
# Stop services
pnpm docker:down
# Complete reset (removes volumes)
pnpm docker:clean
```
## Services Included
| Service | Port | Purpose |
|---------|------|---------|
| PostgreSQL | 5432 | Database |
| Redis | 6379 | Caching, sessions |
| MinIO | 9000 (API), 9001 (Console) | S3-compatible storage |
### MinIO Access
- **Console**: http://localhost:9001
- **Username**: `minioadmin`
- **Password**: `minioadmin`
## Docker Templates
Templates are in `docker/templates/` for creating new Dockerfiles:
### NestJS Backend
```bash
# Copy template
cp docker/templates/Dockerfile.nestjs apps/myproject/apps/backend/Dockerfile
```
Build arguments:
- `SERVICE_PATH`: Path to service
- `PORT`: Service port
- `HEALTH_PATH`: Health check endpoint
```bash
docker build \
--build-arg SERVICE_PATH=apps/chat/apps/backend \
--build-arg PORT=3002 \
--build-arg HEALTH_PATH=/api/health \
-t chat-backend:latest \
-f docker/templates/Dockerfile.nestjs \
.
```
### SvelteKit Web
```bash
docker build \
--build-arg SERVICE_PATH=apps/chat/apps/web \
--build-arg PORT=3000 \
-t chat-web:latest \
-f docker/templates/Dockerfile.sveltekit \
.
```
### Astro Landing
Static site served with Nginx:
```bash
docker build \
--build-arg SERVICE_PATH=apps/chat/apps/landing \
-t chat-landing:latest \
-f docker/templates/Dockerfile.astro \
.
```
## Building Images
### Development Build
```bash
# Build single service
docker build -t service-name:dev -f apps/project/apps/service/Dockerfile .
# Build with cache
docker build --cache-from service-name:latest -t service-name:dev .
```
### Production Build
```bash
# Multi-platform build
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t service-name:latest \
.
```
## Running Containers
```bash
# Run with environment file
docker run -d \
--name chat-backend \
--env-file .env.production \
-p 3002:3002 \
chat-backend:latest
# View logs
docker logs -f chat-backend
# Execute shell in container
docker exec -it chat-backend sh
```
## Docker Compose
### Development
```bash
# Start services
pnpm docker:up
# View status
docker compose -f docker-compose.dev.yml ps
# View specific service logs
docker compose -f docker-compose.dev.yml logs -f postgres
```
### Production
```bash
# Deploy to production
docker compose -f docker-compose.production.yml up -d
# Rolling update
docker compose -f docker-compose.production.yml up -d --no-deps service-name
# Scale service
docker compose up -d --scale service=3 service
```
## Best Practices
### 1. Optimize Layer Caching
```dockerfile
# Good - cache dependencies
COPY package.json pnpm-lock.yaml ./
RUN pnpm install
COPY . .
# Bad - invalidates cache on every change
COPY . .
RUN pnpm install
```
### 2. Use Multi-Stage Builds
```dockerfile
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY . .
RUN pnpm install && pnpm build
# Production stage
FROM node:20-alpine AS production
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/main.js"]
```
### 3. Security
```dockerfile
# Use non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nestjs -u 1001
USER nestjs
```
### 4. Health Checks
```dockerfile
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
```
### 5. Resource Limits
```yaml
services:
backend:
deploy:
resources:
limits:
cpus: '1'
memory: 512M
```
## Troubleshooting
### Container Won't Start
```bash
# View logs
docker logs container-name
# Check exit code
docker inspect --format='{{.State.ExitCode}}' container-name
# Run interactively
docker run -it --rm image-name sh
```
### Out of Disk Space
```bash
# Check usage
docker system df
# Clean everything unused
docker system prune -a --volumes
```
### Network Issues
```bash
# List networks
docker network ls
# Test connectivity
docker exec container1 ping container2
```
### Permission Issues
```bash
# Check ownership
docker exec container-name ls -la /app
# Fix in Dockerfile
RUN chown -R nodejs:nodejs /app
```
## Advanced
### BuildKit
```bash
export DOCKER_BUILDKIT=1
docker build .
```
### Custom Networks
```bash
docker network create --driver bridge my-network
docker run --network my-network image-name
```
### Volume Backup
```bash
docker run --rm \
-v my-data:/data \
-v $(pwd):/backup \
alpine tar czf /backup/backup.tar.gz /data
```

View file

@ -0,0 +1,185 @@
---
title: Environment Variables
description: Centralized environment variable management in the Manacore monorepo.
---
import { Tabs, TabItem, Aside, Steps } from '@astrojs/starlight/components';
# Environment Variables
Manacore uses a centralized environment variable system. All development variables are managed from a single file.
## Quick Start
```bash
# After cloning, install dependencies (auto-generates .env files)
pnpm install
# Or manually generate .env files
pnpm setup:env
```
That's it! All app-specific `.env` files are generated from `.env.development`.
## How It Works
```
.env.development # Central config (committed)
scripts/generate-env.mjs # Transforms variables
apps/**/apps/**/.env # Generated files (gitignored)
```
The generator reads `.env.development` and creates app-specific `.env` files with the correct prefixes for each platform:
| Platform | Prefix | Example |
|----------|--------|---------|
| Expo (mobile) | `EXPO_PUBLIC_` | `EXPO_PUBLIC_SUPABASE_URL` |
| SvelteKit (web) | `PUBLIC_` | `PUBLIC_SUPABASE_URL` |
| NestJS (backend) | None | `SUPABASE_URL` |
## File Locations
### Source File
- **`.env.development`** - Single source of truth, committed to git
### Generated Files (gitignored)
- `services/mana-core-auth/.env`
- `apps/chat/apps/backend/.env`
- `apps/chat/apps/mobile/.env`
- `apps/chat/apps/web/.env`
- And more...
## Variable Reference
### Shared Variables
| Variable | Description | Used By |
|----------|-------------|---------|
| `MANA_CORE_AUTH_URL` | Auth service URL | All apps |
| `JWT_PRIVATE_KEY` | JWT signing key | mana-core-auth |
| `JWT_PUBLIC_KEY` | JWT verification key | All backends |
| `POSTGRES_USER` | Database user | Docker, backends |
| `POSTGRES_PASSWORD` | Database password | Docker, backends |
| `REDIS_HOST` | Redis host | mana-core-auth |
| `REDIS_PORT` | Redis port | mana-core-auth |
### Mana Core Auth Service
| Variable | Description | Default |
|----------|-------------|---------|
| `MANA_CORE_AUTH_PORT` | Service port | `3001` |
| `MANA_CORE_AUTH_DATABASE_URL` | PostgreSQL connection | - |
| `JWT_ACCESS_TOKEN_EXPIRY` | Access token TTL | `15m` |
| `JWT_REFRESH_TOKEN_EXPIRY` | Refresh token TTL | `7d` |
| `JWT_ISSUER` | JWT issuer claim | `manacore` |
| `JWT_AUDIENCE` | JWT audience claim | `manacore` |
| `STRIPE_SECRET_KEY` | Stripe secret key | - |
| `CREDITS_SIGNUP_BONUS` | Credits on signup | `150` |
| `CREDITS_DAILY_FREE` | Daily free credits | `5` |
### Project-Specific Variables
<Tabs>
<TabItem label="Chat">
| Variable | Description |
|----------|-------------|
| `CHAT_BACKEND_PORT` | Backend port (3002) |
| `CHAT_DATABASE_URL` | PostgreSQL connection |
| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI endpoint |
| `AZURE_OPENAI_API_KEY` | Azure OpenAI key |
</TabItem>
<TabItem label="Picture">
| Variable | Description |
|----------|-------------|
| `PICTURE_BACKEND_PORT` | Backend port (3006) |
| `REPLICATE_API_KEY` | Replicate AI key |
</TabItem>
<TabItem label="Zitare">
| Variable | Description |
|----------|-------------|
| `ZITARE_BACKEND_PORT` | Backend port (3007) |
| `ZITARE_DATABASE_URL` | PostgreSQL connection |
</TabItem>
</Tabs>
## Adding New Variables
<Steps>
1. **Add to `.env.development`**
```bash
MY_NEW_PROJECT_API_KEY=your-api-key
MY_NEW_PROJECT_URL=https://api.example.com
```
2. **Update the Generator Script**
Edit `scripts/generate-env.mjs`:
```javascript
{
path: 'apps/my-project/apps/backend/.env',
vars: {
API_KEY: (env) => env.MY_NEW_PROJECT_API_KEY,
API_URL: (env) => env.MY_NEW_PROJECT_URL,
},
},
```
3. **Regenerate**
```bash
pnpm setup:env
```
</Steps>
## Local Overrides
If you need to override variables locally:
1. The generated `.env` files are gitignored
2. You can manually edit them after generation
3. Or create `.env.local` files (also gitignored) that some frameworks auto-load
<Aside type="caution">
Running `pnpm setup:env` will overwrite your changes in `.env` files. Use `.env.local` for persistent overrides.
</Aside>
## Docker Integration
The root `.env.development` is also used by Docker Compose:
```bash
# Start all services with shared env
pnpm docker:up
```
## Troubleshooting
### "Variable is undefined" Error
1. Check if the variable exists in `.env.development`
2. Run `pnpm setup:env` to regenerate
3. Restart your dev server (env changes require restart)
### Expo Not Picking Up Changes
Expo caches environment variables. Clear the cache:
```bash
cd apps/<project>/apps/mobile
npx expo start -c
```
## Security Notes
<Aside type="danger">
- `.env.development` contains **development-only** values
- Never put production secrets in this file
- The JWT keys are for local development only
- Use separate secrets management for production
</Aside>

View file

@ -0,0 +1,227 @@
---
title: Local Development
description: Set up and run Manacore applications locally with automatic database setup.
---
import { Tabs, TabItem, Steps, Aside } from '@astrojs/starlight/components';
# Local Development
This guide explains how to set up and run applications locally with automatic database setup.
## Quick Start
For any project with a backend, use the `dev:*:full` command:
```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
```
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
## Available Full 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 |
## Prerequisites
Before running any `dev:*:full` command:
<Steps>
1. **Start Docker infrastructure**
This starts PostgreSQL, Redis, and MinIO:
```bash
pnpm docker:up
```
2. **Generate environment files**
This runs automatically on `pnpm install`, but you can run manually:
```bash
pnpm setup:env
```
</Steps>
## Database Setup Commands
### Individual Service Setup
```bash
pnpm setup:db:auth # Setup mana-core-auth database + schema
pnpm setup:db:chat # Setup chat database + schema
pnpm setup:db:zitare # Setup zitare 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
```
### Setup All Databases
```bash
pnpm setup:db # Creates ALL databases and pushes ALL schemas
```
This is useful when setting up a fresh environment or after pulling new schema changes.
## How It Works
### 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
### Setup Script
The `scripts/setup-databases.sh` script:
1. **Creates database** if it doesn't exist (using `psql`)
2. **Pushes schema** using `drizzle-kit push --force`
The `--force` flag auto-approves schema changes without interactive prompts.
## 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 |
For these, use the regular dev commands:
```bash
pnpm dev:manacore:web
pnpm dev:manadeck:app
```
## Troubleshooting
### Database doesn't exist
If you see `database "xxx" does not exist`:
<Tabs>
<TabItem label="Option 1: Setup Script">
```bash
pnpm setup:db:chat # or whichever service
```
</TabItem>
<TabItem label="Option 2: Manual">
```bash
PGPASSWORD=devpassword psql -h localhost -U manacore -d postgres -c "CREATE DATABASE chat;"
```
</TabItem>
</Tabs>
### Schema out of date
If you see errors about missing tables/columns:
```bash
# Push the latest schema
pnpm --filter @chat/backend db:push --force
```
### Port already in use
If auth (port 3001) is already running:
```bash
# Check what's using the port
lsof -i :3001
# Kill the process if needed
kill <PID>
```
### Fresh Start (Nuclear Option)
To completely reset all databases:
```bash
# Stop and remove all containers + volumes
pnpm docker:clean
# Start fresh
pnpm docker:up
# Setup all databases
pnpm setup:db
```
## Adding a New Application
<Aside type="tip">
See the [Project Structure](/getting-started/project-structure) guide for the full directory layout.
</Aside>
### Step 1: Create Project Structure
```
apps/newproject/
├── apps/
│ ├── backend/ # NestJS API (if needed)
│ ├── mobile/ # Expo React Native app
│ ├── web/ # SvelteKit web app
│ └── landing/ # Astro marketing page
├── packages/ # Project-specific shared code
├── package.json # Workspace root
└── CLAUDE.md # Project documentation
```
### Step 2: Configure Backend Database
If your backend uses Drizzle ORM:
1. **Add database to Docker init** (`docker/init-db/01-create-databases.sql`):
```sql
CREATE DATABASE IF NOT EXISTS newproject;
GRANT ALL PRIVILEGES ON DATABASE newproject TO manacore;
```
2. **Add DATABASE_URL to `.env.development`**:
```env
NEWPROJECT_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/newproject
```
3. **Update `scripts/generate-env.mjs`** to generate the backend `.env` file.
### Step 3: Add Package.json Scripts
Add to root `package.json`:
```json
{
"scripts": {
"dev:newproject:full": "./scripts/setup-databases.sh newproject && ./scripts/setup-databases.sh auth && concurrently \"pnpm dev:auth\" \"pnpm dev:newproject:backend\" \"pnpm dev:newproject:web\"",
"setup:db:newproject": "./scripts/setup-databases.sh newproject"
}
}
```
### Step 4: Test
```bash
pnpm setup:db:newproject
pnpm dev:newproject:full
```

View file

@ -0,0 +1,285 @@
---
title: Testing
description: Testing patterns and practices in the Manacore monorepo.
---
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
# Testing
Manacore uses Jest for backend testing and Vitest for frontend testing.
## Quick Start
```bash
# Run all tests
pnpm test
# Run tests for specific project
pnpm --filter @chat/backend test
# Run tests in watch mode
pnpm --filter @chat/backend test:watch
# Run with coverage
pnpm --filter @chat/backend test:cov
```
## Test Structure
Tests are colocated with source files:
```
src/
├── users/
│ ├── users.service.ts
│ ├── users.service.spec.ts # Unit test
│ ├── users.controller.ts
│ └── users.controller.spec.ts
└── __tests__/
└── users.e2e.spec.ts # E2E test
```
## Unit Testing
### NestJS Services
```typescript
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { DRIZZLE } from '@manacore/shared-drizzle';
describe('UsersService', () => {
let service: UsersService;
let mockDb: any;
beforeEach(async () => {
mockDb = {
select: jest.fn().mockReturnThis(),
from: jest.fn().mockReturnThis(),
where: jest.fn().mockResolvedValue([{ id: '1', email: 'test@example.com' }]),
insert: jest.fn().mockReturnThis(),
values: jest.fn().mockReturnThis(),
returning: jest.fn().mockResolvedValue([{ id: '1', email: 'test@example.com' }]),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{ provide: DRIZZLE, useValue: mockDb },
],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should find user by id', async () => {
const user = await service.findById('1');
expect(user).toEqual({ id: '1', email: 'test@example.com' });
});
it('should create user', async () => {
const user = await service.create({ email: 'new@example.com', name: 'Test' });
expect(mockDb.insert).toHaveBeenCalled();
expect(user.email).toBe('test@example.com');
});
});
```
### NestJS Controllers
```typescript
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
describe('UsersController', () => {
let controller: UsersController;
let mockService: Partial<UsersService>;
beforeEach(async () => {
mockService = {
findById: jest.fn().mockResolvedValue({ id: '1', email: 'test@example.com' }),
create: jest.fn().mockResolvedValue({ id: '2', email: 'new@example.com' }),
};
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [{ provide: UsersService, useValue: mockService }],
}).compile();
controller = module.get<UsersController>(UsersController);
});
it('should get user', async () => {
const user = await controller.getUser('1');
expect(user.email).toBe('test@example.com');
expect(mockService.findById).toHaveBeenCalledWith('1');
});
});
```
## Frontend Testing
### SvelteKit Components (Vitest)
```typescript
import { render, screen } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';
import Button from './Button.svelte';
describe('Button', () => {
it('renders with text', () => {
render(Button, { props: { label: 'Click me' } });
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('handles click', async () => {
const { component } = render(Button, { props: { label: 'Click' } });
let clicked = false;
component.$on('click', () => { clicked = true; });
await screen.getByText('Click').click();
expect(clicked).toBe(true);
});
});
```
### React Native (Jest)
```typescript
import { render, screen, fireEvent } from '@testing-library/react-native';
import { Button } from './Button';
describe('Button', () => {
it('renders correctly', () => {
render(<Button title="Press me" onPress={() => {}} />);
expect(screen.getByText('Press me')).toBeTruthy();
});
it('calls onPress when pressed', () => {
const onPress = jest.fn();
render(<Button title="Press" onPress={onPress} />);
fireEvent.press(screen.getByText('Press'));
expect(onPress).toHaveBeenCalled();
});
});
```
## E2E Testing
### NestJS with Supertest
```typescript
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('Users (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
it('/users (GET)', () => {
return request(app.getHttpServer())
.get('/users')
.expect(200)
.expect((res) => {
expect(Array.isArray(res.body)).toBe(true);
});
});
it('/users (POST)', () => {
return request(app.getHttpServer())
.post('/users')
.send({ email: 'test@example.com', name: 'Test User' })
.expect(201)
.expect((res) => {
expect(res.body.email).toBe('test@example.com');
});
});
});
```
## Mock Factories
Create reusable mock factories:
```typescript
// test/factories/user.factory.ts
export const createMockUser = (overrides = {}) => ({
id: 'user-123',
email: 'test@example.com',
name: 'Test User',
createdAt: new Date(),
...overrides,
});
// Usage
const user = createMockUser({ name: 'Custom Name' });
```
## Test Configuration
### Jest Config (Backend)
```javascript
// jest.config.js
module.exports = {
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: 'src',
testRegex: '.*\\.spec\\.ts$',
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
},
collectCoverageFrom: ['**/*.(t|j)s'],
coverageDirectory: '../coverage',
testEnvironment: 'node',
};
```
### Vitest Config (Frontend)
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [svelte({ hot: !process.env.VITEST })],
test: {
include: ['src/**/*.{test,spec}.{js,ts}'],
environment: 'jsdom',
globals: true,
},
});
```
## Best Practices
<Aside type="tip" title="Testing Guidelines">
1. **Test behavior, not implementation** - Focus on what the code does, not how
2. **Use descriptive test names** - `it('should return 404 when user not found')`
3. **One assertion per test** - Makes failures easier to diagnose
4. **Mock external dependencies** - Database, APIs, file system
5. **Keep tests fast** - Unit tests should run in milliseconds
</Aside>
### Coverage Goals
- **Unit tests**: 80% coverage for business logic
- **Integration tests**: Critical paths and API endpoints
- **E2E tests**: Happy path user flows

View file

@ -0,0 +1,74 @@
---
title: Introduction
description: Welcome to the Manacore documentation - learn about our multi-app ecosystem platform.
---
import { Card, CardGrid } from '@astrojs/starlight/components';
# Welcome to Manacore
Manacore is a **multi-app ecosystem platform** built as a pnpm monorepo. It provides shared infrastructure, authentication, and common patterns for building modern web and mobile applications.
## What is Manacore?
Manacore is designed to streamline the development of multiple interconnected applications:
- **Shared Authentication** via Mana Core Auth (EdDSA JWT)
- **Unified Database Patterns** with Drizzle ORM and PostgreSQL
- **Cross-Platform Apps** with SvelteKit (web), Expo (mobile), and NestJS (backend)
- **Shared UI Components** for consistent design across all apps
- **Centralized Services** for search, storage, and more
## Quick Links
<CardGrid>
<Card title="Quick Start" icon="rocket">
Get up and running in minutes with our [Quick Start Guide](/getting-started/quick-start).
</Card>
<Card title="Project Structure" icon="folder">
Understand the [monorepo organization](/getting-started/project-structure).
</Card>
<Card title="Local Development" icon="laptop">
Set up your [local development environment](/development/local-development).
</Card>
<Card title="Architecture" icon="puzzle">
Learn about the [system architecture](/architecture/overview).
</Card>
</CardGrid>
## Projects in the Ecosystem
| Project | Description | Status |
|---------|-------------|--------|
| **Chat** | AI chat application with multiple models | Stable |
| **Picture** | AI image generation | Stable |
| **Zitare** | Daily inspiration quotes | Stable |
| **ManaDeck** | Card/deck management | Beta |
| **Contacts** | Contact management | Beta |
| **Calendar** | Calendar & scheduling | Alpha |
| **Matrix** | Matrix chat client | Alpha |
## Technology Stack
### Frontend
- **Web**: SvelteKit 2.x + Svelte 5 (Runes mode)
- **Mobile**: Expo SDK 52+ with React Native
- **Landing Pages**: Astro 5.x
### Backend
- **API**: NestJS 10-11
- **Database**: PostgreSQL with Drizzle ORM
- **Cache**: Redis
- **Storage**: S3-compatible (MinIO local, Hetzner production)
### Infrastructure
- **Package Manager**: pnpm 9.15+
- **Build System**: Turborepo
- **Deployment**: Cloudflare Pages, Docker
- **Auth**: Mana Core Auth (Better Auth + EdDSA JWT)
## Getting Help
- Check the [FAQ](/faq) for common questions
- Browse the [API Reference](/api) for endpoint details
- Join our community on Discord

View file

@ -0,0 +1,146 @@
---
title: Project Structure
description: Understand how the Manacore monorepo is organized.
---
import { FileTree } from '@astrojs/starlight/components';
# Project Structure
Manacore uses a pnpm workspace monorepo with Turborepo for build orchestration.
## Overview
<FileTree>
- apps/ Main product applications
- chat/
- apps/
- backend/ NestJS API
- mobile/ Expo React Native
- web/ SvelteKit app
- landing/ Astro marketing page
- packages/ Chat-specific packages
- picture/
- zitare/
- contacts/
- calendar/
- ...
- services/ Standalone microservices
- mana-core-auth/ Central authentication
- mana-search/ Search & content extraction
- mana-tts/ Text-to-speech
- mana-stt/ Speech-to-text
- packages/ Shared packages
- shared-auth/ Client auth utilities
- shared-nestjs-auth/ NestJS auth guards
- shared-storage/ S3 storage utilities
- shared-ui/ React Native components
- shared-types/ TypeScript types
- shared-utils/ Utility functions
- ...
- docker/ Docker configurations
- docs/ Documentation source files
- .claude/ Claude Code guidelines
</FileTree>
## Apps Structure
Each application in `apps/` follows a consistent structure:
```
apps/{project}/
├── apps/
│ ├── backend/ # NestJS API (optional)
│ ├── mobile/ # Expo app (optional)
│ ├── web/ # SvelteKit app
│ └── landing/ # Astro landing page
├── packages/ # Project-specific packages
├── package.json # Project root (for turbo)
└── CLAUDE.md # Project documentation
```
### App Types
| Type | Framework | Purpose |
|------|-----------|---------|
| `backend` | NestJS 10-11 | REST API, business logic, database access |
| `web` | SvelteKit 2 + Svelte 5 | Main web application |
| `mobile` | Expo SDK 52+ | iOS and Android app |
| `landing` | Astro 5 | Marketing/landing page |
## Services
The `services/` directory contains standalone microservices:
| Service | Port | Purpose |
|---------|------|---------|
| `mana-core-auth` | 3001 | Central authentication (EdDSA JWT) |
| `mana-search` | 3021 | Web search & content extraction |
| `mana-tts` | 3031 | Text-to-speech generation |
| `mana-stt` | 3032 | Speech-to-text transcription |
## Shared Packages
Packages in `packages/` are shared across all applications:
### Authentication
- `@manacore/shared-auth` - Client-side auth utilities
- `@manacore/shared-nestjs-auth` - NestJS JWT guards
- `@mana-core/nestjs-integration` - Full NestJS auth + credits module
### UI & Styling
- `@manacore/shared-ui` - React Native components
- `@manacore/shared-landing-ui` - Astro landing page components
- `@manacore/shared-theme` - Theme configuration
- `@manacore/shared-tailwind` - Tailwind presets
### Data & Utilities
- `@manacore/shared-types` - Common TypeScript types
- `@manacore/shared-utils` - Utility functions
- `@manacore/shared-storage` - S3 storage utilities
- `@manacore/shared-i18n` - Internationalization
## Workspace Configuration
### pnpm-workspace.yaml
```yaml
packages:
- 'apps/*'
- 'apps/*/apps/*'
- 'apps/*/packages/*'
- 'services/*'
- 'packages/*'
```
### turbo.json
Turborepo handles task orchestration with:
- **Parallel execution** for independent tasks
- **Dependency ordering** for build tasks
- **Caching** for faster rebuilds
## Naming Conventions
| Type | Pattern | Example |
|------|---------|---------|
| App package | `@{project}/{app}` | `@chat/backend` |
| Shared package | `@manacore/shared-{name}` | `@manacore/shared-auth` |
| Service | `@mana-{name}/service` | `@mana-search/service` |
## Adding a New Project
1. Create the directory structure:
```bash
mkdir -p apps/myproject/apps/{backend,web,landing}
```
2. Add a `CLAUDE.md` with project documentation
3. Create `package.json` files for each app
4. Add commands to root `package.json`
5. Configure in `turbo.json` if needed
See the existing projects as templates.

View file

@ -0,0 +1,128 @@
---
title: Quick Start
description: Get up and running with Manacore in minutes.
---
import { Steps, Tabs, TabItem, Code } from '@astrojs/starlight/components';
# Quick Start
Get the Manacore monorepo running locally in just a few steps.
## Prerequisites
Before you begin, ensure you have:
- **Node.js** 20 or higher
- **pnpm** 9.15.0 (`npm install -g pnpm@9.15.0`)
- **Docker** and Docker Compose
- **Git**
## Installation
<Steps>
1. **Clone the repository**
```bash
git clone https://github.com/manacore/manacore-monorepo.git
cd manacore-monorepo
```
2. **Install dependencies**
```bash
pnpm install
```
3. **Start infrastructure services**
This starts PostgreSQL, Redis, and MinIO (S3-compatible storage):
```bash
pnpm docker:up
```
4. **Setup environment variables**
Generate environment files for all apps:
```bash
pnpm setup:env
```
5. **Start an application**
Use `dev:*:full` commands for the best experience - they automatically set up databases:
<Tabs>
<TabItem label="Chat">
```bash
pnpm dev:chat:full
```
Opens at `http://localhost:5173` (web) and `http://localhost:3002` (API)
</TabItem>
<TabItem label="Zitare">
```bash
pnpm dev:zitare:full
```
Opens at `http://localhost:5174` (web) and `http://localhost:3007` (API)
</TabItem>
<TabItem label="Picture">
```bash
pnpm dev:picture:full
```
Opens at `http://localhost:5175` (web)
</TabItem>
</Tabs>
</Steps>
## What `dev:*:full` Does
The `full` commands automatically:
1. **Create the database** if it doesn't exist
2. **Push the latest schema** with Drizzle
3. **Start Mana Core Auth** (authentication service)
4. **Start the backend** (NestJS API)
5. **Start the web app** (SvelteKit)
All processes run concurrently with color-coded output.
## Verify Installation
Once running, you should be able to:
1. **Open the web app** at the URL shown in the terminal
2. **Check the API health** endpoint:
```bash
curl http://localhost:3002/api/v1/health
```
3. **Access MinIO console** at `http://localhost:9001` (user: `minioadmin`, password: `minioadmin`)
## Next Steps
- Learn about the [Project Structure](/getting-started/project-structure)
- Configure [Environment Variables](/development/environment-variables)
- Understand the [Architecture](/architecture/overview)
## Troubleshooting
### Docker not running
```bash
# Make sure Docker Desktop is running, then:
pnpm docker:up
```
### Port already in use
Check what's using the port and stop it:
```bash
lsof -i :3002 # Check port 3002
kill -9 <PID> # Stop the process
```
### Database connection issues
Ensure PostgreSQL is running:
```bash
docker ps # Should show postgres container
pnpm docker:logs postgres # Check logs
```

View file

@ -0,0 +1,305 @@
---
title: Code Style
description: Coding standards and formatting conventions for Manacore.
---
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
# Code Style
Consistent code style across the monorepo ensures readability and maintainability.
## Formatting
All projects use the same base formatting rules:
| Setting | Value |
|---------|-------|
| Indentation | Tabs (displayed as 2 spaces) |
| Quotes | Single quotes |
| Line width | 100 characters |
| Semicolons | Yes |
| Trailing commas | ES5 style |
### Prettier Configuration
```json
{
"useTabs": true,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"semi": true,
"trailingComma": "es5"
}
```
## Naming Conventions
### Files and Directories
| Type | Convention | Example |
|------|------------|---------|
| Components | PascalCase | `UserProfile.tsx`, `Button.svelte` |
| Utilities | camelCase | `formatDate.ts`, `cn.ts` |
| Types | camelCase | `user.types.ts` |
| Constants | SCREAMING_SNAKE | `constants.ts` (values inside) |
| Test files | `*.spec.ts` or `*.test.ts` | `users.service.spec.ts` |
### Code
<Tabs>
<TabItem label="TypeScript">
```typescript
// Classes - PascalCase
class UserService {}
// Interfaces - PascalCase, no "I" prefix
interface User {}
interface CreateUserDto {}
// Types - PascalCase
type UserRole = 'admin' | 'user';
// Functions - camelCase
function getUserById(id: string) {}
// Variables - camelCase
const currentUser = null;
// Constants - SCREAMING_SNAKE_CASE
const MAX_RETRIES = 3;
const API_BASE_URL = '/api';
// Enums - PascalCase (values too)
enum UserStatus {
Active = 'active',
Inactive = 'inactive',
}
```
</TabItem>
<TabItem label="Svelte">
```svelte
<script lang="ts">
// Props - camelCase
let { userName, onUpdate }: Props = $props();
// State - camelCase
let isLoading = $state(false);
let userData = $state<User | null>(null);
// Derived - camelCase
let fullName = $derived(`${userData?.first} ${userData?.last}`);
// Functions - camelCase
function handleSubmit() {}
</script>
<!-- Events - on:eventname (lowercase) -->
<button onclick={handleSubmit}>Submit</button>
```
</TabItem>
<TabItem label="React Native">
```tsx
// Components - PascalCase
function UserProfile({ user }: Props) {
// Hooks - use prefix
const [isLoading, setIsLoading] = useState(false);
const queryClient = useQueryClient();
// Handlers - handle prefix
const handlePress = () => {};
return <View />;
}
// Styles - camelCase
const styles = StyleSheet.create({
container: {},
headerText: {},
});
```
</TabItem>
</Tabs>
## TypeScript Guidelines
### Strict Mode
All projects use TypeScript strict mode:
```json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
```
### Type Annotations
```typescript
// Prefer explicit return types for public functions
function getUser(id: string): Promise<User | null> {
// ...
}
// Use inference for simple cases
const users = []; // Inferred as never[], be explicit!
const users: User[] = []; // Better
// Prefer interfaces for objects
interface User {
id: string;
name: string;
}
// Use types for unions/intersections
type UserRole = 'admin' | 'user' | 'guest';
type UserWithRole = User & { role: UserRole };
```
### Avoid `any`
```typescript
// Bad
function processData(data: any) {}
// Good
function processData<T>(data: T) {}
function processData(data: unknown) {}
function processData(data: Record<string, unknown>) {}
```
## Import Organization
Organize imports in this order:
```typescript
// 1. Node.js built-ins
import { readFile } from 'fs/promises';
// 2. External packages
import { Injectable } from '@nestjs/common';
import { eq } from 'drizzle-orm';
// 3. Monorepo packages
import { JwtAuthGuard } from '@manacore/shared-nestjs-auth';
// 4. Relative imports - parent directories
import { AppModule } from '../app.module';
// 5. Relative imports - same directory
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
```
## Comments
### When to Comment
```typescript
// Good - explains WHY, not WHAT
// We retry 3 times because the external API has occasional timeouts
const MAX_RETRIES = 3;
// Bad - explains WHAT (obvious from code)
// Set max retries to 3
const MAX_RETRIES = 3;
```
### JSDoc for Public APIs
```typescript
/**
* Fetches a user by their ID.
*
* @param id - The user's UUID
* @returns The user object or null if not found
* @throws {UnauthorizedException} If the caller lacks permission
*/
async function getUserById(id: string): Promise<User | null> {
// ...
}
```
### TODO Comments
```typescript
// TODO: Implement pagination (issue #123)
// FIXME: This breaks when user.name is null
// HACK: Workaround for library bug, remove after v2.0
```
## Error Handling
### Use Result Types
```typescript
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
// Usage
async function findUser(id: string): Promise<Result<User, 'NOT_FOUND'>> {
const user = await db.users.find(id);
if (!user) {
return { ok: false, error: 'NOT_FOUND' };
}
return { ok: true, value: user };
}
```
### Throw Only for Exceptional Cases
```typescript
// Good - expected case, use Result
const result = await findUser(id);
if (!result.ok) {
return res.status(404).json({ error: 'User not found' });
}
// Good - truly exceptional, throw
if (!process.env.DATABASE_URL) {
throw new Error('DATABASE_URL is required');
}
```
## Best Practices
<Aside type="tip">
1. **Keep functions small** - Under 50 lines ideally
2. **Single responsibility** - One function, one job
3. **Descriptive names** - `getUserByEmail` not `getUser2`
4. **Early returns** - Reduce nesting
5. **Avoid magic numbers** - Use named constants
6. **Immutability** - Prefer `const`, avoid mutations
</Aside>
### Early Returns
```typescript
// Bad - nested
function processUser(user: User | null) {
if (user) {
if (user.isActive) {
if (user.hasPermission) {
// actual logic
}
}
}
}
// Good - early returns
function processUser(user: User | null) {
if (!user) return;
if (!user.isActive) return;
if (!user.hasPermission) return;
// actual logic
}
```

View file

@ -0,0 +1,412 @@
---
title: Database Patterns
description: Drizzle ORM patterns and database best practices in Manacore.
---
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
# Database Patterns
Manacore uses **Drizzle ORM** with PostgreSQL for type-safe database access.
## Schema Design
### Basic Table
```typescript
import {
pgTable,
uuid,
varchar,
text,
timestamp,
boolean,
integer,
} from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
// Primary key - always UUID
id: uuid('id').primaryKey().defaultRandom(),
// Required fields
email: varchar('email', { length: 255 }).notNull().unique(),
name: varchar('name', { length: 100 }).notNull(),
// Optional fields
bio: text('bio'),
avatarUrl: varchar('avatar_url', { length: 500 }),
// Soft delete pattern
deletedAt: timestamp('deleted_at'),
// Audit fields - always include
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
```
### Foreign Keys
```typescript
export const posts = pgTable('posts', {
id: uuid('id').primaryKey().defaultRandom(),
title: varchar('title', { length: 255 }).notNull(),
content: text('content'),
// Foreign key with cascade delete
authorId: uuid('author_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
// Foreign key with set null
categoryId: uuid('category_id')
.references(() => categories.id, { onDelete: 'set null' }),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
```
### Indexes
```typescript
import { pgTable, uuid, varchar, index, uniqueIndex } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: varchar('email', { length: 255 }).notNull(),
organizationId: uuid('organization_id'),
status: varchar('status', { length: 20 }),
}, (table) => ({
// Single column index
emailIdx: uniqueIndex('users_email_idx').on(table.email),
// Composite index
orgStatusIdx: index('users_org_status_idx').on(table.organizationId, table.status),
}));
```
### Enums
```typescript
import { pgEnum, pgTable, uuid } from 'drizzle-orm/pg-core';
// Define enum
export const userRoleEnum = pgEnum('user_role', ['admin', 'user', 'guest']);
export const statusEnum = pgEnum('status', ['active', 'inactive', 'pending']);
// Use in table
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
role: userRoleEnum('role').default('user').notNull(),
status: statusEnum('status').default('pending').notNull(),
});
```
### JSON Columns
```typescript
import { pgTable, uuid, jsonb } from 'drizzle-orm/pg-core';
// Type for JSON structure
interface UserSettings {
theme: 'light' | 'dark';
notifications: boolean;
language: string;
}
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
settings: jsonb('settings').$type<UserSettings>().default({
theme: 'light',
notifications: true,
language: 'en',
}),
});
```
## Query Patterns
### Basic Queries
```typescript
import { eq, and, or, like, ilike, isNull, isNotNull } from 'drizzle-orm';
// Select all
const allUsers = await db.select().from(users);
// Select with condition
const activeUsers = await db
.select()
.from(users)
.where(eq(users.status, 'active'));
// Select specific columns
const emails = await db
.select({ email: users.email, name: users.name })
.from(users);
// Multiple conditions
const result = await db
.select()
.from(users)
.where(
and(
eq(users.status, 'active'),
isNull(users.deletedAt),
or(
eq(users.role, 'admin'),
eq(users.role, 'moderator')
)
)
);
// Pattern matching
const matched = await db
.select()
.from(users)
.where(ilike(users.name, '%john%'));
```
### Joins
```typescript
import { eq } from 'drizzle-orm';
// Inner join
const postsWithAuthors = await db
.select({
post: posts,
author: users,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id));
// Left join
const usersWithPosts = await db
.select()
.from(users)
.leftJoin(posts, eq(users.id, posts.authorId));
```
### Ordering and Pagination
```typescript
import { desc, asc } from 'drizzle-orm';
// Order by
const sorted = await db
.select()
.from(users)
.orderBy(desc(users.createdAt));
// Pagination
const page = 1;
const pageSize = 20;
const paginated = await db
.select()
.from(users)
.orderBy(desc(users.createdAt))
.limit(pageSize)
.offset((page - 1) * pageSize);
```
### Aggregations
```typescript
import { count, sum, avg, min, max } from 'drizzle-orm';
// Count
const [{ total }] = await db
.select({ total: count() })
.from(users)
.where(eq(users.status, 'active'));
// Group by
const countByRole = await db
.select({
role: users.role,
count: count(),
})
.from(users)
.groupBy(users.role);
```
### Insert
```typescript
// Single insert
const [newUser] = await db
.insert(users)
.values({
email: 'user@example.com',
name: 'John Doe',
})
.returning();
// Bulk insert
const newUsers = await db
.insert(users)
.values([
{ email: 'user1@example.com', name: 'User 1' },
{ email: 'user2@example.com', name: 'User 2' },
])
.returning();
// Upsert (insert or update)
await db
.insert(users)
.values({ email: 'user@example.com', name: 'John' })
.onConflictDoUpdate({
target: users.email,
set: { name: 'John Updated' },
});
```
### Update
```typescript
// Update with returning
const [updated] = await db
.update(users)
.set({
name: 'New Name',
updatedAt: new Date(),
})
.where(eq(users.id, userId))
.returning();
// Conditional update
await db
.update(users)
.set({ status: 'inactive' })
.where(
and(
eq(users.status, 'active'),
lt(users.lastLoginAt, thirtyDaysAgo)
)
);
```
### Delete
```typescript
// Hard delete
await db.delete(users).where(eq(users.id, userId));
// Soft delete (preferred)
await db
.update(users)
.set({ deletedAt: new Date() })
.where(eq(users.id, userId));
```
## Transactions
```typescript
import { db } from './drizzle';
// Basic transaction
await db.transaction(async (tx) => {
const [user] = await tx
.insert(users)
.values({ email: 'user@example.com', name: 'John' })
.returning();
await tx
.insert(profiles)
.values({ userId: user.id, bio: 'Hello!' });
// If anything throws, entire transaction rolls back
});
// With savepoints
await db.transaction(async (tx) => {
await tx.insert(users).values({ /* ... */ });
try {
await tx.insert(optionalData).values({ /* ... */ });
} catch (e) {
// This specific insert failed, but transaction continues
console.log('Optional insert failed, continuing...');
}
await tx.insert(requiredData).values({ /* ... */ });
});
```
## Service Pattern
```typescript
@Injectable()
export class UsersService {
constructor(@Inject(DRIZZLE) private db: DrizzleDB) {}
async findAll(options?: { status?: string; page?: number; limit?: number }) {
const { status, page = 1, limit = 20 } = options || {};
const query = this.db
.select()
.from(users)
.where(
and(
isNull(users.deletedAt),
status ? eq(users.status, status) : undefined
)
)
.orderBy(desc(users.createdAt))
.limit(limit)
.offset((page - 1) * limit);
return query;
}
async findById(id: string) {
const [user] = await this.db
.select()
.from(users)
.where(and(eq(users.id, id), isNull(users.deletedAt)));
return user || null;
}
async create(data: CreateUserDto) {
const [user] = await this.db
.insert(users)
.values(data)
.returning();
return user;
}
async update(id: string, data: UpdateUserDto) {
const [user] = await this.db
.update(users)
.set({ ...data, updatedAt: new Date() })
.where(eq(users.id, id))
.returning();
return user;
}
async softDelete(id: string) {
await this.db
.update(users)
.set({ deletedAt: new Date() })
.where(eq(users.id, id));
}
}
```
## Best Practices
<Aside type="tip">
1. **Always use UUIDs** for primary keys
2. **Include audit fields** (createdAt, updatedAt) on all tables
3. **Prefer soft deletes** for important data
4. **Use transactions** for multi-step operations
5. **Add indexes** for frequently queried columns
6. **Use enums** for fixed sets of values
7. **Type your JSON columns** with `$type<T>()`
</Aside>

View file

@ -0,0 +1,322 @@
---
title: Design & UX
description: UI patterns, animations, and accessibility guidelines for Manacore.
---
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
# Design & UX
Consistent design patterns across all Manacore applications for a unified user experience.
## Design Principles
1. **Consistency** - Same patterns across all apps
2. **Simplicity** - Clean, uncluttered interfaces
3. **Accessibility** - Usable by everyone
4. **Performance** - Fast, responsive interactions
5. **Feedback** - Clear system status communication
## Color System
### Semantic Colors
| Purpose | Light Mode | Dark Mode | Usage |
|---------|------------|-----------|-------|
| **Primary** | `blue-500` | `blue-400` | CTAs, links, active states |
| **Success** | `green-500` | `green-400` | Confirmations, completed |
| **Warning** | `amber-500` | `amber-400` | Caution, attention |
| **Error** | `red-500` | `red-400` | Errors, destructive actions |
| **Neutral** | `gray-*` | `gray-*` | Text, backgrounds, borders |
### Tailwind Usage
```tsx
// Primary actions
<button className="bg-blue-500 hover:bg-blue-600 text-white">
Submit
</button>
// Success state
<div className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
Success!
</div>
// Error state
<span className="text-red-500">Error message</span>
// Neutral backgrounds
<div className="bg-white dark:bg-gray-900">
<p className="text-gray-900 dark:text-gray-100">Content</p>
</div>
```
## Typography
### Scale
| Size | Class | Usage |
|------|-------|-------|
| xs | `text-xs` | Labels, captions |
| sm | `text-sm` | Secondary text, metadata |
| base | `text-base` | Body text |
| lg | `text-lg` | Emphasis, subheadings |
| xl | `text-xl` | Section headings |
| 2xl | `text-2xl` | Page headings |
| 3xl+ | `text-3xl` | Hero text, landing pages |
### Font Weights
```tsx
// Regular content
<p className="font-normal">Body text</p>
// Emphasis
<p className="font-medium">Important text</p>
// Headings
<h2 className="font-semibold">Section Title</h2>
// Strong emphasis
<h1 className="font-bold">Page Title</h1>
```
## Spacing
Use Tailwind's spacing scale consistently:
| Space | Class | Pixels | Usage |
|-------|-------|--------|-------|
| 1 | `p-1`, `m-1` | 4px | Tight spacing |
| 2 | `p-2`, `m-2` | 8px | Compact elements |
| 3 | `p-3`, `m-3` | 12px | Default padding |
| 4 | `p-4`, `m-4` | 16px | Card padding |
| 6 | `p-6`, `m-6` | 24px | Section spacing |
| 8 | `p-8`, `m-8` | 32px | Large sections |
```tsx
// Card with consistent spacing
<div className="p-4 space-y-3">
<h3 className="text-lg font-semibold">Title</h3>
<p className="text-gray-600">Description</p>
<button className="mt-4">Action</button>
</div>
// List with gaps
<ul className="space-y-2">
<li>Item 1</li>
<li>Item 2</li>
</ul>
```
## Components
### Buttons
<Tabs>
<TabItem label="Variants">
```tsx
// Primary - main actions
<button className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg">
Primary
</button>
// Secondary - alternative actions
<button className="bg-gray-200 hover:bg-gray-300 text-gray-900 px-4 py-2 rounded-lg">
Secondary
</button>
// Ghost - subtle actions
<button className="hover:bg-gray-100 text-gray-700 px-4 py-2 rounded-lg">
Ghost
</button>
// Destructive - dangerous actions
<button className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg">
Delete
</button>
```
</TabItem>
<TabItem label="States">
```tsx
// Disabled
<button className="bg-gray-300 text-gray-500 cursor-not-allowed" disabled>
Disabled
</button>
// Loading
<button className="bg-blue-500 text-white flex items-center gap-2" disabled>
<Spinner className="w-4 h-4 animate-spin" />
Loading...
</button>
```
</TabItem>
</Tabs>
### Cards
```tsx
// Basic card
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-4">
<h3 className="font-semibold">Card Title</h3>
<p className="text-gray-600 dark:text-gray-400 mt-2">Card content</p>
</div>
// Interactive card
<button className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-4 hover:shadow-md hover:border-blue-300 transition-all w-full text-left">
<h3 className="font-semibold">Clickable Card</h3>
</button>
```
### Forms
```tsx
// Input
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Enter text..."
/>
// With label and error
<div className="space-y-1">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Email
</label>
<input
type="email"
className="w-full px-3 py-2 border border-red-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
/>
<p className="text-sm text-red-500">Invalid email address</p>
</div>
```
## Loading States
### Skeletons
```tsx
// Text skeleton
<div className="animate-pulse">
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2 mt-2"></div>
</div>
// Card skeleton
<div className="animate-pulse p-4 border rounded-xl">
<div className="h-6 bg-gray-200 dark:bg-gray-700 rounded w-1/3 mb-4"></div>
<div className="space-y-2">
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-5/6"></div>
</div>
</div>
```
### Spinners
```tsx
// Simple spinner
<div className="w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
// Full page loading
<div className="fixed inset-0 bg-white/80 dark:bg-gray-900/80 flex items-center justify-center">
<div className="w-8 h-8 border-3 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
</div>
```
## Animations
### Transitions
```tsx
// Fade in
<div className="transition-opacity duration-200 opacity-0 data-[visible=true]:opacity-100">
// Slide up
<div className="transition-all duration-200 translate-y-2 opacity-0 data-[visible=true]:translate-y-0 data-[visible=true]:opacity-100">
// Scale
<button className="transition-transform hover:scale-105 active:scale-95">
```
### Motion Guidelines
- **Duration**: 150-300ms for micro-interactions, 300-500ms for larger transitions
- **Easing**: Use `ease-out` for entrances, `ease-in` for exits
- **Purpose**: Every animation should have a purpose (feedback, guidance, delight)
## Accessibility
<Aside type="caution">
Accessibility is not optional. All apps must meet WCAG 2.1 AA standards.
</Aside>
### Requirements
1. **Color contrast** - 4.5:1 for text, 3:1 for large text
2. **Focus indicators** - Visible focus rings on all interactive elements
3. **Keyboard navigation** - All features usable without mouse
4. **Screen readers** - Proper ARIA labels and semantic HTML
5. **Motion** - Respect `prefers-reduced-motion`
### Implementation
```tsx
// Focus visible
<button className="focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500">
// Screen reader text
<button aria-label="Close menu">
<XIcon className="w-5 h-5" aria-hidden="true" />
</button>
// Reduced motion
<div className="motion-safe:animate-bounce motion-reduce:animate-none">
// Skip link
<a href="#main" className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4">
Skip to main content
</a>
```
## Dark Mode
All components must support dark mode:
```tsx
// Background
<div className="bg-white dark:bg-gray-900">
// Text
<p className="text-gray-900 dark:text-gray-100">
// Borders
<div className="border-gray-200 dark:border-gray-700">
// Hover states
<button className="hover:bg-gray-100 dark:hover:bg-gray-800">
```
## Responsive Design
Mobile-first approach with breakpoints:
| Breakpoint | Min Width | Usage |
|------------|-----------|-------|
| (default) | 0px | Mobile |
| `sm:` | 640px | Large phones |
| `md:` | 768px | Tablets |
| `lg:` | 1024px | Laptops |
| `xl:` | 1280px | Desktops |
```tsx
// Responsive grid
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
// Responsive padding
<div className="p-4 md:p-6 lg:p-8">
// Hide/show
<div className="hidden md:block">Desktop only</div>
<div className="md:hidden">Mobile only</div>
```

View file

@ -0,0 +1,312 @@
---
title: Error Handling
description: Go-style Result types and error handling patterns in Manacore.
---
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
# Error Handling
Manacore uses **Go-style Result types** for explicit error handling, avoiding unexpected exceptions.
## Result Type Pattern
### Definition
```typescript
// types/result.ts
export type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
// Helper functions
export const ok = <T>(value: T): Result<T, never> => ({
ok: true,
value,
});
export const err = <E>(error: E): Result<never, E> => ({
ok: false,
error,
});
```
### Usage
```typescript
import { Result, ok, err } from '../types/result';
// Define error types
type UserError = 'NOT_FOUND' | 'INVALID_EMAIL' | 'ALREADY_EXISTS';
// Function with Result return type
async function findUserByEmail(email: string): Promise<Result<User, UserError>> {
if (!isValidEmail(email)) {
return err('INVALID_EMAIL');
}
const user = await db.users.findByEmail(email);
if (!user) {
return err('NOT_FOUND');
}
return ok(user);
}
// Calling the function
const result = await findUserByEmail('user@example.com');
if (!result.ok) {
switch (result.error) {
case 'NOT_FOUND':
throw new NotFoundException('User not found');
case 'INVALID_EMAIL':
throw new BadRequestException('Invalid email format');
case 'ALREADY_EXISTS':
throw new ConflictException('User already exists');
}
}
// TypeScript knows result.value is User here
return result.value;
```
## Why Result Types?
### Problems with Exceptions
```typescript
// Bad - caller doesn't know what can throw
async function getUser(id: string): Promise<User> {
const user = await db.users.find(id);
if (!user) throw new Error('Not found'); // Hidden!
return user;
}
// Caller has no idea this can throw
const user = await getUser(id); // 💥 might explode
```
### Benefits of Results
```typescript
// Good - explicit about possible failures
async function getUser(id: string): Promise<Result<User, 'NOT_FOUND'>> {
const user = await db.users.find(id);
if (!user) return err('NOT_FOUND');
return ok(user);
}
// Caller MUST handle the error
const result = await getUser(id);
if (!result.ok) {
// Handle error
}
// TypeScript ensures result.value exists here
```
## Error Type Design
### Use String Literals
```typescript
// Good - specific, exhaustive
type CreateUserError =
| 'EMAIL_TAKEN'
| 'INVALID_EMAIL'
| 'WEAK_PASSWORD'
| 'RATE_LIMITED';
// Bad - too generic
type CreateUserError = Error | string;
```
### Include Context When Needed
```typescript
type ValidationError = {
code: 'VALIDATION_FAILED';
field: string;
message: string;
};
type DatabaseError = {
code: 'DATABASE_ERROR';
originalError: Error;
};
type CreateUserError = ValidationError | DatabaseError | 'EMAIL_TAKEN';
// Usage
function validateUser(data: unknown): Result<UserDto, ValidationError> {
if (!data.email) {
return err({
code: 'VALIDATION_FAILED',
field: 'email',
message: 'Email is required',
});
}
// ...
}
```
## Pattern in Services
### Service Layer
```typescript
@Injectable()
export class UsersService {
async create(dto: CreateUserDto): Promise<Result<User, CreateUserError>> {
// Check for existing user
const existing = await this.findByEmail(dto.email);
if (existing.ok) {
return err('EMAIL_TAKEN');
}
// Validate password
if (!this.isStrongPassword(dto.password)) {
return err('WEAK_PASSWORD');
}
// Create user
try {
const user = await this.db.insert(users).values(dto).returning();
return ok(user[0]);
} catch (e) {
return err('DATABASE_ERROR');
}
}
async findById(id: string): Promise<Result<User, 'NOT_FOUND'>> {
const [user] = await this.db.select().from(users).where(eq(users.id, id));
return user ? ok(user) : err('NOT_FOUND');
}
}
```
### Controller Layer
```typescript
@Controller('api/v1/users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
async create(@Body() dto: CreateUserDto) {
const result = await this.usersService.create(dto);
if (!result.ok) {
switch (result.error) {
case 'EMAIL_TAKEN':
throw new ConflictException('Email already registered');
case 'WEAK_PASSWORD':
throw new BadRequestException('Password too weak');
case 'DATABASE_ERROR':
throw new InternalServerErrorException('Failed to create user');
}
}
return result.value;
}
@Get(':id')
async findOne(@Param('id') id: string) {
const result = await this.usersService.findById(id);
if (!result.ok) {
throw new NotFoundException('User not found');
}
return result.value;
}
}
```
## Combining Results
### Sequential Operations
```typescript
async function processOrder(orderId: string): Promise<Result<Order, OrderError>> {
// Step 1: Find order
const orderResult = await findOrder(orderId);
if (!orderResult.ok) return orderResult;
// Step 2: Validate
const validationResult = validateOrder(orderResult.value);
if (!validationResult.ok) return validationResult;
// Step 3: Process payment
const paymentResult = await processPayment(orderResult.value);
if (!paymentResult.ok) return paymentResult;
// All successful
return ok(orderResult.value);
}
```
### Parallel Operations
```typescript
async function getUserWithPosts(
userId: string
): Promise<Result<{ user: User; posts: Post[] }, 'USER_NOT_FOUND' | 'POSTS_FAILED'>> {
const [userResult, postsResult] = await Promise.all([
findUser(userId),
findPosts(userId),
]);
if (!userResult.ok) return err('USER_NOT_FOUND');
if (!postsResult.ok) return err('POSTS_FAILED');
return ok({
user: userResult.value,
posts: postsResult.value,
});
}
```
## When to Throw
<Aside type="caution">
Use exceptions only for **truly exceptional** situations:
</Aside>
```typescript
// Throw - configuration/programming errors
if (!process.env.DATABASE_URL) {
throw new Error('DATABASE_URL must be set');
}
// Throw - assertion violations (should never happen)
if (user.id !== request.userId) {
throw new Error('Invariant violated: user mismatch');
}
// Result - expected business cases
if (!user) {
return err('NOT_FOUND'); // Expected - user might not exist
}
```
## HTTP Error Mapping
| Result Error | HTTP Status | NestJS Exception |
|--------------|-------------|------------------|
| `NOT_FOUND` | 404 | `NotFoundException` |
| `UNAUTHORIZED` | 401 | `UnauthorizedException` |
| `FORBIDDEN` | 403 | `ForbiddenException` |
| `VALIDATION_FAILED` | 400 | `BadRequestException` |
| `CONFLICT` / `ALREADY_EXISTS` | 409 | `ConflictException` |
| `RATE_LIMITED` | 429 | `ThrottlerException` |
| `DATABASE_ERROR` | 500 | `InternalServerErrorException` |
## Best Practices
1. **Be specific** - Use descriptive error codes, not generic strings
2. **Exhaustive handling** - Handle all error cases with `switch`
3. **Don't swallow errors** - Always handle or propagate
4. **Document errors** - List possible errors in JSDoc
5. **Keep errors at boundaries** - Convert Results to HTTP errors in controllers

View file

@ -0,0 +1,57 @@
---
title: Manacore Documentation
description: Documentation for the Manacore ecosystem - a multi-app platform with shared infrastructure.
template: splash
hero:
tagline: Build modern applications with shared infrastructure
image:
file: ../../assets/logo-light.svg
actions:
- text: Get Started
link: /getting-started/introduction/
icon: right-arrow
variant: primary
- text: View on GitHub
link: https://github.com/manacore/manacore-monorepo
icon: external
---
import { Card, CardGrid } from '@astrojs/starlight/components';
## Quick Start
```bash
git clone https://github.com/manacore/manacore-monorepo.git
cd manacore-monorepo
pnpm install
pnpm docker:up
pnpm dev:chat:full
```
## Features
<CardGrid stagger>
<Card title="Shared Authentication" icon="shield">
Central auth service with EdDSA JWT tokens across all apps.
</Card>
<Card title="Multiple Apps" icon="puzzle">
Chat, Picture, Zitare, ManaDeck, and more - all sharing infrastructure.
</Card>
<Card title="Modern Stack" icon="rocket">
SvelteKit, Expo, NestJS, Drizzle ORM, Tailwind CSS.
</Card>
<Card title="Easy Deployment" icon="cloud">
Deploy to Cloudflare Pages, Docker, or self-host.
</Card>
</CardGrid>
## Projects
| Project | Description |
|---------|-------------|
| **Chat** | AI chat with multiple models |
| **Picture** | AI image generation |
| **Zitare** | Daily inspiration quotes |
| **ManaDeck** | Card & deck management |
| **Contacts** | Contact management |
| **Calendar** | Calendar & scheduling |

View file

@ -0,0 +1,171 @@
---
title: Chat
description: AI chat application with multiple models.
---
import { Aside, Tabs, TabItem, Badge } from '@astrojs/starlight/components';
# Chat
<Badge text="Stable" variant="success" />
AI chat application supporting multiple language models including local (Ollama) and cloud providers (OpenRouter).
## Overview
| Component | Technology | Port |
|-----------|------------|------|
| Backend | NestJS | 3002 |
| Web | SvelteKit | 5173 |
| Mobile | Expo | - |
| Landing | Astro | - |
## Quick Start
```bash
# Start everything (auth + database + backend + web)
pnpm dev:chat:full
# Or individual components
pnpm dev:chat:backend
pnpm dev:chat:web
pnpm dev:chat:mobile
```
## Architecture
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Client │────>│ Backend │────>│ AI Models │
│ (Web/Mobile)│ │ (NestJS) │ │ Ollama/API │
└─────────────┘ └──────┬──────┘ └─────────────┘
┌─────────────┐
│ PostgreSQL │
└─────────────┘
```
## API Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/health` | GET | Health check |
| `/api/v1/chat/models` | GET | List available AI models |
| `/api/v1/chat/completions` | POST | Create chat completion |
| `/api/v1/conversations` | GET | List user conversations |
| `/api/v1/conversations/:id` | GET | Get conversation details |
| `/api/v1/conversations/:id/messages` | GET | Get conversation messages |
| `/api/v1/conversations` | POST | Create new conversation |
| `/api/v1/conversations/:id/messages` | POST | Add message to conversation |
## AI Models
### Local Models (Ollama - Free)
| Model | Best For |
|-------|----------|
| Gemma 3 4B | Everyday tasks (default) |
| Llama 3.2 | General conversation |
| Mistral | Code assistance |
### Cloud Models (OpenRouter - Paid)
| Model | Price | Best For |
|-------|-------|----------|
| Llama 3.1 8B | $0.05/M | Fast cloud alternative |
| Llama 3.1 70B | $0.35/M | Complex reasoning |
| DeepSeek V3 | $0.14/M | Reasoning at low cost |
| Claude 3.5 Sonnet | $3/M | Best quality |
| GPT-4o Mini | $0.15/M | Balanced performance |
## Environment Variables
### Backend
```env
# AI Models
OPENROUTER_API_KEY=sk-or-v1-xxx
OLLAMA_URL=http://localhost:11434
OLLAMA_TIMEOUT=120000
# Database
DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/chat
# Auth
MANA_CORE_AUTH_URL=http://localhost:3001
# Server
PORT=3002
```
### Web
```env
PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
PUBLIC_BACKEND_URL=http://localhost:3002
```
## Database Schema
```typescript
// Conversations
export const conversations = pgTable('conversations', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull(),
title: varchar('title', { length: 255 }),
modelId: varchar('model_id', { length: 100 }),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
// Messages
export const messages = pgTable('messages', {
id: uuid('id').primaryKey().defaultRandom(),
conversationId: uuid('conversation_id')
.notNull()
.references(() => conversations.id, { onDelete: 'cascade' }),
role: varchar('role', { length: 20 }).notNull(), // 'user' | 'assistant'
content: text('content').notNull(),
createdAt: timestamp('created_at').defaultNow(),
});
```
## Features
- **Multi-model support** - Switch between local and cloud models
- **Conversation history** - Persistent chat history
- **Streaming responses** - Real-time AI responses
- **Code highlighting** - Syntax highlighting in responses
- **Markdown rendering** - Rich text formatting
- **Mobile app** - Native iOS and Android app
## Development
### Seed Database
First time setup requires seeding AI models:
```bash
pnpm --filter @chat/backend db:push
pnpm --filter @chat/backend db:seed
```
### Run Tests
```bash
pnpm --filter @chat/backend test
pnpm --filter @chat/backend test:e2e
```
### Open Database GUI
```bash
pnpm --filter @chat/backend db:studio
```
## Links
- **Web App**: http://localhost:5173
- **API**: http://localhost:3002
- **Drizzle Studio**: http://localhost:4983

View file

@ -0,0 +1,99 @@
---
title: Projects
description: Overview of all projects in the Manacore ecosystem.
---
import { Card, CardGrid, Badge } from '@astrojs/starlight/components';
# Projects
Manacore contains multiple interconnected applications, each serving a specific purpose.
## Active Projects
<CardGrid>
<Card title="Chat" icon="comment">
AI chat with multiple models (Ollama, OpenRouter)
**Stack**: NestJS, SvelteKit, Expo
[View Details](/projects/chat)
</Card>
<Card title="Picture" icon="image">
AI image generation
**Stack**: NestJS, SvelteKit, Expo
[View Details](/projects/picture)
</Card>
<Card title="Zitare" icon="quote">
Daily inspiration quotes
**Stack**: NestJS, SvelteKit, Expo
[View Details](/projects/zitare)
</Card>
<Card title="ManaDeck" icon="card">
Card & deck management
**Stack**: NestJS, SvelteKit, Expo
[View Details](/projects/manadeck)
</Card>
<Card title="Contacts" icon="person">
Contact management
**Stack**: NestJS, SvelteKit
[View Details](/projects/contacts)
</Card>
<Card title="Calendar" icon="calendar">
Calendar & scheduling
**Stack**: NestJS, SvelteKit
[View Details](/projects/calendar)
</Card>
</CardGrid>
## Project Status
| Project | Backend | Web | Mobile | Status |
|---------|---------|-----|--------|--------|
| Chat | 3002 | 5173 | Yes | Stable |
| Picture | 3006 | 5175 | Yes | Stable |
| Zitare | 3007 | 5177 | Yes | Stable |
| ManaDeck | 3009 | 5178 | Yes | Beta |
| Contacts | 3015 | 5184 | No | Beta |
| Calendar | 3014 | 5179 | No | Alpha |
| Clock | 3017 | 5187 | No | Alpha |
| Todo | 3018 | 5188 | No | Alpha |
| Matrix | - | 5180 | No | Alpha |
| Mail | - | 5190 | No | Alpha |
## Microservices
| Service | Port | Purpose |
|---------|------|---------|
| mana-core-auth | 3001 | Central authentication |
| mana-search | 3021 | Web search & extraction |
| mana-tts | 3031 | Text-to-speech |
| mana-stt | 3032 | Speech-to-text |
## Start Any Project
```bash
# Use dev:*:full for the best experience
pnpm dev:chat:full
pnpm dev:picture:full
pnpm dev:zitare:full
pnpm dev:contacts:full
pnpm dev:calendar:full
```
These commands automatically set up databases and start all required services.

View file

@ -0,0 +1,66 @@
/* Manacore Docs - Custom Starlight Styles */
/* Override Starlight CSS variables */
:root {
--sl-color-accent-low: #0c4a6e;
--sl-color-accent: #0ea5e9;
--sl-color-accent-high: #bae6fd;
--sl-color-white: #ffffff;
--sl-color-gray-1: #f1f5f9;
--sl-color-gray-2: #e2e8f0;
--sl-color-gray-3: #cbd5e1;
--sl-color-gray-4: #94a3b8;
--sl-color-gray-5: #64748b;
--sl-color-gray-6: #475569;
--sl-color-black: #0f172a;
}
:root[data-theme='dark'] {
--sl-color-accent-low: #082f49;
--sl-color-accent: #0ea5e9;
--sl-color-accent-high: #7dd3fc;
--sl-color-white: #0f172a;
--sl-color-gray-1: #1e293b;
--sl-color-gray-2: #334155;
--sl-color-gray-3: #475569;
--sl-color-gray-4: #64748b;
--sl-color-gray-5: #94a3b8;
--sl-color-gray-6: #cbd5e1;
--sl-color-black: #f8fafc;
}
/* Custom font settings */
html {
font-family: 'Inter', system-ui, sans-serif;
}
code,
pre {
font-family: 'JetBrains Mono', 'Menlo', monospace;
}
/* Better code block styling */
.expressive-code pre {
border-radius: 0.5rem;
}
/* Sidebar improvements */
.sidebar-content {
scrollbar-width: thin;
}
/* Custom badge styles for project status */
.badge-stable {
background-color: #10b981;
color: white;
}
.badge-beta {
background-color: #f59e0b;
color: white;
}
.badge-alpha {
background-color: #ef4444;
color: white;
}

View file

@ -0,0 +1,44 @@
import starlightPlugin from '@astrojs/starlight-tailwind';
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
theme: {
extend: {
colors: {
// Manacore brand colors
accent: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
950: '#082f49',
},
gray: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
950: '#020617',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'Menlo', 'monospace'],
},
},
},
plugins: [starlightPlugin()],
};

9
apps/docs/tsconfig.json Normal file
View file

@ -0,0 +1,9 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}

6
apps/docs/wrangler.toml Normal file
View file

@ -0,0 +1,6 @@
# Cloudflare Pages configuration for Manacore Docs
# Deployed via GitHub Actions (Direct Upload)
name = "manacore-docs"
compatibility_date = "2024-12-01"
pages_build_output_dir = "dist"

View file

@ -209,6 +209,9 @@
"deploy:landing:mail": "pnpm --filter @mail/landing build && npx wrangler pages deploy apps/mail/apps/landing/dist --project-name=mail-landing",
"deploy:landing:moodlit": "pnpm --filter @moodlit/landing build && npx wrangler pages deploy apps/moodlit/apps/landing/dist --project-name=moodlit-landing",
"deploy:landing:all": "pnpm deploy:landing:calendar && pnpm deploy:landing:chat && pnpm deploy:landing:picture && pnpm deploy:landing:manacore && pnpm deploy:landing:manadeck && pnpm deploy:landing:zitare && pnpm deploy:landing:presi && pnpm deploy:landing:clock && pnpm deploy:landing:mail && pnpm deploy:landing:nutriphi",
"dev:docs": "pnpm --filter @manacore/docs dev",
"build:docs": "pnpm --filter @manacore/docs build",
"deploy:docs": "pnpm --filter @manacore/docs build && npx wrangler pages deploy apps/docs/dist --project-name=manacore-docs",
"cf:login": "npx wrangler login",
"cf:projects:list": "npx wrangler pages project list",
"cf:projects:create": "echo 'Creating Cloudflare Pages projects...' && npx wrangler pages project create chat-landing --production-branch=main && npx wrangler pages project create picture-landing --production-branch=main && npx wrangler pages project create manacore-landing --production-branch=main && npx wrangler pages project create manadeck-landing --production-branch=main && npx wrangler pages project create zitare-landing --production-branch=main",

820
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff