3416 lines
96 KiB
Markdown
3416 lines
96 KiB
Markdown
# MoreMinimore SAAS - Technical Specification
|
|
|
|
## Executive Summary
|
|
|
|
Transform MoreMinimore from a local Electron app into a full-featured SAAS platform for AI-powered web application development with Easypanel deployment integration.
|
|
|
|
## Table of Contents
|
|
|
|
1. [Architecture Overview](#architecture-overview)
|
|
2. [Technology Stack](#technology-stack)
|
|
3. [Database Schema](#database-schema)
|
|
4. [Authentication & Authorization](#authentication--authorization)
|
|
5. [User Roles & Permissions](#user-roles--permissions)
|
|
6. [Core Features](#core-features)
|
|
7. [Deployment Flow](#deployment-flow)
|
|
8. [UI/UX Pro Max Integration](#uiux-pro-max-integration)
|
|
9. [Easypanel Integration](#easypanel-integration)
|
|
10. [Billing & Pricing](#billing--pricing)
|
|
11. [Code Storage Strategy](#code-storage-strategy)
|
|
12. [Migration Strategy](#migration-strategy)
|
|
13. [Security Considerations](#security-considerations)
|
|
14. [Performance Requirements](#performance-requirements)
|
|
15. [Development Phases](#development-phases)
|
|
|
|
---
|
|
|
|
## Architecture Overview
|
|
|
|
### System Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ MoreMinimore SAAS │
|
|
│ (Next.js Web Application) │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Application Layer │
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ Auth API │ │ Project API │ │ Billing API │ │
|
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ User API │ │ Deploy API │ │ Admin API │ │
|
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Service Layer │
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ AI Service │ │ Git Service │ │ Deploy Svc │ │
|
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ Billing Svc │ │ Email Svc │ │ Notif Svc │ │
|
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Data Layer │
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ PostgreSQL │ │ Redis │ │ Gitea │ │
|
|
│ │ (Primary) │ │ (Cache) │ │ (Backup) │ │
|
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ External Services │
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ Stripe │ │ Easypanel │ │ AI Providers│ │
|
|
│ │ (Billing) │ │ (Deploy) │ │ (OpenAI,etc)│ │
|
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Key Architectural Decisions
|
|
|
|
1. **Monolithic Next.js App**: Simplifies deployment and development
|
|
2. **PostgreSQL as Primary DB**: Robust, scalable, supports complex queries
|
|
3. **Redis for Caching**: Session management, rate limiting, temporary data
|
|
4. **Gitea for Code Backup**: Version control, collaboration, backup
|
|
5. **Easypanel for Production Deployment**: One-click deployment for users
|
|
|
|
---
|
|
|
|
## Technology Stack
|
|
|
|
### Frontend
|
|
|
|
- **Framework**: Next.js 15 (App Router)
|
|
- **UI Library**: React 19
|
|
- **Styling**: Tailwind CSS 4
|
|
- **Components**: shadcn/ui (Radix UI primitives)
|
|
- **State Management**: Zustand (global state) + React Query (server state)
|
|
- **Routing**: Next.js App Router
|
|
- **Forms**: React Hook Form + Zod validation
|
|
- **Markdown**: react-markdown + remark-gfm
|
|
- **Code Editor**: Monaco Editor (via @monaco-editor/react)
|
|
|
|
### Backend
|
|
|
|
- **Runtime**: Node.js 20+
|
|
- **API**: Next.js API Routes (App Router)
|
|
- **Database ORM**: Drizzle ORM
|
|
- **Authentication**: Custom JWT implementation
|
|
- **File Upload**: Uploadthing or similar
|
|
- **Email**: Resend or SendGrid
|
|
- **Background Jobs**: BullMQ (Redis-based) or similar
|
|
|
|
### Database
|
|
|
|
- **Primary**: PostgreSQL 16+
|
|
- **Cache**: Redis 7+
|
|
- **ORM**: Drizzle ORM
|
|
- **Migrations**: Drizzle Kit
|
|
|
|
### DevOps
|
|
|
|
- **Hosting**: VPS (shared resources)
|
|
- **Reverse Proxy**: Nginx
|
|
- **Process Manager**: PM2
|
|
- **SSL**: Let's Encrypt (Certbot)
|
|
- **Monitoring**: Custom logging + error tracking
|
|
- **CI/CD**: GitHub Actions
|
|
|
|
### External Services
|
|
|
|
- **Payment**: Stripe
|
|
- **Deployment**: Easypanel API
|
|
- **Code Backup**: Gitea (self-hosted)
|
|
- **AI Providers**: OpenAI, Anthropic, Google, etc. (bring your own key)
|
|
|
|
---
|
|
|
|
## Database Schema
|
|
|
|
### Core Tables
|
|
|
|
```sql
|
|
-- Users table
|
|
CREATE TABLE users (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
email VARCHAR(255) UNIQUE NOT NULL,
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
full_name VARCHAR(255),
|
|
role VARCHAR(50) NOT NULL, -- 'admin', 'co_admin', 'owner', 'user'
|
|
avatar_url TEXT,
|
|
email_verified BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|
last_login_at TIMESTAMP,
|
|
is_active BOOLEAN DEFAULT TRUE
|
|
);
|
|
|
|
-- Organizations (for multi-tenancy)
|
|
CREATE TABLE organizations (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name VARCHAR(255) NOT NULL,
|
|
slug VARCHAR(255) UNIQUE NOT NULL,
|
|
owner_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
|
stripe_customer_id VARCHAR(255),
|
|
subscription_tier VARCHAR(50) DEFAULT 'free', -- 'free', 'pro', 'enterprise'
|
|
subscription_status VARCHAR(50) DEFAULT 'active', -- 'active', 'past_due', 'canceled', 'trialing'
|
|
trial_ends_at TIMESTAMP,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
-- Organization Members (for Owner/User relationships)
|
|
CREATE TABLE organization_members (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
|
|
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
|
role VARCHAR(50) NOT NULL, -- 'owner', 'admin', 'member', 'viewer'
|
|
permissions JSONB, -- Granular permissions
|
|
invited_by UUID REFERENCES users(id),
|
|
joined_at TIMESTAMP DEFAULT NOW(),
|
|
UNIQUE(organization_id, user_id)
|
|
);
|
|
|
|
-- Projects (formerly 'apps')
|
|
CREATE TABLE projects (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
slug VARCHAR(255) NOT NULL,
|
|
gitea_repo_id INTEGER,
|
|
gitea_repo_url TEXT,
|
|
easypanel_project_id VARCHAR(255),
|
|
easypanel_app_id VARCHAR(255),
|
|
easypanel_database_id VARCHAR(255),
|
|
deployment_url TEXT,
|
|
install_command TEXT DEFAULT 'npm install',
|
|
start_command TEXT DEFAULT 'npm start',
|
|
build_command TEXT DEFAULT 'npm run build',
|
|
environment_variables JSONB DEFAULT '{}',
|
|
status VARCHAR(50) DEFAULT 'draft', -- 'draft', 'building', 'deployed', 'error'
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|
last_deployed_at TIMESTAMP,
|
|
UNIQUE(organization_id, slug)
|
|
);
|
|
|
|
-- Project Versions
|
|
CREATE TABLE project_versions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
|
|
version_number VARCHAR(50) NOT NULL,
|
|
commit_hash VARCHAR(255),
|
|
gitea_commit_id VARCHAR(255),
|
|
is_current BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
UNIQUE(project_id, version_number)
|
|
);
|
|
|
|
-- Chats (conversations)
|
|
CREATE TABLE chats (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
|
|
title VARCHAR(255),
|
|
created_by UUID REFERENCES users(id),
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
-- Messages
|
|
CREATE TABLE messages (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
chat_id UUID REFERENCES chats(id) ON DELETE CASCADE,
|
|
role VARCHAR(50) NOT NULL, -- 'user', 'assistant', 'system'
|
|
content TEXT NOT NULL,
|
|
metadata JSONB, -- Tool calls, tokens used, etc.
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
-- Prompts (templates)
|
|
CREATE TABLE prompts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
title VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
content TEXT NOT NULL,
|
|
category VARCHAR(100),
|
|
is_public BOOLEAN DEFAULT FALSE,
|
|
created_by UUID REFERENCES users(id),
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
-- AI Model Providers
|
|
CREATE TABLE ai_providers (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name VARCHAR(255) NOT NULL,
|
|
api_base_url TEXT NOT NULL,
|
|
env_var_name VARCHAR(100),
|
|
is_builtin BOOLEAN DEFAULT TRUE,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
-- AI Models
|
|
CREATE TABLE ai_models (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
display_name VARCHAR(255) NOT NULL,
|
|
api_name VARCHAR(255) NOT NULL,
|
|
provider_id UUID REFERENCES ai_providers(id) ON DELETE CASCADE,
|
|
description TEXT,
|
|
max_output_tokens INTEGER,
|
|
context_window INTEGER,
|
|
is_available BOOLEAN DEFAULT TRUE,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
-- User API Keys (for AI providers)
|
|
CREATE TABLE user_api_keys (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
|
provider_id UUID REFERENCES ai_providers(id) ON DELETE CASCADE,
|
|
encrypted_key TEXT NOT NULL,
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|
UNIQUE(user_id, provider_id)
|
|
);
|
|
|
|
-- Design Systems (from UI/UX Pro Max)
|
|
CREATE TABLE design_systems (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
|
|
name VARCHAR(255) NOT NULL,
|
|
pattern VARCHAR(255),
|
|
style VARCHAR(255),
|
|
color_palette JSONB,
|
|
typography JSONB,
|
|
effects JSONB,
|
|
anti_patterns JSONB,
|
|
generated_by_ai BOOLEAN DEFAULT TRUE,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
-- Deployment Logs
|
|
CREATE TABLE deployment_logs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
|
|
version_id UUID REFERENCES project_versions(id),
|
|
status VARCHAR(50) NOT NULL, -- 'pending', 'success', 'failed'
|
|
logs TEXT,
|
|
error_message TEXT,
|
|
started_at TIMESTAMP DEFAULT NOW(),
|
|
completed_at TIMESTAMP
|
|
);
|
|
|
|
-- Billing/Invoices
|
|
CREATE TABLE invoices (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
|
|
stripe_invoice_id VARCHAR(255) UNIQUE,
|
|
amount DECIMAL(10, 2) NOT NULL,
|
|
currency VARCHAR(3) DEFAULT 'USD',
|
|
status VARCHAR(50) NOT NULL, -- 'draft', 'open', 'paid', 'void', 'uncollectible'
|
|
due_date TIMESTAMP,
|
|
paid_at TIMESTAMP,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
-- Subscription Events
|
|
CREATE TABLE subscription_events (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
|
|
event_type VARCHAR(100) NOT NULL, -- 'created', 'updated', 'canceled', 'trial_ended'
|
|
stripe_event_id VARCHAR(255),
|
|
metadata JSONB,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
-- Audit Logs
|
|
CREATE TABLE audit_logs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
organization_id UUID REFERENCES organizations(id) ON DELETE SET NULL,
|
|
action VARCHAR(255) NOT NULL,
|
|
resource_type VARCHAR(100),
|
|
resource_id UUID,
|
|
metadata JSONB,
|
|
ip_address INET,
|
|
user_agent TEXT,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
-- Sessions (for JWT refresh tokens)
|
|
CREATE TABLE sessions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
|
refresh_token_hash VARCHAR(255) NOT NULL,
|
|
expires_at TIMESTAMP NOT NULL,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
device_info JSONB
|
|
);
|
|
|
|
-- Email Verification Tokens
|
|
CREATE TABLE email_verification_tokens (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
|
token VARCHAR(255) UNIQUE NOT NULL,
|
|
expires_at TIMESTAMP NOT NULL,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
-- Password Reset Tokens
|
|
CREATE TABLE password_reset_tokens (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
|
token VARCHAR(255) UNIQUE NOT NULL,
|
|
expires_at TIMESTAMP NOT NULL,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
-- Indexes for performance
|
|
CREATE INDEX idx_users_email ON users(email);
|
|
CREATE INDEX idx_organizations_slug ON organizations(slug);
|
|
CREATE INDEX idx_organizations_owner ON organizations(owner_id);
|
|
CREATE INDEX idx_org_members_org ON organization_members(organization_id);
|
|
CREATE INDEX idx_org_members_user ON organization_members(user_id);
|
|
CREATE INDEX idx_projects_org ON projects(organization_id);
|
|
CREATE INDEX idx_projects_slug ON projects(organization_id, slug);
|
|
CREATE INDEX idx_chats_project ON chats(project_id);
|
|
CREATE INDEX idx_messages_chat ON messages(chat_id);
|
|
CREATE INDEX idx_messages_created ON messages(created_at);
|
|
CREATE INDEX idx_deployment_logs_project ON deployment_logs(project_id);
|
|
CREATE INDEX idx_audit_logs_user ON audit_logs(user_id);
|
|
CREATE INDEX idx_audit_logs_org ON audit_logs(organization_id);
|
|
CREATE INDEX idx_audit_logs_created ON audit_logs(created_at);
|
|
```
|
|
|
|
---
|
|
|
|
## Authentication & Authorization
|
|
|
|
### Authentication Flow
|
|
|
|
```
|
|
┌─────────────┐
|
|
│ User │
|
|
└──────┬──────┘
|
|
│
|
|
│ 1. POST /api/auth/register
|
|
│ { email, password, fullName }
|
|
▼
|
|
┌─────────────────┐
|
|
│ Next.js API │
|
|
│ - Validate │
|
|
│ - Hash password│
|
|
│ - Create user │
|
|
│ - Send email │
|
|
└────────┬────────┘
|
|
│
|
|
│ 2. Email verification
|
|
▼
|
|
┌─────────────────┐
|
|
│ User clicks │
|
|
│ verification │
|
|
│ link │
|
|
└────────┬────────┘
|
|
│
|
|
│ 3. POST /api/auth/login
|
|
│ { email, password }
|
|
▼
|
|
┌─────────────────┐
|
|
│ Next.js API │
|
|
│ - Verify creds │
|
|
│ - Generate JWT │
|
|
│ - Set cookies │
|
|
└────────┬────────┘
|
|
│
|
|
│ 4. Response with JWT
|
|
▼
|
|
┌─────────────────┐
|
|
│ Client │
|
|
│ - Store JWT │
|
|
│ - Use in API │
|
|
└─────────────────┘
|
|
```
|
|
|
|
### JWT Structure
|
|
|
|
```typescript
|
|
// Access Token (15 minutes)
|
|
interface AccessTokenPayload {
|
|
userId: string;
|
|
email: string;
|
|
role: UserRole;
|
|
organizationId?: string; // For non-admin users
|
|
iat: number;
|
|
exp: number;
|
|
}
|
|
|
|
// Refresh Token (7 days)
|
|
interface RefreshTokenPayload {
|
|
userId: string;
|
|
sessionId: string;
|
|
iat: number;
|
|
exp: number;
|
|
}
|
|
```
|
|
|
|
### Authorization Middleware
|
|
|
|
```typescript
|
|
// Middleware to check permissions
|
|
export async function requireAuth(
|
|
request: NextRequest,
|
|
allowedRoles?: UserRole[],
|
|
) {
|
|
const token = request.cookies.get("access_token")?.value;
|
|
|
|
if (!token) {
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
|
|
try {
|
|
const payload = verifyJWT(token) as AccessTokenPayload;
|
|
|
|
if (allowedRoles && !allowedRoles.includes(payload.role)) {
|
|
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
}
|
|
|
|
return payload;
|
|
} catch (error) {
|
|
return NextResponse.json({ error: "Invalid token" }, { status: 401 });
|
|
}
|
|
}
|
|
|
|
// Check organization membership
|
|
export async function requireOrgMembership(
|
|
request: NextRequest,
|
|
orgId: string,
|
|
) {
|
|
const user = await requireAuth(request);
|
|
|
|
if (user.role === "admin" || user.role === "co_admin") {
|
|
return user;
|
|
}
|
|
|
|
// Check if user is member of organization
|
|
const member = await db.query.organizationMembers.findFirst({
|
|
where: eq(organizationMembers.organizationId, orgId),
|
|
with: { user: true },
|
|
});
|
|
|
|
if (!member) {
|
|
return NextResponse.json({ error: "Not a member" }, { status: 403 });
|
|
}
|
|
|
|
return { ...user, orgRole: member.role, permissions: member.permissions };
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## User Roles & Permissions
|
|
|
|
### Role Definitions
|
|
|
|
#### 1. Admin
|
|
|
|
- **Scope**: Global system control
|
|
- **Permissions**:
|
|
- Full access to all organizations and projects
|
|
- Manage all users (create, update, delete, ban)
|
|
- Manage system settings
|
|
- View all audit logs
|
|
- Manage billing for all organizations
|
|
- Access admin dashboard
|
|
- Override any restrictions
|
|
|
|
#### 2. Co-Admin
|
|
|
|
- **Scope**: Global settings and AI model management
|
|
- **Permissions**:
|
|
- Manage AI models and providers
|
|
- Update system-wide settings
|
|
- View system metrics and logs
|
|
- Manage pricing tiers
|
|
- Access admin dashboard (limited)
|
|
- Cannot delete organizations or users
|
|
|
|
#### 3. Owner
|
|
|
|
- **Scope**: Their own organization
|
|
- **Permissions**:
|
|
- Full control over their organization
|
|
- Create, update, delete projects
|
|
- Invite and manage team members
|
|
- Set member permissions
|
|
- Manage organization billing
|
|
- Deploy projects to Easypanel
|
|
- Access organization analytics
|
|
- Export/import project data
|
|
|
|
#### 4. User (Organization Member)
|
|
|
|
- **Scope**: Assigned projects within organization
|
|
- **Permissions** (configurable by Owner):
|
|
- **View**: Read-only access to assigned projects
|
|
- **Edit**: Can modify code and chat
|
|
- **Deploy**: Can deploy to Easypanel
|
|
- **Manage**: Can manage project settings
|
|
- **Invite**: Can invite other members (if granted)
|
|
|
|
### Permission Matrix
|
|
|
|
| Action | Admin | Co-Admin | Owner | User (View) | User (Edit) | User (Deploy) |
|
|
| ---------------------- | ----- | -------- | ----- | ----------- | ----------- | ------------- |
|
|
| View all organizations | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
| Manage users | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
| Manage AI models | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
| View system logs | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
| Create projects | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
|
| Edit own projects | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ |
|
|
| Deploy projects | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
|
|
| Invite members | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
|
| Manage billing | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
|
| View analytics | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
|
|
|
|
---
|
|
|
|
## Core Features
|
|
|
|
### 1. User Management
|
|
|
|
#### Registration
|
|
|
|
- Email/password registration
|
|
- Email verification required
|
|
- Optional social login (Google, GitHub) - future enhancement
|
|
|
|
#### Login
|
|
|
|
- Email/password login
|
|
- Remember me option
|
|
- Session management with refresh tokens
|
|
|
|
#### Profile Management
|
|
|
|
- Update profile information
|
|
- Change password
|
|
- Manage API keys for AI providers
|
|
- Two-factor authentication (future)
|
|
|
|
#### Team Management (for Owners)
|
|
|
|
- Invite team members via email
|
|
- Set member roles and permissions
|
|
- Remove members
|
|
- View member activity
|
|
|
|
### 2. Project Management
|
|
|
|
#### Create Project
|
|
|
|
- Project name and description
|
|
- Auto-generate unique slug
|
|
- Select template (optional)
|
|
- Initialize Git repository in Gitea
|
|
|
|
#### Project Dashboard
|
|
|
|
- Overview of project status
|
|
- Recent activity
|
|
- Deployment status
|
|
- Team members access
|
|
|
|
#### Project Settings
|
|
|
|
- Update project details
|
|
- Configure build commands
|
|
- Set environment variables
|
|
- Manage deployment settings
|
|
- Delete project
|
|
|
|
#### Version Control
|
|
|
|
- View version history
|
|
- Compare versions
|
|
- Rollback to previous version
|
|
- Tag releases
|
|
|
|
### 3. AI-Powered Development
|
|
|
|
#### Chat Interface
|
|
|
|
- Natural language conversation with AI
|
|
- Context-aware responses
|
|
- Code generation and modification
|
|
- Real-time preview
|
|
|
|
#### Code Editor
|
|
|
|
- Monaco editor integration
|
|
- Syntax highlighting
|
|
- Auto-completion
|
|
- File tree navigation
|
|
- Multiple file editing
|
|
|
|
#### Design System Generation (UI/UX Pro Max)
|
|
|
|
- Automatic design system generation
|
|
- Industry-specific recommendations
|
|
- Color palette selection
|
|
- Typography pairing
|
|
- Style guidelines
|
|
|
|
#### Preview
|
|
|
|
- Live preview of generated code
|
|
- Responsive design testing
|
|
- Interactive components
|
|
|
|
### 4. Deployment Management
|
|
|
|
#### Easypanel Integration
|
|
|
|
- One-click deployment
|
|
- Automatic database creation
|
|
- Environment variable management
|
|
- Deployment logs
|
|
- Rollback capability
|
|
|
|
#### Deployment History
|
|
|
|
- View all deployments
|
|
- Compare deployments
|
|
- Rollback to previous version
|
|
- Deployment analytics
|
|
|
|
### 5. Billing & Subscription
|
|
|
|
#### Subscription Tiers
|
|
|
|
- **Free**: 1 project, limited AI tokens, community support
|
|
- **Pro**: 10 projects, unlimited AI tokens, priority support, custom domains
|
|
- **Enterprise**: Unlimited projects, dedicated support, SLA, advanced features
|
|
|
|
#### Billing Management
|
|
|
|
- View invoices
|
|
- Update payment method
|
|
- Change subscription tier
|
|
- Cancel subscription
|
|
- Usage analytics
|
|
|
|
### 6. Admin Dashboard
|
|
|
|
#### System Overview
|
|
|
|
- Total users and organizations
|
|
- Active projects
|
|
- Revenue metrics
|
|
- System health
|
|
|
|
#### User Management
|
|
|
|
- View all users
|
|
- Manage user accounts
|
|
- View user activity
|
|
- Ban/unban users
|
|
|
|
#### Organization Management
|
|
|
|
- View all organizations
|
|
- Manage subscriptions
|
|
- View organization analytics
|
|
|
|
#### System Settings
|
|
|
|
- AI model configuration
|
|
- Pricing tiers management
|
|
- System-wide settings
|
|
- Maintenance mode
|
|
|
|
---
|
|
|
|
## Deployment Flow
|
|
|
|
### Complete Deployment Workflow
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 1. User develops project in MoreMinimore │
|
|
│ - Chat with AI to generate code │
|
|
│ - Use UI/UX Pro Max for design │
|
|
│ - Preview in browser │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 2. User clicks "Deploy to Easypanel" │
|
|
│ - Select deployment settings │
|
|
│ - Configure environment variables │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 3. MoreMinimore creates version │
|
|
│ - Commit changes to Gitea │
|
|
│ - Create project version record │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 4. MoreMinimore calls Easypanel API │
|
|
│ - Create database (PostgreSQL) │
|
|
│ - Create application │
|
|
│ - Configure environment variables │
|
|
│ - Set build and start commands │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 5. Easypanel provisions resources │
|
|
│ - Spins up PostgreSQL database │
|
|
│ - Deploys application container │
|
|
│ - Configures networking │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 6. Easypanel pulls code from Gitea │
|
|
│ - Clone repository │
|
|
│ - Install dependencies │
|
|
│ - Build application │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 7. Application is live │
|
|
│ - User receives deployment URL │
|
|
│ - Health checks pass │
|
|
│ - Monitoring enabled │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 8. Future updates │
|
|
│ - User makes changes in MoreMinimore │
|
|
│ - Clicks "Update Deployment" │
|
|
│ - Easypanel pulls latest code and redeploys │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Update Deployment Flow
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 1. User makes changes to project │
|
|
│ - Chat with AI to modify code │
|
|
│ - Test in preview │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 2. User clicks "Update Deployment" │
|
|
│ - Review changes │
|
|
│ - Confirm update │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 3. MoreMinimore commits to Gitea │
|
|
│ - Create new commit │
|
|
│ - Update project version │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 4. MoreMinimore triggers Easypanel redeploy │
|
|
│ - Call Easypanel API │
|
|
│ - Trigger deployment │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 5. Easypanel pulls latest code │
|
|
│ - Git pull from Gitea │
|
|
│ - Rebuild application │
|
|
│ - Restart services │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 6. Update complete │
|
|
│ - Zero-downtime deployment (if supported) │
|
|
│ - User notified │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## UI/UX Pro Max Integration
|
|
|
|
### Integration Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ MoreMinimore SAAS │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Design System Service │
|
|
│ ┌──────────────────────────────────────────────────────┐ │
|
|
│ │ 1. Analyze user request │ │
|
|
│ │ 2. Call UI/UX Pro Max Python script │ │
|
|
│ │ 3. Parse design system output │ │
|
|
│ │ 4. Store in database │ │
|
|
│ │ 5. Return to frontend │ │
|
|
│ └──────────────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ UI/UX Pro Max (Python Script) │
|
|
│ - Multi-domain search │
|
|
│ - Reasoning engine │
|
|
│ - Design system generation │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Implementation Strategy
|
|
|
|
#### 1. Install UI/UX Pro Max
|
|
|
|
```bash
|
|
# Install CLI globally
|
|
npm install -g uipro-cli
|
|
|
|
# Or copy skill files to project
|
|
# .claude/skills/ui-ux-pro-max/
|
|
# .shared/ui-ux-pro-max/
|
|
```
|
|
|
|
#### 2. Create Design System Service
|
|
|
|
```typescript
|
|
// src/lib/services/design-system.service.ts
|
|
import { exec } from "child_process";
|
|
import { promisify } from "util";
|
|
|
|
const execAsync = promisify(exec);
|
|
|
|
export interface DesignSystem {
|
|
pattern: string;
|
|
style: string;
|
|
colors: {
|
|
primary: string;
|
|
secondary: string;
|
|
accent: string;
|
|
background: string;
|
|
text: string;
|
|
};
|
|
typography: {
|
|
heading: string;
|
|
body: string;
|
|
googleFontsUrl: string;
|
|
};
|
|
effects: string[];
|
|
antiPatterns: string[];
|
|
checklist: string[];
|
|
}
|
|
|
|
export class DesignSystemService {
|
|
async generateDesignSystem(
|
|
projectDescription: string,
|
|
projectName?: string,
|
|
): Promise<DesignSystem> {
|
|
const args = [
|
|
"python3",
|
|
".claude/skills/ui-ux-pro-max/scripts/search.py",
|
|
`"${projectDescription}"`,
|
|
"--design-system",
|
|
"-f",
|
|
"json",
|
|
];
|
|
|
|
if (projectName) {
|
|
args.push("-p", `"${projectName}"`);
|
|
}
|
|
|
|
const { stdout, stderr } = await execAsync(args.join(" "));
|
|
|
|
if (stderr) {
|
|
console.error("Design system generation error:", stderr);
|
|
}
|
|
|
|
const designSystem = JSON.parse(stdout);
|
|
return designSystem;
|
|
}
|
|
|
|
async persistDesignSystem(
|
|
projectId: string,
|
|
designSystem: DesignSystem,
|
|
pageName?: string,
|
|
): Promise<void> {
|
|
// Store in database
|
|
await db.insert(designSystems).values({
|
|
projectId,
|
|
name: designSystem.style,
|
|
pattern: designSystem.pattern,
|
|
style: designSystem.style,
|
|
colorPalette: designSystem.colors,
|
|
typography: designSystem.typography,
|
|
effects: designSystem.effects,
|
|
antiPatterns: designSystem.antiPatterns,
|
|
generatedByAi: true,
|
|
});
|
|
|
|
// Optionally persist to files for hierarchical retrieval
|
|
if (pageName) {
|
|
await this.persistToFile(projectId, designSystem, pageName);
|
|
}
|
|
}
|
|
|
|
private async persistToFile(
|
|
projectId: string,
|
|
designSystem: DesignSystem,
|
|
pageName?: string,
|
|
): Promise<void> {
|
|
// Implementation for file-based persistence
|
|
// Creates design-system/MASTER.md and design-system/pages/{page}.md
|
|
}
|
|
|
|
async getDesignSystem(
|
|
projectId: string,
|
|
pageName?: string,
|
|
): Promise<DesignSystem | null> {
|
|
// Hierarchical retrieval logic
|
|
if (pageName) {
|
|
const pageSystem = await db.query.designSystems.findFirst({
|
|
where: and(
|
|
eq(designSystems.projectId, projectId),
|
|
eq(designSystems.name, pageName),
|
|
),
|
|
});
|
|
|
|
if (pageSystem) {
|
|
return this.deserializeDesignSystem(pageSystem);
|
|
}
|
|
}
|
|
|
|
const masterSystem = await db.query.designSystems.findFirst({
|
|
where: eq(designSystems.projectId, projectId),
|
|
});
|
|
|
|
return masterSystem ? this.deserializeDesignSystem(masterSystem) : null;
|
|
}
|
|
|
|
private deserializeDesignSystem(record: any): DesignSystem {
|
|
return {
|
|
pattern: record.pattern,
|
|
style: record.style,
|
|
colors: record.colorPalette,
|
|
typography: record.typography,
|
|
effects: record.effects,
|
|
antiPatterns: record.antiPatterns,
|
|
checklist: [],
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 3. AI Prompt Enhancement
|
|
|
|
When generating code, the AI will be instructed to:
|
|
|
|
1. Read the design system from the database
|
|
2. Apply the design system guidelines
|
|
3. Generate code that matches the design specifications
|
|
4. Validate against anti-patterns
|
|
|
|
```typescript
|
|
// Example prompt enhancement
|
|
const enhancedPrompt = `
|
|
You are building a ${projectDescription}.
|
|
|
|
Design System:
|
|
${JSON.stringify(designSystem, null, 2)}
|
|
|
|
Please generate code that follows this design system:
|
|
- Use the specified color palette
|
|
- Apply the recommended typography
|
|
- Implement the suggested effects
|
|
- Avoid the anti-patterns listed
|
|
- Follow the pre-delivery checklist
|
|
|
|
Generate production-ready code with proper styling and best practices.
|
|
`;
|
|
```
|
|
|
|
---
|
|
|
|
## Easypanel Integration
|
|
|
|
|
|
### Overview
|
|
|
|
Easypanel is used for deploying user applications to production. The integration uses Easypanel's tRPC API for authentication, database creation, application deployment, and service management.
|
|
|
|
**Base URL**: `https://panel.moreminimore.com/api`
|
|
|
|
**API Documentation**: https://panel.moreminimore.com/api#/
|
|
|
|
---
|
|
|
|
### Authentication
|
|
|
|
Easypanel uses email/password authentication to obtain a bearer token.
|
|
|
|
#### Login Endpoint
|
|
|
|
**URL**: `POST /api/trpc/auth.login`
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"json": {
|
|
"email": "${EASYPANEL_EMAIL}",
|
|
"password": "${EASYPANEL_PASSWORD}"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
[
|
|
{
|
|
"result": {
|
|
"data": {
|
|
"json": {
|
|
"token": "cmkko1glb000p07pfa226232b"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
```
|
|
|
|
**Environment Variables**:
|
|
- `EASYPANEL_EMAIL`: Easypanel admin email
|
|
- `EASYPANEL_PASSWORD`: Easypanel admin password
|
|
|
|
**Implementation**:
|
|
```typescript
|
|
async login(): Promise<string> {
|
|
const response = await fetch(`${this.baseUrl}/api/trpc/auth.login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
json: {
|
|
email: process.env.EASYPANEL_EMAIL,
|
|
password: process.env.EASYPANEL_PASSWORD
|
|
}
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
return data[0].result.data.json.token;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Service Naming Convention
|
|
|
|
All services follow a consistent naming pattern for easy identification and troubleshooting.
|
|
|
|
**Format**: `{username}-{project_id}`
|
|
|
|
**Examples**:
|
|
- App: `kunthawat-More`
|
|
- Database: `kunthawat-More-db`
|
|
|
|
**Duplicate Handling**: If service name already exists, append running number:
|
|
- `kunthawat-More-2`
|
|
- `kunthawat-More-3`
|
|
|
|
**Implementation**:
|
|
```typescript
|
|
async generateServiceName(
|
|
username: string,
|
|
projectId: string,
|
|
type: 'app' | 'database'
|
|
): Promise<string> {
|
|
const baseName = `${username}-${projectId}`;
|
|
const serviceName = type === 'database' ? `${baseName}-db` : baseName;
|
|
|
|
const exists = await this.serviceExists(serviceName);
|
|
|
|
if (!exists) {
|
|
return serviceName;
|
|
}
|
|
|
|
let counter = 2;
|
|
let uniqueName = `${serviceName}-${counter}`;
|
|
|
|
while (await this.serviceExists(uniqueName)) {
|
|
counter++;
|
|
uniqueName = `${serviceName}-${counter}`;
|
|
}
|
|
|
|
return uniqueName;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Database Creation
|
|
|
|
Create a MariaDB database for the application.
|
|
|
|
#### Create Database Endpoint
|
|
|
|
**URL**: `POST /api/trpc/services.mariadb.createService`
|
|
|
|
**Headers**:
|
|
```
|
|
Authorization: Bearer {token}
|
|
```
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"json": {
|
|
"projectName": "database",
|
|
"serviceName": "{username}-{project_id}-db",
|
|
"databaseName": "{username}-{project_id}-db",
|
|
"user": "wp_user",
|
|
"image": "mariadb:11",
|
|
"exposedPort": 0,
|
|
"password": "{generated_password}",
|
|
"rootPassword": "{generated_root_password}",
|
|
"resources": {
|
|
"memoryReservation": 0,
|
|
"memoryLimit": 0,
|
|
"cpuReservation": 0,
|
|
"cpuLimit": 0
|
|
},
|
|
"backup": {
|
|
"enabled": false,
|
|
"schedule": "",
|
|
"destinationId": "",
|
|
"prefix": "",
|
|
"databaseName": ""
|
|
},
|
|
"phpMyAdmin": {
|
|
"enabled": false,
|
|
"token": ""
|
|
},
|
|
"dbGate": {
|
|
"enabled": false,
|
|
"token": ""
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
[
|
|
{
|
|
"result": {
|
|
"data": {
|
|
"json": {
|
|
"type": "mariadb",
|
|
"projectName": "database",
|
|
"name": "db-justtest",
|
|
"databaseName": "db-justtest",
|
|
"user": "wp_user",
|
|
"image": "mariadb:11",
|
|
"enabled": true,
|
|
"exposedPort": 0,
|
|
"password": "5edwdr930g4jtpawzzpy",
|
|
"rootPassword": "caab9krg3udej4cg2m46",
|
|
"env": null,
|
|
"command": null
|
|
},
|
|
"meta": {
|
|
"values": {
|
|
"env": ["undefined"],
|
|
"command": ["undefined"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
```
|
|
|
|
**Database Connection String**:
|
|
```
|
|
mariadb://{username}:{password}@{projectName}_{serviceName}:3306/{databaseName}
|
|
```
|
|
|
|
**Example**:
|
|
```
|
|
mariadb://wp_user:5edwdr930g4jtpawzzpy@database_kunthawat-More-db:3306/kunthawat-More-db
|
|
```
|
|
|
|
**Implementation**:
|
|
```typescript
|
|
async createDatabase(
|
|
username: string,
|
|
projectId: string
|
|
): Promise<DatabaseInfo> {
|
|
const serviceName = await this.generateServiceName(username, projectId, 'database');
|
|
const password = this.generatePassword();
|
|
const rootPassword = this.generatePassword();
|
|
|
|
const response = await fetch(`${this.baseUrl}/api/trpc/services.mariadb.createService`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
json: {
|
|
projectName: 'database',
|
|
serviceName,
|
|
databaseName: serviceName,
|
|
user: 'wp_user',
|
|
image: 'mariadb:11',
|
|
exposedPort: 0,
|
|
password,
|
|
rootPassword,
|
|
resources: {
|
|
memoryReservation: 0,
|
|
memoryLimit: 0,
|
|
cpuReservation: 0,
|
|
cpuLimit: 0
|
|
},
|
|
backup: {
|
|
enabled: false,
|
|
schedule: '',
|
|
destinationId: '',
|
|
prefix: '',
|
|
databaseName: ''
|
|
},
|
|
phpMyAdmin: {
|
|
enabled: false,
|
|
token: ''
|
|
},
|
|
dbGate: {
|
|
enabled: false,
|
|
token: ''
|
|
}
|
|
}
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
const result = data[0].result.data.json;
|
|
|
|
const connectionString = `mariadb://${result.user}:${result.password}@${result.projectName}_${result.name}:3306/${result.databaseName}`;
|
|
|
|
return {
|
|
serviceName: result.name,
|
|
databaseName: result.databaseName,
|
|
user: result.user,
|
|
password: result.password,
|
|
connectionString
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Application Deployment
|
|
|
|
Deploy an application from Gitea repository.
|
|
|
|
#### Create Application Endpoint
|
|
|
|
**URL**: `POST /api/trpc/services.app.createService`
|
|
|
|
**Headers**:
|
|
```
|
|
Authorization: Bearer {token}
|
|
```
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"json": {
|
|
"projectName": "database",
|
|
"serviceName": "{username}-{project_id}",
|
|
"source": {
|
|
"type": "git",
|
|
"repo": "https://gitea.moreminimore.com/{username}/{username}-{project_id}.git",
|
|
"ref": "main",
|
|
"path": "/"
|
|
},
|
|
"build": {
|
|
"type": "dockerfile",
|
|
"file": "Dockerfile"
|
|
},
|
|
"domains": [
|
|
{
|
|
"host": "{username}-{serviceName}.moreminimore.com",
|
|
"https": true,
|
|
"path": "/",
|
|
"wildcard": false,
|
|
"destinationType": "service",
|
|
"serviceDestination": {
|
|
"protocol": "http",
|
|
"port": 80,
|
|
"path": "/",
|
|
"projectName": "database",
|
|
"serviceName": "{username}-{project_id}"
|
|
}
|
|
}
|
|
],
|
|
"mounts": [
|
|
{
|
|
"type": "volume",
|
|
"name": "wp_data",
|
|
"mountPath": "/var/www/html"
|
|
}
|
|
],
|
|
"env": "DATABASE_URL={database_connection_string}\nNODE_ENV=production"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
[
|
|
{
|
|
"result": {
|
|
"data": {
|
|
"json": {
|
|
"projectName": "database",
|
|
"name": "justtest2",
|
|
"type": "app",
|
|
"enabled": true,
|
|
"token": "9dd5b81fef7fe4bf9f3b2e2e7bd83b19a6fe311d4a88b101",
|
|
"primaryDomainId": "cmkkq46xb000t07pf81mp63qv",
|
|
"source": {
|
|
"type": "git",
|
|
"repo": "https://github.com/kunthawat/TradingAgents-crypto.git",
|
|
"ref": "feature/gold-trading-support",
|
|
"path": "/"
|
|
},
|
|
"build": {
|
|
"type": "dockerfile",
|
|
"file": "Dockerfile"
|
|
},
|
|
"env": "",
|
|
"deploy": {
|
|
"replicas": 1,
|
|
"command": null,
|
|
"zeroDowntime": true
|
|
},
|
|
"mounts": [
|
|
{
|
|
"type": "volume",
|
|
"name": "wp_data",
|
|
"mountPath": "/var/www/html"
|
|
}
|
|
],
|
|
"ports": []
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
```
|
|
|
|
**Implementation**:
|
|
```typescript
|
|
async createApplication(
|
|
username: string,
|
|
projectId: string,
|
|
giteaRepoUrl: string,
|
|
databaseConnectionString: string,
|
|
customDomain?: string
|
|
): Promise<ApplicationInfo> {
|
|
const serviceName = await this.generateServiceName(username, projectId, 'app');
|
|
const defaultDomain = `${username}-${serviceName}.moreminimore.com`;
|
|
const domain = customDomain || defaultDomain;
|
|
|
|
const response = await fetch(`${this.baseUrl}/api/trpc/services.app.createService`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
json: {
|
|
projectName: 'database',
|
|
serviceName,
|
|
source: {
|
|
type: 'git',
|
|
repo: giteaRepoUrl,
|
|
ref: 'main',
|
|
path: '/'
|
|
},
|
|
build: {
|
|
type: 'dockerfile',
|
|
file: 'Dockerfile'
|
|
},
|
|
domains: [
|
|
{
|
|
host: domain,
|
|
https: true,
|
|
path: '/',
|
|
wildcard: false,
|
|
destinationType: 'service',
|
|
serviceDestination: {
|
|
protocol: 'http',
|
|
port: 80,
|
|
path: '/',
|
|
projectName: 'database',
|
|
serviceName
|
|
}
|
|
}
|
|
],
|
|
mounts: [
|
|
{
|
|
type: 'volume',
|
|
name: 'wp_data',
|
|
mountPath: '/var/www/html'
|
|
}
|
|
],
|
|
env: `DATABASE_URL=${databaseConnectionString}\nNODE_ENV=production`
|
|
}
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
const result = data[0].result.data.json;
|
|
|
|
return {
|
|
serviceName: result.name,
|
|
domain,
|
|
primaryDomainId: result.primaryDomainId,
|
|
deploymentUrl: `https://${domain}`
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Update/Redeploy Application
|
|
|
|
Trigger a redeploy of an existing application.
|
|
|
|
#### Update Deploy Endpoint
|
|
|
|
**URL**: `POST /api/trpc/services.app.updateDeploy`
|
|
|
|
**Headers**:
|
|
```
|
|
Authorization: Bearer {token}
|
|
```
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"json": {
|
|
"projectName": "database",
|
|
"serviceName": "{username}-{project_id}",
|
|
"deploy": {}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Implementation**:
|
|
```typescript
|
|
async updateDeploy(
|
|
username: string,
|
|
projectId: string
|
|
): Promise<void> {
|
|
const serviceName = await this.getServiceName(username, projectId);
|
|
|
|
await fetch(`${this.baseUrl}/api/trpc/services.app.updateDeploy`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
json: {
|
|
projectName: 'database',
|
|
serviceName,
|
|
deploy: {}
|
|
}
|
|
})
|
|
});
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Deployment Status
|
|
|
|
Check the status and details of a deployed application.
|
|
|
|
#### Inspect Service Endpoint
|
|
|
|
**URL**: `GET /api/trpc/services.app.inspectService`
|
|
|
|
**Query Parameters**:
|
|
- `projectName`: "database"
|
|
- `serviceName`: "{username}-{project_id}"
|
|
|
|
**Headers**:
|
|
```
|
|
Authorization: Bearer {token}
|
|
```
|
|
|
|
**Implementation**:
|
|
```typescript
|
|
async inspectService(
|
|
username: string,
|
|
projectId: string
|
|
): Promise<ServiceInfo> {
|
|
const serviceName = await this.getServiceName(username, projectId);
|
|
|
|
const response = await fetch(
|
|
`${this.baseUrl}/api/trpc/services.app.inspectService?projectName=database&serviceName=${serviceName}`,
|
|
{
|
|
method: 'GET',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.token}`
|
|
}
|
|
}
|
|
);
|
|
|
|
const data = await response.json();
|
|
return data[0].result.data.json;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### List Services
|
|
|
|
List all projects and services.
|
|
|
|
#### List Projects and Services Endpoint
|
|
|
|
**URL**: `GET /api/trpc/projects.listProjectsAndServices`
|
|
|
|
**Headers**:
|
|
```
|
|
Authorization: Bearer {token}
|
|
```
|
|
|
|
**Implementation**:
|
|
```typescript
|
|
async listProjectsAndServices(): Promise<Project[]> {
|
|
const response = await fetch(
|
|
`${this.baseUrl}/api/trpc/projects.listProjectsAndServices`,
|
|
{
|
|
method: 'GET',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.token}`
|
|
}
|
|
}
|
|
);
|
|
|
|
const data = await response.json();
|
|
return data[0].result.data.json;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Delete Service
|
|
|
|
Delete an application service.
|
|
|
|
#### Destroy Service Endpoint
|
|
|
|
**URL**: `POST /api/trpc/services.app.destroyService`
|
|
|
|
**Headers**:
|
|
```
|
|
Authorization: Bearer {token}
|
|
```
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"json": {
|
|
"projectName": "database",
|
|
"serviceName": "{username}-{project_id}"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Implementation**:
|
|
```typescript
|
|
async destroyService(
|
|
username: string,
|
|
projectId: string
|
|
): Promise<void> {
|
|
const serviceName = await this.getServiceName(username, projectId);
|
|
|
|
await fetch(`${this.baseUrl}/api/trpc/services.app.destroyService`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
json: {
|
|
projectName: 'database',
|
|
serviceName
|
|
}
|
|
})
|
|
});
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Stop/Start Service
|
|
|
|
Control the running state of a service.
|
|
|
|
#### Stop Service Endpoint
|
|
|
|
**URL**: `POST /api/trpc/services.app.stopService`
|
|
|
|
**Headers**:
|
|
```
|
|
Authorization: Bearer {token}
|
|
```
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"json": {
|
|
"projectName": "database",
|
|
"serviceName": "{username}-{project_id}"
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Start Service Endpoint
|
|
|
|
**URL**: `POST /api/trpc/services.app.startService`
|
|
|
|
**Headers**:
|
|
```
|
|
Authorization: Bearer {token}
|
|
```
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"json": {
|
|
"projectName": "database",
|
|
"serviceName": "{username}-{project_id}"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Implementation**:
|
|
```typescript
|
|
async stopService(
|
|
username: string,
|
|
projectId: string
|
|
): Promise<void> {
|
|
const serviceName = await this.getServiceName(username, projectId);
|
|
|
|
await fetch(`${this.baseUrl}/api/trpc/services.app.stopService`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
json: {
|
|
projectName: 'database',
|
|
serviceName
|
|
}
|
|
})
|
|
});
|
|
}
|
|
|
|
async startService(
|
|
username: string,
|
|
projectId: string
|
|
): Promise<void> {
|
|
const serviceName = await this.getServiceName(username, projectId);
|
|
|
|
await fetch(`${this.baseUrl}/api/trpc/services.app.startService`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
json: {
|
|
projectName: 'database',
|
|
serviceName
|
|
}
|
|
})
|
|
});
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Domain Management
|
|
|
|
Update the domain of a deployed application.
|
|
|
|
#### Update Domain Endpoint
|
|
|
|
**URL**: `POST /api/trpc/domains.updateDomain`
|
|
|
|
**Headers**:
|
|
```
|
|
Authorization: Bearer {token}
|
|
```
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"json": {
|
|
"id": "{domain_id}",
|
|
"host": "{custom_domain}",
|
|
"https": true,
|
|
"path": "/",
|
|
"middlewares": [],
|
|
"certificateResolver": "letsencrypt",
|
|
"wildcard": false,
|
|
"destinationType": "service",
|
|
"serviceDestination": {
|
|
"protocol": "http",
|
|
"port": 80,
|
|
"path": "/",
|
|
"projectName": "database",
|
|
"serviceName": "{username}-{project_id}"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Workflow**:
|
|
1. Get domain ID from `inspectService` endpoint
|
|
2. Call `updateDomain` with new domain
|
|
|
|
**Implementation**:
|
|
```typescript
|
|
async updateDomain(
|
|
username: string,
|
|
projectId: string,
|
|
newDomain: string
|
|
): Promise<void> {
|
|
const serviceInfo = await this.inspectService(username, projectId);
|
|
const domainId = serviceInfo.primaryDomainId;
|
|
const serviceName = await this.getServiceName(username, projectId);
|
|
|
|
await fetch(`${this.baseUrl}/api/trpc/domains.updateDomain`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
json: {
|
|
id: domainId,
|
|
host: newDomain,
|
|
https: true,
|
|
path: '/',
|
|
middlewares: [],
|
|
certificateResolver: 'letsencrypt',
|
|
wildcard: false,
|
|
destinationType: 'service',
|
|
serviceDestination: {
|
|
protocol: 'http',
|
|
port: 80,
|
|
path: '/',
|
|
projectName: 'database',
|
|
serviceName
|
|
}
|
|
}
|
|
})
|
|
});
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Dockerfile Generation
|
|
|
|
Applications must have a Dockerfile for deployment. The system will auto-generate a Dockerfile based on the project type.
|
|
|
|
#### Next.js Dockerfile Template
|
|
|
|
```dockerfile
|
|
FROM node:20-alpine AS base
|
|
|
|
# Install dependencies only when needed
|
|
FROM base AS deps
|
|
RUN apk add --no-cache libc6-compat
|
|
WORKDIR /app
|
|
|
|
COPY package.json package-lock.json* ./
|
|
COPY npmrc* ./
|
|
RUN npm ci
|
|
|
|
# Rebuild the source code only when needed
|
|
FROM base AS builder
|
|
WORKDIR /app
|
|
COPY --from=deps /app/node_modules ./node_modules
|
|
COPY . .
|
|
|
|
RUN npm run build
|
|
|
|
# Production image, copy all the files and run next
|
|
FROM base AS runner
|
|
WORKDIR /app
|
|
|
|
ENV NODE_ENV production
|
|
|
|
RUN addgroup --system --gid 1001 nodejs
|
|
RUN adduser --system --uid 1001 nextjs
|
|
|
|
COPY --from=builder /app/public ./public
|
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
|
|
USER nextjs
|
|
|
|
EXPOSE 3000
|
|
|
|
ENV PORT 3000
|
|
ENV HOSTNAME "0.0.0.0"
|
|
|
|
CMD ["node", "server.js"]
|
|
```
|
|
|
|
#### React Dockerfile Template
|
|
|
|
```dockerfile
|
|
FROM node:20-alpine AS builder
|
|
|
|
WORKDIR /app
|
|
|
|
COPY package.json package-lock.json* ./
|
|
RUN npm ci
|
|
|
|
COPY . .
|
|
RUN npm run build
|
|
|
|
FROM nginx:alpine
|
|
|
|
COPY --from=builder /app/build /usr/share/nginx/html
|
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
|
|
EXPOSE 80
|
|
|
|
CMD ["nginx", "-g", "daemon off;"]
|
|
```
|
|
|
|
**Implementation**:
|
|
```typescript
|
|
async generateDockerfile(projectType: 'nextjs' | 'react' | 'node'): Promise<string> {
|
|
const templates = {
|
|
nextjs: this.getNextjsDockerfile(),
|
|
react: this.getReactDockerfile(),
|
|
node: this.getNodeDockerfile()
|
|
};
|
|
|
|
return templates[projectType];
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Complete Easypanel Service
|
|
|
|
```typescript
|
|
// src/lib/services/easypanel.service.ts
|
|
export class EasypanelService {
|
|
private token: string | null = null;
|
|
private baseUrl: string = 'https://panel.moreminimore.com/api';
|
|
|
|
constructor() {
|
|
this.token = null;
|
|
}
|
|
|
|
private async ensureAuthenticated(): Promise<void> {
|
|
if (!this.token) {
|
|
this.token = await this.login();
|
|
}
|
|
}
|
|
|
|
async login(): Promise<string> {
|
|
const response = await fetch(`${this.baseUrl}/api/trpc/auth.login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
json: {
|
|
email: process.env.EASYPANEL_EMAIL,
|
|
password: process.env.EASYPANEL_PASSWORD
|
|
}
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
this.token = data[0].result.data.json.token;
|
|
return this.token;
|
|
}
|
|
|
|
async generateServiceName(
|
|
username: string,
|
|
projectId: string,
|
|
type: 'app' | 'database'
|
|
): Promise<string> {
|
|
const baseName = `${username}-${projectId}`;
|
|
const serviceName = type === 'database' ? `${baseName}-db` : baseName;
|
|
|
|
const exists = await this.serviceExists(serviceName);
|
|
|
|
if (!exists) {
|
|
return serviceName;
|
|
}
|
|
|
|
let counter = 2;
|
|
let uniqueName = `${serviceName}-${counter}`;
|
|
|
|
while (await this.serviceExists(uniqueName)) {
|
|
counter++;
|
|
uniqueName = `${serviceName}-${counter}`;
|
|
}
|
|
|
|
return uniqueName;
|
|
}
|
|
|
|
async createDatabase(
|
|
username: string,
|
|
projectId: string
|
|
): Promise<DatabaseInfo> {
|
|
await this.ensureAuthenticated();
|
|
|
|
const serviceName = await this.generateServiceName(username, projectId, 'database');
|
|
const password = this.generatePassword();
|
|
const rootPassword = this.generatePassword();
|
|
|
|
const response = await fetch(`${this.baseUrl}/api/trpc/services.mariadb.createService`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
json: {
|
|
projectName: 'database',
|
|
serviceName,
|
|
databaseName: serviceName,
|
|
user: 'wp_user',
|
|
image: 'mariadb:11',
|
|
exposedPort: 0,
|
|
password,
|
|
rootPassword,
|
|
resources: {
|
|
memoryReservation: 0,
|
|
memoryLimit: 0,
|
|
cpuReservation: 0,
|
|
cpuLimit: 0
|
|
},
|
|
backup: {
|
|
enabled: false,
|
|
schedule: '',
|
|
destinationId: '',
|
|
prefix: '',
|
|
databaseName: ''
|
|
},
|
|
phpMyAdmin: {
|
|
enabled: false,
|
|
token: ''
|
|
},
|
|
dbGate: {
|
|
enabled: false,
|
|
token: ''
|
|
}
|
|
}
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
const result = data[0].result.data.json;
|
|
|
|
const connectionString = `mariadb://${result.user}:${result.password}@${result.projectName}_${result.name}:3306/${result.databaseName}`;
|
|
|
|
return {
|
|
serviceName: result.name,
|
|
databaseName: result.databaseName,
|
|
user: result.user,
|
|
password: result.password,
|
|
connectionString
|
|
};
|
|
}
|
|
|
|
async createApplication(
|
|
username: string,
|
|
projectId: string,
|
|
giteaRepoUrl: string,
|
|
databaseConnectionString: string,
|
|
customDomain?: string
|
|
): Promise<ApplicationInfo> {
|
|
await this.ensureAuthenticated();
|
|
|
|
const serviceName = await this.generateServiceName(username, projectId, 'app');
|
|
const defaultDomain = `${username}-${serviceName}.moreminimore.com`;
|
|
const domain = customDomain || defaultDomain;
|
|
|
|
const response = await fetch(`${this.baseUrl}/api/trpc/services.app.createService`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
json: {
|
|
projectName: 'database',
|
|
serviceName,
|
|
source: {
|
|
type: 'git',
|
|
repo: giteaRepoUrl,
|
|
ref: 'main',
|
|
path: '/'
|
|
},
|
|
build: {
|
|
type: 'dockerfile',
|
|
file: 'Dockerfile'
|
|
},
|
|
domains: [
|
|
{
|
|
host: domain,
|
|
https: true,
|
|
path: '/',
|
|
wildcard: false,
|
|
destinationType: 'service',
|
|
serviceDestination: {
|
|
protocol: 'http',
|
|
port: 80,
|
|
path: '/',
|
|
projectName: 'database',
|
|
serviceName
|
|
}
|
|
}
|
|
],
|
|
mounts: [
|
|
{
|
|
type: 'volume',
|
|
name: 'wp_data',
|
|
mountPath: '/var/www/html'
|
|
}
|
|
],
|
|
env: `DATABASE_URL=${databaseConnectionString}\nNODE_ENV=production`
|
|
}
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
const result = data[0].result.data.json;
|
|
|
|
return {
|
|
serviceName: result.name,
|
|
domain,
|
|
primaryDomainId: result.primaryDomainId,
|
|
deploymentUrl: `https://${domain}`
|
|
};
|
|
}
|
|
|
|
async updateDeploy(
|
|
username: string,
|
|
projectId: string
|
|
): Promise<void> {
|
|
await this.ensureAuthenticated();
|
|
|
|
const serviceName = await this.getServiceName(username, projectId);
|
|
|
|
await fetch(`${this.baseUrl}/api/trpc/services.app.updateDeploy`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
json: {
|
|
projectName: 'database',
|
|
serviceName,
|
|
deploy: {}
|
|
}
|
|
})
|
|
});
|
|
}
|
|
|
|
async inspectService(
|
|
username: string,
|
|
projectId: string
|
|
): Promise<ServiceInfo> {
|
|
await this.ensureAuthenticated();
|
|
|
|
const serviceName = await this.getServiceName(username, projectId);
|
|
|
|
const response = await fetch(
|
|
`${this.baseUrl}/api/trpc/services.app.inspectService?projectName=database&serviceName=${serviceName}`,
|
|
{
|
|
method: 'GET',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.token}`
|
|
}
|
|
}
|
|
);
|
|
|
|
const data = await response.json();
|
|
return data[0].result.data.json;
|
|
}
|
|
|
|
async updateDomain(
|
|
username: string,
|
|
projectId: string,
|
|
newDomain: string
|
|
): Promise<void> {
|
|
await this.ensureAuthenticated();
|
|
|
|
const serviceInfo = await this.inspectService(username, projectId);
|
|
const domainId = serviceInfo.primaryDomainId;
|
|
const serviceName = await this.getServiceName(username, projectId);
|
|
|
|
await fetch(`${this.baseUrl}/api/trpc/domains.updateDomain`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
json: {
|
|
id: domainId,
|
|
host: newDomain,
|
|
https: true,
|
|
path: '/',
|
|
middlewares: [],
|
|
certificateResolver: 'letsencrypt',
|
|
wildcard: false,
|
|
destinationType: 'service',
|
|
serviceDestination: {
|
|
protocol: 'http',
|
|
port: 80,
|
|
path: '/',
|
|
projectName: 'database',
|
|
serviceName
|
|
}
|
|
}
|
|
})
|
|
});
|
|
}
|
|
|
|
async destroyService(
|
|
username: string,
|
|
projectId: string
|
|
): Promise<void> {
|
|
await this.ensureAuthenticated();
|
|
|
|
const serviceName = await this.getServiceName(username, projectId);
|
|
|
|
await fetch(`${this.baseUrl}/api/trpc/services.app.destroyService`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
json: {
|
|
projectName: 'database',
|
|
serviceName
|
|
}
|
|
})
|
|
});
|
|
}
|
|
|
|
private generatePassword(): string {
|
|
return Math.random().toString(36).substring(2, 15) +
|
|
Math.random().toString(36).substring(2, 15);
|
|
}
|
|
|
|
private async serviceExists(serviceName: string): Promise<boolean> {
|
|
// Implementation to check if service exists
|
|
// Use listProjectsAndServices endpoint
|
|
return false;
|
|
}
|
|
|
|
private async getServiceName(username: string, projectId: string): Promise<string> {
|
|
// Implementation to retrieve stored service name
|
|
// Should be stored in database when created
|
|
return `${username}-${projectId}`;
|
|
}
|
|
}
|
|
|
|
// Type definitions
|
|
interface DatabaseInfo {
|
|
serviceName: string;
|
|
databaseName: string;
|
|
user: string;
|
|
password: string;
|
|
connectionString: string;
|
|
}
|
|
|
|
interface ApplicationInfo {
|
|
serviceName: string;
|
|
domain: string;
|
|
primaryDomainId: string;
|
|
deploymentUrl: string;
|
|
}
|
|
|
|
interface ServiceInfo {
|
|
projectName: string;
|
|
name: string;
|
|
type: string;
|
|
enabled: boolean;
|
|
primaryDomainId: string;
|
|
source: {
|
|
type: string;
|
|
repo: string;
|
|
ref: string;
|
|
path: string;
|
|
};
|
|
build: {
|
|
type: string;
|
|
file: string;
|
|
};
|
|
deploy: {
|
|
replicas: number;
|
|
command: string | null;
|
|
zeroDowntime: boolean;
|
|
};
|
|
mounts: Array<{
|
|
type: string;
|
|
name: string;
|
|
mountPath: string;
|
|
}>;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Error Handling
|
|
|
|
When deployment fails, display user-friendly error message:
|
|
|
|
```
|
|
"Deployment failed. Please contact us."
|
|
```
|
|
|
|
Include the actual API response for debugging purposes (logged internally).
|
|
|
|
**Implementation**:
|
|
```typescript
|
|
try {
|
|
await this.easypanelService.createApplication(...);
|
|
} catch (error) {
|
|
console.error('Easypanel API error:', error);
|
|
throw new Error('Deployment failed. Please contact us.');
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Environment Variables
|
|
|
|
Add to `.env.local`:
|
|
|
|
```env
|
|
# Easypanel
|
|
EASYPANEL_EMAIL=kunthawat@moreminimore.com
|
|
EASYPANEL_PASSWORD=Coolm@n1234mo
|
|
EASYPANEL_API_URL=https://panel.moreminimore.com/api
|
|
```
|
|
|
|
---
|
|
|
|
### API Reference
|
|
|
|
For complete API documentation, visit: https://panel.moreminimore.com/api#/
|
|
|
|
Key endpoints:
|
|
- Authentication: `/api/trpc/auth.login`
|
|
- Database: `/api/trpc/services.mariadb.createService`
|
|
- Application: `/api/trpc/services.app.createService`
|
|
- Update Deploy: `/api/trpc/services.app.updateDeploy`
|
|
- Inspect Service: `/api/trpc/services.app.inspectService`
|
|
- List Services: `/api/trpc/projects.listProjectsAndServices`
|
|
- Delete Service: `/api/trpc/services.app.destroyService`
|
|
- Stop Service: `/api/trpc/services.app.stopService`
|
|
- Start Service: `/api/trpc/services.app.startService`
|
|
- Update Domain: `/api/trpc/domains.updateDomain`
|
|
|
|
|
|
---
|
|
|
|
## Billing & Pricing
|
|
|
|
### Subscription Tiers
|
|
|
|
| Feature | Free | Pro | Enterprise |
|
|
| ----------------- | ------------ | --------- | ---------- |
|
|
| Price | $0/month | $29/month | Custom |
|
|
| Projects | 1 | 10 | Unlimited |
|
|
| AI Tokens | 10,000/month | Unlimited | Unlimited |
|
|
| Team Members | 1 | 5 | Unlimited |
|
|
| Deployments | 1 | 10 | Unlimited |
|
|
| Custom Domains | ❌ | ✅ | ✅ |
|
|
| Priority Support | ❌ | ✅ | ✅ |
|
|
| SLA | ❌ | ❌ | 99.9% |
|
|
| Advanced Features | ❌ | ✅ | ✅ |
|
|
|
|
### Stripe Integration
|
|
|
|
```typescript
|
|
// src/lib/services/billing.service.ts
|
|
import Stripe from "stripe";
|
|
|
|
export class BillingService {
|
|
private stripe: Stripe;
|
|
|
|
constructor(secretKey: string) {
|
|
this.stripe = new Stripe(secretKey);
|
|
}
|
|
|
|
async createCustomer(organizationId: string, email: string): Promise<string> {
|
|
const customer = await this.stripe.customers.create({
|
|
email,
|
|
metadata: { organizationId },
|
|
});
|
|
|
|
// Update organization with Stripe customer ID
|
|
await db
|
|
.update(organizations)
|
|
.set({ stripeCustomerId: customer.id })
|
|
.where(eq(organizations.id, organizationId));
|
|
|
|
return customer.id;
|
|
}
|
|
|
|
async createSubscription(
|
|
customerId: string,
|
|
priceId: string,
|
|
organizationId: string,
|
|
): Promise<Stripe.Subscription> {
|
|
const subscription = await this.stripe.subscriptions.create({
|
|
customer: customerId,
|
|
items: [{ price: priceId }],
|
|
payment_behavior: "default_incomplete",
|
|
payment_settings: { save_default_payment_method: "on_subscription" },
|
|
expand: ["latest_invoice.payment_intent"],
|
|
});
|
|
|
|
// Update organization subscription
|
|
await db
|
|
.update(organizations)
|
|
.set({
|
|
subscriptionTier: this.getTierFromPriceId(priceId),
|
|
subscriptionStatus: subscription.status,
|
|
trialEndsAt: subscription.trial_end
|
|
? new Date(subscription.trial_end * 1000)
|
|
: null,
|
|
})
|
|
.where(eq(organizations.id, organizationId));
|
|
|
|
// Log subscription event
|
|
await this.logSubscriptionEvent(organizationId, "created", subscription.id);
|
|
|
|
return subscription;
|
|
}
|
|
|
|
async handleWebhook(event: Stripe.Event): Promise<void> {
|
|
switch (event.type) {
|
|
case "customer.subscription.created":
|
|
case "customer.subscription.updated":
|
|
await this.handleSubscriptionUpdated(
|
|
event.data.object as Stripe.Subscription,
|
|
);
|
|
break;
|
|
|
|
case "customer.subscription.deleted":
|
|
await this.handleSubscriptionDeleted(
|
|
event.data.object as Stripe.Subscription,
|
|
);
|
|
break;
|
|
|
|
case "invoice.payment_succeeded":
|
|
await this.handleInvoicePaymentSucceeded(
|
|
event.data.object as Stripe.Invoice,
|
|
);
|
|
break;
|
|
|
|
case "invoice.payment_failed":
|
|
await this.handleInvoicePaymentFailed(
|
|
event.data.object as Stripe.Invoice,
|
|
);
|
|
break;
|
|
|
|
default:
|
|
console.log(`Unhandled event type: ${event.type}`);
|
|
}
|
|
}
|
|
|
|
private async handleSubscriptionUpdated(
|
|
subscription: Stripe.Subscription,
|
|
): Promise<void> {
|
|
const organizationId = subscription.metadata.organizationId;
|
|
|
|
await db
|
|
.update(organizations)
|
|
.set({
|
|
subscriptionStatus: subscription.status,
|
|
subscriptionTier: this.getTierFromPriceId(
|
|
subscription.items.data[0].price.id,
|
|
),
|
|
})
|
|
.where(eq(organizations.id, organizationId));
|
|
|
|
await this.logSubscriptionEvent(organizationId, "updated", subscription.id);
|
|
}
|
|
|
|
private async handleSubscriptionDeleted(
|
|
subscription: Stripe.Subscription,
|
|
): Promise<void> {
|
|
const organizationId = subscription.metadata.organizationId;
|
|
|
|
await db
|
|
.update(organizations)
|
|
.set({
|
|
subscriptionStatus: "canceled",
|
|
subscriptionTier: "free",
|
|
})
|
|
.where(eq(organizations.id, organizationId));
|
|
|
|
await this.logSubscriptionEvent(
|
|
organizationId,
|
|
"canceled",
|
|
subscription.id,
|
|
);
|
|
}
|
|
|
|
private async handleInvoicePaymentSucceeded(
|
|
invoice: Stripe.Invoice,
|
|
): Promise<void> {
|
|
const organizationId = invoice.customer_metadata?.organizationId;
|
|
|
|
// Create invoice record
|
|
await db.insert(invoices).values({
|
|
organizationId,
|
|
stripeInvoiceId: invoice.id,
|
|
amount: invoice.amount_paid / 100,
|
|
currency: invoice.currency.toUpperCase(),
|
|
status: "paid",
|
|
paidAt: new Date(invoice.status_transitions.paid_at! * 1000),
|
|
});
|
|
|
|
// Update organization status if it was past_due
|
|
if (invoice.subscription) {
|
|
await db
|
|
.update(organizations)
|
|
.set({ subscriptionStatus: "active" })
|
|
.where(
|
|
eq(
|
|
organizations.stripeSubscriptionId,
|
|
invoice.subscription as string,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
private async handleInvoicePaymentFailed(
|
|
invoice: Stripe.Invoice,
|
|
): Promise<void> {
|
|
const organizationId = invoice.customer_metadata?.organizationId;
|
|
|
|
// Create invoice record
|
|
await db.insert(invoices).values({
|
|
organizationId,
|
|
stripeInvoiceId: invoice.id,
|
|
amount: invoice.amount_due / 100,
|
|
currency: invoice.currency.toUpperCase(),
|
|
status: "open",
|
|
dueDate: new Date(invoice.due_date! * 1000),
|
|
});
|
|
|
|
// Update organization status
|
|
if (invoice.subscription) {
|
|
await db
|
|
.update(organizations)
|
|
.set({ subscriptionStatus: "past_due" })
|
|
.where(
|
|
eq(
|
|
organizations.stripeSubscriptionId,
|
|
invoice.subscription as string,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
private getTierFromPriceId(priceId: string): SubscriptionTier {
|
|
// Map price IDs to tiers
|
|
if (priceId.includes("pro")) return "pro";
|
|
if (priceId.includes("enterprise")) return "enterprise";
|
|
return "free";
|
|
}
|
|
|
|
private async logSubscriptionEvent(
|
|
organizationId: string,
|
|
eventType: string,
|
|
stripeEventId: string,
|
|
): Promise<void> {
|
|
await db.insert(subscriptionEvents).values({
|
|
organizationId,
|
|
eventType,
|
|
stripeEventId,
|
|
createdAt: new Date(),
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Code Storage Strategy
|
|
|
|
### Decision: PostgreSQL for Code Storage
|
|
|
|
**Rationale:**
|
|
|
|
- **Simplicity**: Single database for all data
|
|
- **Consistency**: ACID transactions for code + metadata
|
|
- **Backup**: Easy to backup entire system
|
|
- **Scalability**: PostgreSQL can handle large text fields
|
|
- **Performance**: Good enough for typical project sizes
|
|
- **Gitea for Backup**: Gitea provides version control and backup
|
|
|
|
### Implementation
|
|
|
|
```typescript
|
|
// Store project files in database
|
|
CREATE TABLE project_files (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
|
|
path TEXT NOT NULL, -- File path (e.g., "src/app/page.tsx")
|
|
content TEXT NOT NULL,
|
|
language VARCHAR(50), -- File type (typescript, javascript, etc.)
|
|
size INTEGER, -- File size in bytes
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|
UNIQUE(project_id, path)
|
|
);
|
|
|
|
// Index for performance
|
|
CREATE INDEX idx_project_files_project ON project_files(project_id);
|
|
CREATE INDEX idx_project_files_path ON project_files(project_id, path);
|
|
```
|
|
|
|
### File Operations
|
|
|
|
```typescript
|
|
// src/lib/services/file.service.ts
|
|
export class FileService {
|
|
async getFile(projectId: string, path: string): Promise<ProjectFile | null> {
|
|
return await db.query.projectFiles.findFirst({
|
|
where: and(
|
|
eq(projectFiles.projectId, projectId),
|
|
eq(projectFiles.path, path),
|
|
),
|
|
});
|
|
}
|
|
|
|
async saveFile(
|
|
projectId: string,
|
|
path: string,
|
|
content: string,
|
|
): Promise<void> {
|
|
await db
|
|
.insert(projectFiles)
|
|
.values({
|
|
projectId,
|
|
path,
|
|
content,
|
|
language: this.getLanguageFromPath(path),
|
|
size: Buffer.byteLength(content, "utf8"),
|
|
})
|
|
.onConflictDoUpdate({
|
|
target: [projectFiles.projectId, projectFiles.path],
|
|
set: {
|
|
content,
|
|
updatedAt: new Date(),
|
|
},
|
|
});
|
|
}
|
|
|
|
async deleteFile(projectId: string, path: string): Promise<void> {
|
|
await db
|
|
.delete(projectFiles)
|
|
.where(
|
|
and(eq(projectFiles.projectId, projectId), eq(projectFiles.path, path)),
|
|
);
|
|
}
|
|
|
|
async listFiles(
|
|
projectId: string,
|
|
directory?: string,
|
|
): Promise<ProjectFile[]> {
|
|
const conditions = [eq(projectFiles.projectId, projectId)];
|
|
|
|
if (directory) {
|
|
conditions.push(like(projectFiles.path, `${directory}%`));
|
|
}
|
|
|
|
return await db.query.projectFiles.findMany({
|
|
where: and(...conditions),
|
|
orderBy: [asc(projectFiles.path)],
|
|
});
|
|
}
|
|
|
|
async exportProject(projectId: string): Promise<Record<string, string>> {
|
|
const files = await this.listFiles(projectId);
|
|
const exportData: Record<string, string> = {};
|
|
|
|
for (const file of files) {
|
|
exportData[file.path] = file.content;
|
|
}
|
|
|
|
return exportData;
|
|
}
|
|
|
|
private getLanguageFromPath(path: string): string {
|
|
const ext = path.split(".").pop();
|
|
const languageMap: Record<string, string> = {
|
|
ts: "typescript",
|
|
tsx: "typescript",
|
|
js: "javascript",
|
|
jsx: "javascript",
|
|
css: "css",
|
|
html: "html",
|
|
json: "json",
|
|
md: "markdown",
|
|
};
|
|
|
|
return languageMap[ext || ""] || "text";
|
|
}
|
|
}
|
|
```
|
|
|
|
### Gitea Integration for Backup
|
|
|
|
```typescript
|
|
// src/lib/services/gitea.service.ts
|
|
export class GiteaService {
|
|
private apiUrl: string;
|
|
private token: string;
|
|
|
|
constructor(apiUrl: string, token: string) {
|
|
this.apiUrl = apiUrl;
|
|
this.token = token;
|
|
}
|
|
|
|
async createRepository(
|
|
projectId: string,
|
|
projectName: string,
|
|
): Promise<GiteaRepo> {
|
|
const response = await fetch(`${this.apiUrl}/api/v1/user/repos`, {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `token ${this.token}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
name: projectName,
|
|
private: true,
|
|
auto_init: true,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error("Failed to create Gitea repository");
|
|
}
|
|
|
|
const repo = await response.json();
|
|
|
|
// Update project with Gitea info
|
|
await db
|
|
.update(projects)
|
|
.set({
|
|
giteaRepoId: repo.id,
|
|
giteaRepoUrl: repo.clone_url,
|
|
})
|
|
.where(eq(projects.id, projectId));
|
|
|
|
return repo;
|
|
}
|
|
|
|
async commitProject(projectId: string): Promise<GiteaCommit> {
|
|
const project = await this.getProject(projectId);
|
|
if (!project.giteaRepoUrl) {
|
|
throw new Error("Project not linked to Gitea");
|
|
}
|
|
|
|
// Export all files
|
|
const files = await this.fileService.exportProject(projectId);
|
|
|
|
// Create commit with all files
|
|
const commit = await this.createCommit(
|
|
project.giteaRepoId,
|
|
"main",
|
|
`Update project - ${new Date().toISOString()}`,
|
|
files,
|
|
);
|
|
|
|
return commit;
|
|
}
|
|
|
|
private async createCommit(
|
|
repoId: number,
|
|
branch: string,
|
|
message: string,
|
|
files: Record<string, string>,
|
|
): Promise<GiteaCommit> {
|
|
// Implementation for creating commit in Gitea
|
|
// This would use Gitea's API to create/update files
|
|
// For simplicity, this is a placeholder
|
|
|
|
const response = await fetch(
|
|
`${this.apiUrl}/api/v1/repos/${repoId}/contents`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `token ${this.token}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
branch,
|
|
message,
|
|
files,
|
|
}),
|
|
},
|
|
);
|
|
|
|
return await response.json();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Migration Strategy
|
|
|
|
### Local to Cloud Migration Options
|
|
|
|
When users deploy to Easypanel, they will be asked:
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Deploy to Easypanel │
|
|
├─────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ How would you like to handle your local data? │
|
|
│ │
|
|
│ ○ Start Fresh │
|
|
│ Create a new project without importing local data │
|
|
│ │
|
|
│ ● Import Local Data │
|
|
│ Import your chats, messages, and project files │
|
|
│ │
|
|
│ ┌─────────────────────────────────────────────────────┐ │
|
|
│ │ Import Options: │ │
|
|
│ │ │ │
|
|
│ │ ☑ Import chat history │ │
|
|
│ │ ☑ Import project files │ │
|
|
│ │ ☐ Import AI model settings │ │
|
|
│ │ ☐ Import environment variables │ │
|
|
│ │ │ │
|
|
│ │ [Cancel] [Import & Deploy] │ │
|
|
│ └─────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Migration Implementation
|
|
|
|
```typescript
|
|
// src/lib/services/migration.service.ts
|
|
export class MigrationService {
|
|
async migrateFromLocal(
|
|
localDbPath: string,
|
|
projectId: string,
|
|
options: MigrationOptions,
|
|
): Promise<MigrationResult> {
|
|
const result: MigrationResult = {
|
|
chatsImported: 0,
|
|
messagesImported: 0,
|
|
filesImported: 0,
|
|
errors: [],
|
|
};
|
|
|
|
try {
|
|
// Connect to local SQLite database
|
|
const localDb = await this.connectToLocalDb(localDbPath);
|
|
|
|
// Import chats if selected
|
|
if (options.importChats) {
|
|
const localChats = await localDb.query.chats.findMany();
|
|
for (const chat of localChats) {
|
|
await db.insert(chats).values({
|
|
projectId,
|
|
title: chat.title,
|
|
createdBy: options.userId,
|
|
createdAt: chat.createdAt,
|
|
updatedAt: chat.updatedAt,
|
|
});
|
|
result.chatsImported++;
|
|
}
|
|
}
|
|
|
|
// Import messages if selected
|
|
if (options.importMessages) {
|
|
const localMessages = await localDb.query.messages.findMany();
|
|
for (const message of localMessages) {
|
|
await db.insert(messages).values({
|
|
chatId: this.mapChatId(message.chatId, projectId),
|
|
role: message.role,
|
|
content: message.content,
|
|
metadata: {
|
|
approvalState: message.approvalState,
|
|
sourceCommitHash: message.sourceCommitHash,
|
|
commitHash: message.commitHash,
|
|
requestId: message.requestId,
|
|
maxTokensUsed: message.maxTokensUsed,
|
|
},
|
|
createdAt: message.createdAt,
|
|
});
|
|
result.messagesImported++;
|
|
}
|
|
}
|
|
|
|
// Import project files if selected
|
|
if (options.importFiles) {
|
|
const projectPath = options.localProjectPath;
|
|
const files = await this.scanProjectFiles(projectPath);
|
|
|
|
for (const file of files) {
|
|
const content = await fs.readFile(file.path, "utf-8");
|
|
const relativePath = path.relative(projectPath, file.path);
|
|
|
|
await db.insert(projectFiles).values({
|
|
projectId,
|
|
path: relativePath,
|
|
content,
|
|
language: this.getLanguageFromPath(relativePath),
|
|
size: file.size,
|
|
});
|
|
result.filesImported++;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
} catch (error) {
|
|
result.errors.push(error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private async connectToLocalDb(dbPath: string): Promise<Database> {
|
|
// Connect to local SQLite database
|
|
const connection = await connect(dbPath);
|
|
return drizzle(connection, { schema: localSchema });
|
|
}
|
|
|
|
private async scanProjectFiles(projectPath: string): Promise<File[]> {
|
|
const files: File[] = [];
|
|
const entries = await fs.readdir(projectPath, { recursive: true });
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(projectPath, entry);
|
|
const stats = await fs.stat(fullPath);
|
|
|
|
if (stats.isFile()) {
|
|
files.push({
|
|
path: fullPath,
|
|
size: stats.size,
|
|
});
|
|
}
|
|
}
|
|
|
|
return files;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Security Considerations
|
|
|
|
### Authentication Security
|
|
|
|
1. **Password Hashing**: Use bcrypt with cost factor 12
|
|
2. **JWT Security**:
|
|
- Use strong secret keys (environment variables)
|
|
- Short access token expiration (15 minutes)
|
|
- Refresh token rotation
|
|
- Token revocation on logout
|
|
3. **Rate Limiting**:
|
|
- Login attempts: 5 per 15 minutes
|
|
- API requests: 100 per minute per user
|
|
- Password reset: 3 per hour
|
|
|
|
### Data Security
|
|
|
|
1. **Encryption**:
|
|
- Encrypt sensitive data at rest (API keys, secrets)
|
|
- Use TLS 1.3 for all connections
|
|
- Encrypt database backups
|
|
2. **Input Validation**:
|
|
- Validate all user inputs
|
|
- Sanitize file uploads
|
|
- Prevent SQL injection (use parameterized queries)
|
|
3. **Authorization**:
|
|
- Role-based access control (RBAC)
|
|
- Resource-level permissions
|
|
- Audit logging for sensitive actions
|
|
|
|
### API Security
|
|
|
|
1. **CORS**: Configure allowed origins
|
|
2. **CSRF Protection**: Implement CSRF tokens
|
|
3. **XSS Prevention**: Sanitize user-generated content
|
|
4. **Security Headers**:
|
|
- Content-Security-Policy
|
|
- X-Frame-Options
|
|
- X-Content-Type-Options
|
|
- Strict-Transport-Security
|
|
|
|
### Easypanel API Security
|
|
|
|
1. **API Key Management**:
|
|
- Store encrypted in database
|
|
- Rotate regularly
|
|
- Use separate keys for dev/prod
|
|
2. **Webhook Verification**:
|
|
- Verify webhook signatures
|
|
- Validate event types
|
|
- Handle idempotency
|
|
|
|
### Gitea Security
|
|
|
|
1. **Access Tokens**:
|
|
- Use personal access tokens
|
|
- Limit token permissions
|
|
- Rotate tokens regularly
|
|
2. **Repository Security**:
|
|
- Private repositories by default
|
|
- Branch protection rules
|
|
- Require pull requests
|
|
|
|
---
|
|
|
|
## Performance Requirements
|
|
|
|
### Response Times
|
|
|
|
- **API Endpoints**: < 200ms (p95)
|
|
- **Page Load**: < 2s (p95)
|
|
- **Code Generation**: < 30s for typical requests
|
|
- **Deployment**: < 5 minutes for initial deploy
|
|
|
|
### Scalability
|
|
|
|
- **Concurrent Users**: 1000+ concurrent users
|
|
- **Projects**: 10,000+ projects
|
|
- **Database**: Handle 1M+ records
|
|
- **File Storage**: 10GB+ per project
|
|
|
|
### Caching Strategy
|
|
|
|
1. **Redis Cache**:
|
|
|
|
- Session data
|
|
- Frequently accessed project data
|
|
- API responses
|
|
- Rate limiting counters
|
|
|
|
2. **Database Optimization**:
|
|
|
|
- Proper indexing
|
|
- Query optimization
|
|
- Connection pooling
|
|
- Read replicas (if needed)
|
|
|
|
3. **CDN**:
|
|
- Static assets
|
|
- Generated code previews
|
|
- Design system assets
|
|
|
|
---
|
|
|
|
## Development Phases
|
|
|
|
### Phase 1: Foundation (Weeks 1-4)
|
|
|
|
**Goal**: Set up core infrastructure
|
|
|
|
**Tasks**:
|
|
|
|
1. Initialize Next.js project with TypeScript
|
|
2. Set up PostgreSQL database
|
|
3. Configure Drizzle ORM
|
|
4. Implement authentication system
|
|
5. Create user management APIs
|
|
6. Set up Redis for caching
|
|
7. Configure CI/CD pipeline
|
|
|
|
**Deliverables**:
|
|
|
|
- Working Next.js application
|
|
- Database schema implemented
|
|
- User registration/login working
|
|
- Basic admin dashboard
|
|
|
|
### Phase 2: Core Features (Weeks 5-8)
|
|
|
|
**Goal**: Implement project management and AI features
|
|
|
|
**Tasks**:
|
|
|
|
1. Create project management system
|
|
2. Implement chat interface
|
|
3. Integrate AI providers
|
|
4. Set up code editor
|
|
5. Implement file management
|
|
6. Create preview system
|
|
7. Add version control
|
|
|
|
**Deliverables**:
|
|
|
|
- Users can create and manage projects
|
|
- AI-powered code generation working
|
|
- Code editor with syntax highlighting
|
|
- Live preview functionality
|
|
|
|
### Phase 3: UI/UX Pro Max Integration (Weeks 9-10)
|
|
|
|
**Goal**: Integrate design system generation
|
|
|
|
**Tasks**:
|
|
|
|
1. Install UI/UX Pro Max
|
|
2. Create design system service
|
|
3. Implement design system generation
|
|
4. Integrate with AI prompts
|
|
5. Create design system UI
|
|
6. Add design system persistence
|
|
|
|
**Deliverables**:
|
|
|
|
- Automatic design system generation
|
|
- Design system applied to generated code
|
|
- Design system management UI
|
|
|
|
### Phase 4: Easypanel Integration (Weeks 11-13)
|
|
|
|
**Goal**: Implement deployment functionality
|
|
|
|
**Tasks**:
|
|
|
|
1. Create Easypanel API client
|
|
2. Implement database creation
|
|
3. Implement application deployment
|
|
4. Add deployment management
|
|
5. Create deployment UI
|
|
6. Implement update deployment
|
|
7. Add deployment logs
|
|
|
|
**Deliverables**:
|
|
|
|
- One-click deployment to Easypanel
|
|
- Automatic database creation
|
|
- Deployment management UI
|
|
- Update deployment functionality
|
|
|
|
### Phase 5: Gitea Integration (Weeks 14-15)
|
|
|
|
**Goal**: Implement code backup and version control
|
|
|
|
**Tasks**:
|
|
|
|
1. Create Gitea API client
|
|
2. Implement repository creation
|
|
3. Add commit functionality
|
|
4. Implement code backup
|
|
5. Add version history
|
|
6. Create version comparison UI
|
|
|
|
**Deliverables**:
|
|
|
|
- Automatic code backup to Gitea
|
|
- Version history tracking
|
|
- Version comparison UI
|
|
|
|
### Phase 6: Billing & Subscription (Weeks 16-18)
|
|
|
|
**Goal**: Implement billing system
|
|
|
|
**Tasks**:
|
|
|
|
1. Integrate Stripe
|
|
2. Create subscription management
|
|
3. Implement pricing tiers
|
|
4. Add billing UI
|
|
5. Set up webhooks
|
|
6. Create invoice management
|
|
7. Add usage analytics
|
|
|
|
**Deliverables**:
|
|
|
|
- Stripe integration working
|
|
- Subscription management
|
|
- Billing UI
|
|
- Invoice system
|
|
|
|
### Phase 7: Migration & Cleanup (Weeks 19-20)
|
|
|
|
**Goal**: Remove external services and clean up
|
|
|
|
**Tasks**:
|
|
|
|
1. Remove Supabase integration
|
|
2. Remove Neon integration
|
|
3. Remove Vercel integration
|
|
4. Remove Electron dependencies
|
|
5. Clean up unused code
|
|
6. Update documentation
|
|
7. Remove "dyad" branding
|
|
|
|
**Deliverables**:
|
|
|
|
- All external services removed
|
|
- Clean codebase
|
|
- Updated documentation
|
|
|
|
### Phase 8: Testing & Optimization (Weeks 21-22)
|
|
|
|
**Goal**: Ensure quality and performance
|
|
|
|
**Tasks**:
|
|
|
|
1. Write unit tests
|
|
2. Write integration tests
|
|
3. Write E2E tests
|
|
4. Performance optimization
|
|
5. Security audit
|
|
6. Load testing
|
|
7. Bug fixes
|
|
|
|
**Deliverables**:
|
|
|
|
- Comprehensive test suite
|
|
- Performance benchmarks
|
|
- Security audit report
|
|
- Bug-free application
|
|
|
|
### Phase 9: Deployment & Launch (Weeks 23-24)
|
|
|
|
**Goal**: Deploy to production
|
|
|
|
**Tasks**:
|
|
|
|
1. Set up production VPS
|
|
2. Configure Nginx
|
|
3. Set up SSL certificates
|
|
4. Deploy application
|
|
5. Configure monitoring
|
|
6. Set up backups
|
|
7. Create launch checklist
|
|
8. Launch application
|
|
|
|
**Deliverables**:
|
|
|
|
- Production deployment
|
|
- Monitoring and alerting
|
|
- Backup system
|
|
- Live application
|
|
|
|
---
|
|
|
|
## Appendix
|
|
|
|
### Environment Variables
|
|
|
|
```env
|
|
# Database
|
|
DATABASE_URL=postgresql://user:password@localhost:5432/moreminimore
|
|
REDIS_URL=redis://localhost:6379
|
|
|
|
# Authentication
|
|
JWT_SECRET=your-super-secret-jwt-key
|
|
JWT_REFRESH_SECRET=your-super-secret-refresh-key
|
|
|
|
# AI Providers
|
|
OPENAI_API_KEY=sk-...
|
|
ANTHROPIC_API_KEY=sk-ant-...
|
|
GOOGLE_API_KEY=...
|
|
|
|
# Easypanel
|
|
EASYPANEL_API_KEY=your-easypanel-api-key
|
|
EASYPANEL_API_URL=https://panel.moreminimore.com/api
|
|
|
|
# Gitea
|
|
GITEA_API_URL=https://gitea.moreminimore.com/api/v1
|
|
GITEA_TOKEN=your-gitea-token
|
|
|
|
# Stripe
|
|
STRIPE_SECRET_KEY=sk_live_...
|
|
STRIPE_WEBHOOK_SECRET=whsec_...
|
|
STRIPE_PRICE_ID_FREE=price_...
|
|
STRIPE_PRICE_ID_PRO=price_...
|
|
STRIPE_PRICE_ID_ENTERPRISE=price_...
|
|
|
|
# Email
|
|
RESEND_API_KEY=re_...
|
|
|
|
# Application
|
|
NEXT_PUBLIC_APP_URL=https://app.moreminimore.com
|
|
NEXT_PUBLIC_API_URL=https://api.moreminimore.com
|
|
```
|
|
|
|
### API Endpoints
|
|
|
|
#### Authentication
|
|
|
|
- `POST /api/auth/register` - Register new user
|
|
- `POST /api/auth/login` - Login user
|
|
- `POST /api/auth/logout` - Logout user
|
|
- `POST /api/auth/refresh` - Refresh access token
|
|
- `POST /api/auth/forgot-password` - Request password reset
|
|
- `POST /api/auth/reset-password` - Reset password
|
|
- `POST /api/auth/verify-email` - Verify email
|
|
|
|
#### Users
|
|
|
|
- `GET /api/users/me` - Get current user
|
|
- `PATCH /api/users/me` - Update current user
|
|
- `GET /api/users/:id` - Get user by ID (admin only)
|
|
- `GET /api/users` - List all users (admin only)
|
|
|
|
#### Organizations
|
|
|
|
- `POST /api/organizations` - Create organization
|
|
- `GET /api/organizations` - List user's organizations
|
|
- `GET /api/organizations/:id` - Get organization details
|
|
- `PATCH /api/organizations/:id` - Update organization
|
|
- `DELETE /api/organizations/:id` - Delete organization
|
|
|
|
#### Organization Members
|
|
|
|
- `POST /api/organizations/:id/members` - Invite member
|
|
- `GET /api/organizations/:id/members` - List members
|
|
- `PATCH /api/organizations/:id/members/:memberId` - Update member
|
|
- `DELETE /api/organizations/:id/members/:memberId` - Remove member
|
|
|
|
#### Projects
|
|
|
|
- `POST /api/projects` - Create project
|
|
- `GET /api/projects` - List user's projects
|
|
- `GET /api/projects/:id` - Get project details
|
|
- `PATCH /api/projects/:id` - Update project
|
|
- `DELETE /api/projects/:id` - Delete project
|
|
|
|
#### Project Files
|
|
|
|
- `GET /api/projects/:id/files` - List project files
|
|
- `GET /api/projects/:id/files/*` - Get file content
|
|
- `PUT /api/projects/:id/files/*` - Save file
|
|
- `DELETE /api/projects/:id/files/*` - Delete file
|
|
|
|
#### Chats
|
|
|
|
- `POST /api/projects/:id/chats` - Create chat
|
|
- `GET /api/projects/:id/chats` - List chats
|
|
- `GET /api/chats/:id` - Get chat details
|
|
- `DELETE /api/chats/:id` - Delete chat
|
|
|
|
#### Messages
|
|
|
|
- `POST /api/chats/:id/messages` - Send message
|
|
- `GET /api/chats/:id/messages` - List messages
|
|
|
|
#### Deployment
|
|
|
|
- `POST /api/projects/:id/deploy` - Deploy project
|
|
- `POST /api/projects/:id/deploy/update` - Update deployment
|
|
- `GET /api/projects/:id/deployments` - List deployments
|
|
- `GET /api/deployments/:id/logs` - Get deployment logs
|
|
|
|
#### Design Systems
|
|
|
|
- `POST /api/projects/:id/design-system` - Generate design system
|
|
- `GET /api/projects/:id/design-system` - Get design system
|
|
- `PATCH /api/projects/:id/design-system` - Update design system
|
|
|
|
#### Billing
|
|
|
|
- `POST /api/billing/checkout` - Create checkout session
|
|
- `GET /api/billing/subscription` - Get subscription details
|
|
- `POST /api/billing/portal` - Create customer portal session
|
|
- `GET /api/billing/invoices` - List invoices
|
|
|
|
#### Admin
|
|
|
|
- `GET /api/admin/users` - List all users
|
|
- `GET /api/admin/organizations` - List all organizations
|
|
- `GET /api/admin/analytics` - Get system analytics
|
|
- `PATCH /api/admin/settings` - Update system settings
|
|
|
|
---
|
|
|
|
## Conclusion
|
|
|
|
This specification provides a comprehensive roadmap for transforming MoreMinimore from a local Electron app into a full-featured SAAS platform. The architecture is designed to be scalable, secure, and maintainable while providing an excellent user experience.
|
|
|
|
Key highlights:
|
|
|
|
- **Modern Tech Stack**: Next.js 15, PostgreSQL, Redis, Drizzle ORM
|
|
- **Robust Authentication**: Custom JWT with role-based access control
|
|
- **AI-Powered**: Integration with multiple AI providers and UI/UX Pro Max
|
|
- **Easy Deployment**: One-click deployment to Easypanel
|
|
- **Code Backup**: Automatic version control with Gitea
|
|
- **Billing Integration**: Stripe for subscription management
|
|
- **Scalable Architecture**: Designed for growth and performance
|
|
|
|
The development is organized into 9 phases over 24 weeks, with clear deliverables and milestones. This phased approach ensures steady progress and allows for iterative improvements based on feedback.
|
|
|
|
---
|
|
|
|
**Document Version**: 1.0
|
|
**Last Updated**: January 19, 2026
|
|
**Author**: MoreMinimore Development Team
|