Files
websitebuilder/SPECIFICATION.md
Kunthawat Greethong 4d1bb6892b
Some checks failed
CI / build (push) Has been cancelled
CI / test (push) Has been cancelled
CI / e2e (push) Has been cancelled
CI / lint (push) Has been cancelled
Add websitebuilder app
2026-01-26 12:50:12 +07:00

96 KiB

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
  2. Technology Stack
  3. Database Schema
  4. Authentication & Authorization
  5. User Roles & Permissions
  6. Core Features
  7. Deployment Flow
  8. UI/UX Pro Max Integration
  9. Easypanel Integration
  10. Billing & Pricing
  11. Code Storage Strategy
  12. Migration Strategy
  13. Security Considerations
  14. Performance Requirements
  15. 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

-- 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

// 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

// 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

# 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

// 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
// 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": {
    "email": "${EASYPANEL_EMAIL}",
    "password": "${EASYPANEL_PASSWORD}"
  }
}

Response:

[
  {
    "result": {
      "data": {
        "json": {
          "token": "cmkko1glb000p07pfa226232b"
        }
      }
    }
  }
]

Environment Variables:

  • EASYPANEL_EMAIL: Easypanel admin email
  • EASYPANEL_PASSWORD: Easypanel admin password

Implementation:

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:

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": {
    "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:

[
  {
    "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:

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": {
    "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:

[
  {
    "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:

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": {
    "projectName": "database",
    "serviceName": "{username}-{project_id}",
    "deploy": {}
  }
}

Implementation:

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:

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:

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": {
    "projectName": "database",
    "serviceName": "{username}-{project_id}"
  }
}

Implementation:

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": {
    "projectName": "database",
    "serviceName": "{username}-{project_id}"
  }
}

Start Service Endpoint

URL: POST /api/trpc/services.app.startService

Headers:

Authorization: Bearer {token}

Request Body:

{
  "json": {
    "projectName": "database",
    "serviceName": "{username}-{project_id}"
  }
}

Implementation:

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": {
    "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:

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

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

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:

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

// 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:

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:

# 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

// 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

// 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

// 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

// 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

// 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

# 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